Files

226 lines
7.6 KiB
Go

package main
import (
"context"
"fmt"
"log/slog"
"atcr.io/pkg/atproto/did"
"atcr.io/pkg/auth/oauth"
"atcr.io/pkg/labeler"
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/spf13/cobra"
)
var plcCmd = &cobra.Command{
Use: "plc",
Short: "PLC directory management commands",
}
var plcConfigFile string
var (
plcAddRotationKeyFirst bool
plcAddRotationKeyLast bool
)
var plcAddRotationKeyCmd = &cobra.Command{
Use: "add-rotation-key [multibase-key]",
Short: "Add a rotation key to this labeler's PLC identity",
Long: `Add an additional rotation key to the labeler's did:plc document.
If a multibase-encoded private key (K-256 or P-256, starting with 'z') is supplied as
the positional argument, that key is added. If no argument is given, a fresh K-256
keypair is generated and the private half is printed to stdout. Save it offline as
your recovery key, since it will not be shown again.
By default the new key is inserted at the highest priority position (--first), which
allows it to override ops signed by lower-priority keys within PLC's 72-hour recovery
window. Pass --last to append at the lowest priority instead.
The labeler's configured rotation key is used to sign the PLC update.
atcr-labeler plc add-rotation-key --config config.yaml # generate + print
atcr-labeler plc add-rotation-key --config config.yaml --last # append, low priority
atcr-labeler plc add-rotation-key --config config.yaml z... # use supplied key`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
firstSet := cmd.Flags().Changed("first")
lastSet := cmd.Flags().Changed("last")
if firstSet && lastSet {
return fmt.Errorf("--first and --last are mutually exclusive")
}
prepend := !plcAddRotationKeyLast
cfg, err := labeler.LoadConfig(plcConfigFile)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if cfg.Labeler.DIDMethod != "plc" {
return fmt.Errorf("this command only works with did:plc (labeler.did_method is %q)", cfg.Labeler.DIDMethod)
}
ctx := context.Background()
labelerDID, rotationKey, signingKey, err := loadLabelerPLCIdentity(ctx, cfg)
if err != nil {
return err
}
var newKey atcrypto.PrivateKeyExportable
if len(args) == 1 {
newKey, err = atcrypto.ParsePrivateMultibase(args[0])
if err != nil {
return fmt.Errorf("failed to parse key argument: %w", err)
}
}
res, err := did.AddRotationKey(ctx, did.AddRotationKeyOptions{
DID: labelerDID,
PLCDirectoryURL: cfg.PLCDirectoryURL(),
RotationKey: rotationKey,
SigningKey: signingKey,
VerificationKeyName: "atproto_label",
NewKey: newKey,
Prepend: prepend,
})
if err != nil {
return err
}
if res.AlreadyPresent {
fmt.Printf("Key %s is already a rotation key for %s (priority %d of %d)\n",
res.NewKeyDIDKey, labelerDID, res.ExistingAt, res.TotalKeys)
return nil
}
if res.Generated {
fmt.Println("=========================================================================")
fmt.Println("GENERATED NEW ROTATION KEY. SAVE THIS NOW. IT WILL NOT BE SHOWN AGAIN.")
fmt.Println("Store it offline (password manager, paper, hardware token).")
fmt.Println()
fmt.Printf("Private key (multibase): %s\n", res.NewKey.Multibase())
fmt.Printf("Public key (did:key): %s\n", res.NewKeyDIDKey)
fmt.Println("=========================================================================")
}
slog.Info("Added rotation key to PLC identity",
"did", labelerDID,
"new_key", res.NewKeyDIDKey,
"priority", res.InsertedAt,
"total_rotation_keys", res.TotalKeys,
"generated", res.Generated,
)
fmt.Printf("Added rotation key %s to %s (priority %d of %d)\n",
res.NewKeyDIDKey, labelerDID, res.InsertedAt, res.TotalKeys)
return nil
},
}
var plcListRotationKeysCmd = &cobra.Command{
Use: "list-rotation-keys",
Short: "List rotation keys in this labeler's PLC document",
Long: `Fetch the labeler's did:plc document from the PLC directory and print its
rotation keys in priority order (index 0 is highest priority and can override
ops signed by lower-priority keys within PLC's 72-hour recovery window).
The key matching the local labeler.rotation_key is marked as LOCAL.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := labeler.LoadConfig(plcConfigFile)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if cfg.Labeler.DIDMethod != "plc" {
return fmt.Errorf("this command only works with did:plc (labeler.did_method is %q)", cfg.Labeler.DIDMethod)
}
ctx := context.Background()
labelerDID, _, _, err := loadLabelerPLCIdentity(ctx, cfg)
if err != nil {
return err
}
var localRotationKey atcrypto.PrivateKey
if cfg.Labeler.RotationKey != "" {
localRotationKey, err = atcrypto.ParsePrivateMultibase(cfg.Labeler.RotationKey)
if err != nil {
return fmt.Errorf("failed to parse rotation_key from config: %w", err)
}
}
res, err := did.ListRotationKeys(ctx, did.ListRotationKeysOptions{
DID: labelerDID,
PLCDirectoryURL: cfg.PLCDirectoryURL(),
LocalRotationKey: localRotationKey,
})
if err != nil {
return err
}
printRotationKeys(res)
return nil
},
}
// loadLabelerPLCIdentity is the shared "load DID + rotation key + signing key" helper
// used by every PLC command. Mirrors loadHoldPLCIdentity over in cmd/hold/plc.go.
func loadLabelerPLCIdentity(ctx context.Context, cfg *labeler.Config) (string, atcrypto.PrivateKey, *atcrypto.PrivateKeyK256, error) {
labelerDID, _, err := labeler.LoadIdentity(ctx, cfg)
if err != nil {
return "", nil, nil, err
}
if cfg.Labeler.RotationKey == "" {
return "", nil, nil, fmt.Errorf("labeler.rotation_key must be set to sign PLC updates")
}
rotationKey, err := atcrypto.ParsePrivateMultibase(cfg.Labeler.RotationKey)
if err != nil {
return "", nil, nil, fmt.Errorf("failed to parse rotation_key from config: %w", err)
}
signingKey, err := oauth.GenerateOrLoadPDSKey(cfg.SigningKeyPath())
if err != nil {
return "", nil, nil, fmt.Errorf("failed to load signing key: %w", err)
}
return labelerDID, rotationKey, signingKey, nil
}
// printRotationKeys is the shared CLI output for list-rotation-keys, kept identical to
// the hold version since the formatting is service-agnostic.
func printRotationKeys(res *did.ListRotationKeysResult) {
fmt.Printf("DID: %s\n", res.DID)
fmt.Printf("PLC directory: %s\n", res.Directory)
fmt.Printf("Rotation keys (%d):\n", len(res.Keys))
for i, k := range res.Keys {
marker := ""
switch {
case len(res.Keys) == 1:
marker = "(only key)"
case i == 0:
marker = "(highest priority)"
case i == len(res.Keys)-1:
marker = "(lowest priority)"
}
localTag := ""
if res.LocalDIDKey != "" && k == res.LocalDIDKey {
localTag = " [LOCAL — labeler.rotation_key]"
}
fmt.Printf(" [%d] %s %s%s\n", i, k, marker, localTag)
}
if res.LocalDIDKey != "" && !res.LocalPresent {
fmt.Printf("\nWARNING: local rotation_key (%s) is NOT present in the PLC document.\n", res.LocalDIDKey)
fmt.Println("This service cannot sign PLC updates. Possible compromise or out-of-band rotation.")
}
}
func init() {
plcCmd.PersistentFlags().StringVarP(&plcConfigFile, "config", "c", "", "path to YAML configuration file")
plcAddRotationKeyCmd.Flags().BoolVar(&plcAddRotationKeyFirst, "first", true, "insert at highest priority (default)")
plcAddRotationKeyCmd.Flags().BoolVar(&plcAddRotationKeyLast, "last", false, "insert at lowest priority")
plcCmd.AddCommand(plcAddRotationKeyCmd)
plcCmd.AddCommand(plcListRotationKeysCmd)
}