+142
-1
cmd/goat/account_plc.go
+142
-1
cmd/goat/account_plc.go
···
5
5
"encoding/json"
6
6
"fmt"
7
7
"os"
8
+
"slices"
8
9
9
10
"github.com/bluesky-social/indigo/api/agnostic"
10
11
comatproto "github.com/bluesky-social/indigo/api/atproto"
12
+
"github.com/bluesky-social/indigo/atproto/crypto"
13
+
"github.com/bluesky-social/indigo/atproto/syntax"
11
14
12
15
"github.com/urfave/cli/v2"
13
16
)
···
15
18
var cmdAccountPlc = &cli.Command{
16
19
Name: "plc",
17
20
Usage: "sub-commands for managing PLC DID via PDS host",
21
+
Flags: []cli.Flag{
22
+
&cli.StringFlag{
23
+
Name: "plc-host",
24
+
Usage: "method, hostname, and port of PLC registry",
25
+
Value: "https://plc.directory",
26
+
EnvVars: []string{"ATP_PLC_HOST"},
27
+
},
28
+
},
18
29
Subcommands: []*cli.Command{
19
30
&cli.Command{
20
31
Name: "recommended",
···
34
45
Flags: []cli.Flag{
35
46
&cli.StringFlag{
36
47
Name: "token",
37
-
Usage: "2FA token for signing request",
48
+
Usage: "2FA token for PLC operation signing request",
38
49
},
39
50
},
40
51
},
···
43
54
Usage: "submit a PLC operation (via PDS)",
44
55
ArgsUsage: `<json-file>`,
45
56
Action: runAccountPlcSubmit,
57
+
},
58
+
&cli.Command{
59
+
Name: "current",
60
+
Usage: "print current PLC data for account (fetched from directory)",
61
+
Action: runAccountPlcCurrent,
62
+
},
63
+
&cli.Command{
64
+
Name: "add-rotation-key",
65
+
Usage: "add a new rotation key to PLC identity (via PDS)",
66
+
ArgsUsage: `<pubkey>`,
67
+
Action: runAccountPlcAddRotationKey,
68
+
Flags: []cli.Flag{
69
+
&cli.StringFlag{
70
+
Name: "token",
71
+
Usage: "2FA token for PLC operation signing request",
72
+
},
73
+
&cli.BoolFlag{
74
+
Name: "first",
75
+
Usage: "inserts key at the top of key list (highest priority)",
76
+
},
77
+
},
46
78
},
47
79
},
48
80
}
···
168
200
169
201
return nil
170
202
}
203
+
204
+
func runAccountPlcCurrent(cctx *cli.Context) error {
205
+
ctx := context.Background()
206
+
207
+
xrpcc, err := loadAuthClient(ctx)
208
+
if err == ErrNoAuthSession || xrpcc.Auth == nil {
209
+
return fmt.Errorf("auth required, but not logged in")
210
+
} else if err != nil {
211
+
return err
212
+
}
213
+
214
+
did, err := syntax.ParseDID(xrpcc.Auth.Did)
215
+
if err != nil {
216
+
return err
217
+
}
218
+
219
+
plcData, err := fetchPLCData(ctx, cctx.String("plc-host"), did)
220
+
if err != nil {
221
+
return err
222
+
}
223
+
224
+
b, err := json.MarshalIndent(plcData, "", " ")
225
+
if err != nil {
226
+
return err
227
+
}
228
+
fmt.Println(string(b))
229
+
return nil
230
+
}
231
+
232
+
func runAccountPlcAddRotationKey(cctx *cli.Context) error {
233
+
ctx := context.Background()
234
+
235
+
newKeyStr := cctx.Args().First()
236
+
if newKeyStr == "" {
237
+
return fmt.Errorf("need to provide public key argument (as did:key)")
238
+
}
239
+
240
+
// check that it is a valid pubkey
241
+
_, err := crypto.ParsePublicDIDKey(newKeyStr)
242
+
if err != nil {
243
+
return err
244
+
}
245
+
246
+
xrpcc, err := loadAuthClient(ctx)
247
+
if err == ErrNoAuthSession {
248
+
return fmt.Errorf("auth required, but not logged in")
249
+
} else if err != nil {
250
+
return err
251
+
}
252
+
253
+
did, err := syntax.ParseDID(xrpcc.Auth.Did)
254
+
if err != nil {
255
+
return err
256
+
}
257
+
258
+
// 1. fetch current PLC op: plc.directory/{did}/data
259
+
plcData, err := fetchPLCData(ctx, cctx.String("plc-host"), did)
260
+
if err != nil {
261
+
return err
262
+
}
263
+
264
+
if len(plcData.RotationKeys) >= 5 {
265
+
fmt.Println("WARNGING: already have 5 rotation keys, which is the maximum")
266
+
}
267
+
268
+
for _, k := range plcData.RotationKeys {
269
+
if k == newKeyStr {
270
+
return fmt.Errorf("key already registered as a rotation key")
271
+
}
272
+
}
273
+
274
+
// 2. update data
275
+
if cctx.Bool("first") {
276
+
plcData.RotationKeys = slices.Insert(plcData.RotationKeys, 0, newKeyStr)
277
+
} else {
278
+
plcData.RotationKeys = append(plcData.RotationKeys, newKeyStr)
279
+
}
280
+
281
+
// 3. get data signed (using token)
282
+
opBytes, err := json.Marshal(&plcData)
283
+
if err != nil {
284
+
return err
285
+
}
286
+
var body agnostic.IdentitySignPlcOperation_Input
287
+
if err = json.Unmarshal(opBytes, &body); err != nil {
288
+
return fmt.Errorf("failed decoding PLC op JSON: %w", err)
289
+
}
290
+
291
+
token := cctx.String("token")
292
+
if token != "" {
293
+
body.Token = &token
294
+
}
295
+
296
+
resp, err := agnostic.IdentitySignPlcOperation(ctx, xrpcc, &body)
297
+
if err != nil {
298
+
return err
299
+
}
300
+
301
+
// 4. submit signed op
302
+
err = agnostic.IdentitySubmitPlcOperation(ctx, xrpcc, &agnostic.IdentitySubmitPlcOperation_Input{
303
+
Operation: resp.Operation,
304
+
})
305
+
if err != nil {
306
+
return fmt.Errorf("failed submitting PLC op via PDS: %w", err)
307
+
}
308
+
309
+
fmt.Println("Success!")
310
+
return nil
311
+
}
+32
-12
cmd/goat/crypto.go
cmd/goat/key.go
+32
-12
cmd/goat/crypto.go
cmd/goat/key.go
···
8
8
"github.com/urfave/cli/v2"
9
9
)
10
10
11
-
var cmdCrypto = &cli.Command{
12
-
Name: "crypto",
11
+
var cmdKey = &cli.Command{
12
+
Name: "key",
13
13
Usage: "sub-commands for cryptographic keys",
14
14
Subcommands: []*cli.Command{
15
15
&cli.Command{
···
21
21
Aliases: []string{"t"},
22
22
Usage: "indicate curve type (P-256 is default)",
23
23
},
24
+
&cli.BoolFlag{
25
+
Name: "terse",
26
+
Usage: "print just the secret key, in multikey format",
27
+
},
24
28
},
25
-
Action: runCryptoGenerate,
29
+
Action: runKeyGenerate,
26
30
},
27
31
&cli.Command{
28
-
Name: "inspect",
29
-
Usage: "parses and outputs metadata about a public or secret key",
30
-
Action: runCryptoInspect,
32
+
Name: "inspect",
33
+
Usage: "parses and outputs metadata about a public or secret key",
34
+
ArgsUsage: `<key>`,
35
+
Action: runKeyInspect,
31
36
},
32
37
},
33
38
}
34
39
35
-
func runCryptoGenerate(cctx *cli.Context) error {
40
+
func runKeyGenerate(cctx *cli.Context) error {
41
+
var priv crypto.PrivateKey
42
+
var privMultibase string
36
43
switch cctx.String("type") {
37
44
case "", "P-256", "p256", "ES256", "secp256r1":
38
-
priv, err := crypto.GeneratePrivateKeyP256()
45
+
sec, err := crypto.GeneratePrivateKeyP256()
39
46
if err != nil {
40
47
return err
41
48
}
42
-
fmt.Println(priv.Multibase())
49
+
privMultibase = sec.Multibase()
50
+
priv = sec
43
51
case "K-256", "k256", "ES256K", "secp256k1":
44
-
priv, err := crypto.GeneratePrivateKeyK256()
52
+
sec, err := crypto.GeneratePrivateKeyK256()
45
53
if err != nil {
46
54
return err
47
55
}
48
-
fmt.Println(priv.Multibase())
56
+
privMultibase = sec.Multibase()
57
+
priv = sec
49
58
default:
50
59
return fmt.Errorf("unknown key type: %s", cctx.String("type"))
51
60
}
61
+
if cctx.Bool("terse") {
62
+
fmt.Println(privMultibase)
63
+
return nil
64
+
}
65
+
pub, err := priv.PublicKey()
66
+
if err != nil {
67
+
return err
68
+
}
69
+
fmt.Printf("Key Type: %s\n", descKeyType(priv))
70
+
fmt.Printf("Secret Key (Multibase Syntax): save this securely (eg, add to password manager)\n\t%s\n", privMultibase)
71
+
fmt.Printf("Public Key (DID Key Syntax): share or publish this (eg, in DID document)\n\t%s\n", pub.DIDKey())
52
72
return nil
53
73
}
54
74
···
67
87
}
68
88
}
69
89
70
-
func runCryptoInspect(cctx *cli.Context) error {
90
+
func runKeyInspect(cctx *cli.Context) error {
71
91
s := cctx.Args().First()
72
92
if s == "" {
73
93
return fmt.Errorf("need to provide key as an argument")
+1
-1
cmd/goat/main.go
+1
-1
cmd/goat/main.go
+126
-29
cmd/goat/plc.go
+126
-29
cmd/goat/plc.go
···
20
20
Usage: "sub-commands for DID PLCs",
21
21
Flags: []cli.Flag{
22
22
&cli.StringFlag{
23
-
Name: "plc-directory",
24
-
Value: "https://plc.directory",
23
+
Name: "plc-host",
24
+
Usage: "method, hostname, and port of PLC registry",
25
+
Value: "https://plc.directory",
26
+
EnvVars: []string{"ATP_PLC_HOST"},
25
27
},
26
28
},
27
29
Subcommands: []*cli.Command{
28
-
cmdPLCHistory,
29
-
cmdPLCDump,
30
+
&cli.Command{
31
+
Name: "history",
32
+
Usage: "fetch operation log for individual DID",
33
+
ArgsUsage: `<at-identifier>`,
34
+
Flags: []cli.Flag{},
35
+
Action: runPLCHistory,
36
+
},
37
+
&cli.Command{
38
+
Name: "data",
39
+
Usage: "fetch current data (op) for individual DID",
40
+
ArgsUsage: `<at-identifier>`,
41
+
Flags: []cli.Flag{},
42
+
Action: runPLCData,
43
+
},
44
+
&cli.Command{
45
+
Name: "dump",
46
+
Usage: "output full operation log, as JSON lines",
47
+
Flags: []cli.Flag{
48
+
&cli.StringFlag{
49
+
Name: "cursor",
50
+
},
51
+
&cli.BoolFlag{
52
+
Name: "tail",
53
+
},
54
+
},
55
+
Action: runPLCDump,
56
+
},
30
57
},
31
58
}
32
59
33
-
var cmdPLCHistory = &cli.Command{
34
-
Name: "history",
35
-
Usage: "fetch operation log for individual DID",
36
-
ArgsUsage: `<at-identifier>`,
37
-
Flags: []cli.Flag{},
38
-
Action: runPLCHistory,
39
-
}
40
-
41
60
func runPLCHistory(cctx *cli.Context) error {
42
61
ctx := context.Background()
43
-
plcURL := cctx.String("plc-directory")
62
+
plcHost := cctx.String("plc-host")
44
63
s := cctx.Args().First()
45
64
if s == "" {
46
65
return fmt.Errorf("need to provide account identifier as an argument")
47
66
}
48
67
49
68
dir := identity.BaseDirectory{
50
-
PLCURL: plcURL,
69
+
PLCURL: plcHost,
51
70
}
52
71
53
72
id, err := syntax.ParseAtIdentifier(s)
···
75
94
return fmt.Errorf("non-PLC DID method: %s", did.Method())
76
95
}
77
96
78
-
url := fmt.Sprintf("%s/%s/log", plcURL, did)
97
+
url := fmt.Sprintf("%s/%s/log", plcHost, did)
79
98
resp, err := http.Get(url)
80
99
if err != nil {
81
100
return err
82
101
}
102
+
defer resp.Body.Close()
83
103
if resp.StatusCode != http.StatusOK {
84
104
return fmt.Errorf("PLC HTTP request failed")
85
105
}
···
106
126
return nil
107
127
}
108
128
109
-
var cmdPLCDump = &cli.Command{
110
-
Name: "dump",
111
-
Usage: "output full operation log, as JSON lines",
112
-
Flags: []cli.Flag{
113
-
&cli.StringFlag{
114
-
Name: "cursor",
115
-
},
116
-
&cli.BoolFlag{
117
-
Name: "tail",
118
-
},
119
-
},
120
-
Action: runPLCDump,
129
+
func runPLCData(cctx *cli.Context) error {
130
+
ctx := context.Background()
131
+
plcHost := cctx.String("plc-host")
132
+
s := cctx.Args().First()
133
+
if s == "" {
134
+
return fmt.Errorf("need to provide account identifier as an argument")
135
+
}
136
+
137
+
dir := identity.BaseDirectory{
138
+
PLCURL: plcHost,
139
+
}
140
+
141
+
id, err := syntax.ParseAtIdentifier(s)
142
+
if err != nil {
143
+
return err
144
+
}
145
+
var did syntax.DID
146
+
if id.IsDID() {
147
+
did, err = id.AsDID()
148
+
if err != nil {
149
+
return err
150
+
}
151
+
} else {
152
+
hdl, err := id.AsHandle()
153
+
if err != nil {
154
+
return err
155
+
}
156
+
did, err = dir.ResolveHandle(ctx, hdl)
157
+
if err != nil {
158
+
return err
159
+
}
160
+
}
161
+
162
+
if did.Method() != "plc" {
163
+
return fmt.Errorf("non-PLC DID method: %s", did.Method())
164
+
}
165
+
166
+
plcData, err := fetchPLCData(ctx, plcHost, did)
167
+
if err != nil {
168
+
return err
169
+
}
170
+
171
+
b, err := json.MarshalIndent(plcData, "", " ")
172
+
if err != nil {
173
+
return err
174
+
}
175
+
fmt.Println(string(b))
176
+
return nil
121
177
}
122
178
123
179
func runPLCDump(cctx *cli.Context) error {
124
180
ctx := context.Background()
125
-
plcURL := cctx.String("plc-directory")
181
+
plcHost := cctx.String("plc-host")
126
182
client := http.DefaultClient
127
183
tailMode := cctx.Bool("tail")
128
184
···
132
188
}
133
189
var lastCursor string
134
190
135
-
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/export", plcURL), nil)
191
+
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/export", plcHost), nil)
136
192
if err != nil {
137
193
return err
138
194
}
···
203
259
204
260
return nil
205
261
}
262
+
263
+
type PLCService struct {
264
+
Type string `json:"type"`
265
+
Endpoint string `json:"endpoint"`
266
+
}
267
+
268
+
type PLCData struct {
269
+
DID string `json:"did"`
270
+
VerificationMethods map[string]string `json:"verificationMethods"`
271
+
RotationKeys []string `json:"rotationKeys"`
272
+
AlsoKnownAs []string `json:"alsoKnownAs"`
273
+
Services map[string]PLCService `json:"services"`
274
+
}
275
+
276
+
func fetchPLCData(ctx context.Context, plcHost string, did syntax.DID) (*PLCData, error) {
277
+
278
+
if plcHost == "" {
279
+
return nil, fmt.Errorf("PLC host not configured")
280
+
}
281
+
282
+
url := fmt.Sprintf("%s/%s/data", plcHost, did)
283
+
resp, err := http.Get(url)
284
+
if err != nil {
285
+
return nil, err
286
+
}
287
+
defer resp.Body.Close()
288
+
if resp.StatusCode != http.StatusOK {
289
+
return nil, fmt.Errorf("PLC HTTP request failed")
290
+
}
291
+
respBytes, err := io.ReadAll(resp.Body)
292
+
if err != nil {
293
+
return nil, err
294
+
}
295
+
296
+
var d PLCData
297
+
err = json.Unmarshal(respBytes, &d)
298
+
if err != nil {
299
+
return nil, err
300
+
}
301
+
return &d, nil
302
+
}