Files
Simon Law 3093e4523c feature/tailnetlock/tslockjsonv1: turn print functions into JSON converters
This patch pulls the printing and JSON-encoding out of
feature/tailnetlock/tslockjsonv1 into their callers, so that this
package only handles type conversions.

In cmd/tailscale/cli/tailnet-lock.go, it extracts the
printTailnetLockStatus function from runTailnetLockStatus to mirror
printTailnetLockLog and runTailnetLockLog.

Updates #17613

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2026-07-30 16:23:09 -04:00

100 lines
2.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package tslockjsonv1
import (
"bytes"
"encoding/base64"
"fmt"
"tailscale.com/cmd/tailscale/jsonoutput"
"tailscale.com/cmd/tailscale/tslockjsonv1"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
)
// LogResponse returns 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 LogResponse(updates []ipnstate.NetworkLockUpdate) (tslockjsonv1.LogResponse, error) {
var zero tslockjsonv1.LogResponse
resp := tslockjsonv1.LogResponse{
ResponseEnvelope: jsonoutput.ResponseEnvelope{
SchemaVersion: "1",
},
Messages: make([]tslockjsonv1.LogMessage, len(updates)),
}
for i, update := range updates {
var aum tka.AUM
if err := aum.Unserialize(update.Raw); err != nil {
return zero, fmt.Errorf("decoding: %w", err)
}
h := aum.Hash()
if !bytes.Equal(h[:], update.Hash[:]) {
return zero, fmt.Errorf("incorrect AUM hash: got %v, want %v", h, update)
}
resp.Messages[i] = logMessage(aum, update)
}
return resp, nil
}
// logMessage converts a [tka.AUM] and [ipnstate.TailnetLockUpdate]
// the JSON output returned by the CLI, in a stable "v1" format.
func logMessage(aum tka.AUM, update ipnstate.TailnetLockUpdate) tslockjsonv1.LogMessage {
expandedAUM := tslockjsonv1.AUM{}
expandedAUM.MessageKind = aum.MessageKind.String()
if len(aum.PrevAUMHash) > 0 {
expandedAUM.PrevAUMHash = aum.PrevAUMHash.String()
}
if key := aum.Key; key != nil {
expandedAUM.Key = tkaKey(key)
}
if keyID := aum.KeyID; keyID != nil {
expandedAUM.KeyID = fmt.Sprintf("tlpub:%x", keyID)
}
if state := aum.State; state != nil {
expandedState := tslockjsonv1.TKAState{}
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, tkaKey(&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, tslockjsonv1.Signature{
KeyID: fmt.Sprintf("tlpub:%x", signature.KeyID),
Signature: base64.URLEncoding.EncodeToString(signature.Signature),
})
}
return tslockjsonv1.LogMessage{
Hash: aum.Hash().String(),
AUM: expandedAUM,
Raw: base64.URLEncoding.EncodeToString(update.Raw),
}
}