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 <anton@tailscale.com>
This commit is contained in:
Anton Tolchanov 2024-05-28 16:56:05 +01:00 committed by Anton Tolchanov
parent 776a05223b
commit 32120932a5
4 changed files with 55 additions and 9 deletions

View File

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

View File

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

View File

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

View File

@ -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("<error: %s>", 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.
//