mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 05:07:09 +00:00
230 lines
7.7 KiB
Go
230 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"atcr.io/pkg/atproto/did"
|
|
"atcr.io/pkg/auth/oauth"
|
|
"atcr.io/pkg/hold"
|
|
|
|
"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 hold's PLC identity",
|
|
Long: `Add an additional rotation key to the hold'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 hold's configured rotation key is used to sign the PLC update.
|
|
|
|
atcr-hold plc add-rotation-key --config config.yaml # generate + print
|
|
atcr-hold plc add-rotation-key --config config.yaml --last # append, low priority
|
|
atcr-hold 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 := hold.LoadConfig(plcConfigFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
}
|
|
if cfg.Database.DIDMethod != "plc" {
|
|
return fmt.Errorf("this command only works with did:plc (database.did_method is %q)", cfg.Database.DIDMethod)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
holdDID, rotationKey, signingKey, err := loadHoldPLCIdentity(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: holdDID,
|
|
PLCDirectoryURL: cfg.Database.PLCDirectoryURL,
|
|
RotationKey: rotationKey,
|
|
SigningKey: signingKey,
|
|
VerificationKeyName: "atproto",
|
|
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, holdDID, 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", holdDID,
|
|
"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, holdDID, res.InsertedAt, res.TotalKeys)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var plcListRotationKeysCmd = &cobra.Command{
|
|
Use: "list-rotation-keys",
|
|
Short: "List rotation keys in this hold's PLC document",
|
|
Long: `Fetch the hold'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 database.rotation_key is marked as LOCAL.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := hold.LoadConfig(plcConfigFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load config: %w", err)
|
|
}
|
|
if cfg.Database.DIDMethod != "plc" {
|
|
return fmt.Errorf("this command only works with did:plc (database.did_method is %q)", cfg.Database.DIDMethod)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
holdDID, err := did.LoadOrCreate(ctx, cfg.DIDConfig())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve hold DID: %w", err)
|
|
}
|
|
|
|
var localRotationKey atcrypto.PrivateKey
|
|
if cfg.Database.RotationKey != "" {
|
|
localRotationKey, err = atcrypto.ParsePrivateMultibase(cfg.Database.RotationKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse rotation_key from config: %w", err)
|
|
}
|
|
}
|
|
|
|
res, err := did.ListRotationKeys(ctx, did.ListRotationKeysOptions{
|
|
DID: holdDID,
|
|
PLCDirectoryURL: cfg.Database.PLCDirectoryURL,
|
|
LocalRotationKey: localRotationKey,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printRotationKeys(res)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// loadHoldPLCIdentity is the shared "load DID + rotation key + signing key" helper used
|
|
// by every PLC command. It enforces that database.rotation_key is set since every PLC
|
|
// command needs a rotation key to either sign updates or verify the LOCAL marker.
|
|
func loadHoldPLCIdentity(ctx context.Context, cfg *hold.Config) (string, atcrypto.PrivateKey, *atcrypto.PrivateKeyK256, error) {
|
|
holdDID, err := did.LoadOrCreate(ctx, cfg.DIDConfig())
|
|
if err != nil {
|
|
return "", nil, nil, fmt.Errorf("failed to resolve hold DID: %w", err)
|
|
}
|
|
|
|
if cfg.Database.RotationKey == "" {
|
|
return "", nil, nil, fmt.Errorf("database.rotation_key must be set to sign PLC updates")
|
|
}
|
|
rotationKey, err := atcrypto.ParsePrivateMultibase(cfg.Database.RotationKey)
|
|
if err != nil {
|
|
return "", nil, nil, fmt.Errorf("failed to parse rotation_key from config: %w", err)
|
|
}
|
|
|
|
keyPath := cfg.Database.KeyPath
|
|
if keyPath == "" {
|
|
keyPath = cfg.Database.Path + "/signing.key"
|
|
}
|
|
signingKey, err := oauth.GenerateOrLoadPDSKey(keyPath)
|
|
if err != nil {
|
|
return "", nil, nil, fmt.Errorf("failed to load signing key: %w", err)
|
|
}
|
|
return holdDID, rotationKey, signingKey, nil
|
|
}
|
|
|
|
// printRotationKeys is the shared CLI output for `list-rotation-keys`.
|
|
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 — database.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)
|
|
}
|