| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package key
- import (
- "bufio"
- "bytes"
- "crypto/subtle"
- "encoding/hex"
- "errors"
- "fmt"
- "go4.org/mem"
- "golang.org/x/crypto/curve25519"
- "golang.org/x/crypto/nacl/box"
- "tailscale.com/types/structs"
- )
- 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:"
- // nodePublicBinaryPrefix is the prefix used to identify a
- // binary-encoded node public key.
- nodePublicBinaryPrefix = "np"
- // NodePublicRawLen is the length in bytes of a NodePublic, when
- // serialized with AppendTo, Raw32 or WriteRawWithoutAllocating.
- NodePublicRawLen = 32
- )
- // 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
- }
- // NodePrivateFromRaw32 parses a 32-byte raw value as a NodePrivate.
- //
- // Deprecated: only needed to cast from legacy node private key types,
- // do not add more uses unrelated to #3206.
- func NodePrivateFromRaw32(raw mem.RO) NodePrivate {
- if raw.Len() != 32 {
- panic("input has wrong size")
- }
- var ret NodePrivate
- raw.Copy(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
- }
- // AppendText implements encoding.TextAppender.
- func (k NodePrivate) AppendText(b []byte) ([]byte, error) {
- return appendHexKey(b, nodePrivateHexPrefix, k.k[:]), nil
- }
- // MarshalText implements encoding.TextMarshaler.
- func (k NodePrivate) MarshalText() ([]byte, error) {
- return k.AppendText(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[:])
- }
- // NodePublic is the public portion of a NodePrivate.
- type NodePublic struct {
- k [32]byte
- }
- // Shard returns a uint8 number from a public key with
- // mostly-uniform distribution, suitable for sharding.
- func (p NodePublic) Shard() uint8 {
- // A 25519 public key isn't uniformly random, as it ultimately
- // corresponds to a point on the curve.
- // But we don't need perfectly uniformly-random, we need
- // good-enough-for-sharding random, so we haphazardly
- // combine raw values of the key to give us something sufficient.
- s := uint8(p.k[31]) + uint8(p.k[30]) + uint8(p.k[20])
- return s ^ uint8(p.k[2]+p.k[12])
- }
- // Compare returns -1, 0, or 1, depending on whether p orders before p2,
- // using bytes.Compare on the bytes of the public key.
- func (p NodePublic) Compare(p2 NodePublic) int {
- return bytes.Compare(p.k[:], p2.k[:])
- }
- // 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
- }
- // badOldPrefix is a nodekey/discokey prefix that, when base64'd, serializes
- // with a "bad01" ("bad ol'", ~"bad old") prefix. It's used for expired node
- // keys so when we debug a customer issue, the "bad01" can jump out to us. See:
- //
- // https://github.com/tailscale/tailscale/issues/6932
- var badOldPrefix = []byte{109, 167, 116, 213, 215, 116}
- // NodePublicWithBadOldPrefix returns a copy of k with its leading public key
- // bytes mutated such that it base64's to a ShortString of [bad01] ("bad ol'"
- // [expired node key]).
- func NodePublicWithBadOldPrefix(k NodePublic) NodePublic {
- var buf [32]byte
- k.AppendTo(buf[:0])
- copy(buf[:], badOldPrefix)
- return NodePublicFromRaw32(mem.B(buf[:]))
- }
- // 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[:]...)
- }
- // 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 and a few places in the wireguard-go API; don't add
- // more uses.
- func (k NodePublic) Raw32() [32]byte {
- return k.k
- }
- // 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 k as a hex-encoded string with a type prefix.
- func (k NodePublic) String() string {
- bs, err := k.MarshalText()
- if err != nil {
- panic(err)
- }
- return string(bs)
- }
- // AppendText implements encoding.TextAppender. It appends a typed prefix
- // followed by hex encoded represtation of k to b.
- func (k NodePublic) AppendText(b []byte) ([]byte, error) {
- return appendHexKey(b, nodePublicHexPrefix, k.k[:]), nil
- }
- // MarshalText implements encoding.TextMarshaler. It returns a typed prefix
- // followed by a hex encoded representation of k.
- func (k NodePublic) MarshalText() ([]byte, error) {
- return k.AppendText(nil)
- }
- // UnmarshalText implements encoding.TextUnmarshaler. It expects a typed prefix
- // followed by a hex encoded representation of k.
- func (k *NodePublic) UnmarshalText(b []byte) error {
- return parseHex(k.k[:], mem.B(b), mem.S(nodePublicHexPrefix))
- }
- // MarshalBinary implements encoding.BinaryMarshaler.
- func (k NodePublic) MarshalBinary() (data []byte, err error) {
- b := make([]byte, len(nodePublicBinaryPrefix)+NodePublicRawLen)
- copy(b[:len(nodePublicBinaryPrefix)], nodePublicBinaryPrefix)
- copy(b[len(nodePublicBinaryPrefix):], k.k[:])
- return b, nil
- }
- // UnmarshalBinary implements encoding.BinaryUnmarshaler.
- func (k *NodePublic) UnmarshalBinary(in []byte) error {
- data := mem.B(in)
- if !mem.HasPrefix(data, mem.S(nodePublicBinaryPrefix)) {
- return fmt.Errorf("missing/incorrect type prefix %s", nodePublicBinaryPrefix)
- }
- if want, got := len(nodePublicBinaryPrefix)+NodePublicRawLen, data.Len(); want != got {
- return fmt.Errorf("incorrect len for NodePublic (%d != %d)", got, want)
- }
- data.SliceFrom(len(nodePublicBinaryPrefix)).Copy(k.k[:])
- return nil
- }
- // 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)
- }
|