Monorepo for Tangled
at master 122 lines 3.2 kB view raw
1package repodid 2 3import ( 4 "context" 5 "fmt" 6 "net/url" 7 "strings" 8 9 atcrypto "github.com/bluesky-social/indigo/atproto/crypto" 10 "github.com/did-method-plc/go-didplc" 11 "tangled.org/core/idresolver" 12) 13 14type PreparedDID struct { 15 RepoDid string 16 SigningKeyRaw []byte 17 op *didplc.RegularOp 18 plcUrl string 19} 20 21func PrepareRepoDID(plcUrl, knotServiceUrl string) (*PreparedDID, error) { 22 if plcUrl == "" { 23 return nil, fmt.Errorf("PLC directory URL is not configured") 24 } 25 parsed, parseErr := url.Parse(plcUrl) 26 if parseErr != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { 27 return nil, fmt.Errorf("PLC directory URL is invalid: %q", plcUrl) 28 } 29 30 privKey, err := atcrypto.GeneratePrivateKeyK256() 31 if err != nil { 32 return nil, fmt.Errorf("generating signing key: %w", err) 33 } 34 35 pubKey, err := privKey.PublicKey() 36 if err != nil { 37 return nil, fmt.Errorf("deriving public key: %w", err) 38 } 39 40 didKey := pubKey.DIDKey() 41 42 op := didplc.RegularOp{ 43 Type: "plc_operation", 44 RotationKeys: []string{didKey}, 45 VerificationMethods: map[string]string{ 46 "atproto": didKey, 47 }, 48 AlsoKnownAs: []string{}, 49 Services: map[string]didplc.OpService{ 50 "atproto_pds": { 51 Type: "AtprotoPersonalDataServer", 52 Endpoint: knotServiceUrl, 53 }, 54 }, 55 Prev: nil, 56 } 57 58 if err := op.Sign(privKey); err != nil { 59 return nil, fmt.Errorf("signing genesis op: %w", err) 60 } 61 62 repoDid, err := op.DID() 63 if err != nil { 64 return nil, fmt.Errorf("deriving DID from genesis: %w", err) 65 } 66 67 return &PreparedDID{ 68 RepoDid: repoDid, 69 SigningKeyRaw: privKey.Bytes(), 70 op: &op, 71 plcUrl: plcUrl, 72 }, nil 73} 74 75func (p *PreparedDID) Submit(ctx context.Context) error { 76 plcClient := didplc.Client{ 77 DirectoryURL: p.plcUrl, 78 UserAgent: "tangled-knot", 79 } 80 if err := plcClient.Submit(ctx, p.RepoDid, p.op); err != nil { 81 return fmt.Errorf("submitting to PLC directory: %w", err) 82 } 83 return nil 84} 85 86const maxDidWebLength = 256 87 88func VerifyRepoDIDWeb(ctx context.Context, resolver *idresolver.Resolver, repoDid, knotServiceUrl string) error { 89 if !strings.HasPrefix(repoDid, "did:web:") { 90 return fmt.Errorf("expected did:web, got: %s", repoDid) 91 } 92 93 if len(repoDid) > maxDidWebLength { 94 return fmt.Errorf("did:web exceeds maximum length of %d characters", maxDidWebLength) 95 } 96 97 authority := strings.TrimPrefix(repoDid, "did:web:") 98 if colonIdx := strings.IndexByte(authority, ':'); colonIdx >= 0 { 99 authority = authority[:colonIdx] 100 } 101 if authority == "" || strings.ContainsAny(authority, "/#?@ ") { 102 return fmt.Errorf("did:web has invalid authority: %s", repoDid) 103 } 104 105 ident, err := resolver.ResolveIdent(ctx, repoDid) 106 if err != nil { 107 return fmt.Errorf("resolving did:web document: %w", err) 108 } 109 110 if strings.TrimRight(ident.PDSEndpoint(), "/") != strings.TrimRight(knotServiceUrl, "/") { 111 return fmt.Errorf( 112 "did:web service endpoint %q does not match this knot %q", 113 ident.PDSEndpoint(), knotServiceUrl, 114 ) 115 } 116 117 if _, err := ident.PublicKey(); err != nil { 118 return fmt.Errorf("did:web document missing valid atproto verification method: %w", err) 119 } 120 121 return nil 122}