1package identity
2
3import (
4 "fmt"
5 "net/url"
6 "strings"
7
8 "github.com/bluesky-social/indigo/atproto/crypto"
9 "github.com/bluesky-social/indigo/atproto/syntax"
10
11 "github.com/mr-tron/base58"
12)
13
14// Represents an atproto identity. Could be a regular user account, or a service account (eg, feed generator)
15type Identity struct {
16 DID syntax.DID
17
18 // Handle/DID mapping must be bi-directionally verified. If that fails, the Handle should be the special 'handle.invalid' value
19 Handle syntax.Handle
20
21 // These fields represent a parsed subset of a DID document. They are all nullable. Note that the services and keys maps do not preserve order, so they don't exactly round-trip DID documents.
22 AlsoKnownAs []string
23 Services map[string]ServiceEndpoint
24 Keys map[string]VerificationMethod
25}
26
27// Sub-field type for [Identity], representing a crytographic public key declared as a "verificationMethod" in the DID document.
28type VerificationMethod struct {
29 Type string
30 PublicKeyMultibase string
31}
32
33// Sub-field type for [Identity], representing a service endpoint URL declared in the DID document.
34type ServiceEndpoint struct {
35 Type string
36 URL string
37}
38
39// Extracts the information relevant to atproto from an arbitrary DID document.
40//
41// Always returns an invalid Handle field; calling code should only populate that field if it has been bi-directionally verified.
42func ParseIdentity(doc *DIDDocument) Identity {
43 keys := make(map[string]VerificationMethod, len(doc.VerificationMethod))
44 for _, vm := range doc.VerificationMethod {
45 parts := strings.SplitN(vm.ID, "#", 2)
46 if len(parts) < 2 {
47 continue
48 }
49 // ignore keys not controlled by this DID itself
50 if vm.Controller != doc.DID.String() {
51 continue
52 }
53 // don't want to clobber existing entries with same ID fragment
54 if _, ok := keys[parts[1]]; ok {
55 continue
56 }
57 // TODO: verify that ID and type match for atproto-specific services?
58 keys[parts[1]] = VerificationMethod{
59 Type: vm.Type,
60 PublicKeyMultibase: vm.PublicKeyMultibase,
61 }
62 }
63 svc := make(map[string]ServiceEndpoint, len(doc.Service))
64 for _, s := range doc.Service {
65 parts := strings.SplitN(s.ID, "#", 2)
66 if len(parts) < 2 {
67 continue
68 }
69 // don't want to clobber existing entries with same ID fragment
70 if _, ok := svc[parts[1]]; ok {
71 continue
72 }
73 // TODO: verify that ID and type match for atproto-specific services?
74 svc[parts[1]] = ServiceEndpoint{
75 Type: s.Type,
76 URL: s.ServiceEndpoint,
77 }
78 }
79 return Identity{
80 DID: doc.DID,
81 Handle: syntax.HandleInvalid,
82 AlsoKnownAs: doc.AlsoKnownAs,
83 Services: svc,
84 Keys: keys,
85 }
86}
87
88// Helper to generate a DID document based on an identity. Note that there is flexibility around parsing, and this won't necessarily "round-trip" for every valid DID document.
89func (ident *Identity) DIDDocument() DIDDocument {
90 doc := DIDDocument{
91 DID: ident.DID,
92 AlsoKnownAs: ident.AlsoKnownAs,
93 VerificationMethod: make([]DocVerificationMethod, len(ident.Keys)),
94 Service: make([]DocService, len(ident.Services)),
95 }
96 i := 0
97 for k, key := range ident.Keys {
98 doc.VerificationMethod[i] = DocVerificationMethod{
99 ID: fmt.Sprintf("%s#%s", ident.DID, k),
100 Type: key.Type,
101 Controller: ident.DID.String(),
102 PublicKeyMultibase: key.PublicKeyMultibase,
103 }
104 i += 1
105 }
106 i = 0
107 for k, svc := range ident.Services {
108 doc.Service[i] = DocService{
109 ID: fmt.Sprintf("#%s", k),
110 Type: svc.Type,
111 ServiceEndpoint: svc.URL,
112 }
113 i += 1
114 }
115 return doc
116}
117
118// Identifies and parses the atproto repo signing public key, specifically, out of any keys in this identity's DID document.
119//
120// Returns [ErrKeyNotFound] if there is no such key.
121//
122// Note that [crypto.PublicKey] is an interface, not a concrete type.
123func (i *Identity) PublicKey() (crypto.PublicKey, error) {
124 return i.GetPublicKey("atproto")
125}
126
127// Identifies and parses a specified service signing public key out of any keys in this identity's DID document.
128//
129// Returns [ErrKeyNotFound] if there is no such key.
130//
131// Note that [crypto.PublicKey] is an interface, not a concrete type.
132func (i *Identity) GetPublicKey(id string) (crypto.PublicKey, error) {
133 if i.Keys == nil {
134 return nil, ErrKeyNotDeclared
135 }
136 k, ok := i.Keys[id]
137 if !ok {
138 return nil, ErrKeyNotDeclared
139 }
140 switch k.Type {
141 case "Multikey":
142 return crypto.ParsePublicMultibase(k.PublicKeyMultibase)
143 case "EcdsaSecp256r1VerificationKey2019":
144 if len(k.PublicKeyMultibase) < 2 || k.PublicKeyMultibase[0] != 'z' {
145 return nil, fmt.Errorf("identity key not a multibase base58btc string")
146 }
147 keyBytes, err := base58.Decode(k.PublicKeyMultibase[1:])
148 if err != nil {
149 return nil, fmt.Errorf("identity key multibase parsing: %w", err)
150 }
151 return crypto.ParsePublicUncompressedBytesP256(keyBytes)
152 case "EcdsaSecp256k1VerificationKey2019":
153 if len(k.PublicKeyMultibase) < 2 || k.PublicKeyMultibase[0] != 'z' {
154 return nil, fmt.Errorf("identity key not a multibase base58btc string")
155 }
156 keyBytes, err := base58.Decode(k.PublicKeyMultibase[1:])
157 if err != nil {
158 return nil, fmt.Errorf("identity key multibase parsing: %w", err)
159 }
160 return crypto.ParsePublicUncompressedBytesK256(keyBytes)
161 default:
162 return nil, fmt.Errorf("unsupported atproto public key type: %s", k.Type)
163 }
164}
165
166// The home PDS endpoint for this identity, if one is included in the DID document.
167//
168// The endpoint should be an HTTP URL with method, hostname, and optional port. It may or may not include path segments.
169//
170// Returns an empty string if the service isn't found, or if the URL fails to parse.
171func (i *Identity) PDSEndpoint() string {
172 return i.GetServiceEndpoint("atproto_pds")
173}
174
175// Returns the service endpoint URL for specified service ID (the fragment part of identifier, not including the hash symbol).
176//
177// The endpoint should be an HTTP URL with method, hostname, and optional port. It may or may not include path segments.
178//
179// Returns an empty string if the service isn't found, or if the URL fails to parse.
180func (i *Identity) GetServiceEndpoint(id string) string {
181 if i.Services == nil {
182 return ""
183 }
184 endpoint, ok := i.Services[id]
185 if !ok {
186 return ""
187 }
188 _, err := url.Parse(endpoint.URL)
189 if err != nil {
190 return ""
191 }
192 return endpoint.URL
193}
194
195// Returns an atproto handle from the alsoKnownAs URI list for this identifier. Returns an error if there is no handle, or if an at:// URI fails to parse as a handle.
196//
197// Note that this handle is *not* necessarily to be trusted, as it may not have been bi-directionally verified. The 'Handle' field on the 'Identity' should contain either a verified handle, or the special 'handle.invalid' indicator value.
198func (i *Identity) DeclaredHandle() (syntax.Handle, error) {
199 for _, u := range i.AlsoKnownAs {
200 if strings.HasPrefix(u, "at://") && len(u) > len("at://") {
201 hdl, err := syntax.ParseHandle(u[5:])
202 if err != nil {
203 continue
204 }
205 return hdl.Normalize(), nil
206 }
207 }
208 return "", ErrHandleNotDeclared
209}