From 32226aedab80d9798547673c609675f2c5959102 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 22 Dec 2024 21:17:44 -0800 Subject: [PATCH 1/4] progress on key generation --- cmd/goat/crypto.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/cmd/goat/crypto.go b/cmd/goat/crypto.go index b30f613f5..618136bc6 100644 --- a/cmd/goat/crypto.go +++ b/cmd/goat/crypto.go @@ -21,34 +21,54 @@ var cmdCrypto = &cli.Command{ Aliases: []string{"t"}, Usage: "indicate curve type (P-256 is default)", }, + &cli.BoolFlag{ + Name: "terse", + Usage: "print just the secret key, in multikey format", + }, }, Action: runCryptoGenerate, }, &cli.Command{ Name: "inspect", Usage: "parses and outputs metadata about a public or secret key", + ArgsUsage: ``, Action: runCryptoInspect, }, }, } func runCryptoGenerate(cctx *cli.Context) error { + var priv crypto.PrivateKey + var privMultibase string switch cctx.String("type") { case "", "P-256", "p256", "ES256", "secp256r1": - priv, err := crypto.GeneratePrivateKeyP256() + sec, err := crypto.GeneratePrivateKeyP256() if err != nil { return err } - fmt.Println(priv.Multibase()) + privMultibase = sec.Multibase() + priv = sec case "K-256", "k256", "ES256K", "secp256k1": - priv, err := crypto.GeneratePrivateKeyK256() + sec, err := crypto.GeneratePrivateKeyK256() if err != nil { return err } - fmt.Println(priv.Multibase()) + privMultibase = sec.Multibase() + priv = sec default: return fmt.Errorf("unknown key type: %s", cctx.String("type")) } + if cctx.Bool("terse") { + fmt.Println(privMultibase) + return nil + } + pub, err := priv.PublicKey() + if err != nil { + return err + } + fmt.Printf("Key Type: %s\n", descKeyType(priv)) + fmt.Printf("Secret Key (Multibase Syntax): save this securely (eg, add to password manager)\n\t%s\n", privMultibase) + fmt.Printf("Public Key (DID Key Syntax): share or publish this (eg, in DID document)\n\t%s\n", pub.DIDKey()) return nil } From 8e26051fc99893aba27ddd48032e6f71ec7fa413 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 26 Feb 2025 20:33:56 -0800 Subject: [PATCH 2/4] goat: rename crypto command to key --- cmd/goat/{crypto.go => key.go} | 20 ++++++++++---------- cmd/goat/main.go | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) rename cmd/goat/{crypto.go => key.go} (87%) diff --git a/cmd/goat/crypto.go b/cmd/goat/key.go similarity index 87% rename from cmd/goat/crypto.go rename to cmd/goat/key.go index 618136bc6..96413a040 100644 --- a/cmd/goat/crypto.go +++ b/cmd/goat/key.go @@ -8,8 +8,8 @@ import ( "github.com/urfave/cli/v2" ) -var cmdCrypto = &cli.Command{ - Name: "crypto", +var cmdKey = &cli.Command{ + Name: "key", Usage: "sub-commands for cryptographic keys", Subcommands: []*cli.Command{ &cli.Command{ @@ -22,22 +22,22 @@ var cmdCrypto = &cli.Command{ Usage: "indicate curve type (P-256 is default)", }, &cli.BoolFlag{ - Name: "terse", - Usage: "print just the secret key, in multikey format", + Name: "terse", + Usage: "print just the secret key, in multikey format", }, }, - Action: runCryptoGenerate, + Action: runKeyGenerate, }, &cli.Command{ - Name: "inspect", - Usage: "parses and outputs metadata about a public or secret key", + Name: "inspect", + Usage: "parses and outputs metadata about a public or secret key", ArgsUsage: ``, - Action: runCryptoInspect, + Action: runKeyInspect, }, }, } -func runCryptoGenerate(cctx *cli.Context) error { +func runKeyGenerate(cctx *cli.Context) error { var priv crypto.PrivateKey var privMultibase string switch cctx.String("type") { @@ -87,7 +87,7 @@ func descKeyType(val interface{}) string { } } -func runCryptoInspect(cctx *cli.Context) error { +func runKeyInspect(cctx *cli.Context) error { s := cctx.Args().First() if s == "" { return fmt.Errorf("need to provide key as an argument") diff --git a/cmd/goat/main.go b/cmd/goat/main.go index 165ff26af..cfe1ad68f 100644 --- a/cmd/goat/main.go +++ b/cmd/goat/main.go @@ -37,7 +37,7 @@ func run(args []string) error { cmdBsky, cmdRecord, cmdSyntax, - cmdCrypto, + cmdKey, cmdPds, } return app.Run(args) From a9b6599babc862e0de1602a085be2a08d3d6eb38 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 27 Feb 2025 00:12:19 -0800 Subject: [PATCH 3/4] goat: more account plc commands --- cmd/goat/account_plc.go | 142 +++++++++++++++++++++++++++++++++++- cmd/goat/plc.go | 155 ++++++++++++++++++++++++++++++++-------- 2 files changed, 267 insertions(+), 30 deletions(-) diff --git a/cmd/goat/account_plc.go b/cmd/goat/account_plc.go index 5ce62903a..8c90be718 100644 --- a/cmd/goat/account_plc.go +++ b/cmd/goat/account_plc.go @@ -5,9 +5,12 @@ import ( "encoding/json" "fmt" "os" + "slices" "github.com/bluesky-social/indigo/api/agnostic" comatproto "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/atproto/crypto" + "github.com/bluesky-social/indigo/atproto/syntax" "github.com/urfave/cli/v2" ) @@ -15,6 +18,14 @@ import ( var cmdAccountPlc = &cli.Command{ Name: "plc", Usage: "sub-commands for managing PLC DID via PDS host", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "plc-host", + Usage: "method, hostname, and port of PLC registry", + Value: "https://plc.directory", + EnvVars: []string{"ATP_PLC_HOST"}, + }, + }, Subcommands: []*cli.Command{ &cli.Command{ Name: "recommended", @@ -34,7 +45,7 @@ var cmdAccountPlc = &cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "token", - Usage: "2FA token for signing request", + Usage: "2FA token for PLC operation signing request", }, }, }, @@ -44,6 +55,27 @@ var cmdAccountPlc = &cli.Command{ ArgsUsage: ``, Action: runAccountPlcSubmit, }, + &cli.Command{ + Name: "current", + Usage: "print current PLC data for account (fetched from directory)", + Action: runAccountPlcCurrent, + }, + &cli.Command{ + Name: "add-rotation-key", + Usage: "add a new rotation key to PLC identity (via PDS)", + ArgsUsage: ``, + Action: runAccountPlcAddRotationKey, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "token", + Usage: "2FA token for PLC operation signing request", + }, + &cli.BoolFlag{ + Name: "first", + Usage: "inserts key at the top of key list (highest priority)", + }, + }, + }, }, } @@ -168,3 +200,111 @@ func runAccountPlcSubmit(cctx *cli.Context) error { return nil } + +func runAccountPlcCurrent(cctx *cli.Context) error { + ctx := context.Background() + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession || xrpcc.Auth == nil { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + did, err := syntax.ParseDID(xrpcc.Auth.Did) + if err != nil { + return err + } + + plcData, err := fetchPLCData(ctx, cctx.String("plc-host"), did) + if err != nil { + return err + } + + b, err := json.MarshalIndent(plcData, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil +} + +func runAccountPlcAddRotationKey(cctx *cli.Context) error { + ctx := context.Background() + + newKeyStr := cctx.Args().First() + if newKeyStr == "" { + return fmt.Errorf("need to provide public key argument (as did:key)") + } + + // check that it is a valid pubkey + _, err := crypto.ParsePublicDIDKey(newKeyStr) + if err != nil { + return err + } + + xrpcc, err := loadAuthClient(ctx) + if err == ErrNoAuthSession { + return fmt.Errorf("auth required, but not logged in") + } else if err != nil { + return err + } + + did, err := syntax.ParseDID(xrpcc.Auth.Did) + if err != nil { + return err + } + + // 1. fetch current PLC op: plc.directory/{did}/data + plcData, err := fetchPLCData(ctx, cctx.String("plc-host"), did) + if err != nil { + return err + } + + if len(plcData.RotationKeys) >= 5 { + fmt.Println("WARNGING: already have 5 rotation keys, which is the maximum") + } + + for _, k := range plcData.RotationKeys { + if k == newKeyStr { + return fmt.Errorf("key already registered as a rotation key") + } + } + + // 2. update data + if cctx.Bool("first") { + plcData.RotationKeys = slices.Insert(plcData.RotationKeys, 0, newKeyStr) + } else { + plcData.RotationKeys = append(plcData.RotationKeys, newKeyStr) + } + + // 3. get data signed (using token) + opBytes, err := json.Marshal(&plcData) + if err != nil { + return err + } + var body agnostic.IdentitySignPlcOperation_Input + if err = json.Unmarshal(opBytes, &body); err != nil { + return fmt.Errorf("failed decoding PLC op JSON: %w", err) + } + + token := cctx.String("token") + if token != "" { + body.Token = &token + } + + resp, err := agnostic.IdentitySignPlcOperation(ctx, xrpcc, &body) + if err != nil { + return err + } + + // 4. submit signed op + err = agnostic.IdentitySubmitPlcOperation(ctx, xrpcc, &agnostic.IdentitySubmitPlcOperation_Input{ + Operation: resp.Operation, + }) + if err != nil { + return fmt.Errorf("failed submitting PLC op via PDS: %w", err) + } + + return nil +} diff --git a/cmd/goat/plc.go b/cmd/goat/plc.go index 37179fb93..b8f5a9eec 100644 --- a/cmd/goat/plc.go +++ b/cmd/goat/plc.go @@ -20,34 +20,53 @@ var cmdPLC = &cli.Command{ Usage: "sub-commands for DID PLCs", Flags: []cli.Flag{ &cli.StringFlag{ - Name: "plc-directory", - Value: "https://plc.directory", + Name: "plc-host", + Usage: "method, hostname, and port of PLC registry", + Value: "https://plc.directory", + EnvVars: []string{"ATP_PLC_HOST"}, }, }, Subcommands: []*cli.Command{ - cmdPLCHistory, - cmdPLCDump, + &cli.Command{ + Name: "history", + Usage: "fetch operation log for individual DID", + ArgsUsage: ``, + Flags: []cli.Flag{}, + Action: runPLCHistory, + }, + &cli.Command{ + Name: "data", + Usage: "fetch current data (op) for individual DID", + ArgsUsage: ``, + Flags: []cli.Flag{}, + Action: runPLCData, + }, + &cli.Command{ + Name: "dump", + Usage: "output full operation log, as JSON lines", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cursor", + }, + &cli.BoolFlag{ + Name: "tail", + }, + }, + Action: runPLCDump, + }, }, } -var cmdPLCHistory = &cli.Command{ - Name: "history", - Usage: "fetch operation log for individual DID", - ArgsUsage: ``, - Flags: []cli.Flag{}, - Action: runPLCHistory, -} - func runPLCHistory(cctx *cli.Context) error { ctx := context.Background() - plcURL := cctx.String("plc-directory") + plcHost := cctx.String("plc-host") s := cctx.Args().First() if s == "" { return fmt.Errorf("need to provide account identifier as an argument") } dir := identity.BaseDirectory{ - PLCURL: plcURL, + PLCURL: plcHost, } id, err := syntax.ParseAtIdentifier(s) @@ -75,11 +94,12 @@ func runPLCHistory(cctx *cli.Context) error { return fmt.Errorf("non-PLC DID method: %s", did.Method()) } - url := fmt.Sprintf("%s/%s/log", plcURL, did) + url := fmt.Sprintf("%s/%s/log", plcHost, did) resp, err := http.Get(url) if err != nil { return err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("PLC HTTP request failed") } @@ -106,23 +126,59 @@ func runPLCHistory(cctx *cli.Context) error { return nil } -var cmdPLCDump = &cli.Command{ - Name: "dump", - Usage: "output full operation log, as JSON lines", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "cursor", - }, - &cli.BoolFlag{ - Name: "tail", - }, - }, - Action: runPLCDump, +func runPLCData(cctx *cli.Context) error { + ctx := context.Background() + plcHost := cctx.String("plc-host") + s := cctx.Args().First() + if s == "" { + return fmt.Errorf("need to provide account identifier as an argument") + } + + dir := identity.BaseDirectory{ + PLCURL: plcHost, + } + + id, err := syntax.ParseAtIdentifier(s) + if err != nil { + return err + } + var did syntax.DID + if id.IsDID() { + did, err = id.AsDID() + if err != nil { + return err + } + } else { + hdl, err := id.AsHandle() + if err != nil { + return err + } + did, err = dir.ResolveHandle(ctx, hdl) + if err != nil { + return err + } + } + + if did.Method() != "plc" { + return fmt.Errorf("non-PLC DID method: %s", did.Method()) + } + + plcData, err := fetchPLCData(ctx, plcHost, did) + if err != nil { + return err + } + + b, err := json.MarshalIndent(plcData, "", " ") + if err != nil { + return err + } + fmt.Println(string(b)) + return nil } func runPLCDump(cctx *cli.Context) error { ctx := context.Background() - plcURL := cctx.String("plc-directory") + plcHost := cctx.String("plc-host") client := http.DefaultClient tailMode := cctx.Bool("tail") @@ -132,7 +188,7 @@ func runPLCDump(cctx *cli.Context) error { } var lastCursor string - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/export", plcURL), nil) + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/export", plcHost), nil) if err != nil { return err } @@ -203,3 +259,44 @@ func runPLCDump(cctx *cli.Context) error { return nil } + +type PLCService struct { + Type string `json:"type"` + Endpoint string `json:"endpoint"` +} + +type PLCData struct { + DID string `json:"did"` + VerificationMethods map[string]string `json:"verificationMethods"` + RotationKeys []string `json:"rotationKeys"` + AlsoKnownAs []string `json:"alsoKnownAs"` + Services map[string]PLCService `json:"services"` +} + +func fetchPLCData(ctx context.Context, plcHost string, did syntax.DID) (*PLCData, error) { + + if plcHost == "" { + return nil, fmt.Errorf("PLC host not configured") + } + + url := fmt.Sprintf("%s/%s/data", plcHost, did) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("PLC HTTP request failed") + } + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var d PLCData + err = json.Unmarshal(respBytes, &d) + if err != nil { + return nil, err + } + return &d, nil +} From ebfd94409d1f62b54e27c9f93007fad0d352f140 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Thu, 27 Feb 2025 00:31:46 -0800 Subject: [PATCH 4/4] success message --- cmd/goat/account_plc.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/goat/account_plc.go b/cmd/goat/account_plc.go index 8c90be718..910be2f7c 100644 --- a/cmd/goat/account_plc.go +++ b/cmd/goat/account_plc.go @@ -306,5 +306,6 @@ func runAccountPlcAddRotationKey(cctx *cli.Context) error { return fmt.Errorf("failed submitting PLC op via PDS: %w", err) } + fmt.Println("Success!") return nil }