378ee20b9a
This patch stabilises the JSON output, and improves it in the following
ways:
* The AUM hash in Head uses the base32-encoded form of an AUM hash,
consistent with how it's presented elsewhere
* TrustedKeys are the same format as the keys as `tailnet lock log --json`
* SigKind, Pubkey and KeyID are all presented consistently with other
JSON output in NodeKeySignature
* FilteredPeers don't have a NodeKeySignature, because it will always
be empty
For reference, here's the JSON output from the CLI prior to this change:
```json
{
"Enabled": true,
"Head": [
196,
69,
63,
243,
213,
133,
123,
46,
183,
203,
143,
34,
184,
85,
80,
1,
221,
92,
49,
213,
93,
106,
5,
206,
176,
250,
58,
165,
155,
136,
11,
13
],
"PublicKey": "nlpub:0f99af5c02216193963ce9304bb4ca418846eddebe237f37a6de1c59097ed0b8",
"NodeKey": "nodekey:8abfe98b38151748919f6e346ad16436201c3ecd453b01e9d6d3a38e1826000d",
"NodeKeySigned": true,
"NodeKeySignature": {
"SigKind": 1,
"Pubkey": "bnCKv+mLOBUXSJGfbjRq0WQ2IBw+zUU7AenW06OOGCYADQ==",
"KeyID": "D5mvXAIhYZOWPOkwS7TKQYhG7d6+I383pt4cWQl+0Lg=",
"Signature": "4DPW4v6MyLLwQ8AMDm27BVDGABjeC9gg1EfqRdKgzVXi/mJDwY9PTAoX0+0WTRs5SUksWjY0u1CLxq5xgjFGBA==",
"Nested": null,
"WrappingPubkey": "D5mvXAIhYZOWPOkwS7TKQYhG7d6+I383pt4cWQl+0Lg="
},
"TrustedKeys": [
{
"Key": "nlpub:0f99af5c02216193963ce9304bb4ca418846eddebe237f37a6de1c59097ed0b8",
"Metadata": null,
"Votes": 1
},
{
"Key": "nlpub:de2254c040e728140d92bc967d51284e9daea103a28a97a215694c5bda2128b8",
"Metadata": null,
"Votes": 1
}
],
"VisiblePeers": [
{
"Name": "signing2.taila62b.unknown.c.ts.net.",
"ID": 7525920332164264,
"StableID": "nRX6TbAWm121DEVEL",
"TailscaleIPs": [
"100.110.67.20",
"fd7a:115c:a1e0::9c01:4314"
],
"NodeKey": "nodekey:10bf4a5c168051d700a29123cd81568377849da458abef4b328794ca9cae4313",
"NodeKeySignature": {
"SigKind": 1,
"Pubkey": "bnAQv0pcFoBR1wCikSPNgVaDd4SdpFir70syh5TKnK5DEw==",
"KeyID": "D5mvXAIhYZOWPOkwS7TKQYhG7d6+I383pt4cWQl+0Lg=",
"Signature": "h9fhwHiNdkTqOGVQNdW6AVFoio6MFaFobPiK9ydywgmtYxcExJ38b76Tabdc56aNLxf8IfCaRw2VYPcQG2J/AA==",
"Nested": null,
"WrappingPubkey": "3iJUwEDnKBQNkryWfVEoTp2uoQOiipeiFWlMW9ohKLg="
}
}
],
"FilteredPeers": [
{
"Name": "node3.taila62b.unknown.c.ts.net.",
"ID": 5200614049042386,
"StableID": "n3jAr7KNch11DEVEL",
"TailscaleIPs": [
"100.95.29.124",
"fd7a:115c:a1e0::f901:1d7c"
],
"NodeKey": "nodekey:454d2c8602c10574c5ec3a6790f159714802012b7b8bb8d2ab47d637f9df1d7b",
"NodeKeySignature": {
"SigKind": 0,
"Pubkey": null,
"KeyID": null,
"Signature": null,
"Nested": null,
"WrappingPubkey": null
}
}
],
"StateID": 16885615198276932820
}
```
Updates https://github.com/tailscale/corp/issues/22355
Updates https://github.com/tailscale/tailscale/issues/17619
Signed-off-by: Alex Chan <alexc@tailscale.com>
Change-Id: I65b58ff4520033e6b70fc3b1ba7fc91c1f70a960
206 lines
6.1 KiB
Go
206 lines
6.1 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// 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.DisablementSecrets {
|
|
expandedState.DisablementSecrets = append(expandedState.DisablementSecrets, 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"`
|
|
|
|
// DisablementSecrets are KDF-derived values which can be used
|
|
// to turn off the TKA in the event of a consensus-breaking bug.
|
|
DisablementSecrets []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
|
|
}
|