go scratch code for atproto
1package didplc
2
3import (
4 "crypto/sha256"
5 "encoding/base32"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "strings"
11
12 "github.com/bluesky-social/indigo/atproto/crypto"
13
14 "github.com/ipfs/go-cid"
15 cbor "github.com/ipfs/go-ipld-cbor"
16)
17
18type Operation interface {
19 // CID of the full (signed) operation
20 CID() cid.Cid
21 // serializes a copy of the op as CBOR, with the `sig` field omitted
22 UnsignedCBORBytes() []byte
23 // serializes a copy of the op as CBOR, with the `sig` field included
24 SignedCBORBytes() []byte
25 // whether this operation is a genesis (creation) op
26 IsGenesis() bool
27 // whether this operation has a signature or is unsigned
28 IsSigned() bool
29 // returns the DID for a genesis op (errors if this op is not a genesis op)
30 DID() (string, error)
31 // signs the object in-place
32 Sign(priv crypto.PrivateKey) error
33 // verifiy signature. returns crypto.ErrInvalidSignature if appropriate
34 VerifySignature(pub crypto.PublicKey) error
35 // returns a DID doc
36 Doc(did string) (Doc, error)
37}
38
39type OpService struct {
40 Type string `json:"type" cborgen:"type"`
41 Endpoint string `json:"endpoint" cborgen:"endpoint"`
42}
43
44type RegularOp struct {
45 Type string `json:"type,const=plc_operation" cborgen:"type,const=plc_operation"`
46 RotationKeys []string `json:"rotationKeys" cborgen:"rotationKeys"`
47 VerificationMethods map[string]string `json:"verificationMethods" cborgen:"verificationMethods"`
48 AlsoKnownAs []string `json:"alsoKnownAs" cborgen:"alsoKnownAs"`
49 Services map[string]OpService `json:"services" cborgen:"services"`
50 Prev *string `json:"prev" cborgen:"prev"`
51 Sig *string `json:"sig,omitempty" cborgen:"sig,omitempty" refmt:"sig,omitempty"`
52}
53
54type TombstoneOp struct {
55 Type string `json:"type,const=plc_tombstone" cborgen:"type,const=plc_tombstone"`
56 Prev string `json:"prev" cborgen:"prev"`
57 Sig *string `json:"sig,omitempty" cborgen:"sig,omitempty" refmt:"sig,omitempty"`
58}
59
60type LegacyOp struct {
61 Type string `json:"type,const=create" cborgen:"type,const=create"`
62 SigningKey string `json:"signingKey" cborgen:"signingKey"`
63 RecoveryKey string `json:"recoveryKey" cborgen:"recoveryKey"`
64 Handle string `json:"handle" cborgen:"handle"`
65 Service string `json:"service" cborgen:"service"`
66 Prev *string `json:"prev" cborgen:"prev"`
67 Sig *string `json:"sig,omitempty" cborgen:"sig,omitempty" refmt:"sig,omitempty"`
68}
69
70var _ Operation = (*RegularOp)(nil)
71var _ Operation = (*TombstoneOp)(nil)
72var _ Operation = (*LegacyOp)(nil)
73
74// any of: Op, TombstoneOp, or LegacyOp
75type OpEnum struct {
76 Regular *RegularOp
77 Tombstone *TombstoneOp
78 Legacy *LegacyOp
79}
80
81var ErrNotGenesisOp = errors.New("not a genesis PLC operation")
82
83func init() {
84 cbor.RegisterCborType(OpService{})
85 cbor.RegisterCborType(RegularOp{})
86 cbor.RegisterCborType(TombstoneOp{})
87 cbor.RegisterCborType(LegacyOp{})
88}
89
90func computeCID(b []byte) cid.Cid {
91 cidBuilder := cid.V1Builder{Codec: 0x71, MhType: 0x12, MhLength: 0}
92 c, err := cidBuilder.Sum(b)
93 if err != nil {
94 return cid.Undef
95 }
96 return c
97}
98
99func (op *RegularOp) CID() cid.Cid {
100 return computeCID(op.SignedCBORBytes())
101}
102
103func (op *RegularOp) UnsignedCBORBytes() []byte {
104 unsigned := RegularOp{
105 Type: op.Type,
106 RotationKeys: op.RotationKeys,
107 VerificationMethods: op.VerificationMethods,
108 AlsoKnownAs: op.AlsoKnownAs,
109 Services: op.Services,
110 Prev: op.Prev,
111 Sig: nil,
112 }
113
114 out, err := cbor.DumpObject(unsigned)
115 if err != nil {
116 return nil
117 }
118 return out
119}
120
121func (op *RegularOp) SignedCBORBytes() []byte {
122 out, err := cbor.DumpObject(op)
123 if err != nil {
124 return nil
125 }
126 return out
127}
128
129func (op *RegularOp) IsGenesis() bool {
130 return op.Prev == nil
131}
132
133func (op *RegularOp) IsSigned() bool {
134 return op.Sig != nil && *op.Sig != ""
135}
136
137func (op *RegularOp) DID() (string, error) {
138 if !op.IsGenesis() {
139 return "", ErrNotGenesisOp
140 }
141 hash := sha256.Sum256(op.SignedCBORBytes())
142 suffix := base32.StdEncoding.EncodeToString(hash[:])[:24]
143 return "did:plc:" + strings.ToLower(suffix), nil
144}
145
146func signOp(op Operation, priv crypto.PrivateKey) (string, error) {
147 b := op.UnsignedCBORBytes()
148 sig, err := priv.HashAndSign(b)
149 if err != nil {
150 return "", err
151 }
152 b64 := base64.RawURLEncoding.EncodeToString(sig)
153 return b64, nil
154}
155
156func (op *RegularOp) Sign(priv crypto.PrivateKey) error {
157 sig, err := signOp(op, priv)
158 if err != nil {
159 return err
160 }
161 op.Sig = &sig
162 return nil
163}
164
165func verifySigOp(op Operation, pub crypto.PublicKey, sig *string) error {
166 if sig == nil || *sig == "" {
167 return fmt.Errorf("can't verify empty signature")
168 }
169 b := op.UnsignedCBORBytes()
170 sigBytes, err := base64.RawURLEncoding.DecodeString(*sig)
171 if err != nil {
172 return err
173 }
174 return pub.HashAndVerify(b, sigBytes)
175}
176
177// parsing errors are not ignored (will be returned immediately if found)
178func VerifySignatureAny(op Operation, didKeys []string) error {
179 if len(didKeys) == 0 {
180 return fmt.Errorf("no keys to verify against")
181 }
182 for _, dk := range didKeys {
183 pub, err := crypto.ParsePublicDIDKey(dk)
184 if err != nil {
185 return err
186 }
187 err = op.VerifySignature(pub)
188 if err != crypto.ErrInvalidSignature {
189 return err
190 }
191 if nil == err {
192 return nil
193 }
194 }
195 return crypto.ErrInvalidSignature
196}
197
198func (op *RegularOp) VerifySignature(pub crypto.PublicKey) error {
199 return verifySigOp(op, pub, op.Sig)
200}
201
202func (op *RegularOp) Doc(did string) (Doc, error) {
203 svc := []DocService{}
204 for key, s := range op.Services {
205 svc = append(svc, DocService{
206 ID: did + "#" + key,
207 Type: s.Type,
208 ServiceEndpoint: s.Endpoint,
209 })
210 }
211 vm := []DocVerificationMethod{}
212 for name, didKey := range op.VerificationMethods {
213 pub, err := crypto.ParsePublicDIDKey(didKey)
214 if err != nil {
215 return Doc{}, err
216 }
217 vm = append(vm, DocVerificationMethod{
218 ID: did + "#" + name,
219 Type: "Multikey",
220 Controller: did,
221 PublicKeyMultibase: pub.Multibase(),
222 })
223 }
224 doc := Doc{
225 ID: did,
226 AlsoKnownAs: op.AlsoKnownAs,
227 VerificationMethod: vm,
228 Service: svc,
229 }
230 return doc, nil
231}
232
233func (op *LegacyOp) CID() cid.Cid {
234 return computeCID(op.SignedCBORBytes())
235}
236
237func (op *LegacyOp) UnsignedCBORBytes() []byte {
238 unsigned := LegacyOp{
239 Type: op.Type,
240 SigningKey: op.SigningKey,
241 RecoveryKey: op.RecoveryKey,
242 Handle: op.Handle,
243 Service: op.Service,
244 Prev: op.Prev,
245 Sig: nil,
246 }
247 out, err := cbor.DumpObject(unsigned)
248 if err != nil {
249 return nil
250 }
251 return out
252}
253
254func (op *LegacyOp) SignedCBORBytes() []byte {
255 out, err := cbor.DumpObject(op)
256 if err != nil {
257 return nil
258 }
259 return out
260}
261
262func (op *LegacyOp) IsGenesis() bool {
263 return op.Prev == nil
264}
265
266func (op *LegacyOp) IsSigned() bool {
267 return op.Sig != nil && *op.Sig != ""
268}
269
270func (op *LegacyOp) DID() (string, error) {
271 if !op.IsGenesis() {
272 return "", ErrNotGenesisOp
273 }
274 hash := sha256.Sum256(op.SignedCBORBytes())
275 suffix := base32.StdEncoding.EncodeToString(hash[:])[:24]
276 return "did:plc:" + strings.ToLower(suffix), nil
277}
278
279func (op *LegacyOp) Sign(priv crypto.PrivateKey) error {
280 sig, err := signOp(op, priv)
281 if err != nil {
282 return err
283 }
284 op.Sig = &sig
285 return nil
286}
287
288func (op *LegacyOp) VerifySignature(pub crypto.PublicKey) error {
289 return verifySigOp(op, pub, op.Sig)
290}
291
292func (op *LegacyOp) Doc(did string) (Doc, error) {
293 // NOTE: could re-implement this by calling op.RegularOp().Doc()
294 svc := []DocService{
295 DocService{
296 ID: did + "#atproto_pds",
297 Type: "AtprotoPersonalDataServer",
298 ServiceEndpoint: op.Service,
299 },
300 }
301 vm := []DocVerificationMethod{
302 DocVerificationMethod{
303 ID: did + "#atproto",
304 Type: "Multikey",
305 Controller: did,
306 PublicKeyMultibase: strings.TrimPrefix(op.SigningKey, "did:key:"),
307 },
308 }
309 doc := Doc{
310 ID: did,
311 AlsoKnownAs: []string{"at://" + op.Handle},
312 VerificationMethod: vm,
313 Service: svc,
314 }
315 return doc, nil
316}
317
318// converts a legacy "create" op to an (unsigned) "plc_operation"
319func (op *LegacyOp) RegularOp() RegularOp {
320 return RegularOp{
321 RotationKeys: []string{op.RecoveryKey},
322 VerificationMethods: map[string]string{
323 "atproto": op.SigningKey,
324 },
325 AlsoKnownAs: []string{"at://" + op.Handle},
326 Services: map[string]OpService{
327 "atproto_pds": OpService{
328 Type: "AtprotoPersonalDataServer",
329 Endpoint: op.Service,
330 },
331 },
332 Prev: nil, // always a create
333 Sig: nil, // don't have private key
334 }
335}
336
337func (op *TombstoneOp) CID() cid.Cid {
338 return computeCID(op.SignedCBORBytes())
339}
340
341func (op *TombstoneOp) UnsignedCBORBytes() []byte {
342 unsigned := TombstoneOp{
343 Type: op.Type,
344 Prev: op.Prev,
345 Sig: nil,
346 }
347 out, err := cbor.DumpObject(unsigned)
348 if err != nil {
349 return nil
350 }
351 return out
352}
353
354func (op *TombstoneOp) SignedCBORBytes() []byte {
355 out, err := cbor.DumpObject(op)
356 if err != nil {
357 return nil
358 }
359 return out
360}
361
362func (op *TombstoneOp) IsGenesis() bool {
363 return false
364}
365
366func (op *TombstoneOp) IsSigned() bool {
367 return op.Sig != nil && *op.Sig != ""
368}
369
370func (op *TombstoneOp) DID() (string, error) {
371 return "", ErrNotGenesisOp
372}
373
374func (op *TombstoneOp) Sign(priv crypto.PrivateKey) error {
375 sig, err := signOp(op, priv)
376 if err != nil {
377 return err
378 }
379 op.Sig = &sig
380 return nil
381}
382
383func (op *TombstoneOp) VerifySignature(pub crypto.PublicKey) error {
384 return verifySigOp(op, pub, op.Sig)
385}
386
387func (op *TombstoneOp) Doc(did string) (Doc, error) {
388 return Doc{}, fmt.Errorf("tombstones do not have a DID document representation")
389}
390
391func (o *OpEnum) MarshalJSON() ([]byte, error) {
392 if o.Regular != nil {
393 return json.Marshal(o.Regular)
394 } else if o.Legacy != nil {
395 return json.Marshal(o.Legacy)
396 } else if o.Tombstone != nil {
397 return json.Marshal(o.Tombstone)
398 }
399 return nil, fmt.Errorf("can't marshal empty OpEnum")
400}
401
402func (o *OpEnum) UnmarshalJSON(b []byte) error {
403 var typeMap map[string]interface{}
404 err := json.Unmarshal(b, &typeMap)
405 if err != nil {
406 return err
407 }
408 typ, ok := typeMap["type"]
409 if !ok {
410 return fmt.Errorf("did not find expected operation 'type' field")
411 }
412
413 switch typ {
414 case "plc_operation":
415 o.Regular = &RegularOp{}
416 return json.Unmarshal(b, o.Regular)
417 case "create":
418 o.Legacy = &LegacyOp{}
419 return json.Unmarshal(b, o.Legacy)
420 case "plc_tombstone":
421 o.Tombstone = &TombstoneOp{}
422 return json.Unmarshal(b, o.Tombstone)
423 default:
424 return fmt.Errorf("unexpected operation type: %s", typ)
425 }
426}
427
428// returns a new signed PLC operation using the provided atproto-specific metdata
429func NewAtproto(priv crypto.PrivateKey, handle string, pdsEndpoint string, rotationKeys []string) (RegularOp, error) {
430
431 pub, err := priv.PublicKey()
432 if err != nil {
433 return RegularOp{}, err
434 }
435 if len(rotationKeys) == 0 {
436 return RegularOp{}, fmt.Errorf("at least one rotation key is required")
437 }
438 handleURI := "at://" + handle
439 op := RegularOp{
440 RotationKeys: rotationKeys,
441 VerificationMethods: map[string]string{
442 "atproto": pub.DIDKey(),
443 },
444 AlsoKnownAs: []string{handleURI},
445 Services: map[string]OpService{
446 "atproto_pds": OpService{
447 Type: "AtprotoPersonalDataServer",
448 Endpoint: pdsEndpoint,
449 },
450 },
451 Prev: nil,
452 Sig: nil,
453 }
454 if err := op.Sign(priv); err != nil {
455 return RegularOp{}, err
456 }
457 return op, nil
458}
459
460func (oe *OpEnum) AsOperation() Operation {
461 if oe.Regular != nil {
462 return oe.Regular
463 } else if oe.Legacy != nil {
464 return oe.Legacy
465 } else if oe.Tombstone != nil {
466 return oe.Tombstone
467 } else {
468 // TODO; something more safe here?
469 return nil
470 }
471}