This repository has no description
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}