mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
165 lines
4.7 KiB
Go
165 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"atcr.io/pkg/auth/oauth"
|
|
"atcr.io/pkg/hold"
|
|
"atcr.io/pkg/hold/pds"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/atcrypto"
|
|
didplc "github.com/did-method-plc/go-didplc"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var plcCmd = &cobra.Command{
|
|
Use: "plc",
|
|
Short: "PLC directory management commands",
|
|
}
|
|
|
|
var plcConfigFile string
|
|
|
|
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.
|
|
The key must be a multibase-encoded private key (K-256 or P-256, starting with 'z').
|
|
The hold's configured rotation key is used to sign the PLC update.
|
|
|
|
atcr-hold plc add-rotation-key --config config.yaml z...`,
|
|
Args: cobra.ExactArgs(1),
|
|
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()
|
|
|
|
// Resolve the hold's DID
|
|
holdDID, err := pds.LoadOrCreateDID(ctx, pds.DIDConfig{
|
|
DID: cfg.Database.DID,
|
|
DIDMethod: cfg.Database.DIDMethod,
|
|
PublicURL: cfg.Server.PublicURL,
|
|
DBPath: cfg.Database.Path,
|
|
SigningKeyPath: cfg.Database.KeyPath,
|
|
RotationKey: cfg.Database.RotationKey,
|
|
PLCDirectoryURL: cfg.Database.PLCDirectoryURL,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve hold DID: %w", err)
|
|
}
|
|
|
|
// Parse the rotation key from config (required for signing PLC updates)
|
|
if cfg.Database.RotationKey == "" {
|
|
return fmt.Errorf("database.rotation_key must be set to sign PLC updates")
|
|
}
|
|
rotationKey, err := atcrypto.ParsePrivateMultibase(cfg.Database.RotationKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse rotation_key from config: %w", err)
|
|
}
|
|
|
|
// Parse the new key to add (K-256 or P-256)
|
|
newKey, err := atcrypto.ParsePrivateMultibase(args[0])
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse key argument: %w", err)
|
|
}
|
|
newKeyPub, err := newKey.PublicKey()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get public key from argument: %w", err)
|
|
}
|
|
newKeyDIDKey := newKeyPub.DIDKey()
|
|
|
|
// Load signing key for verification methods
|
|
keyPath := cfg.Database.KeyPath
|
|
if keyPath == "" {
|
|
keyPath = cfg.Database.Path + "/signing.key"
|
|
}
|
|
signingKey, err := oauth.GenerateOrLoadPDSKey(keyPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load signing key: %w", err)
|
|
}
|
|
|
|
// Fetch current PLC state
|
|
plcDirectoryURL := cfg.Database.PLCDirectoryURL
|
|
if plcDirectoryURL == "" {
|
|
plcDirectoryURL = "https://plc.directory"
|
|
}
|
|
client := &didplc.Client{DirectoryURL: plcDirectoryURL}
|
|
|
|
opLog, err := client.OpLog(ctx, holdDID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch PLC op log: %w", err)
|
|
}
|
|
if len(opLog) == 0 {
|
|
return fmt.Errorf("empty op log for %s", holdDID)
|
|
}
|
|
|
|
lastEntry := opLog[len(opLog)-1]
|
|
lastOp := lastEntry.Regular
|
|
if lastOp == nil {
|
|
return fmt.Errorf("last PLC operation is not a regular op")
|
|
}
|
|
|
|
// Check if key already present
|
|
for _, k := range lastOp.RotationKeys {
|
|
if k == newKeyDIDKey {
|
|
fmt.Printf("Key %s is already a rotation key for %s\n", newKeyDIDKey, holdDID)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Build updated rotation keys: keep existing, append new
|
|
rotationKeys := make([]string, len(lastOp.RotationKeys))
|
|
copy(rotationKeys, lastOp.RotationKeys)
|
|
rotationKeys = append(rotationKeys, newKeyDIDKey)
|
|
|
|
// Build update: preserve everything else from current state
|
|
sigPub, err := signingKey.PublicKey()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get signing public key: %w", err)
|
|
}
|
|
|
|
prevCID := lastEntry.AsOperation().CID().String()
|
|
|
|
op := &didplc.RegularOp{
|
|
Type: "plc_operation",
|
|
RotationKeys: rotationKeys,
|
|
VerificationMethods: map[string]string{
|
|
"atproto": sigPub.DIDKey(),
|
|
},
|
|
AlsoKnownAs: lastOp.AlsoKnownAs,
|
|
Services: lastOp.Services,
|
|
Prev: &prevCID,
|
|
}
|
|
|
|
if err := op.Sign(rotationKey); err != nil {
|
|
return fmt.Errorf("failed to sign PLC update: %w", err)
|
|
}
|
|
|
|
if err := client.Submit(ctx, holdDID, op); err != nil {
|
|
return fmt.Errorf("failed to submit PLC update: %w", err)
|
|
}
|
|
|
|
slog.Info("Added rotation key to PLC identity",
|
|
"did", holdDID,
|
|
"new_key", newKeyDIDKey,
|
|
"total_rotation_keys", len(rotationKeys),
|
|
)
|
|
fmt.Printf("Added rotation key %s to %s\n", newKeyDIDKey, holdDID)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
plcCmd.PersistentFlags().StringVarP(&plcConfigFile, "config", "c", "", "path to YAML configuration file")
|
|
|
|
plcCmd.AddCommand(plcAddRotationKeyCmd)
|
|
}
|