go scratch code for atproto
at backfill 471 lines 12 kB view raw
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}