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:
Alex Chan
2025-10-22 15:08:36 +01:00
committed by Alex Chan
parent 99b06eac49
commit 139c395d7d
6 changed files with 524 additions and 12 deletions

View 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"`
}

View 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
}

View File

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

View 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)
}
})
}

View File

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

View File

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