This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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