diff --git a/cmd/tailscale/cli/network-lock.go b/cmd/tailscale/cli/network-lock.go index 8d2d5800e..284594abf 100644 --- a/cmd/tailscale/cli/network-lock.go +++ b/cmd/tailscale/cli/network-lock.go @@ -99,7 +99,7 @@ func runNetworkLockInit(ctx context.Context, args []string) error { fmt.Println("You are initializing tailnet lock with trust in the following keys:") for _, k := range keys { - fmt.Printf(" - %x (%s key)\n", k.Public, k.Kind.String()) + fmt.Printf(" - tlpub:%x (%s key)\n", k.Public, k.Kind.String()) } fmt.Println() @@ -172,37 +172,23 @@ func runNetworkLockStatus(ctx context.Context, args []string) error { if st.NodeKeySigned { fmt.Println("This node is accessible under tailnet-lock.") } else { - p, err := st.PublicKey.MarshalText() - if err != nil { - return err - } - 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, p) + 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()) } fmt.Println() } if !st.PublicKey.IsZero() { - p, err := st.PublicKey.MarshalText() - if err != nil { - return err - } - fmt.Printf("This node's tailnet-lock key: %s\n", p) + fmt.Printf("This node's tailnet-lock key: %s\n", st.PublicKey.CLIString()) fmt.Println() } if st.Enabled && len(st.TrustedKeys) > 0 { fmt.Println("Keys trusted to make changes to tailnet-lock:") for _, k := range st.TrustedKeys { - key, err := k.Key.MarshalText() - if err != nil { - return err - } - var line strings.Builder line.WriteString("\t") - line.WriteString(string(key)) + line.WriteString(k.Key.CLIString()) line.WriteString("\t") line.WriteString(fmt.Sprint(k.Votes)) line.WriteString("\t") diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index 1b1375d6f..612e8bc21 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -499,7 +499,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt b.logf("Generated genesis AUM to initialize network lock, trusting the following keys:") for i, k := range genesisAUM.State.Keys { - b.logf(" - key[%d] = nlpub:%x with %d votes", i, k.Public, k.Votes) + b.logf(" - key[%d] = tlpub:%x with %d votes", i, k.Public, k.Votes) } // Phase 1/2 of initialization: Transmit the genesis AUM to Control. diff --git a/types/key/nl.go b/types/key/nl.go index 4a778d58f..bf09944c2 100644 --- a/types/key/nl.go +++ b/types/key/nl.go @@ -15,12 +15,19 @@ import ( const ( // nlPrivateHexPrefix is the prefix used to identify a - // hex-encoded network-lock key. + // hex-encoded tailnet-lock key. nlPrivateHexPrefix = "nlpriv:" // nlPublicHexPrefix is the prefix used to identify the public - // side of a hex-encoded network-lock key. + // side of a hex-encoded tailnet-lock key. nlPublicHexPrefix = "nlpub:" + + // nlPublicHexPrefixCLI is the prefix used for tailnet-lock keys + // when shown on the CLI. + // It's not practical for us to change the prefix everywhere due to + // compatibility with existing clients, but we can support both prefixes + // as well as use the CLI form when presenting to the user. + nlPublicHexPrefixCLI = "tlpub:" ) // NLPrivate is a node-managed network-lock key, used for signing @@ -116,16 +123,30 @@ func NLPublicFromEd25519Unsafe(public ed25519.PublicKey) NLPublic { return out } -// MarshalText implements encoding.TextUnmarshaler. +// UnmarshalText implements encoding.TextUnmarshaler. This function +// is able to decode both the CLI form (tlpub:) & the +// regular form (nlpub:). func (k *NLPublic) UnmarshalText(b []byte) error { + if mem.HasPrefix(mem.B(b), mem.S(nlPublicHexPrefixCLI)) { + return parseHex(k.k[:], mem.B(b), mem.S(nlPublicHexPrefixCLI)) + } return parseHex(k.k[:], mem.B(b), mem.S(nlPublicHexPrefix)) } -// MarshalText implements encoding.TextMarshaler. +// MarshalText implements encoding.TextMarshaler, emitting a +// representation of the form nlpub:. func (k NLPublic) MarshalText() ([]byte, error) { return toHex(k.k[:], nlPublicHexPrefix), nil } +// CLIString returns a marshalled representation suitable for use +// with tailnet lock commands, of the form tlpub: instead of +// the nlpub: form emitted by MarshalText. Both forms can +// be decoded by UnmarshalText. +func (k NLPublic) CLIString() string { + return string(toHex(k.k[:], nlPublicHexPrefixCLI)) +} + // Verifier returns a ed25519.PublicKey that can be used to // verify signatures. func (k NLPublic) Verifier() ed25519.PublicKey { diff --git a/types/key/nl_test.go b/types/key/nl_test.go index f8e36ee57..631363383 100644 --- a/types/key/nl_test.go +++ b/types/key/nl_test.go @@ -37,4 +37,13 @@ func TestNLPrivate(t *testing.T) { if !bytes.Equal(decodedPub.k[:], pub.k[:]) { t.Error("decoded and generated NLPublic bytes differ") } + + // Test decoding with CLI prefix: 'nlpub:' => 'tlpub:' + decodedPub = NLPublic{} + if err := decodedPub.UnmarshalText([]byte(pub.CLIString())); err != nil { + t.Fatal(err) + } + if !bytes.Equal(decodedPub.k[:], pub.k[:]) { + t.Error("decoded and generated NLPublic bytes differ (CLI prefix)") + } }