diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 2e1eae183..e5d7e35ef 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -81,6 +81,15 @@ func (u StableNodeID) IsZero() bool { // NodeKey is the curve25519 public key for a node. type NodeKey [32]byte +// NodeKeyFromNodePublic returns k converted to a NodeKey. +// +// Deprecated: exists only as a compatibility bridge while NodeKey +// gets removed from the codebase. Do not introduce new uses that +// aren't related to #3206. +func NodeKeyFromNodePublic(k key.NodePublic) NodeKey { + return k.Raw32() +} + // DiscoKey is the curve25519 public key for path discovery key. // It's never written to disk or reused between network start-ups. type DiscoKey [32]byte diff --git a/types/key/node.go b/types/key/node.go new file mode 100644 index 000000000..6618e8c5e --- /dev/null +++ b/types/key/node.go @@ -0,0 +1,333 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package key + +import ( + "bufio" + "bytes" + "crypto/subtle" + "encoding/hex" + "errors" + + "go4.org/mem" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/nacl/box" + "tailscale.com/types/structs" + "tailscale.com/types/wgkey" +) + +const ( + // nodePrivateHexPrefix is the prefix used to identify a + // hex-encoded node private key. + // + // This prefix name is a little unfortunate, in that it comes from + // WireGuard's own key types, and we've used it for both key types + // we persist to disk (machine and node keys). But we're stuck + // with it for now, barring another round of tricky migration. + nodePrivateHexPrefix = "privkey:" + + // nodePublicHexPrefix is the prefix used to identify a + // hex-encoded node public key. + // + // This prefix is used in the control protocol, so cannot be + // changed. + nodePublicHexPrefix = "nodekey:" +) + +// NodePrivate is a node key, used for WireGuard tunnels and +// communication with DERP servers. +type NodePrivate struct { + _ structs.Incomparable // because == isn't constant-time + k [32]byte +} + +// NewNode creates and returns a new node private key. +func NewNode() NodePrivate { + var ret NodePrivate + rand(ret.k[:]) + // WireGuard does its own clamping, so this would be unnecessary - + // but we also use this key for DERP comms, which does require + // clamping. + clamp25519Private(ret.k[:]) + return ret +} + +func ParseNodePrivateUntyped(raw mem.RO) (NodePrivate, error) { + var ret NodePrivate + if err := parseHex(ret.k[:], raw, mem.B(nil)); err != nil { + return NodePrivate{}, err + } + return ret, nil +} + +// IsZero reports whether k is the zero value. +func (k NodePrivate) IsZero() bool { + return k.Equal(NodePrivate{}) +} + +// Equal reports whether k and other are the same key. +func (k NodePrivate) Equal(other NodePrivate) bool { + return subtle.ConstantTimeCompare(k.k[:], other.k[:]) == 1 +} + +// Public returns the NodePublic for k. +// Panics if NodePrivate is zero. +func (k NodePrivate) Public() NodePublic { + if k.IsZero() { + panic("can't take the public key of a zero NodePrivate") + } + var ret NodePublic + curve25519.ScalarBaseMult(&ret.k, &k.k) + return ret +} + +// MarshalText implements encoding.TextMarshaler. +func (k NodePrivate) MarshalText() ([]byte, error) { + return toHex(k.k[:], nodePrivateHexPrefix), nil +} + +// MarshalText implements encoding.TextUnmarshaler. +func (k *NodePrivate) UnmarshalText(b []byte) error { + return parseHex(k.k[:], mem.B(b), mem.S(nodePrivateHexPrefix)) +} + +// SealTo wraps cleartext into a NaCl box (see +// golang.org/x/crypto/nacl) to p, authenticated from k, using a +// random nonce. +// +// The returned ciphertext is a 24-byte nonce concatenated with the +// box value. +func (k NodePrivate) SealTo(p NodePublic, cleartext []byte) (ciphertext []byte) { + if k.IsZero() || p.IsZero() { + panic("can't seal with zero keys") + } + var nonce [24]byte + rand(nonce[:]) + return box.Seal(nonce[:], cleartext, &nonce, &p.k, &k.k) +} + +// OpenFrom opens the NaCl box ciphertext, which must be a value +// created by SealTo, and returns the inner cleartext if ciphertext is +// a valid box from p to k. +func (k NodePrivate) OpenFrom(p NodePublic, ciphertext []byte) (cleartext []byte, ok bool) { + if k.IsZero() || p.IsZero() { + panic("can't open with zero keys") + } + if len(ciphertext) < 24 { + return nil, false + } + nonce := (*[24]byte)(ciphertext) + return box.Open(nil, ciphertext[len(nonce):], nonce, &p.k, &k.k) +} + +func (k NodePrivate) UntypedHexString() string { + return hex.EncodeToString(k.k[:]) +} + +// AsPrivate returns k converted to a Private. +// +// Deprecated: exists only as a compatibility bridge while Private +// gets removed from the codebase. Do not introduce new uses that +// aren't related to #3206. +func (k NodePrivate) AsPrivate() Private { + return k.k +} + +// AsWGPrivate returns k converted to a wgkey.Private. +// +// Deprecated: exists only as a compatibility bridge while +// wgkey.Private gets removed from the codebase. Do not introduce new +// uses that aren't related to #3206. +func (k NodePrivate) AsWGPrivate() wgkey.Private { + return k.k +} + +// NodePublic is the public portion of a NodePrivate. +type NodePublic struct { + k [32]byte +} + +// ParseNodePublicUntyped parses an untyped 64-character hex value +// as a NodePublic. +// +// Deprecated: this function is risky to use, because it cannot verify +// that the hex string was intended to be a NodePublic. This can +// lead to accidentally decoding one type of key as another. For new +// uses that don't require backwards compatibility with the untyped +// string format, please use MarshalText/UnmarshalText. +func ParseNodePublicUntyped(raw mem.RO) (NodePublic, error) { + var ret NodePublic + if err := parseHex(ret.k[:], raw, mem.B(nil)); err != nil { + return NodePublic{}, err + } + return ret, nil +} + +// NodePublicFromRaw32 parses a 32-byte raw value as a NodePublic. +// +// This should be used only when deserializing a NodePublic from a +// binary protocol. +func NodePublicFromRaw32(raw mem.RO) NodePublic { + if raw.Len() != 32 { + panic("input has wrong size") + } + var ret NodePublic + raw.Copy(ret.k[:]) + return ret +} + +// IsZero reports whether k is the zero value. +func (k NodePublic) IsZero() bool { + return k == NodePublic{} +} + +// ShortString returns the Tailscale conventional debug representation +// of a public key: the first five base64 digits of the key, in square +// brackets. +func (k NodePublic) ShortString() string { + return debug32(k.k) +} + +// AppendTo appends k, serialized as a 32-byte binary value, to +// buf. Returns the new slice. +func (k NodePublic) AppendTo(buf []byte) []byte { + return append(buf, k.k[:]...) +} + +// RawLen returns the length of k when to the format handled by +// ReadRawWithoutAllocating and WriteRawWithoutAllocating. +func (k NodePublic) RawLen() int { + return 32 +} + +// ReadRawWithoutAllocating initializes k with bytes read from br. +// The reading is done ~4x slower than io.ReadFull, but in exchange is +// allocation-free. +func (k *NodePublic) ReadRawWithoutAllocating(br *bufio.Reader) error { + var z NodePublic + if *k != z { + return errors.New("refusing to read into non-zero NodePublic") + } + // This is ~4x slower than io.ReadFull, but using io.ReadFull + // causes one extra alloc, which is significant for the DERP + // server that consumes this method. So, process stuff slower but + // without allocation. + // + // Dear future: if io.ReadFull stops causing stuff to escape, you + // should switch back to that. + for i := range k.k { + b, err := br.ReadByte() + if err != nil { + return err + } + k.k[i] = b + } + return nil +} + +// WriteRawWithoutAllocating writes out k as 32 bytes to bw. +// The writing is done ~3x slower than bw.Write, but in exchange is +// allocation-free. +func (k NodePublic) WriteRawWithoutAllocating(bw *bufio.Writer) error { + // Equivalent to bw.Write(k.k[:]), but without causing an + // escape-related alloc. + // + // Dear future: if bw.Write(k.k[:]) stops causing stuff to escape, + // you should switch back to that. + for _, b := range k.k { + err := bw.WriteByte(b) + if err != nil { + return err + } + } + return nil +} + +// Raw32 returns k encoded as 32 raw bytes. +// +// Deprecated: only needed for a single legacy use in the control +// server, don't add more uses. +func (k NodePublic) Raw32() [32]byte { + var ret [32]byte + copy(ret[:], k.k[:]) + return ret +} + +// Less reports whether k orders before other, using an undocumented +// deterministic ordering. +func (k NodePublic) Less(other NodePublic) bool { + return bytes.Compare(k.k[:], other.k[:]) < 0 +} + +// UntypedHexString returns k, encoded as an untyped 64-character hex +// string. +// +// Deprecated: this function is risky to use, because it produces +// serialized values that do not identify themselves as a +// NodePublic, allowing other code to potentially parse it back in +// as the wrong key type. For new uses that don't require backwards +// compatibility with the untyped string format, please use +// MarshalText/UnmarshalText. +func (k NodePublic) UntypedHexString() string { + return hex.EncodeToString(k.k[:]) +} + +// String returns the output of MarshalText as a string. +func (k NodePublic) String() string { + bs, err := k.MarshalText() + if err != nil { + panic(err) + } + return string(bs) +} + +// MarshalText implements encoding.TextMarshaler. +func (k NodePublic) MarshalText() ([]byte, error) { + return toHex(k.k[:], nodePublicHexPrefix), nil +} + +// MarshalText implements encoding.TextUnmarshaler. +func (k *NodePublic) UnmarshalText(b []byte) error { + return parseHex(k.k[:], mem.B(b), mem.S(nodePublicHexPrefix)) +} + +// WireGuardGoString prints k in the same format used by wireguard-go. +func (k NodePublic) WireGuardGoString() string { + // This implementation deliberately matches the overly complicated + // implementation in wireguard-go. + b64 := func(input byte) byte { + return input + 'A' + byte(((25-int(input))>>8)&6) - byte(((51-int(input))>>8)&75) - byte(((61-int(input))>>8)&15) + byte(((62-int(input))>>8)&3) + } + b := []byte("peer(____…____)") + const first = len("peer(") + const second = len("peer(____…") + b[first+0] = b64((k.k[0] >> 2) & 63) + b[first+1] = b64(((k.k[0] << 4) | (k.k[1] >> 4)) & 63) + b[first+2] = b64(((k.k[1] << 2) | (k.k[2] >> 6)) & 63) + b[first+3] = b64(k.k[2] & 63) + b[second+0] = b64(k.k[29] & 63) + b[second+1] = b64((k.k[30] >> 2) & 63) + b[second+2] = b64(((k.k[30] << 4) | (k.k[31] >> 4)) & 63) + b[second+3] = b64((k.k[31] << 2) & 63) + return string(b) +} + +// AsPublic returns k converted to a Public. +// +// Deprecated: exists only as a compatibility bridge while Public +// gets removed from the codebase. Do not introduce new uses that +// aren't related to #3206. +func (k NodePublic) AsPublic() Public { + return k.k +} + +// AsWGKey returns k converted to a wgkey.Key. +// +// Deprecated: exists only as a compatibility bridge while +// wgkey.Key gets removed from the codebase. Do not introduce new +// uses that aren't related to #3206. +func (k NodePublic) AsWGKey() wgkey.Key { + return k.k +} diff --git a/types/key/node_test.go b/types/key/node_test.go new file mode 100644 index 000000000..1f9042d8f --- /dev/null +++ b/types/key/node_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package key + +import ( + "bufio" + "bytes" + "encoding/json" + "strings" + "testing" +) + +func TestNodeKey(t *testing.T) { + k := NewNode() + if k.IsZero() { + t.Fatal("NodePrivate should not be zero") + } + + p := k.Public() + if p.IsZero() { + t.Fatal("NodePublic should not be zero") + } + + bs, err := p.MarshalText() + if err != nil { + t.Fatal(err) + } + if full, got := string(bs), ":"+p.UntypedHexString(); !strings.HasSuffix(full, got) { + t.Fatalf("NodePublic.UntypedHexString is not a suffix of the typed serialization, got %q want suffix of %q", got, full) + } + + z := NodePublic{} + if !z.IsZero() { + t.Fatal("IsZero(NodePublic{}) is false") + } + if s := z.ShortString(); s != "" { + t.Fatalf("NodePublic{}.ShortString() is %q, want \"\"", s) + } +} + +func TestNodeSerialization(t *testing.T) { + serialized := `{ + "Priv": "privkey:40ab1b58e9076c7a4d9d07291f5edf9d1aa017eb949624ba683317f48a640369", + "Pub":"nodekey:50d20b455ecf12bc453f83c2cfdb2a24925d06cf2598dcaa54e91af82ce9f765" + }` + + // Carefully check that the expected serialized data decodes and + // re-encodes to the expected keys. These types are serialized to + // disk all over the place and need to be stable. + priv := NodePrivate{ + k: [32]uint8{ + 0x40, 0xab, 0x1b, 0x58, 0xe9, 0x7, 0x6c, 0x7a, 0x4d, 0x9d, 0x7, + 0x29, 0x1f, 0x5e, 0xdf, 0x9d, 0x1a, 0xa0, 0x17, 0xeb, 0x94, + 0x96, 0x24, 0xba, 0x68, 0x33, 0x17, 0xf4, 0x8a, 0x64, 0x3, 0x69, + }, + } + pub := NodePublic{ + k: [32]uint8{ + 0x50, 0xd2, 0xb, 0x45, 0x5e, 0xcf, 0x12, 0xbc, 0x45, 0x3f, 0x83, + 0xc2, 0xcf, 0xdb, 0x2a, 0x24, 0x92, 0x5d, 0x6, 0xcf, 0x25, 0x98, + 0xdc, 0xaa, 0x54, 0xe9, 0x1a, 0xf8, 0x2c, 0xe9, 0xf7, 0x65, + }, + } + + type keypair struct { + Priv NodePrivate + Pub NodePublic + } + + var a keypair + if err := json.Unmarshal([]byte(serialized), &a); err != nil { + t.Fatal(err) + } + if !a.Priv.Equal(priv) { + t.Errorf("wrong deserialization of private key, got %#v want %#v", a.Priv, priv) + } + if a.Pub != pub { + t.Errorf("wrong deserialization of public key, got %#v want %#v", a.Pub, pub) + } + + bs, err := json.MarshalIndent(a, "", " ") + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + json.Indent(&b, []byte(serialized), "", " ") + if got, want := string(bs), b.String(); got != want { + t.Error("json serialization doesn't roundtrip") + } +} + +func TestNodeReadRawWithoutAllocating(t *testing.T) { + buf := make([]byte, 32) + for i := range buf { + buf[i] = 0x42 + } + r := bytes.NewReader(buf) + br := bufio.NewReader(r) + got := testing.AllocsPerRun(1000, func() { + r.Reset(buf) + br.Reset(r) + var k NodePublic + if err := k.ReadRawWithoutAllocating(br); err != nil { + t.Fatalf("ReadRawWithoutAllocating: %v", err) + } + }) + if want := 0.0; got != want { + t.Fatalf("ReadRawWithoutAllocating got %f allocs, want %f", got, want) + } +} + +func TestNodeWriteRawWithoutAllocating(t *testing.T) { + buf := make([]byte, 0, 32) + w := bytes.NewBuffer(buf) + bw := bufio.NewWriter(w) + got := testing.AllocsPerRun(1000, func() { + w.Reset() + bw.Reset(w) + var k NodePublic + if err := k.WriteRawWithoutAllocating(bw); err != nil { + t.Fatalf("WriteRawWithoutAllocating: %v", err) + } + }) + if want := 0.0; got != want { + t.Fatalf("WriteRawWithoutAllocating got %f allocs, want %f", got, want) + } +} diff --git a/types/key/util.go b/types/key/util.go index c5a792fb6..987fa28c5 100644 --- a/types/key/util.go +++ b/types/key/util.go @@ -103,9 +103,15 @@ func debug32(k [32]byte) string { if k == [32]byte{} { return "" } - var b [45]byte // 32 bytes expands to 44 bytes in base64, plus 1 for the leading '[' - base64.StdEncoding.Encode(b[1:], k[:]) - b[0] = '[' - b[6] = ']' - return string(b[:7]) + + // The goal here is to generate "[" + base64.StdEncoding.EncodeToString(k[:])[:5] + "]". + // Since we only care about the first 5 characters, it suffices to encode the first 4 bytes of k. + // Encoding those 4 bytes requires 8 bytes. + // Make dst have size 9, to fit the leading '[' plus those 8 bytes. + // We slice the unused ones away at the end. + dst := make([]byte, 9) + dst[0] = '[' + base64.StdEncoding.Encode(dst[1:], k[:4]) + dst[6] = ']' + return string(dst[:7]) }