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. //