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 indigoCrypto "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// 36// Returns: 37// - indigoCrypto.PublicKey for secp256k1 (ES256K) keys - use indigo for verification 38// - *ecdsa.PublicKey for NIST curves (P-256, P-384, P-521) - compatible with golang-jwt 39func (f *DIDKeyFetcher) FetchPublicKey(ctx context.Context, issuer, token string) (interface{}, error) { 40 // Only handle DID issuers 41 if !strings.HasPrefix(issuer, "did:") { 42 return nil, fmt.Errorf("DIDKeyFetcher only handles DID issuers, got: %s", issuer) 43 } 44 45 // Parse the DID 46 did, err := syntax.ParseDID(issuer) 47 if err != nil { 48 return nil, fmt.Errorf("invalid DID format: %w", err) 49 } 50 51 // Resolve the DID to get the identity (includes public keys) 52 ident, err := f.directory.LookupDID(ctx, did) 53 if err != nil { 54 return nil, fmt.Errorf("failed to resolve DID %s: %w", issuer, err) 55 } 56 57 // Get the atproto signing key from the DID document 58 pubKey, err := ident.PublicKey() 59 if err != nil { 60 return nil, fmt.Errorf("failed to get public key from DID document: %w", err) 61 } 62 63 // Convert to JWK format to check curve type 64 jwk, err := pubKey.JWK() 65 if err != nil { 66 return nil, fmt.Errorf("failed to convert public key to JWK: %w", err) 67 } 68 69 // For secp256k1 (ES256K), return indigo's PublicKey directly 70 // since Go's crypto/ecdsa doesn't support this curve 71 if jwk.Curve == "secp256k1" { 72 return pubKey, nil 73 } 74 75 // For NIST curves, convert to Go's ecdsa.PublicKey for golang-jwt compatibility 76 return atcryptoJWKToECDSA(jwk) 77} 78 79// atcryptoJWKToECDSA converts an indigoCrypto.JWK to a Go ecdsa.PublicKey. 80// Note: secp256k1 is handled separately in FetchPublicKey by returning indigo's PublicKey directly. 81func atcryptoJWKToECDSA(jwk *indigoCrypto.JWK) (*ecdsa.PublicKey, error) { 82 if jwk.KeyType != "EC" { 83 return nil, fmt.Errorf("unsupported JWK key type: %s (expected EC)", jwk.KeyType) 84 } 85 86 // Decode X and Y coordinates (base64url, no padding) 87 xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X) 88 if err != nil { 89 return nil, fmt.Errorf("invalid JWK X coordinate encoding: %w", err) 90 } 91 yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y) 92 if err != nil { 93 return nil, fmt.Errorf("invalid JWK Y coordinate encoding: %w", err) 94 } 95 96 var ecCurve elliptic.Curve 97 switch jwk.Curve { 98 case "P-256": 99 ecCurve = elliptic.P256() 100 case "P-384": 101 ecCurve = elliptic.P384() 102 case "P-521": 103 ecCurve = elliptic.P521() 104 default: 105 // secp256k1 should be handled before calling this function 106 return nil, fmt.Errorf("unsupported JWK curve for Go ecdsa: %s (secp256k1 uses indigo)", jwk.Curve) 107 } 108 109 // Create the public key 110 pubKey := &ecdsa.PublicKey{ 111 Curve: ecCurve, 112 X: new(big.Int).SetBytes(xBytes), 113 Y: new(big.Int).SetBytes(yBytes), 114 } 115 116 // Validate point is on curve 117 if !ecCurve.IsOnCurve(pubKey.X, pubKey.Y) { 118 return nil, fmt.Errorf("invalid public key: point not on curve") 119 } 120 121 return pubKey, nil 122}