···
18
+
indigoCrypto "github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/golang-jwt/jwt/v5"
···
return verifiedClaims, nil
276
-
// verifyAsymmetricToken verifies a JWT using RSA or ECDSA with a public key from JWKS
277
+
// verifyAsymmetricToken verifies a JWT using RSA or ECDSA with a public key from JWKS.
278
+
// For ES256K (secp256k1), uses indigo's crypto package since golang-jwt doesn't support it.
func verifyAsymmetricToken(ctx context.Context, tokenString, issuer string, keyFetcher JWKSFetcher) (*Claims, error) {
280
+
// Parse header to check algorithm
281
+
header, err := ParseJWTHeader(tokenString)
283
+
return nil, fmt.Errorf("failed to parse JWT header: %w", err)
286
+
// ES256K (secp256k1) requires special handling via indigo's crypto package
287
+
// golang-jwt doesn't recognize ES256K as a valid signing method
288
+
if header.Alg == "ES256K" {
289
+
return verifyES256KToken(ctx, tokenString, issuer, keyFetcher)
292
+
// For standard algorithms (ES256, ES384, ES512, RS256, etc.), use golang-jwt
publicKey, err := keyFetcher.FetchPublicKey(ctx, issuer, tokenString)
return nil, fmt.Errorf("failed to fetch public key: %w", err)
···
return verifiedClaims, nil
328
+
// verifyES256KToken verifies a JWT signed with ES256K (secp256k1) using indigo's crypto package.
329
+
// This is necessary because golang-jwt doesn't support ES256K as a signing method.
330
+
func verifyES256KToken(ctx context.Context, tokenString, issuer string, keyFetcher JWKSFetcher) (*Claims, error) {
331
+
// Fetch the public key - for ES256K, the fetcher returns a JWK map or indigo PublicKey
332
+
keyData, err := keyFetcher.FetchPublicKey(ctx, issuer, tokenString)
334
+
return nil, fmt.Errorf("failed to fetch public key for ES256K: %w", err)
337
+
// Convert to indigo PublicKey based on what the fetcher returned
338
+
var pubKey indigoCrypto.PublicKey
339
+
switch k := keyData.(type) {
340
+
case indigoCrypto.PublicKey:
341
+
// Already an indigo PublicKey (from DIDKeyFetcher or updated JWKSFetcher)
343
+
case map[string]interface{}:
344
+
// Raw JWK map - parse with indigo
345
+
pubKey, err = parseJWKMapToIndigoPublicKey(k)
347
+
return nil, fmt.Errorf("failed to parse ES256K JWK: %w", err)
350
+
return nil, fmt.Errorf("ES256K verification requires indigo PublicKey or JWK map, got %T", keyData)
353
+
// Verify signature using indigo
354
+
if err := verifyJWTSignatureWithIndigoKey(tokenString, pubKey); err != nil {
355
+
return nil, fmt.Errorf("ES256K signature verification failed: %w", err)
358
+
// Parse claims (signature already verified)
359
+
claims, err := parseJWTClaimsManually(tokenString)
361
+
return nil, fmt.Errorf("failed to parse ES256K JWT claims: %w", err)
364
+
if err := validateClaims(claims); err != nil {
371
+
// parseJWKMapToIndigoPublicKey converts a JWK map to an indigo PublicKey.
372
+
// This uses indigo's crypto package which supports all atProto curves including secp256k1.
373
+
func parseJWKMapToIndigoPublicKey(jwkMap map[string]interface{}) (indigoCrypto.PublicKey, error) {
374
+
// Convert map to JSON bytes for indigo's parser
375
+
jwkBytes, err := json.Marshal(jwkMap)
377
+
return nil, fmt.Errorf("failed to serialize JWK: %w", err)
380
+
// Parse with indigo's crypto package - supports all atProto curves
381
+
pubKey, err := indigoCrypto.ParsePublicJWKBytes(jwkBytes)
383
+
return nil, fmt.Errorf("failed to parse JWK with indigo: %w", err)
389
+
// verifyJWTSignatureWithIndigoKey verifies a JWT signature using indigo's crypto package.
390
+
// This works for all ECDSA algorithms including ES256K (secp256k1).
391
+
func verifyJWTSignatureWithIndigoKey(tokenString string, pubKey indigoCrypto.PublicKey) error {
392
+
parts := strings.Split(tokenString, ".")
393
+
if len(parts) != 3 {
394
+
return fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
397
+
// The signing input is "header.payload" (without decoding)
398
+
signingInput := parts[0] + "." + parts[1]
400
+
// Decode the signature from base64url
401
+
signature, err := base64.RawURLEncoding.DecodeString(parts[2])
403
+
return fmt.Errorf("failed to decode JWT signature: %w", err)
406
+
// Use indigo's verification - HashAndVerifyLenient handles hashing internally
407
+
// and accepts both low-S and high-S signatures for maximum compatibility
408
+
if err := pubKey.HashAndVerifyLenient([]byte(signingInput), signature); err != nil {
409
+
return fmt.Errorf("signature verification failed: %w", err)
415
+
// parseJWTClaimsManually parses JWT claims without using golang-jwt.
416
+
// This is used for ES256K tokens where golang-jwt would reject the algorithm.
417
+
func parseJWTClaimsManually(tokenString string) (*Claims, error) {
418
+
parts := strings.Split(tokenString, ".")
419
+
if len(parts) != 3 {
420
+
return nil, fmt.Errorf("invalid JWT format: expected 3 parts, got %d", len(parts))
424
+
claimsBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
426
+
return nil, fmt.Errorf("failed to decode JWT claims: %w", err)
429
+
// Parse into raw map first
430
+
var rawClaims map[string]interface{}
431
+
if err := json.Unmarshal(claimsBytes, &rawClaims); err != nil {
432
+
return nil, fmt.Errorf("failed to parse JWT claims: %w", err)
435
+
// Build Claims struct
436
+
claims := &Claims{}
438
+
// Extract sub (subject/DID)
439
+
if sub, ok := rawClaims["sub"].(string); ok {
440
+
claims.Subject = sub
443
+
// Extract iss (issuer)
444
+
if iss, ok := rawClaims["iss"].(string); ok {
445
+
claims.Issuer = iss
448
+
// Extract aud (audience) - can be string or array
449
+
switch aud := rawClaims["aud"].(type) {
451
+
claims.Audience = jwt.ClaimStrings{aud}
452
+
case []interface{}:
453
+
for _, a := range aud {
454
+
if s, ok := a.(string); ok {
455
+
claims.Audience = append(claims.Audience, s)
460
+
// Extract exp (expiration)
461
+
if exp, ok := rawClaims["exp"].(float64); ok {
462
+
t := time.Unix(int64(exp), 0)
463
+
claims.ExpiresAt = jwt.NewNumericDate(t)
466
+
// Extract iat (issued at)
467
+
if iat, ok := rawClaims["iat"].(float64); ok {
468
+
t := time.Unix(int64(iat), 0)
469
+
claims.IssuedAt = jwt.NewNumericDate(t)
472
+
// Extract nbf (not before)
473
+
if nbf, ok := rawClaims["nbf"].(float64); ok {
474
+
t := time.Unix(int64(nbf), 0)
475
+
claims.NotBefore = jwt.NewNumericDate(t)
478
+
// Extract jti (JWT ID)
479
+
if jti, ok := rawClaims["jti"].(string); ok {
484
+
if scope, ok := rawClaims["scope"].(string); ok {
485
+
claims.Scope = scope
488
+
// Extract cnf (confirmation) for DPoP binding
489
+
if cnf, ok := rawClaims["cnf"].(map[string]interface{}); ok {
490
+
claims.Confirmation = cnf
// validateClaims performs additional validation on JWT claims
func validateClaims(claims *Claims) error {
···
Y string `json:"y,omitempty"` // EC y coordinate
384
-
// ToPublicKey converts a JWK to a public key (RSA or ECDSA)
567
+
// ToPublicKey converts a JWK to a public key (RSA, ECDSA, or indigo for secp256k1).
570
+
// - *rsa.PublicKey for RSA keys
571
+
// - *ecdsa.PublicKey for NIST EC curves (P-256, P-384, P-521)
572
+
// - map[string]interface{} for secp256k1 (ES256K) - parsed by indigo
func (j *JWK) ToPublicKey() (interface{}, error) {
return j.toRSAPublicKey()
578
+
// For secp256k1, return raw JWK map for indigo to parse
579
+
if j.Crv == "secp256k1" {
580
+
return j.toJWKMap(), nil
return nil, fmt.Errorf("unsupported key type: %s", j.Kty)
588
+
// toJWKMap converts the JWK struct to a map for indigo parsing
589
+
func (j *JWK) toJWKMap() map[string]interface{} {
590
+
m := map[string]interface{}{
// toRSAPublicKey converts a JWK to an RSA public key