A community based topic aggregation platform built on atproto
1package auth 2 3import ( 4 "context" 5 "crypto/ecdsa" 6 "crypto/elliptic" 7 "encoding/base64" 8 "fmt" 9 "math/big" 10 "strings" 11 12 "github.com/bluesky-social/indigo/atproto/atcrypto" 13 indigoIdentity "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15) 16 17// DIDKeyFetcher fetches public keys from DID documents for JWT verification. 18// This is the primary method for atproto service authentication, where: 19// - The JWT issuer is the user's DID (e.g., did:plc:abc123) 20// - The signing key is published in the user's DID document 21// - Verification happens by resolving the DID and checking the signature 22type DIDKeyFetcher struct { 23 directory indigoIdentity.Directory 24} 25 26// NewDIDKeyFetcher creates a new DID-based key fetcher. 27func NewDIDKeyFetcher(directory indigoIdentity.Directory) *DIDKeyFetcher { 28 return &DIDKeyFetcher{ 29 directory: directory, 30 } 31} 32 33// FetchPublicKey fetches the public key for verifying a JWT from the issuer's DID document. 34// For DID issuers (did:plc: or did:web:), resolves the DID and extracts the signing key. 35// Returns an *ecdsa.PublicKey suitable for use with jwt-go. 36func (f *DIDKeyFetcher) FetchPublicKey(ctx context.Context, issuer, token string) (interface{}, error) { 37 // Only handle DID issuers 38 if !strings.HasPrefix(issuer, "did:") { 39 return nil, fmt.Errorf("DIDKeyFetcher only handles DID issuers, got: %s", issuer) 40 } 41 42 // Parse the DID 43 did, err := syntax.ParseDID(issuer) 44 if err != nil { 45 return nil, fmt.Errorf("invalid DID format: %w", err) 46 } 47 48 // Resolve the DID to get the identity (includes public keys) 49 ident, err := f.directory.LookupDID(ctx, did) 50 if err != nil { 51 return nil, fmt.Errorf("failed to resolve DID %s: %w", issuer, err) 52 } 53 54 // Get the atproto signing key from the DID document 55 pubKey, err := ident.PublicKey() 56 if err != nil { 57 return nil, fmt.Errorf("failed to get public key from DID document: %w", err) 58 } 59 60 // Convert to JWK format to extract coordinates 61 jwk, err := pubKey.JWK() 62 if err != nil { 63 return nil, fmt.Errorf("failed to convert public key to JWK: %w", err) 64 } 65 66 // Convert atcrypto JWK to Go ecdsa.PublicKey 67 return atcryptoJWKToECDSA(jwk) 68} 69 70// atcryptoJWKToECDSA converts an atcrypto.JWK to a Go ecdsa.PublicKey 71func atcryptoJWKToECDSA(jwk *atcrypto.JWK) (*ecdsa.PublicKey, error) { 72 if jwk.KeyType != "EC" { 73 return nil, fmt.Errorf("unsupported JWK key type: %s (expected EC)", jwk.KeyType) 74 } 75 76 // Decode X and Y coordinates (base64url, no padding) 77 xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X) 78 if err != nil { 79 return nil, fmt.Errorf("invalid JWK X coordinate encoding: %w", err) 80 } 81 yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y) 82 if err != nil { 83 return nil, fmt.Errorf("invalid JWK Y coordinate encoding: %w", err) 84 } 85 86 var ecCurve elliptic.Curve 87 switch jwk.Curve { 88 case "P-256": 89 ecCurve = elliptic.P256() 90 case "P-384": 91 ecCurve = elliptic.P384() 92 case "P-521": 93 ecCurve = elliptic.P521() 94 case "secp256k1": 95 // secp256k1 (K-256) is used by some atproto implementations 96 // Go's standard library doesn't include secp256k1, but we can still 97 // construct the key - jwt-go may not support it directly 98 return nil, fmt.Errorf("secp256k1 curve requires special handling for JWT verification") 99 default: 100 return nil, fmt.Errorf("unsupported JWK curve: %s", jwk.Curve) 101 } 102 103 // Create the public key 104 pubKey := &ecdsa.PublicKey{ 105 Curve: ecCurve, 106 X: new(big.Int).SetBytes(xBytes), 107 Y: new(big.Int).SetBytes(yBytes), 108 } 109 110 // Validate point is on curve 111 if !ecCurve.IsOnCurve(pubKey.X, pubKey.Y) { 112 return nil, fmt.Errorf("invalid public key: point not on curve") 113 } 114 115 return pubKey, nil 116}