tka,types/key: implement NLPrivate glue for tailnet key authority keys

Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
Tom DNetto 2022-07-29 11:03:23 -07:00 committed by Tom
parent 7a74466998
commit 023d4e2216
11 changed files with 188 additions and 68 deletions

View File

@ -8,6 +8,11 @@
"fmt" "fmt"
) )
// Types implementing Signer can sign update messages.
type Signer interface {
SignAUM(*AUM) error
}
// UpdateBuilder implements a builder for changes to the tailnet // UpdateBuilder implements a builder for changes to the tailnet
// key authority. // key authority.
// //
@ -15,7 +20,7 @@
// must then be applied to all Authority objects using Inform(). // must then be applied to all Authority objects using Inform().
type UpdateBuilder struct { type UpdateBuilder struct {
a *Authority a *Authority
signer func(*AUM) error signer Signer
state State state State
parent AUMHash parent AUMHash
@ -29,7 +34,7 @@ func (b *UpdateBuilder) mkUpdate(update AUM) error {
update.PrevAUMHash = prevHash update.PrevAUMHash = prevHash
if b.signer != nil { 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) 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. // Updates are specified by calling methods on the returned UpdatedBuilder.
// Call Finalize() when you are done to obtain the specific update messages // Call Finalize() when you are done to obtain the specific update messages
// which actuate the changes. // which actuate the changes.
func (a *Authority) NewUpdater(signer func(*AUM) error) *UpdateBuilder { func (a *Authority) NewUpdater(signer Signer) *UpdateBuilder {
return &UpdateBuilder{ return &UpdateBuilder{
a: a, a: a,
signer: signer, signer: signer,

View File

@ -5,11 +5,19 @@
package tka package tka
import ( import (
"crypto/ed25519"
"testing" "testing"
"github.com/google/go-cmp/cmp" "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) { func TestAuthorityBuilderAddKey(t *testing.T) {
pub, priv := testingKey25519(t, 1) pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2} key := Key{Kind: Key25519, Public: pub, Votes: 2}
@ -17,7 +25,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
a, _, err := Create(&Mem{}, State{ a, _, err := Create(&Mem{}, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, priv) }, signer25519(priv))
if err != nil { if err != nil {
t.Fatalf("Create() failed: %v", err) t.Fatalf("Create() failed: %v", err)
} }
@ -25,10 +33,7 @@ func TestAuthorityBuilderAddKey(t *testing.T) {
pub2, _ := testingKey25519(t, 2) pub2, _ := testingKey25519(t, 2)
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1} key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
b := a.NewUpdater(func(update *AUM) error { b := a.NewUpdater(signer25519(priv))
update.sign25519(priv)
return nil
})
if err := b.AddKey(key2); err != nil { if err := b.AddKey(key2); err != nil {
t.Fatalf("AddKey(%v) failed: %v", key2, err) t.Fatalf("AddKey(%v) failed: %v", key2, err)
} }
@ -56,15 +61,12 @@ func TestAuthorityBuilderRemoveKey(t *testing.T) {
a, _, err := Create(&Mem{}, State{ a, _, err := Create(&Mem{}, State{
Keys: []Key{key, key2}, Keys: []Key{key, key2},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, priv) }, signer25519(priv))
if err != nil { if err != nil {
t.Fatalf("Create() failed: %v", err) t.Fatalf("Create() failed: %v", err)
} }
b := a.NewUpdater(func(update *AUM) error { b := a.NewUpdater(signer25519(priv))
update.sign25519(priv)
return nil
})
if err := b.RemoveKey(key2.ID()); err != nil { if err := b.RemoveKey(key2.ID()); err != nil {
t.Fatalf("RemoveKey(%v) failed: %v", key2, err) t.Fatalf("RemoveKey(%v) failed: %v", key2, err)
} }
@ -90,15 +92,12 @@ func TestAuthorityBuilderSetKeyVote(t *testing.T) {
a, _, err := Create(&Mem{}, State{ a, _, err := Create(&Mem{}, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, priv) }, signer25519(priv))
if err != nil { if err != nil {
t.Fatalf("Create() failed: %v", err) t.Fatalf("Create() failed: %v", err)
} }
b := a.NewUpdater(func(update *AUM) error { b := a.NewUpdater(signer25519(priv))
update.sign25519(priv)
return nil
})
if err := b.SetKeyVote(key.ID(), 5); err != nil { if err := b.SetKeyVote(key.ID(), 5); err != nil {
t.Fatalf("SetKeyVote(%v) failed: %v", key.ID(), err) t.Fatalf("SetKeyVote(%v) failed: %v", key.ID(), err)
} }
@ -128,15 +127,12 @@ func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
a, _, err := Create(&Mem{}, State{ a, _, err := Create(&Mem{}, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, priv) }, signer25519(priv))
if err != nil { if err != nil {
t.Fatalf("Create() failed: %v", err) t.Fatalf("Create() failed: %v", err)
} }
b := a.NewUpdater(func(update *AUM) error { b := a.NewUpdater(signer25519(priv))
update.sign25519(priv)
return nil
})
if err := b.SetKeyMeta(key.ID(), map[string]string{"b": "c"}); err != nil { if err := b.SetKeyMeta(key.ID(), map[string]string{"b": "c"}); err != nil {
t.Fatalf("SetKeyMeta(%v) failed: %v", key, err) t.Fatalf("SetKeyMeta(%v) failed: %v", key, err)
} }
@ -166,7 +162,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
a, _, err := Create(&Mem{}, State{ a, _, err := Create(&Mem{}, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, priv) }, signer25519(priv))
if err != nil { if err != nil {
t.Fatalf("Create() failed: %v", err) t.Fatalf("Create() failed: %v", err)
} }
@ -174,10 +170,7 @@ func TestAuthorityBuilderMultiple(t *testing.T) {
pub2, _ := testingKey25519(t, 2) pub2, _ := testingKey25519(t, 2)
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1} key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
b := a.NewUpdater(func(update *AUM) error { b := a.NewUpdater(signer25519(priv))
update.sign25519(priv)
return nil
})
if err := b.AddKey(key2); err != nil { if err := b.AddKey(key2); err != nil {
t.Fatalf("AddKey(%v) failed: %v", key2, err) t.Fatalf("AddKey(%v) failed: %v", key2, err)
} }

View File

@ -53,9 +53,8 @@ type Key struct {
// Clone makes an independent copy of Key. // Clone makes an independent copy of Key.
// //
// NOTE: There is a difference between a nil slice and an empty // NOTE: There is a difference between a nil slice and an empty slice for encoding purposes,
// slice for encoding purposes, so an implementation of Clone() // so an implementation of Clone() must take care to preserve this.
// must take care to preserve this.
func (k Key) Clone() Key { func (k Key) Clone() Key {
out := k out := k

View File

@ -7,7 +7,6 @@
import ( import (
"bytes" "bytes"
"crypto/ed25519"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -99,6 +98,7 @@ func computeChainCandidates(storage Chonk, lastKnownOldest *AUMHash, maxIter int
// AUM in the chain, possibly applying fork resolution logic. // AUM in the chain, possibly applying fork resolution logic.
// //
// In other words: given an AUM with 3 children like this: // In other words: given an AUM with 3 children like this:
//
// / - 1 // / - 1
// P - 2 // P - 2
// \ - 3 // \ - 3
@ -473,7 +473,7 @@ func Open(storage Chonk) (*Authority, error) {
// //
// Do not use this to initialize a TKA that already exists, use Open() // Do not use this to initialize a TKA that already exists, use Open()
// or Bootstrap() instead. // 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. // Generate & sign a checkpoint, our genesis update.
genesis := AUM{ genesis := AUM{
MessageKind: AUMCheckpoint, 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. // This serves as an easy way to validate the given state.
return nil, AUM{}, fmt.Errorf("invalid state: %v", err) 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) a, err := Bootstrap(storage, genesis)
return a, genesis, err return a, genesis, err

View File

@ -301,7 +301,7 @@ func TestCreateBootstrapAuthority(t *testing.T) {
a1, genesisAUM, err := Create(&Mem{}, State{ a1, genesisAUM, err := Create(&Mem{}, State{
Keys: []Key{key}, Keys: []Key{key},
DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})}, DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
}, priv) }, signer25519(priv))
if err != nil { if err != nil {
t.Fatalf("Create() failed: %v", err) t.Fatalf("Create() failed: %v", err)
} }

74
types/key/nl.go Normal file
View File

@ -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
}

47
types/key/nl_test.go Normal file
View File

@ -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)
}
}