···
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"
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
22
+
type DIDKeyFetcher struct {
23
+
directory indigoIdentity.Directory
26
+
// NewDIDKeyFetcher creates a new DID-based key fetcher.
27
+
func NewDIDKeyFetcher(directory indigoIdentity.Directory) *DIDKeyFetcher {
28
+
return &DIDKeyFetcher{
29
+
directory: directory,
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.
36
+
func (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)
43
+
did, err := syntax.ParseDID(issuer)
45
+
return nil, fmt.Errorf("invalid DID format: %w", err)
48
+
// Resolve the DID to get the identity (includes public keys)
49
+
ident, err := f.directory.LookupDID(ctx, did)
51
+
return nil, fmt.Errorf("failed to resolve DID %s: %w", issuer, err)
54
+
// Get the atproto signing key from the DID document
55
+
pubKey, err := ident.PublicKey()
57
+
return nil, fmt.Errorf("failed to get public key from DID document: %w", err)
60
+
// Convert to JWK format to extract coordinates
61
+
jwk, err := pubKey.JWK()
63
+
return nil, fmt.Errorf("failed to convert public key to JWK: %w", err)
66
+
// Convert atcrypto JWK to Go ecdsa.PublicKey
67
+
return atcryptoJWKToECDSA(jwk)
70
+
// atcryptoJWKToECDSA converts an atcrypto.JWK to a Go ecdsa.PublicKey
71
+
func 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)
76
+
// Decode X and Y coordinates (base64url, no padding)
77
+
xBytes, err := base64.RawURLEncoding.DecodeString(jwk.X)
79
+
return nil, fmt.Errorf("invalid JWK X coordinate encoding: %w", err)
81
+
yBytes, err := base64.RawURLEncoding.DecodeString(jwk.Y)
83
+
return nil, fmt.Errorf("invalid JWK Y coordinate encoding: %w", err)
86
+
var ecCurve elliptic.Curve
89
+
ecCurve = elliptic.P256()
91
+
ecCurve = elliptic.P384()
93
+
ecCurve = elliptic.P521()
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")
100
+
return nil, fmt.Errorf("unsupported JWK curve: %s", jwk.Curve)
103
+
// Create the public key
104
+
pubKey := &ecdsa.PublicKey{
106
+
X: new(big.Int).SetBytes(xBytes),
107
+
Y: new(big.Int).SetBytes(yBytes),
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")