mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-01 09:32:08 +00:00
cmd/tailscale/cli: stabilise the output of tailscale lock log --json
This patch changes the behaviour of `tailscale lock log --json` to make
it more useful for users. It also introduces versioning of our JSON output.
## Changes to `tailscale lock log --json`
Previously this command would print the hash and base64-encoded bytes of
each AUM, and users would need their own CBOR decoder to interpret it in
a useful way:
```json
[
{
"Hash": [
80,
136,
151,
…
],
"Change": "checkpoint",
"Raw": "pAEFAvYFpQH2AopYIAkPN+8V3cJpkoC5ZY2+RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm+NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl/D93I1M56/rowU+UIlYIPZ/SxT9EA2Idy9kaCbsFzjX/s3Ms7584wWGbWd/f/QAWCBHYZzYiAPpQ+NXN+1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK/RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz+dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav/k6dDF4GiAVgg5Eh00epI7PPW2sjKCc/nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc+m45ay5PB/OB4AA9Fdki4KJq9Ll+PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
}
]
```
Now we print the AUM in an expanded form that can be easily read by scripts,
although we include the raw bytes for verification and auditing.
```json
{
"SchemaVersion": "1",
"Messages": [
{
"Hash": "KCEJPRKNSXJG2TPH3EHQRLJNLIIK2DV53FUNPADWA7BZJWBDRXZQ",
"AUM": {
"MessageKind": "checkpoint",
"PrevAUMHash": null,
"Key": null,
"KeyID": null,
"State": {
…
},
"Votes": null,
"Meta": null,
"Signatures": [
{
"KeyID": "tlpub:e44874d1ea48ecf3d6dac8ca09cfe70dc958ad83b656393432016c3ed229c8d6",
"Signature": "8yAKKHPpuOWsuTwfzgeAAPRXZIuCiavS5fjxeiCR2JTlYaU23NxNChezg7tVlEXdH+z151u9na/PQknxsSBwBQ=="
}
]
},
"Raw": "pAEFAvYFpQH2AopYIAkPN-8V3cJpkoC5ZY2-RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm-NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl_D93I1M56_rowU-UIlYIPZ_SxT9EA2Idy9kaCbsFzjX_s3Ms7584wWGbWd_f_QAWCBHYZzYiAPpQ-NXN-1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK_RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz-dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav_k6dDF4GiAVgg5Eh00epI7PPW2sjKCc_nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc-m45ay5PB_OB4AA9Fdki4KJq9Ll-PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
}
]
}
```
This output was previously marked as unstable, and it wasn't very useful,
so changing it should be fine.
## Versioning our JSON output
This patch introduces a way to version our JSON output on the CLI, so we
can make backwards-incompatible changes in future without breaking existing
scripts or integrations.
You can run this command in two ways:
```
tailscale lock log --json
tailscale lock log --json=1
```
Passing an explicit version number allows you to pick a specific JSON schema.
If we ever want to change the schema, we increment the version number and
users must opt-in to the new output.
A bare `--json` flag will always return schema version 1, for compatibility
with existing scripts.
Updates https://github.com/tailscale/tailscale/issues/17613
Updates https://github.com/tailscale/corp/issues/23258
Signed-off-by: Alex Chan <alexc@tailscale.com>
Change-Id: I897f78521cc1a81651f5476228c0882d7b723606
This commit is contained in:
84
cmd/tailscale/cli/jsonoutput/jsonoutput.go
Normal file
84
cmd/tailscale/cli/jsonoutput/jsonoutput.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package jsonoutput provides stable and versioned JSON serialisation for CLI output.
|
||||
// This allows us to provide stable output to scripts/clients, but also make
|
||||
// breaking changes to the output when it's useful.
|
||||
//
|
||||
// Historically we only used `--json` as a boolean flag, so changing the output
|
||||
// could break scripts that rely on the existing format.
|
||||
//
|
||||
// This package allows callers to pass a version number to `--json` and get
|
||||
// a consistent output. We'll bump the version when we make a breaking change
|
||||
// that's likely to break scripts that rely on the existing output, e.g. if
|
||||
// we remove a field or change the type/format.
|
||||
//
|
||||
// Passing just the boolean flag `--json` will always return v1, to preserve
|
||||
// compatibility with scripts written before we versioned our output.
|
||||
package jsonoutput
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// JSONSchemaVersion implements flag.Value, and tracks whether the CLI has
|
||||
// been called with `--json`, and if so, with what value.
|
||||
type JSONSchemaVersion struct {
|
||||
// IsSet tracks if the flag was provided at all.
|
||||
IsSet bool
|
||||
|
||||
// Value tracks the desired schema version, which defaults to 1 if
|
||||
// the user passes `--json` without an argument.
|
||||
Value int
|
||||
}
|
||||
|
||||
// String returns the default value which is printed in the CLI help text.
|
||||
func (v *JSONSchemaVersion) String() string {
|
||||
if v.IsSet {
|
||||
return strconv.Itoa(v.Value)
|
||||
} else {
|
||||
return "(not set)"
|
||||
}
|
||||
}
|
||||
|
||||
// Set is called when the user passes the flag as a command-line argument.
|
||||
func (v *JSONSchemaVersion) Set(s string) error {
|
||||
if v.IsSet {
|
||||
return errors.New("received multiple instances of --json; only pass it once")
|
||||
}
|
||||
|
||||
v.IsSet = true
|
||||
|
||||
// If the user doesn't supply a schema version, default to 1.
|
||||
// This ensures that any existing scripts will continue to get their
|
||||
// current output.
|
||||
if s == "true" {
|
||||
v.Value = 1
|
||||
return nil
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid integer value passed to --json: %q", s)
|
||||
}
|
||||
v.Value = version
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsBoolFlag tells the flag package that JSONSchemaVersion can be set
|
||||
// without an argument.
|
||||
func (v *JSONSchemaVersion) IsBoolFlag() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ResponseEnvelope is a set of fields common to all versioned JSON output.
|
||||
type ResponseEnvelope struct {
|
||||
// SchemaVersion is the version of the JSON output, e.g. "1", "2", "3"
|
||||
SchemaVersion string
|
||||
|
||||
// ResponseWarning tells a user if a newer version of the JSON output
|
||||
// is available.
|
||||
ResponseWarning string `json:"_WARNING,omitzero"`
|
||||
}
|
||||
203
cmd/tailscale/cli/jsonoutput/network-lock-v1.go
Normal file
203
cmd/tailscale/cli/jsonoutput/network-lock-v1.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package jsonoutput
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tka"
|
||||
)
|
||||
|
||||
// PrintNetworkLockJSONV1 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 PrintNetworkLockJSONV1(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 = toExpandedKeyV1(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, toExpandedKeyV1(&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),
|
||||
}
|
||||
}
|
||||
|
||||
// toExpandedKeyV1 converts a [tka.Key] to the JSON output returned
|
||||
// by the CLI.
|
||||
func toExpandedKeyV1(key *tka.Key) expandedKeyV1 {
|
||||
return expandedKeyV1{
|
||||
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 expandedKeyV1 `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"`
|
||||
}
|
||||
|
||||
// expandedAUMV1 is the expanded version of a [tka.Key], which describes
|
||||
// the public components of a key known to network-lock.
|
||||
type expandedKeyV1 struct {
|
||||
Kind string
|
||||
|
||||
// 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 []expandedKeyV1
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
jsonv1 "encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/cmd/tailscale/cli/jsonoutput"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tsconst"
|
||||
@@ -219,7 +221,7 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if nlStatusArgs.json {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc := jsonv1.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(st)
|
||||
}
|
||||
@@ -600,7 +602,7 @@ func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
|
||||
|
||||
var nlLogArgs struct {
|
||||
limit int
|
||||
json bool
|
||||
json jsonoutput.JSONSchemaVersion
|
||||
}
|
||||
|
||||
var nlLogCmd = &ffcli.Command{
|
||||
@@ -612,7 +614,7 @@ var nlLogCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock log")
|
||||
fs.IntVar(&nlLogArgs.limit, "limit", 50, "max number of updates to list")
|
||||
fs.BoolVar(&nlLogArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
fs.Var(&nlLogArgs.json, "json", "output in JSON format")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
@@ -678,7 +680,7 @@ func nlDescribeUpdate(update ipnstate.NetworkLockUpdate, color bool) (string, er
|
||||
|
||||
default:
|
||||
// Print a JSON encoding of the AUM as a fallback.
|
||||
e := json.NewEncoder(&stanza)
|
||||
e := jsonv1.NewEncoder(&stanza)
|
||||
e.SetIndent("", "\t")
|
||||
if err := e.Encode(aum); err != nil {
|
||||
return "", err
|
||||
@@ -702,14 +704,21 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if nlLogArgs.json {
|
||||
enc := json.NewEncoder(Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(updates)
|
||||
}
|
||||
|
||||
out, useColor := colorableOutput()
|
||||
|
||||
return printNetworkLockLog(updates, out, nlLogArgs.json, useColor)
|
||||
}
|
||||
|
||||
func printNetworkLockLog(updates []ipnstate.NetworkLockUpdate, out io.Writer, jsonSchema jsonoutput.JSONSchemaVersion, useColor bool) error {
|
||||
if jsonSchema.IsSet {
|
||||
if jsonSchema.Value == 1 {
|
||||
return jsonoutput.PrintNetworkLockJSONV1(out, updates)
|
||||
} else {
|
||||
return fmt.Errorf("unrecognised version: %q", jsonSchema.Value)
|
||||
}
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
stanza, err := nlDescribeUpdate(update, useColor)
|
||||
if err != nil {
|
||||
|
||||
204
cmd/tailscale/cli/network-lock_test.go
Normal file
204
cmd/tailscale/cli/network-lock_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/cmd/tailscale/cli/jsonoutput"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
func TestNetworkLockLogOutput(t *testing.T) {
|
||||
votes := uint(1)
|
||||
aum1 := tka.AUM{
|
||||
MessageKind: tka.AUMAddKey,
|
||||
Key: &tka.Key{
|
||||
Kind: tka.Key25519,
|
||||
Votes: 1,
|
||||
Public: []byte{2, 2},
|
||||
},
|
||||
}
|
||||
h1 := aum1.Hash()
|
||||
aum2 := tka.AUM{
|
||||
MessageKind: tka.AUMRemoveKey,
|
||||
KeyID: []byte{3, 3},
|
||||
PrevAUMHash: h1[:],
|
||||
Signatures: []tkatype.Signature{
|
||||
{
|
||||
KeyID: []byte{3, 4},
|
||||
Signature: []byte{4, 5},
|
||||
},
|
||||
},
|
||||
Meta: map[string]string{"en": "three", "de": "drei", "es": "tres"},
|
||||
}
|
||||
h2 := aum2.Hash()
|
||||
aum3 := tka.AUM{
|
||||
MessageKind: tka.AUMCheckpoint,
|
||||
PrevAUMHash: h2[:],
|
||||
State: &tka.State{
|
||||
Keys: []tka.Key{
|
||||
{
|
||||
Kind: tka.Key25519,
|
||||
Votes: 1,
|
||||
Public: []byte{1, 1},
|
||||
Meta: map[string]string{"en": "one", "de": "eins", "es": "uno"},
|
||||
},
|
||||
},
|
||||
DisablementSecrets: [][]byte{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9},
|
||||
},
|
||||
},
|
||||
Votes: &votes,
|
||||
}
|
||||
|
||||
updates := []ipnstate.NetworkLockUpdate{
|
||||
{
|
||||
Hash: aum3.Hash(),
|
||||
Change: aum3.MessageKind.String(),
|
||||
Raw: aum3.Serialize(),
|
||||
},
|
||||
{
|
||||
Hash: aum2.Hash(),
|
||||
Change: aum2.MessageKind.String(),
|
||||
Raw: aum2.Serialize(),
|
||||
},
|
||||
{
|
||||
Hash: aum1.Hash(),
|
||||
Change: aum1.MessageKind.String(),
|
||||
Raw: aum1.Serialize(),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("human-readable", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var outBuf bytes.Buffer
|
||||
json := jsonoutput.JSONSchemaVersion{}
|
||||
useColor := false
|
||||
|
||||
printNetworkLockLog(updates, &outBuf, json, useColor)
|
||||
|
||||
t.Logf("%s", outBuf.String())
|
||||
|
||||
want := `update 4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA (checkpoint)
|
||||
Disablement values:
|
||||
- 010203
|
||||
- 040506
|
||||
- 070809
|
||||
Keys:
|
||||
Type: 25519
|
||||
KeyID: tlpub:0101
|
||||
Metadata: map[de:eins en:one es:uno]
|
||||
|
||||
update BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ (remove-key)
|
||||
KeyID: tlpub:0303
|
||||
|
||||
update UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA (add-key)
|
||||
Type: 25519
|
||||
KeyID: tlpub:0202
|
||||
|
||||
`
|
||||
|
||||
if diff := cmp.Diff(outBuf.String(), want); diff != "" {
|
||||
t.Fatalf("wrong output (-got, +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
jsonV1 := `{
|
||||
"SchemaVersion": "1",
|
||||
"Messages": [
|
||||
{
|
||||
"Hash": "4M4Q3IXBARPQMFVXHJBDCYQMWU5H5FBKD7MFF75HE4O5JMIWR2UA",
|
||||
"AUM": {
|
||||
"MessageKind": "checkpoint",
|
||||
"PrevAUMHash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ",
|
||||
"State": {
|
||||
"DisablementSecrets": [
|
||||
"010203",
|
||||
"040506",
|
||||
"070809"
|
||||
],
|
||||
"Keys": [
|
||||
{
|
||||
"Kind": "25519",
|
||||
"Votes": 1,
|
||||
"Public": "tlpub:0101",
|
||||
"Meta": {
|
||||
"de": "eins",
|
||||
"en": "one",
|
||||
"es": "uno"
|
||||
}
|
||||
}
|
||||
],
|
||||
"StateID1": 0,
|
||||
"StateID2": 0
|
||||
},
|
||||
"Votes": 1
|
||||
},
|
||||
"Raw": "pAEFAlggCqtbndUNv4_i-JrrVbGywbw5dNWNZYysEm02CCgf3q8FowH2AoNDAQIDQwQFBkMHCAkDgaQBAQIBA0IBAQyjYmRlZGVpbnNiZW5jb25lYmVzY3VubwYB"
|
||||
},
|
||||
{
|
||||
"Hash": "BKVVXHOVBW7Y7YXYTLVVLMNSYG6DS5GVRVSYZLASNU3AQKA732XQ",
|
||||
"AUM": {
|
||||
"MessageKind": "remove-key",
|
||||
"PrevAUMHash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA",
|
||||
"KeyID": "tlpub:0303",
|
||||
"Meta": {
|
||||
"de": "drei",
|
||||
"en": "three",
|
||||
"es": "tres"
|
||||
},
|
||||
"Signatures": [
|
||||
{
|
||||
"KeyID": "tlpub:0304",
|
||||
"Signature": "BAU="
|
||||
}
|
||||
]
|
||||
},
|
||||
"Raw": "pQECAlggopKFFOhcPaARv2QQU90-kWozQFAG3Hqja7Vez-_EZIAEQgMDB6NiZGVkZHJlaWJlbmV0aHJlZWJlc2R0cmVzF4GiAUIDBAJCBAU="
|
||||
},
|
||||
{
|
||||
"Hash": "UKJIKFHILQ62AEN7MQIFHXJ6SFVDGQCQA3OHVI3LWVPM736EMSAA",
|
||||
"AUM": {
|
||||
"MessageKind": "add-key",
|
||||
"Key": {
|
||||
"Kind": "25519",
|
||||
"Votes": 1,
|
||||
"Public": "tlpub:0202"
|
||||
}
|
||||
},
|
||||
"Raw": "owEBAvYDowEBAgEDQgIC"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
t.Run("json-1", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Logf("BOOM")
|
||||
|
||||
var outBuf bytes.Buffer
|
||||
json := jsonoutput.JSONSchemaVersion{
|
||||
IsSet: true,
|
||||
Value: 1,
|
||||
}
|
||||
useColor := false
|
||||
|
||||
printNetworkLockLog(updates, &outBuf, json, useColor)
|
||||
|
||||
want := jsonV1
|
||||
t.Logf("%s", outBuf.String())
|
||||
|
||||
if diff := cmp.Diff(outBuf.String(), want); diff != "" {
|
||||
t.Fatalf("wrong output (-got, +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -85,6 +85,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/cmd/tailscale/cli/ffcomplete/internal from tailscale.com/cmd/tailscale/cli/ffcomplete
|
||||
tailscale.com/cmd/tailscale/cli/jsonoutput from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
||||
tailscale.com/control/controlhttp from tailscale.com/control/ts2021
|
||||
tailscale.com/control/controlhttp/controlhttpcommon from tailscale.com/control/controlhttp
|
||||
|
||||
15
tka/aum.go
15
tka/aum.go
@@ -55,6 +55,17 @@ func (h AUMHash) IsZero() bool {
|
||||
return h == (AUMHash{})
|
||||
}
|
||||
|
||||
// PrevAUMHash represents the BLAKE2s digest of an Authority Update Message (AUM).
|
||||
// Unlike an AUMHash, this can be empty if there is no previous AUM hash
|
||||
// (which occurs in the genesis AUM).
|
||||
type PrevAUMHash []byte
|
||||
|
||||
// String returns the PrevAUMHash encoded as base32.
|
||||
// This is suitable for use as a filename, and for storing in text-preferred media.
|
||||
func (h PrevAUMHash) String() string {
|
||||
return base32StdNoPad.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// AUMKind describes valid AUM types.
|
||||
type AUMKind uint8
|
||||
|
||||
@@ -119,8 +130,8 @@ func (k AUMKind) String() string {
|
||||
// behavior of old clients (which will ignore the field).
|
||||
// - No floats!
|
||||
type AUM struct {
|
||||
MessageKind AUMKind `cbor:"1,keyasint"`
|
||||
PrevAUMHash []byte `cbor:"2,keyasint"`
|
||||
MessageKind AUMKind `cbor:"1,keyasint"`
|
||||
PrevAUMHash PrevAUMHash `cbor:"2,keyasint"`
|
||||
|
||||
// Key encodes a public key to be added to the key authority.
|
||||
// This field is used for AddKey AUMs.
|
||||
|
||||
Reference in New Issue
Block a user