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}