4ffb92d7f6
This avoids putting "DisablementSecrets" in the JSON output from `tailscale lock log`, which is potentially scary to somebody who doesn't understand the distinction. AUMs are stored and transmitted in CBOR-encoded format, which uses an integer rather than a string key, so this doesn't break already-created TKAs. Fixes #19189 Change-Id: I15b4e81a7cef724a450bafcfa0b938da223c78c9 Signed-off-by: Alex Chan <alexc@tailscale.com>
210 lines
6.3 KiB
Go
210 lines
6.3 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_tailnetlock
|
|
|
|
package jsonoutput
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
|
|
"tailscale.com/ipn/ipnstate"
|
|
"tailscale.com/tka"
|
|
)
|
|
|
|
// PrintNetworkLockLogJSONV1 prints the stored TKA state as a JSON object to the CLI,
|
|
// in a stable "v1" format.
|
|
//
|
|
// This format includes:
|
|
//
|
|
// - the AUM hash as a base32-encoded string
|
|
// - the raw AUM as base64-encoded bytes
|
|
// - the expanded AUM, which prints named fields for consumption by other tools
|
|
func PrintNetworkLockLogJSONV1(out io.Writer, updates []ipnstate.NetworkLockUpdate) error {
|
|
messages := make([]logMessageV1, len(updates))
|
|
|
|
for i, update := range updates {
|
|
var aum tka.AUM
|
|
if err := aum.Unserialize(update.Raw); err != nil {
|
|
return fmt.Errorf("decoding: %w", err)
|
|
}
|
|
|
|
h := aum.Hash()
|
|
|
|
if !bytes.Equal(h[:], update.Hash[:]) {
|
|
return fmt.Errorf("incorrect AUM hash: got %v, want %v", h, update)
|
|
}
|
|
|
|
messages[i] = toLogMessageV1(aum, update)
|
|
}
|
|
|
|
result := struct {
|
|
ResponseEnvelope
|
|
Messages []logMessageV1
|
|
}{
|
|
ResponseEnvelope: ResponseEnvelope{
|
|
SchemaVersion: "1",
|
|
},
|
|
Messages: messages,
|
|
}
|
|
|
|
enc := json.NewEncoder(out)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(result)
|
|
}
|
|
|
|
// toLogMessageV1 converts a [tka.AUM] and [ipnstate.NetworkLockUpdate] to the
|
|
// JSON output returned by the CLI.
|
|
func toLogMessageV1(aum tka.AUM, update ipnstate.NetworkLockUpdate) logMessageV1 {
|
|
expandedAUM := expandedAUMV1{}
|
|
expandedAUM.MessageKind = aum.MessageKind.String()
|
|
if len(aum.PrevAUMHash) > 0 {
|
|
expandedAUM.PrevAUMHash = aum.PrevAUMHash.String()
|
|
}
|
|
if key := aum.Key; key != nil {
|
|
expandedAUM.Key = toTKAKeyV1(key)
|
|
}
|
|
if keyID := aum.KeyID; keyID != nil {
|
|
expandedAUM.KeyID = fmt.Sprintf("tlpub:%x", keyID)
|
|
}
|
|
if state := aum.State; state != nil {
|
|
expandedState := expandedStateV1{}
|
|
if h := state.LastAUMHash; h != nil {
|
|
expandedState.LastAUMHash = h.String()
|
|
}
|
|
for _, secret := range state.DisablementValues {
|
|
expandedState.DisablementValues = append(expandedState.DisablementValues, fmt.Sprintf("%x", secret))
|
|
}
|
|
for _, key := range state.Keys {
|
|
expandedState.Keys = append(expandedState.Keys, toTKAKeyV1(&key))
|
|
}
|
|
expandedState.StateID1 = state.StateID1
|
|
expandedState.StateID2 = state.StateID2
|
|
expandedAUM.State = expandedState
|
|
}
|
|
if votes := aum.Votes; votes != nil {
|
|
expandedAUM.Votes = *votes
|
|
}
|
|
expandedAUM.Meta = aum.Meta
|
|
for _, signature := range aum.Signatures {
|
|
expandedAUM.Signatures = append(expandedAUM.Signatures, expandedSignatureV1{
|
|
KeyID: fmt.Sprintf("tlpub:%x", signature.KeyID),
|
|
Signature: base64.URLEncoding.EncodeToString(signature.Signature),
|
|
})
|
|
}
|
|
|
|
return logMessageV1{
|
|
Hash: aum.Hash().String(),
|
|
AUM: expandedAUM,
|
|
Raw: base64.URLEncoding.EncodeToString(update.Raw),
|
|
}
|
|
}
|
|
|
|
// toTKAKeyV1 converts a [tka.Key] to the JSON output returned
|
|
// by the CLI.
|
|
func toTKAKeyV1(key *tka.Key) tkaKeyV1 {
|
|
return tkaKeyV1{
|
|
Kind: key.Kind.String(),
|
|
Votes: key.Votes,
|
|
Public: fmt.Sprintf("tlpub:%x", key.Public),
|
|
Meta: key.Meta,
|
|
}
|
|
}
|
|
|
|
// logMessageV1 is the JSON representation of an AUM as both raw bytes and
|
|
// in its expanded form, and the CLI output is a list of these entries.
|
|
type logMessageV1 struct {
|
|
// The BLAKE2s digest of the CBOR-encoded AUM. This is printed as a
|
|
// base32-encoded string, e.g. KCE…XZQ
|
|
Hash string
|
|
|
|
// The expanded form of the AUM, which presents the fields in a more
|
|
// accessible format than doing a CBOR decoding.
|
|
AUM expandedAUMV1
|
|
|
|
// The raw bytes of the CBOR-encoded AUM, encoded as base64.
|
|
// This is useful for verifying the AUM hash.
|
|
Raw string
|
|
}
|
|
|
|
// expandedAUMV1 is the expanded version of a [tka.AUM], designed so external tools
|
|
// can read the AUM without knowing our CBOR definitions.
|
|
type expandedAUMV1 struct {
|
|
MessageKind string
|
|
PrevAUMHash string `json:"PrevAUMHash,omitzero"`
|
|
|
|
// Key encodes a public key to be added to the key authority.
|
|
// This field is used for AddKey AUMs.
|
|
Key tkaKeyV1 `json:"Key,omitzero"`
|
|
|
|
// KeyID references a public key which is part of the key authority.
|
|
// This field is used for RemoveKey and UpdateKey AUMs.
|
|
KeyID string `json:"KeyID,omitzero"`
|
|
|
|
// State describes the full state of the key authority.
|
|
// This field is used for Checkpoint AUMs.
|
|
State expandedStateV1 `json:"State,omitzero"`
|
|
|
|
// Votes and Meta describe properties of a key in the key authority.
|
|
// These fields are used for UpdateKey AUMs.
|
|
Votes uint `json:"Votes,omitzero"`
|
|
Meta map[string]string `json:"Meta,omitzero"`
|
|
|
|
// Signatures lists the signatures over this AUM.
|
|
Signatures []expandedSignatureV1 `json:"Signatures,omitzero"`
|
|
}
|
|
|
|
// tkaKeyV1 is the expanded version of a [tka.Key], which describes
|
|
// the public components of a key known to network-lock.
|
|
type tkaKeyV1 struct {
|
|
Kind string `json:"Kind,omitzero"`
|
|
|
|
// Votes describes the weight applied to signatures using this key.
|
|
Votes uint
|
|
|
|
// Public encodes the public key of the key as a hex string.
|
|
Public string
|
|
|
|
// Meta describes arbitrary metadata about the key. This could be
|
|
// used to store the name of the key, for instance.
|
|
Meta map[string]string `json:"Meta,omitzero"`
|
|
}
|
|
|
|
// expandedStateV1 is the expanded version of a [tka.State], which describes
|
|
// Tailnet Key Authority state at an instant in time.
|
|
type expandedStateV1 struct {
|
|
// LastAUMHash is the blake2s digest of the last-applied AUM.
|
|
LastAUMHash string `json:"LastAUMHash,omitzero"`
|
|
|
|
// DisablementValues are KDF-derived values used to verify that a caller
|
|
// possesses a valid DisablementSecret. These values are used during the
|
|
// Tailnet Lock deactivation process.
|
|
//
|
|
// These are safe to share publicly or store in the clear. They cannot be
|
|
// used to derive the original DisablementSecret.
|
|
DisablementValues []string
|
|
|
|
// Keys are the public keys of either:
|
|
//
|
|
// 1. The signing nodes currently trusted by the TKA.
|
|
// 2. Ephemeral keys that were used to generate pre-signed auth keys.
|
|
Keys []tkaKeyV1
|
|
|
|
// StateID's are nonce's, generated on enablement and fixed for
|
|
// the lifetime of the Tailnet Key Authority.
|
|
StateID1 uint64
|
|
StateID2 uint64
|
|
}
|
|
|
|
// expandedSignatureV1 is the expanded form of a [tka.Signature], which
|
|
// describes a signature over an AUM. This signature can be verified
|
|
// using the key referenced by KeyID.
|
|
type expandedSignatureV1 struct {
|
|
KeyID string
|
|
Signature string
|
|
}
|