Skip to content

Commit

Permalink
goat: basic account PLC key commands (#959)
Browse files Browse the repository at this point in the history
eg:

```sh
goat key generate

goat account login -u $HANDLE -p $PASSWORD

goat account plc current

goat account plc request-token

goat account plc add-rotation-key --token $PLCTOKEN $PUBKEY

goat account plc current
```
  • Loading branch information
bnewbold authored Feb 28, 2025
2 parents 24b790d + ebfd944 commit b5b2806
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 43 deletions.
143 changes: 142 additions & 1 deletion cmd/goat/account_plc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@ 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"
)

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",
Expand All @@ -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",
},
},
},
Expand All @@ -44,6 +55,27 @@ var cmdAccountPlc = &cli.Command{
ArgsUsage: `<json-file>`,
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: `<pubkey>`,
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)",
},
},
},
},
}

Expand Down Expand Up @@ -168,3 +200,112 @@ 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)
}

fmt.Println("Success!")
return nil
}
44 changes: 32 additions & 12 deletions cmd/goat/crypto.go → cmd/goat/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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,
Action: runKeyGenerate,
},
&cli.Command{
Name: "inspect",
Usage: "parses and outputs metadata about a public or secret key",
Action: runCryptoInspect,
Name: "inspect",
Usage: "parses and outputs metadata about a public or secret key",
ArgsUsage: `<key>`,
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") {
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
}

Expand All @@ -67,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")
Expand Down
2 changes: 1 addition & 1 deletion cmd/goat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func run(args []string) error {
cmdBsky,
cmdRecord,
cmdSyntax,
cmdCrypto,
cmdKey,
cmdPds,
}
return app.Run(args)
Expand Down
Loading

0 comments on commit b5b2806

Please sign in to comment.