mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
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 <anton@tailscale.com>
This commit is contained in:
parent
8f7588900a
commit
4651827f20
@ -7,8 +7,6 @@
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@ -491,7 +489,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
tryingNewKey := c.tryingNewKey
|
tryingNewKey := c.tryingNewKey
|
||||||
serverKey := c.serverLegacyKey
|
serverKey := c.serverLegacyKey
|
||||||
serverNoiseKey := c.serverNoiseKey
|
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()
|
hi := c.hostInfoLocked()
|
||||||
backendLogID := hi.BackendLogID
|
backendLogID := hi.BackendLogID
|
||||||
expired := !c.expiry.IsZero() && c.expiry.Before(c.clock.Now())
|
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
|
// 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
|
// to being a regular pre-auth key there was a suffix with information to
|
||||||
// generate a tailnet-lock signature.
|
// generate a tailnet-lock signature.
|
||||||
nk, err := tryingNewKey.Public().MarshalBinary()
|
nodeKeySignature, err = tka.SignByCredential(wrappedKey, wrappedSig, tryingNewKey.Public())
|
||||||
if err != nil {
|
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 == "" {
|
if backendLogID == "" {
|
||||||
@ -1644,43 +1634,6 @@ func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyStat
|
|||||||
res.Body.Close()
|
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) {
|
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
||||||
if !nodeKey.IsZero() {
|
if !nodeKey.IsZero() {
|
||||||
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package controlclient
|
package controlclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@ -147,42 +146,3 @@ func TestTsmpPing(t *testing.T) {
|
|||||||
t.Fatal(err)
|
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(<unwrapped-key>).key = %q, want %q", k, want)
|
|
||||||
}
|
|
||||||
if isWrapped {
|
|
||||||
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
|
|
||||||
}
|
|
||||||
if sig != nil {
|
|
||||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
|
|
||||||
}
|
|
||||||
if priv != nil {
|
|
||||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).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(<wrapped-key>).key = %q, want %q", k, want)
|
|
||||||
}
|
|
||||||
if !isWrapped {
|
|
||||||
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
|
|
||||||
}
|
|
||||||
|
|
||||||
if sig == nil {
|
|
||||||
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -556,6 +556,11 @@ func TestTKAFilterNetmap(t *testing.T) {
|
|||||||
t.Fatalf("tka.Create() failed: %v", err)
|
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()
|
n1, n2, n3, n4, n5 := key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode()
|
||||||
n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv)
|
n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -585,6 +590,27 @@ func TestTKAFilterNetmap(t *testing.T) {
|
|||||||
|
|
||||||
n5Rotated, n5RotatedSig := resign(n5nl, n5InitialSig.Serialize())
|
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{
|
nm := &netmap.NetworkMap{
|
||||||
Peers: nodeViews([]*tailcfg.Node{
|
Peers: nodeViews([]*tailcfg.Node{
|
||||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
{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: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
|
||||||
{ID: 50, Key: n5.Public(), KeySignature: n5InitialSig.Serialize()}, // rotated
|
{ID: 50, Key: n5.Public(), KeySignature: n5InitialSig.Serialize()}, // rotated
|
||||||
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
|
{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)
|
b.tkaFilterNetmapLocked(nm)
|
||||||
|
|
||||||
want := nodeViews([]*tailcfg.Node{
|
want := nodeViews([]*tailcfg.Node{
|
||||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||||
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
|
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
|
||||||
|
{ID: 6, Key: n6.Public(), KeySignature: n6Sig},
|
||||||
})
|
})
|
||||||
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
|
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
|
||||||
return x.Raw32() == y.Raw32()
|
return x.Raw32() == y.Raw32()
|
||||||
|
63
tka/sig.go
63
tka/sig.go
@ -6,6 +6,7 @@
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -14,6 +15,7 @@
|
|||||||
"github.com/hdevalence/ed25519consensus"
|
"github.com/hdevalence/ed25519consensus"
|
||||||
"golang.org/x/crypto/blake2s"
|
"golang.org/x/crypto/blake2s"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/tkatype"
|
"tailscale.com/types/tkatype"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -379,3 +381,64 @@ func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.Marsha
|
|||||||
|
|
||||||
return newSig.Serialize(), nil
|
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
|
||||||
|
}
|
||||||
|
@ -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(<unwrapped-key>).key = %q, want %q", k, want)
|
||||||
|
}
|
||||||
|
if isWrapped {
|
||||||
|
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
|
||||||
|
}
|
||||||
|
if sig != nil {
|
||||||
|
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
|
||||||
|
}
|
||||||
|
if priv != nil {
|
||||||
|
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).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(<wrapped-key>).key = %q, want %q", k, want)
|
||||||
|
}
|
||||||
|
if !isWrapped {
|
||||||
|
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sig == nil {
|
||||||
|
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user