···1212 "github.com/bluesky-social/indigo/atproto/syntax"
1313)
14141515+// Indicates that resolution process completed successfully, but handle does not exist.
1516var ErrHandleNotFound = errors.New("handle not found")
16171718// Does not cross-verify, just does the handle resolution step.
1819func ResolveHandleDNS(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
2020+ // TODO: timeout
2121+ // TODO: mechanism to control resolution; context? separate method?
19222023 res, err := net.LookupTXT("_atproto." + handle.String())
2424+ // look for NXDOMAIN
2525+ var dnsErr *net.DNSError
2626+ if errors.As(err, &dnsErr) {
2727+ if dnsErr.IsNotFound {
2828+ return "", ErrHandleNotFound
2929+ }
3030+ }
2131 if err != nil {
2232 return "", fmt.Errorf("handle DNS resolution failed: %w", err)
2333 }
···3646}
37473848func ResolveHandleWellKnown(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
3939- // NOTE: could pull a client or transport from context
4949+ // TODO: could pull a client or transport from context?
5050+ // TODO: timeout
4051 c := http.DefaultClient
41524253 req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/.well-known/atproto-did", handle), nil)
···47584859 resp, err := c.Do(req)
4960 if err != nil {
6161+ // look for NXDOMAIN
6262+ var dnsErr *net.DNSError
6363+ if errors.As(err, &dnsErr) {
6464+ if dnsErr.IsNotFound {
6565+ return "", ErrHandleNotFound
6666+ }
6767+ }
5068 return "", fmt.Errorf("failed to resolve handle (%s) through HTTP well-known route: %s", handle, err)
5169 }
5270 if resp.StatusCode != 200 {
···6482 line := strings.TrimSpace(string(b))
6583 return syntax.ParseDID(line)
6684}
8585+8686+func ResolveHandle(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
8787+ did, dnsErr := ResolveHandleDNS(ctx, handle)
8888+ if dnsErr == nil {
8989+ return did, nil
9090+ }
9191+ did, httpErr := ResolveHandleWellKnown(ctx, handle)
9292+ if httpErr == nil {
9393+ return did, nil
9494+ }
9595+9696+ // return the most specific/helpful error
9797+ if dnsErr != ErrHandleNotFound {
9898+ return "", dnsErr
9999+ }
100100+ if httpErr != ErrHandleNotFound {
101101+ return "", httpErr
102102+ }
103103+ return "", dnsErr
104104+}
+126-19
atproto/identity/identity.go
···11package identity
2233import (
44+ "context"
55+ "fmt"
66+ "strings"
77+88+ "github.com/bluesky-social/indigo/atproto/crypto"
49 "github.com/bluesky-social/indigo/atproto/syntax"
1010+1111+ "github.com/mr-tron/base58"
512)
613714// API for doing account lookups by DID or handle, with bi-directional verification handled automatically.
815//
99-// Handles which fail to resolve or don't match DID alsoKnownAs are an error. DIDs which resolve but the handle does not resolve back to the DID return an Account where the Handle is the special `handle.invalid` value.
1616+// Handles which fail to resolve or don't match DID alsoKnownAs are an error. DIDs which resolve but the handle does not resolve back to the DID return an Identity where the Handle is the special `handle.invalid` value.
1017//
1118// Some example implementations of this interface would be:
1219// - naive direct resolution on every call
1320// - API client, which just makes requests to PDS (or other remote service)
1421// - simple in-memory caching wrapper layer to reduce network hits
1522// - services with backing datastore to do sophisticated caching, TTL, auto-refresh, etc
1616-type AccountCatalog interface {
1717- LookupHandle(h syntax.Handle) (*Account, error)
1818- LookupDID(d syntax.DID) (*Account, error)
2323+type Catalog interface {
2424+ LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error)
2525+ LookupDID(ctx context.Context, d syntax.DID) (*Identity, error)
2626+ Lookup(ctx context.Context, i syntax.AtIdentifier) (*Identity, error)
2727+ // TODO: add "flush" methods to purge caches?
2828+}
2929+3030+var DefaultPLCURL = "https://plc.directory"
3131+3232+func DefaultCatalog() Catalog {
3333+ cat := NewCacheCatalog(DefaultPLCURL)
3434+ return &cat
1935}
20363737+// TODO: DIDHistory() helper, returns log of declared handle, PDS location, and public key? or maybe this is something best left to did-method-plc helper library
3838+2139// Represents an atproto identity. Could be a regular user account, or a service account (eg, feed generator)
2222-type Account struct {
2323- // these fields are required and non-nullable
2424- DID syntax.DID
4040+type Identity struct {
4141+ DID syntax.DID
4242+4343+ // Handle/DID mapping must be bi-directionally verified. If that fails, the Handle should be the special 'handle.invalid' value
4444+ // TODO: should we make this nullable, instead of 'handle.invalid'?
2545 Handle syntax.Handle
26462727- // these fields are nullable
4747+ // These fields represent a parsed subset of a DID document. They are all nullable.
4848+ // TODO: should we just embed DIDDocument here?
2849 AlsoKnownAs []string
2929- Services map[string]Service
3030- Keys map[string]Key
5050+ // TODO: this doesn't preserve order (doesn't round-trip)
5151+ Services map[string]Service
5252+ // TODO: this doesn't preserve order (doesn't round-trip)
5353+ Keys map[string]Key
5454+5555+ // If a valid atproto repo signing public key was parsed, it can be cached here. This is nullable/optional.
5656+ ParsedPublicKey crypto.PublicKey
3157}
32583359type Key struct {
···4066 URL string
4167}
42684343-func (a *Account) SigningKey() *Key {
4444- if a.Keys == nil {
4545- return nil
6969+func ParseIdentity(doc *DIDDocument) Identity {
7070+ keys := make(map[string]Key, len(doc.VerificationMethod))
7171+ for _, vm := range doc.VerificationMethod {
7272+ parts := strings.SplitN(vm.ID, "#", 2)
7373+ if len(parts) < 2 {
7474+ continue
7575+ }
7676+ // ignore keys not controlled by this DID itself
7777+ if vm.Controller != doc.DID.String() {
7878+ continue
7979+ }
8080+ // don't want to clobber existing entries with same ID fragment
8181+ if _, ok := keys[parts[1]]; ok {
8282+ continue
8383+ }
8484+ // TODO: verify that ID and type match for atproto-specific services?
8585+ keys[parts[1]] = Key{
8686+ Type: vm.Type,
8787+ PublicKeyMultibase: vm.PublicKeyMultibase,
8888+ }
8989+ }
9090+ svc := make(map[string]Service, len(doc.Service))
9191+ for _, s := range doc.Service {
9292+ parts := strings.SplitN(s.ID, "#", 2)
9393+ if len(parts) < 2 {
9494+ continue
9595+ }
9696+ // don't want to clobber existing entries with same ID fragment
9797+ if _, ok := svc[parts[1]]; ok {
9898+ continue
9999+ }
100100+ // TODO: verify that ID and type match for atproto-specific services?
101101+ svc[parts[1]] = Service{
102102+ Type: s.Type,
103103+ URL: s.ServiceEndpoint,
104104+ }
46105 }
4747- atp, ok := a.Keys["atproto"]
106106+ return Identity{
107107+ DID: doc.DID,
108108+ Handle: syntax.Handle("invalid.handle"),
109109+ AlsoKnownAs: doc.AlsoKnownAs,
110110+ Services: svc,
111111+ Keys: keys,
112112+ }
113113+}
114114+115115+func (i *Identity) PublicKey() (crypto.PublicKey, error) {
116116+ if i.ParsedPublicKey != nil {
117117+ return i.ParsedPublicKey, nil
118118+ }
119119+ if i.Keys == nil {
120120+ return nil, fmt.Errorf("identity has no atproto public key attached")
121121+ }
122122+ k, ok := i.Keys["atproto"]
48123 if !ok {
4949- return nil
124124+ return nil, fmt.Errorf("identity has no atproto public key attached")
50125 }
5151- return &atp
126126+ switch k.Type {
127127+ case "Multikey":
128128+ return crypto.ParsePublicMultibase(k.PublicKeyMultibase)
129129+ case "EcdsaSecp256r1VerificationKey2019":
130130+ if len(k.PublicKeyMultibase) < 2 || k.PublicKeyMultibase[0] != 'z' {
131131+ return nil, fmt.Errorf("identity key not a multibase base58btc string")
132132+ }
133133+ keyBytes, err := base58.Decode(k.PublicKeyMultibase[1:])
134134+ if err != nil {
135135+ return nil, fmt.Errorf("identity key multibase parsing: %w", err)
136136+ }
137137+ return crypto.ParsePublicUncompressedBytesP256(keyBytes)
138138+ case "EcdsaSecp256k1VerificationKey2019":
139139+ if len(k.PublicKeyMultibase) < 2 || k.PublicKeyMultibase[0] != 'z' {
140140+ return nil, fmt.Errorf("identity key not a multibase base58btc string")
141141+ }
142142+ keyBytes, err := base58.Decode(k.PublicKeyMultibase[1:])
143143+ if err != nil {
144144+ return nil, fmt.Errorf("identity key multibase parsing: %w", err)
145145+ }
146146+ return crypto.ParsePublicUncompressedBytesK256(keyBytes)
147147+ default:
148148+ return nil, fmt.Errorf("unsupported atproto public key type: %s", k.Type)
149149+ }
52150}
531515454-func (a *Account) PDS() string {
5555- if a.Services == nil {
152152+func (i *Identity) PDSEndpoint() string {
153153+ if i.Services == nil {
56154 return ""
57155 }
5858- atp, ok := a.Services["atproto_pds"]
156156+ atp, ok := i.Services["atproto_pds"]
59157 if !ok {
60158 return ""
61159 }
62160 return atp.URL
63161}
162162+163163+func (i *Identity) DeclaredHandle() (syntax.Handle, error) {
164164+ for _, u := range i.AlsoKnownAs {
165165+ if strings.HasPrefix(u, "at://") && len(u) > len("at://") {
166166+ return syntax.ParseHandle(u[5:])
167167+ }
168168+ }
169169+ return "", fmt.Errorf("DID document contains no atproto handle")
170170+}