From 4651827f2031364dde485a6ba0267690ab6f5461 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Thu, 27 Jun 2024 12:02:15 +0100 Subject: [PATCH] tka: test SigCredential signatures and netmap filtering This change moves handling of wrapped auth keys to the `tka` package and adds a test covering auth key originating signatures (SigCredential) in netmap. Updates tailscale/corp#19764 Signed-off-by: Anton Tolchanov --- control/controlclient/direct.go | 53 ++--------------------- control/controlclient/direct_test.go | 40 ------------------ ipn/ipnlocal/network-lock_test.go | 32 ++++++++++++-- tka/sig.go | 63 ++++++++++++++++++++++++++++ tka/sig_test.go | 39 +++++++++++++++++ 5 files changed, 133 insertions(+), 94 deletions(-) diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 28d952210..b1d991ce6 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -7,8 +7,6 @@ "bufio" "bytes" "context" - "crypto/ed25519" - "encoding/base64" "encoding/binary" "encoding/json" "errors" @@ -491,7 +489,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new tryingNewKey := c.tryingNewKey serverKey := c.serverLegacyKey serverNoiseKey := c.serverNoiseKey - authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf) + authKey, isWrapped, wrappedSig, wrappedKey := tka.DecodeWrappedAuthkey(c.authKey, c.logf) hi := c.hostInfoLocked() backendLogID := hi.BackendLogID expired := !c.expiry.IsZero() && c.expiry.Before(c.clock.Now()) @@ -588,18 +586,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new // We were given a wrapped pre-auth key, which means that in addition // to being a regular pre-auth key there was a suffix with information to // generate a tailnet-lock signature. - nk, err := tryingNewKey.Public().MarshalBinary() + nodeKeySignature, err = tka.SignByCredential(wrappedKey, wrappedSig, tryingNewKey.Public()) if err != nil { - return false, "", nil, fmt.Errorf("marshalling node-key: %w", err) + return false, "", nil, err } - sig := &tka.NodeKeySignature{ - SigKind: tka.SigRotation, - Pubkey: nk, - Nested: wrappedSig, - } - sigHash := sig.SigHash() - sig.Signature = ed25519.Sign(wrappedKey, sigHash[:]) - nodeKeySignature = sig.Serialize() } if backendLogID == "" { @@ -1644,43 +1634,6 @@ func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyStat res.Body.Close() } -// decodeWrappedAuthkey separates wrapping information from an authkey, if any. -// In all cases the authkey is returned, sans wrapping information if any. -// -// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature -// and private key. -func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapped bool, sig *tka.NodeKeySignature, priv ed25519.PrivateKey) { - authKey, suffix, found := strings.Cut(key, "--TL") - if !found { - return key, false, nil, nil - } - sigBytes, privBytes, found := strings.Cut(suffix, "-") - if !found { - logf("decoding wrapped auth-key: did not find delimiter") - return key, false, nil, nil - } - - rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes) - if err != nil { - logf("decoding wrapped auth-key: signature decode: %v", err) - return key, false, nil, nil - } - rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes) - if err != nil { - logf("decoding wrapped auth-key: priv decode: %v", err) - return key, false, nil, nil - } - - sig = new(tka.NodeKeySignature) - if err := sig.Unserialize([]byte(rawSig)); err != nil { - logf("decoding wrapped auth-key: signature: %v", err) - return key, false, nil, nil - } - priv = ed25519.PrivateKey(rawPriv) - - return authKey, true, sig, priv -} - func addLBHeader(req *http.Request, nodeKey key.NodePublic) { if !nodeKey.IsZero() { req.Header.Add(tailcfg.LBHeader, nodeKey.String()) diff --git a/control/controlclient/direct_test.go b/control/controlclient/direct_test.go index 48f9617db..e2a6f9fa4 100644 --- a/control/controlclient/direct_test.go +++ b/control/controlclient/direct_test.go @@ -4,7 +4,6 @@ package controlclient import ( - "crypto/ed25519" "encoding/json" "net/http" "net/http/httptest" @@ -147,42 +146,3 @@ func TestTsmpPing(t *testing.T) { t.Fatal(err) } } - -func TestDecodeWrappedAuthkey(t *testing.T) { - k, isWrapped, sig, priv := decodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil) - if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want { - t.Errorf("decodeWrappedAuthkey().key = %q, want %q", k, want) - } - if isWrapped { - t.Error("decodeWrappedAuthkey().isWrapped = true, want false") - } - if sig != nil { - t.Errorf("decodeWrappedAuthkey().sig = %v, want nil", sig) - } - if priv != nil { - t.Errorf("decodeWrappedAuthkey().priv = %v, want nil", priv) - } - - k, isWrapped, sig, priv = decodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil) - if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want { - t.Errorf("decodeWrappedAuthkey().key = %q, want %q", k, want) - } - if !isWrapped { - t.Error("decodeWrappedAuthkey().isWrapped = false, want true") - } - - if sig == nil { - t.Fatal("decodeWrappedAuthkey().sig = nil, want non-nil signature") - } - sigHash := sig.SigHash() - if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) { - t.Error("signature failed to verify") - } - - // Make sure the private is correct by using it. - someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4}) - if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) { - t.Error("failed to use priv") - } - -} diff --git a/ipn/ipnlocal/network-lock_test.go b/ipn/ipnlocal/network-lock_test.go index 38a7327bb..16e713c7e 100644 --- a/ipn/ipnlocal/network-lock_test.go +++ b/ipn/ipnlocal/network-lock_test.go @@ -556,6 +556,11 @@ func TestTKAFilterNetmap(t *testing.T) { t.Fatalf("tka.Create() failed: %v", err) } + b := &LocalBackend{ + logf: t.Logf, + tka: &tkaState{authority: authority}, + } + n1, n2, n3, n4, n5 := key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode() n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv) if err != nil { @@ -585,6 +590,27 @@ func TestTKAFilterNetmap(t *testing.T) { n5Rotated, n5RotatedSig := resign(n5nl, n5InitialSig.Serialize()) + nodeFromAuthKey := func(authKey string) (key.NodePrivate, tkatype.MarshaledSignature) { + _, isWrapped, sig, priv := tka.DecodeWrappedAuthkey(authKey, t.Logf) + if !isWrapped { + t.Errorf("expected wrapped key") + } + + node := key.NewNode() + nodeSig, err := tka.SignByCredential(priv, sig, node.Public()) + if err != nil { + t.Error(err) + } + return node, nodeSig + } + + preauth, err := b.NetworkLockWrapPreauthKey("tskey-auth-k7UagY1CNTRL-ZZZZZ", nlPriv) + if err != nil { + t.Fatal(err) + } + + n6, n6Sig := nodeFromAuthKey(preauth) + nm := &netmap.NetworkMap{ Peers: nodeViews([]*tailcfg.Node{ {ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()}, @@ -593,18 +619,16 @@ func TestTKAFilterNetmap(t *testing.T) { {ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature {ID: 50, Key: n5.Public(), KeySignature: n5InitialSig.Serialize()}, // rotated {ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig}, + {ID: 6, Key: n6.Public(), KeySignature: n6Sig}, }), } - b := &LocalBackend{ - logf: t.Logf, - tka: &tkaState{authority: authority}, - } b.tkaFilterNetmapLocked(nm) want := nodeViews([]*tailcfg.Node{ {ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()}, {ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig}, + {ID: 6, Key: n6.Public(), KeySignature: n6Sig}, }) nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool { return x.Raw32() == y.Raw32() diff --git a/tka/sig.go b/tka/sig.go index 34f2ed167..4fdeb6a02 100644 --- a/tka/sig.go +++ b/tka/sig.go @@ -6,6 +6,7 @@ import ( "bytes" "crypto/ed25519" + "encoding/base64" "errors" "fmt" "strings" @@ -14,6 +15,7 @@ "github.com/hdevalence/ed25519consensus" "golang.org/x/crypto/blake2s" "tailscale.com/types/key" + "tailscale.com/types/logger" "tailscale.com/types/tkatype" ) @@ -379,3 +381,64 @@ func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.Marsha return newSig.Serialize(), nil } + +// SignByCredential signs a node public key by a private key which has its +// signing authority delegated by a SigCredential signature. This is used by +// wrapped auth keys. +func SignByCredential(privKey []byte, wrapped *NodeKeySignature, nodeKey key.NodePublic) (tkatype.MarshaledSignature, error) { + if wrapped.SigKind != SigCredential { + return nil, fmt.Errorf("wrapped signature must be a credential, got %v", wrapped.SigKind) + } + + nk, err := nodeKey.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("marshalling node-key: %w", err) + } + + sig := &NodeKeySignature{ + SigKind: SigRotation, + Pubkey: nk, + Nested: wrapped, + } + sigHash := sig.SigHash() + sig.Signature = ed25519.Sign(privKey, sigHash[:]) + return sig.Serialize(), nil +} + +// DecodeWrappedAuthkey separates wrapping information from an authkey, if any. +// In all cases the authkey is returned, sans wrapping information if any. +// +// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature +// and private key. +func DecodeWrappedAuthkey(wrappedAuthKey string, logf logger.Logf) (authKey string, isWrapped bool, sig *NodeKeySignature, priv ed25519.PrivateKey) { + authKey, suffix, found := strings.Cut(wrappedAuthKey, "--TL") + if !found { + return wrappedAuthKey, false, nil, nil + } + sigBytes, privBytes, found := strings.Cut(suffix, "-") + if !found { + // TODO: propagate these errors to `tailscale up` output? + logf("decoding wrapped auth-key: did not find delimiter") + return wrappedAuthKey, false, nil, nil + } + + rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes) + if err != nil { + logf("decoding wrapped auth-key: signature decode: %v", err) + return wrappedAuthKey, false, nil, nil + } + rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes) + if err != nil { + logf("decoding wrapped auth-key: priv decode: %v", err) + return wrappedAuthKey, false, nil, nil + } + + sig = new(NodeKeySignature) + if err := sig.Unserialize(rawSig); err != nil { + logf("decoding wrapped auth-key: signature: %v", err) + return wrappedAuthKey, false, nil, nil + } + priv = ed25519.PrivateKey(rawPriv) + + return authKey, true, sig, priv +} diff --git a/tka/sig_test.go b/tka/sig_test.go index 305eb64eb..42a6fd7f7 100644 --- a/tka/sig_test.go +++ b/tka/sig_test.go @@ -439,3 +439,42 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) { }) } } + +func TestDecodeWrappedAuthkey(t *testing.T) { + k, isWrapped, sig, priv := DecodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil) + if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want { + t.Errorf("decodeWrappedAuthkey().key = %q, want %q", k, want) + } + if isWrapped { + t.Error("decodeWrappedAuthkey().isWrapped = true, want false") + } + if sig != nil { + t.Errorf("decodeWrappedAuthkey().sig = %v, want nil", sig) + } + if priv != nil { + t.Errorf("decodeWrappedAuthkey().priv = %v, want nil", priv) + } + + k, isWrapped, sig, priv = DecodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil) + if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want { + t.Errorf("decodeWrappedAuthkey().key = %q, want %q", k, want) + } + if !isWrapped { + t.Error("decodeWrappedAuthkey().isWrapped = false, want true") + } + + if sig == nil { + t.Fatal("decodeWrappedAuthkey().sig = nil, want non-nil signature") + } + sigHash := sig.SigHash() + if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) { + t.Error("signature failed to verify") + } + + // Make sure the private is correct by using it. + someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4}) + if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) { + t.Error("failed to use priv") + } + +}