porting all github actions from bluesky-social/indigo to tangled CI
at main 7.1 kB view raw
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}