-
Notifications
You must be signed in to change notification settings - Fork 85
/
did.go
144 lines (127 loc) · 4.54 KB
/
did.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package identity
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
)
type DIDDocument struct {
DID syntax.DID `json:"id"`
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
VerificationMethod []DocVerificationMethod `json:"verificationMethod,omitempty"`
Service []DocService `json:"service,omitempty"`
}
type DocVerificationMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}
type DocService struct {
ID string `json:"id"`
Type string `json:"type"`
ServiceEndpoint string `json:"serviceEndpoint"`
}
// WARNING: this does *not* bi-directionally verify account metadata; it only implements direct DID-to-DID-document lookup for the supported DID methods, and parses the resulting DID Doc into an Identity struct
func (d *BaseDirectory) ResolveDID(ctx context.Context, did syntax.DID) (*DIDDocument, error) {
start := time.Now()
switch did.Method() {
case "web":
doc, err := d.ResolveDIDWeb(ctx, did)
elapsed := time.Since(start)
slog.Debug("resolve DID", "did", did, "err", err, "duration_ms", elapsed.Milliseconds())
return doc, err
case "plc":
doc, err := d.ResolveDIDPLC(ctx, did)
elapsed := time.Since(start)
slog.Debug("resolve DID", "did", did, "err", err, "duration_ms", elapsed.Milliseconds())
return doc, err
default:
return nil, fmt.Errorf("DID method not supported: %s", did.Method())
}
}
func (d *BaseDirectory) ResolveDIDWeb(ctx context.Context, did syntax.DID) (*DIDDocument, error) {
if did.Method() != "web" {
return nil, fmt.Errorf("expected a did:web, got: %s", did)
}
hostname := did.Identifier()
handle, err := syntax.ParseHandle(hostname)
if err != nil {
return nil, fmt.Errorf("did:web identifier not a simple hostname: %s", hostname)
}
if !handle.AllowedTLD() {
return nil, fmt.Errorf("did:web hostname has disallowed TLD: %s", hostname)
}
// TODO: allow ctx to specify unsafe http:// resolution, for testing?
if d.DIDWebLimitFunc != nil {
if err := d.DIDWebLimitFunc(ctx, hostname); err != nil {
return nil, fmt.Errorf("did:web limit func returned an error for (%s): %w", hostname, err)
}
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+hostname+"/.well-known/did.json", nil)
if err != nil {
return nil, fmt.Errorf("constructing HTTP request for did:web resolution: %w", err)
}
resp, err := d.HTTPClient.Do(req)
// look for NXDOMAIN
var dnsErr *net.DNSError
if errors.As(err, &dnsErr) {
if dnsErr.IsNotFound {
return nil, fmt.Errorf("%w: DNS NXDOMAIN", ErrDIDNotFound)
}
}
if err != nil {
return nil, fmt.Errorf("%w: did:web HTTP well-known fetch: %w", ErrDIDResolutionFailed, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("%w: did:web HTTP status 404", ErrDIDNotFound)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: did:web HTTP status %d", ErrDIDResolutionFailed, resp.StatusCode)
}
var doc DIDDocument
if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
return nil, fmt.Errorf("%w: JSON DID document parse: %w", ErrDIDResolutionFailed, err)
}
return &doc, nil
}
func (d *BaseDirectory) ResolveDIDPLC(ctx context.Context, did syntax.DID) (*DIDDocument, error) {
if did.Method() != "plc" {
return nil, fmt.Errorf("expected a did:plc, got: %s", did)
}
plcURL := d.PLCURL
if plcURL == "" {
plcURL = DefaultPLCURL
}
if d.PLCLimiter != nil {
if err := d.PLCLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("failed to wait for PLC limiter: %w", err)
}
}
req, err := http.NewRequestWithContext(ctx, "GET", plcURL+"/"+did.String(), nil)
if err != nil {
return nil, fmt.Errorf("constructing HTTP request for did:plc resolution: %w", err)
}
resp, err := d.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("%w: PLC directory lookup: %w", ErrDIDResolutionFailed, err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("%w: PLC directory 404", ErrDIDNotFound)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: PLC directory status %d", ErrDIDResolutionFailed, resp.StatusCode)
}
var doc DIDDocument
if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
return nil, fmt.Errorf("%w: JSON DID document parse: %w", ErrDIDResolutionFailed, err)
}
return &doc, nil
}