From 023d4e221657276a60474c93a49d989b2cffd35c Mon Sep 17 00:00:00 2001 From: Tom DNetto Date: Fri, 29 Jul 2022 11:03:23 -0700 Subject: [PATCH] tka,types/key: implement NLPrivate glue for tailnet key authority keys Signed-off-by: Tom DNetto --- tka/aum.go | 24 +++++++------- tka/builder.go | 11 +++++-- tka/builder_test.go | 43 +++++++++++-------------- tka/chaintest_test.go | 6 ++-- tka/key.go | 5 ++- tka/state.go | 10 +++--- tka/tka.go | 28 ++++++++-------- tka/tka_test.go | 2 +- types/key/nl.go | 74 +++++++++++++++++++++++++++++++++++++++++++ types/key/nl_test.go | 47 +++++++++++++++++++++++++++ types/key/util.go | 6 ++-- 11 files changed, 188 insertions(+), 68 deletions(-) create mode 100644 types/key/nl.go create mode 100644 types/key/nl_test.go diff --git a/tka/aum.go b/tka/aum.go index 5d0181feb..f3de57bf3 100644 --- a/tka/aum.go +++ b/tka/aum.go @@ -77,20 +77,20 @@ func (k AUMKind) String() string { // AUM describes an Authority Update Message. // // The rules for adding new types of AUMs (MessageKind): -// - CBOR key IDs must never be changed. -// - New AUM types must not change semantics that are manipulated by other -// AUM types. -// - The serialization of existing data cannot change (in other words, if -// an existing serialization test in aum_test.go fails, you need to try a -// different approach). +// - CBOR key IDs must never be changed. +// - New AUM types must not change semantics that are manipulated by other +// AUM types. +// - The serialization of existing data cannot change (in other words, if +// an existing serialization test in aum_test.go fails, you need to try a +// different approach). // // The rules for adding new fields are as follows: -// - Must all be optional. -// - An unset value must not result in serialization overhead. This is -// necessary so the serialization of older AUMs stays the same. -// - New processing semantics of the new fields must be compatible with the -// behavior of old clients (which will ignore the field). -// - No floats! +// - Must all be optional. +// - An unset value must not result in serialization overhead. This is +// necessary so the serialization of older AUMs stays the same. +// - New processing semantics of the new fields must be compatible with the +// behavior of old clients (which will ignore the field). +// - No floats! type AUM struct { MessageKind AUMKind `cbor:"1,keyasint"` PrevAUMHash []byte `cbor:"2,keyasint"` diff --git a/tka/builder.go b/tka/builder.go index da062995b..7cbf7bb0c 100644 --- a/tka/builder.go +++ b/tka/builder.go @@ -8,6 +8,11 @@ import ( "fmt" ) +// Types implementing Signer can sign update messages. +type Signer interface { + SignAUM(*AUM) error +} + // UpdateBuilder implements a builder for changes to the tailnet // key authority. // @@ -15,7 +20,7 @@ import ( // must then be applied to all Authority objects using Inform(). type UpdateBuilder struct { a *Authority - signer func(*AUM) error + signer Signer state State parent AUMHash @@ -29,7 +34,7 @@ func (b *UpdateBuilder) mkUpdate(update AUM) error { update.PrevAUMHash = prevHash if b.signer != nil { - if err := b.signer(&update); err != nil { + if err := b.signer.SignAUM(&update); err != nil { return fmt.Errorf("signing failed: %v", err) } } @@ -101,7 +106,7 @@ func (b *UpdateBuilder) Finalize() ([]AUM, error) { // Updates are specified by calling methods on the returned UpdatedBuilder. // Call Finalize() when you are done to obtain the specific update messages // which actuate the changes. -func (a *Authority) NewUpdater(signer func(*AUM) error) *UpdateBuilder { +func (a *Authority) NewUpdater(signer Signer) *UpdateBuilder { return &UpdateBuilder{ a: a, signer: signer, diff --git a/tka/builder_test.go b/tka/builder_test.go index 5f7aeb66f..34cca18a4 100644 --- a/tka/builder_test.go +++ b/tka/builder_test.go @@ -5,11 +5,19 @@ package tka import ( + "crypto/ed25519" "testing" "github.com/google/go-cmp/cmp" ) +type signer25519 ed25519.PrivateKey + +func (s signer25519) SignAUM(update *AUM) error { + update.sign25519(ed25519.PrivateKey(s)) + return nil +} + func TestAuthorityBuilderAddKey(t *testing.T) { pub, priv := testingKey25519(t, 1) key := Key{Kind: Key25519, Public: pub, Votes: 2} @@ -17,7 +25,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) { a, _, err := Create(&Mem{}, State{ Keys: []Key{key}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, - }, priv) + }, signer25519(priv)) if err != nil { t.Fatalf("Create() failed: %v", err) } @@ -25,10 +33,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) { pub2, _ := testingKey25519(t, 2) key2 := Key{Kind: Key25519, Public: pub2, Votes: 1} - b := a.NewUpdater(func(update *AUM) error { - update.sign25519(priv) - return nil - }) + b := a.NewUpdater(signer25519(priv)) if err := b.AddKey(key2); err != nil { t.Fatalf("AddKey(%v) failed: %v", key2, err) } @@ -56,15 +61,12 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) { a, _, err := Create(&Mem{}, State{ Keys: []Key{key, key2}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, - }, priv) + }, signer25519(priv)) if err != nil { t.Fatalf("Create() failed: %v", err) } - b := a.NewUpdater(func(update *AUM) error { - update.sign25519(priv) - return nil - }) + b := a.NewUpdater(signer25519(priv)) if err := b.RemoveKey(key2.ID()); err != nil { t.Fatalf("RemoveKey(%v) failed: %v", key2, err) } @@ -90,15 +92,12 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) { a, _, err := Create(&Mem{}, State{ Keys: []Key{key}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, - }, priv) + }, signer25519(priv)) if err != nil { t.Fatalf("Create() failed: %v", err) } - b := a.NewUpdater(func(update *AUM) error { - update.sign25519(priv) - return nil - }) + b := a.NewUpdater(signer25519(priv)) if err := b.SetKeyVote(key.ID(), 5); err != nil { t.Fatalf("SetKeyVote(%v) failed: %v", key.ID(), err) } @@ -128,15 +127,12 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) { a, _, err := Create(&Mem{}, State{ Keys: []Key{key}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, - }, priv) + }, signer25519(priv)) if err != nil { t.Fatalf("Create() failed: %v", err) } - b := a.NewUpdater(func(update *AUM) error { - update.sign25519(priv) - return nil - }) + b := a.NewUpdater(signer25519(priv)) if err := b.SetKeyMeta(key.ID(), map[string]string{"b": "c"}); err != nil { t.Fatalf("SetKeyMeta(%v) failed: %v", key, err) } @@ -166,7 +162,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) { a, _, err := Create(&Mem{}, State{ Keys: []Key{key}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, - }, priv) + }, signer25519(priv)) if err != nil { t.Fatalf("Create() failed: %v", err) } @@ -174,10 +170,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) { pub2, _ := testingKey25519(t, 2) key2 := Key{Kind: Key25519, Public: pub2, Votes: 1} - b := a.NewUpdater(func(update *AUM) error { - update.sign25519(priv) - return nil - }) + b := a.NewUpdater(signer25519(priv)) if err := b.AddKey(key2); err != nil { t.Fatalf("AddKey(%v) failed: %v", key2, err) } diff --git a/tka/chaintest_test.go b/tka/chaintest_test.go index acbcc05ab..79cc2ca74 100644 --- a/tka/chaintest_test.go +++ b/tka/chaintest_test.go @@ -58,15 +58,15 @@ type testChain struct { // // Input is expected to be a graph & tweaks, looking like this: // -// G1 -> A -> B -// | -> C +// G1 -> A -> B +// | -> C // // which defines AUMs G1, A, B, and C; with G1 having no parent, A having // G1 as a parent, and both B & C having A as a parent. // // Tweaks are specified like this: // -// . = +// . = // // for example: G1.hashSeed = 2 // diff --git a/tka/key.go b/tka/key.go index be275ecd9..c4a6b49c4 100644 --- a/tka/key.go +++ b/tka/key.go @@ -53,9 +53,8 @@ type Key struct { // Clone makes an independent copy of Key. // -// NOTE: There is a difference between a nil slice and an empty -// slice for encoding purposes, so an implementation of Clone() -// must take care to preserve this. +// NOTE: There is a difference between a nil slice and an empty slice for encoding purposes, +// so an implementation of Clone() must take care to preserve this. func (k Key) Clone() Key { out := k diff --git a/tka/state.go b/tka/state.go index 52bb7eb8f..c1351a0e5 100644 --- a/tka/state.go +++ b/tka/state.go @@ -53,8 +53,8 @@ func (s State) GetKey(key KeyID) (Key, error) { // Clone makes an independent copy of State. // // NOTE: There is a difference between a nil slice and an empty -// slice for encoding purposes, so an implementation of Clone() -// must take care to preserve this. +// slice for encoding purposes, so an implementation of Clone() +// must take care to preserve this. func (s State) Clone() State { out := State{} @@ -117,9 +117,9 @@ func (s State) checkDisablement(secret []byte) bool { // to the current state. // // Specifically, the rules are: -// - The last AUM hash must match (transitively, this implies that this -// update follows the last update message applied to the state machine) -// - Or, the state machine knows no parent (its brand new). +// - The last AUM hash must match (transitively, this implies that this +// update follows the last update message applied to the state machine) +// - Or, the state machine knows no parent (its brand new). func (s State) parentMatches(update AUM) bool { if s.LastAUMHash == nil { return true diff --git a/tka/tka.go b/tka/tka.go index c384f6cdc..c876e17b1 100644 --- a/tka/tka.go +++ b/tka/tka.go @@ -7,7 +7,6 @@ package tka import ( "bytes" - "crypto/ed25519" "errors" "fmt" "os" @@ -99,9 +98,10 @@ func computeChainCandidates(storage Chonk, lastKnownOldest *AUMHash, maxIter int // AUM in the chain, possibly applying fork resolution logic. // // In other words: given an AUM with 3 children like this: -// / - 1 -// P - 2 -// \ - 3 +// +// / - 1 +// P - 2 +// \ - 3 // // pickNextAUM will determine and return the correct branch. // @@ -354,13 +354,13 @@ func computeActiveAncestor(storage Chonk, chains []chain) (AUMHash, error) { // the ancestor. // // The algorithm is as follows: -// 1. Determine all possible 'head' (like in git) states. -// 2. Filter these possible chains based on whether the ancestor was -// formerly (in a previous run) part of the chain. -// 3. Compute the state of the state machine at this ancestor. This is -// needed for fast-forward, as each update operates on the state of -// the update preceeding it. -// 4. Iteratively apply updates till we reach head ('fast forward'). +// 1. Determine all possible 'head' (like in git) states. +// 2. Filter these possible chains based on whether the ancestor was +// formerly (in a previous run) part of the chain. +// 3. Compute the state of the state machine at this ancestor. This is +// needed for fast-forward, as each update operates on the state of +// the update preceeding it. +// 4. Iteratively apply updates till we reach head ('fast forward'). func computeActiveChain(storage Chonk, lastKnownOldest *AUMHash, maxIter int) (chain, error) { chains, err := computeChainCandidates(storage, lastKnownOldest, maxIter) if err != nil { @@ -473,7 +473,7 @@ func Open(storage Chonk) (*Authority, error) { // // Do not use this to initialize a TKA that already exists, use Open() // or Bootstrap() instead. -func Create(storage Chonk, state State, signer ed25519.PrivateKey) (*Authority, AUM, error) { +func Create(storage Chonk, state State, signer Signer) (*Authority, AUM, error) { // Generate & sign a checkpoint, our genesis update. genesis := AUM{ MessageKind: AUMCheckpoint, @@ -483,7 +483,9 @@ func Create(storage Chonk, state State, signer ed25519.PrivateKey) (*Authority, // This serves as an easy way to validate the given state. return nil, AUM{}, fmt.Errorf("invalid state: %v", err) } - genesis.sign25519(signer) + if err := signer.SignAUM(&genesis); err != nil { + return nil, AUM{}, fmt.Errorf("signing failed: %v", err) + } a, err := Bootstrap(storage, genesis) return a, genesis, err diff --git a/tka/tka_test.go b/tka/tka_test.go index 413f1ac4c..92d12e74f 100644 --- a/tka/tka_test.go +++ b/tka/tka_test.go @@ -301,7 +301,7 @@ func TestCreateBootstrapAuthority(t *testing.T) { a1, genesisAUM, err := Create(&Mem{}, State{ Keys: []Key{key}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, - }, priv) + }, signer25519(priv)) if err != nil { t.Fatalf("Create() failed: %v", err) } diff --git a/types/key/nl.go b/types/key/nl.go new file mode 100644 index 000000000..4e3801e22 --- /dev/null +++ b/types/key/nl.go @@ -0,0 +1,74 @@ +// Copyright (c) 2022 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 ( + "crypto/ed25519" + + "go4.org/mem" + "tailscale.com/tka" + "tailscale.com/types/structs" +) + +const ( + // nlPrivateHexPrefix is the prefix used to identify a + // hex-encoded network-lock key. + nlPrivateHexPrefix = "nlpriv:" +) + +// NLPrivate is a node-managed network-lock key, used for signing +// node-key signatures and authority update messages. +type NLPrivate struct { + _ structs.Incomparable // because == isn't constant-time + k [ed25519.PrivateKeySize]byte +} + +// NewNLPrivate creates and returns a new network-lock key. +func NewNLPrivate() NLPrivate { + // ed25519.GenerateKey 'clamps' the key, not that it + // matters given we don't do Diffie-Hellman. + _, priv, err := ed25519.GenerateKey(nil) // nil == crypto/rand + if err != nil { + panic(err) + } + + var out NLPrivate + copy(out.k[:], priv) + return out +} + +// MarshalText implements encoding.TextUnmarshaler. +func (k *NLPrivate) UnmarshalText(b []byte) error { + return parseHex(k.k[:], mem.B(b), mem.S(nlPrivateHexPrefix)) +} + +// MarshalText implements encoding.TextMarshaler. +func (k NLPrivate) MarshalText() ([]byte, error) { + return toHex(k.k[:], nlPrivateHexPrefix), nil +} + +// Public returns the public component of this key. +func (k NLPrivate) Public() ed25519.PublicKey { + return ed25519.PrivateKey(k.k[:]).Public().(ed25519.PublicKey) +} + +// KeyID returns an identifier for this key. +func (k NLPrivate) KeyID() tka.KeyID { + return tka.Key{ + Kind: tka.Key25519, + Public: k.Public(), + }.ID() +} + +// SignAUM implements tka.UpdateSigner. +func (k NLPrivate) SignAUM(a *tka.AUM) error { + sigHash := a.SigHash() + + a.Signatures = append(a.Signatures, tka.Signature{ + KeyID: k.KeyID(), + Signature: ed25519.Sign(k.k[:], sigHash[:]), + }) + return nil +} diff --git a/types/key/nl_test.go b/types/key/nl_test.go new file mode 100644 index 000000000..fe0f71191 --- /dev/null +++ b/types/key/nl_test.go @@ -0,0 +1,47 @@ +// Copyright (c) 2022 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 ( + "bytes" + "testing" + + "tailscale.com/tka" +) + +func TestNLPrivate(t *testing.T) { + p := NewNLPrivate() + + encoded, err := p.MarshalText() + if err != nil { + t.Fatal(err) + } + var decoded NLPrivate + if err := decoded.UnmarshalText(encoded); err != nil { + t.Fatal(err) + } + if !bytes.Equal(decoded.k[:], p.k[:]) { + t.Error("decoded and generated NLPrivate bytes differ") + } + + // Test that NLPrivate implements tka.Signer by making a new + // authority. + k := tka.Key{Kind: tka.Key25519, Public: p.Public(), Votes: 1} + _, aum, err := tka.Create(&tka.Mem{}, tka.State{ + Keys: []tka.Key{k}, + DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)}, + }, p) + if err != nil { + t.Fatalf("tka.Create() failed: %v", err) + } + + // Make sure the generated genesis AUM was signed. + if got, want := len(aum.Signatures), 1; got != want { + t.Fatalf("len(signatures) = %d, want %d", got, want) + } + if err := aum.Signatures[0].Verify(aum.SigHash(), k); err != nil { + t.Errorf("signature did not verify: %v", err) + } +} diff --git a/types/key/util.go b/types/key/util.go index 987fa28c5..ffb8da083 100644 --- a/types/key/util.go +++ b/types/key/util.go @@ -42,9 +42,9 @@ func rand(b []byte) { // existing uses and whether you should clamp private keys at // creation. // -// - NaCl box: yes, clamp at creation. -// - WireGuard (userspace uapi or kernel): no, do not clamp. -// - Noise protocols: no, do not clamp. +// - NaCl box: yes, clamp at creation. +// - WireGuard (userspace uapi or kernel): no, do not clamp. +// - Noise protocols: no, do not clamp. func clamp25519Private(b []byte) { b[0] &= 248 b[31] = (b[31] & 127) | 64