From 32120932a5ff2673a49e7e1365791b13c93e72ef Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Tue, 28 May 2024 16:56:05 +0100 Subject: [PATCH] cmd/tailscale/cli: print node signature in `tailscale lock status` - Add current node signature to `ipnstate.NetworkLockStatus`; - Print current node signature in a human-friendly format as part of `tailscale lock status`. Examples: ``` $ tailscale lock status Tailnet lock is ENABLED. This node is accessible under tailnet lock. Node signature: SigKind: direct Pubkey: [OTB3a] KeyID: tlpub:44a0e23cd53a4b8acc02f6732813d8f5ba8b35d02d48bf94c9f1724ebe31c943 WrappingPubkey: tlpub:44a0e23cd53a4b8acc02f6732813d8f5ba8b35d02d48bf94c9f1724ebe31c943 This node's tailnet-lock key: tlpub:44a0e23cd53a4b8acc02f6732813d8f5ba8b35d02d48bf94c9f1724ebe31c943 Trusted signing keys: tlpub:44a0e23cd53a4b8acc02f6732813d8f5ba8b35d02d48bf94c9f1724ebe31c943 1 (self) tlpub:6fa21d242a202b290de85926ba3893a6861888679a73bc3a43f49539d67c9764 1 (pre-auth key kq3NzejWoS11KTM59) ``` For a node created via a signed auth key: ``` This node is accessible under tailnet lock. Node signature: SigKind: rotation Pubkey: [e3nAO] Nested: SigKind: credential KeyID: tlpub:6fa21d242a202b290de85926ba3893a6861888679a73bc3a43f49539d67c9764 WrappingPubkey: tlpub:3623b0412cab0029cb1918806435709b5947ae03554050f20caf66629f21220a ``` For a node that rotated its key a few times: ``` This node is accessible under tailnet lock. Node signature: SigKind: rotation Pubkey: [DOzL4] Nested: SigKind: rotation Pubkey: [S/9yU] Nested: SigKind: rotation Pubkey: [9E9v4] Nested: SigKind: direct Pubkey: [3QHTJ] KeyID: tlpub:44a0e23cd53a4b8acc02f6732813d8f5ba8b35d02d48bf94c9f1724ebe31c943 WrappingPubkey: tlpub:2faa280025d3aba0884615f710d8c50590b052c01a004c2b4c2c9434702ae9d0 ``` Updates tailscale/corp#19764 Signed-off-by: Anton Tolchanov --- cmd/tailscale/cli/network-lock.go | 3 ++- ipn/ipnlocal/network-lock.go | 21 +++++++++++------- ipn/ipnstate/ipnstate.go | 4 ++++ tka/sig.go | 36 +++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/cmd/tailscale/cli/network-lock.go b/cmd/tailscale/cli/network-lock.go index 0aa73fc1e..6d9dd35f1 100644 --- a/cmd/tailscale/cli/network-lock.go +++ b/cmd/tailscale/cli/network-lock.go @@ -222,7 +222,8 @@ func runNetworkLockStatus(ctx context.Context, args []string) error { if st.Enabled && st.NodeKey != nil && !st.PublicKey.IsZero() { if st.NodeKeySigned { - fmt.Println("This node is accessible under tailnet lock.") + fmt.Println("This node is accessible under tailnet lock. Node signature:") + fmt.Println(st.NodeKeySignature.String()) } else { fmt.Println("This node is LOCKED OUT by tailnet-lock, and action is required to establish connectivity.") fmt.Printf("Run the following command on a node with a trusted key:\n\ttailscale lock sign %v %s\n", st.NodeKey, st.PublicKey.CLIString()) diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index 015177c5b..a07ca6faf 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -423,8 +423,12 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { copy(head[:], h[:]) var selfAuthorized bool + nodeKeySignature := &tka.NodeKeySignature{} if b.netMap != nil { selfAuthorized = b.tka.authority.NodeKeyAuthorized(b.netMap.SelfNode.Key(), b.netMap.SelfNode.KeySignature().AsSlice()) == nil + if err := nodeKeySignature.Unserialize(b.netMap.SelfNode.KeySignature().AsSlice()); err != nil { + b.logf("failed to decode self node key signature: %v", err) + } } keys := b.tka.authority.Keys() @@ -445,14 +449,15 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { stateID1, _ := b.tka.authority.StateIDs() return &ipnstate.NetworkLockStatus{ - Enabled: true, - Head: &head, - PublicKey: nlPriv.Public(), - NodeKey: nodeKey, - NodeKeySigned: selfAuthorized, - TrustedKeys: outKeys, - FilteredPeers: filtered, - StateID: stateID1, + Enabled: true, + Head: &head, + PublicKey: nlPriv.Public(), + NodeKey: nodeKey, + NodeKeySigned: selfAuthorized, + NodeKeySignature: nodeKeySignature, + TrustedKeys: outKeys, + FilteredPeers: filtered, + StateID: stateID1, } } diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 869c4b8c6..b38d75e5a 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -18,6 +18,7 @@ import ( "time" "tailscale.com/tailcfg" + "tailscale.com/tka" "tailscale.com/types/key" "tailscale.com/types/ptr" "tailscale.com/types/views" @@ -126,6 +127,9 @@ type NetworkLockStatus struct { // NodeKeySigned is true if our node is authorized by network-lock. NodeKeySigned bool + // NodeKeySignature is the current signature of this node's key. + NodeKeySignature *tka.NodeKeySignature + // TrustedKeys describes the keys currently trusted to make changes // to network-lock. TrustedKeys []TKAKey diff --git a/tka/sig.go b/tka/sig.go index 212f5431e..189645394 100644 --- a/tka/sig.go +++ b/tka/sig.go @@ -8,6 +8,7 @@ import ( "crypto/ed25519" "errors" "fmt" + "strings" "github.com/fxamacker/cbor/v2" "github.com/hdevalence/ed25519consensus" @@ -96,6 +97,41 @@ type NodeKeySignature struct { WrappingPubkey []byte `cbor:"6,keyasint,omitempty"` } +// String returns a human-readable representation of the NodeKeySignature, +// making it easy to see nested signatures. +func (s NodeKeySignature) String() string { + var b strings.Builder + var addToBuf func(NodeKeySignature, int) + addToBuf = func(sig NodeKeySignature, depth int) { + indent := strings.Repeat(" ", depth) + b.WriteString(indent + "SigKind: " + sig.SigKind.String() + "\n") + if len(sig.Pubkey) > 0 { + var pubKey string + var np key.NodePublic + if err := np.UnmarshalBinary(sig.Pubkey); err != nil { + pubKey = fmt.Sprintf("", err) + } else { + pubKey = np.ShortString() + } + b.WriteString(indent + "Pubkey: " + pubKey + "\n") + } + if len(sig.KeyID) > 0 { + keyID := key.NLPublicFromEd25519Unsafe(sig.KeyID).CLIString() + b.WriteString(indent + "KeyID: " + keyID + "\n") + } + if len(sig.WrappingPubkey) > 0 { + pubKey := key.NLPublicFromEd25519Unsafe(sig.WrappingPubkey).CLIString() + b.WriteString(indent + "WrappingPubkey: " + pubKey + "\n") + } + if sig.Nested != nil { + b.WriteString(indent + "Nested:\n") + addToBuf(*sig.Nested, depth+1) + } + } + addToBuf(s, 0) + return strings.TrimSpace(b.String()) +} + // UnverifiedWrappingPublic returns the public key which must sign a // signature which embeds this one, if any. //