···
14
-
indigoCrypto "github.com/bluesky-social/indigo/atproto/atcrypto"
15
-
"github.com/golang-jwt/jwt/v5"
16
-
"github.com/google/uuid"
19
-
// === Test Helpers ===
21
-
// testECKey holds a test ES256 key pair
22
-
type testECKey struct {
23
-
privateKey *ecdsa.PrivateKey
24
-
publicKey *ecdsa.PublicKey
25
-
jwk map[string]interface{}
29
-
// generateTestES256Key generates a test ES256 key pair and JWK
30
-
func generateTestES256Key(t *testing.T) *testECKey {
33
-
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
35
-
t.Fatalf("Failed to generate test key: %v", err)
38
-
// Encode public key coordinates as base64url
39
-
xBytes := privateKey.PublicKey.X.Bytes()
40
-
yBytes := privateKey.PublicKey.Y.Bytes()
42
-
// P-256 coordinates must be 32 bytes (pad if needed)
43
-
xBytes = padTo32Bytes(xBytes)
44
-
yBytes = padTo32Bytes(yBytes)
46
-
x := base64.RawURLEncoding.EncodeToString(xBytes)
47
-
y := base64.RawURLEncoding.EncodeToString(yBytes)
49
-
jwk := map[string]interface{}{
56
-
// Calculate thumbprint
57
-
thumbprint, err := CalculateJWKThumbprint(jwk)
59
-
t.Fatalf("Failed to calculate thumbprint: %v", err)
63
-
privateKey: privateKey,
64
-
publicKey: &privateKey.PublicKey,
66
-
thumbprint: thumbprint,
70
-
// padTo32Bytes pads a byte slice to 32 bytes (required for P-256 coordinates)
71
-
func padTo32Bytes(b []byte) []byte {
75
-
padded := make([]byte, 32)
76
-
copy(padded[32-len(b):], b)
80
-
// createDPoPProof creates a DPoP proof JWT for testing
81
-
func createDPoPProof(t *testing.T, key *testECKey, method, uri string, iat time.Time, jti string) string {
84
-
claims := &DPoPClaims{
85
-
RegisteredClaims: jwt.RegisteredClaims{
87
-
IssuedAt: jwt.NewNumericDate(iat),
93
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
94
-
token.Header["typ"] = "dpop+jwt"
95
-
token.Header["jwk"] = key.jwk
97
-
tokenString, err := token.SignedString(key.privateKey)
99
-
t.Fatalf("Failed to create DPoP proof: %v", err)
105
-
// === JWK Thumbprint Tests (RFC 7638) ===
107
-
func TestCalculateJWKThumbprint_EC_P256(t *testing.T) {
108
-
// Test with known values from RFC 7638 Appendix A (adapted for P-256)
109
-
jwk := map[string]interface{}{
112
-
"x": "WKn-ZIGevcwGIyyrzFoZNBdaq9_TsqzGl96oc0CWuis",
113
-
"y": "y77t-RvAHRKTsSGdIYUfweuOvwrvDD-Q3Hv5J0fSKbE",
116
-
thumbprint, err := CalculateJWKThumbprint(jwk)
118
-
t.Fatalf("CalculateJWKThumbprint failed: %v", err)
121
-
if thumbprint == "" {
122
-
t.Error("Expected non-empty thumbprint")
125
-
// Verify it's valid base64url
126
-
_, err = base64.RawURLEncoding.DecodeString(thumbprint)
128
-
t.Errorf("Thumbprint is not valid base64url: %v", err)
131
-
// Verify length (SHA-256 produces 32 bytes = 43 base64url chars)
132
-
if len(thumbprint) != 43 {
133
-
t.Errorf("Expected thumbprint length 43, got %d", len(thumbprint))
137
-
func TestCalculateJWKThumbprint_Deterministic(t *testing.T) {
138
-
// Same key should produce same thumbprint
139
-
jwk := map[string]interface{}{
142
-
"x": "test-x-coordinate",
143
-
"y": "test-y-coordinate",
146
-
thumbprint1, err := CalculateJWKThumbprint(jwk)
148
-
t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
151
-
thumbprint2, err := CalculateJWKThumbprint(jwk)
153
-
t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
156
-
if thumbprint1 != thumbprint2 {
157
-
t.Errorf("Thumbprints are not deterministic: %s != %s", thumbprint1, thumbprint2)
161
-
func TestCalculateJWKThumbprint_DifferentKeys(t *testing.T) {
162
-
// Different keys should produce different thumbprints
163
-
jwk1 := map[string]interface{}{
166
-
"x": "coordinate-x-1",
167
-
"y": "coordinate-y-1",
170
-
jwk2 := map[string]interface{}{
173
-
"x": "coordinate-x-2",
174
-
"y": "coordinate-y-2",
177
-
thumbprint1, err := CalculateJWKThumbprint(jwk1)
179
-
t.Fatalf("First CalculateJWKThumbprint failed: %v", err)
182
-
thumbprint2, err := CalculateJWKThumbprint(jwk2)
184
-
t.Fatalf("Second CalculateJWKThumbprint failed: %v", err)
187
-
if thumbprint1 == thumbprint2 {
188
-
t.Error("Different keys produced same thumbprint (collision)")
192
-
func TestCalculateJWKThumbprint_MissingKty(t *testing.T) {
193
-
jwk := map[string]interface{}{
199
-
_, err := CalculateJWKThumbprint(jwk)
201
-
t.Error("Expected error for missing kty, got nil")
203
-
if err != nil && !contains(err.Error(), "missing kty") {
204
-
t.Errorf("Expected error about missing kty, got: %v", err)
208
-
func TestCalculateJWKThumbprint_EC_MissingCrv(t *testing.T) {
209
-
jwk := map[string]interface{}{
215
-
_, err := CalculateJWKThumbprint(jwk)
217
-
t.Error("Expected error for missing crv, got nil")
219
-
if err != nil && !contains(err.Error(), "missing crv") {
220
-
t.Errorf("Expected error about missing crv, got: %v", err)
224
-
func TestCalculateJWKThumbprint_EC_MissingX(t *testing.T) {
225
-
jwk := map[string]interface{}{
231
-
_, err := CalculateJWKThumbprint(jwk)
233
-
t.Error("Expected error for missing x, got nil")
235
-
if err != nil && !contains(err.Error(), "missing x") {
236
-
t.Errorf("Expected error about missing x, got: %v", err)
240
-
func TestCalculateJWKThumbprint_EC_MissingY(t *testing.T) {
241
-
jwk := map[string]interface{}{
247
-
_, err := CalculateJWKThumbprint(jwk)
249
-
t.Error("Expected error for missing y, got nil")
251
-
if err != nil && !contains(err.Error(), "missing y") {
252
-
t.Errorf("Expected error about missing y, got: %v", err)
256
-
func TestCalculateJWKThumbprint_RSA(t *testing.T) {
257
-
// Test RSA key thumbprint calculation
258
-
jwk := map[string]interface{}{
261
-
"n": "test-modulus",
264
-
thumbprint, err := CalculateJWKThumbprint(jwk)
266
-
t.Fatalf("CalculateJWKThumbprint failed for RSA: %v", err)
269
-
if thumbprint == "" {
270
-
t.Error("Expected non-empty thumbprint for RSA key")
274
-
func TestCalculateJWKThumbprint_OKP(t *testing.T) {
275
-
// Test OKP (Octet Key Pair) thumbprint calculation
276
-
jwk := map[string]interface{}{
279
-
"x": "test-x-coordinate",
282
-
thumbprint, err := CalculateJWKThumbprint(jwk)
284
-
t.Fatalf("CalculateJWKThumbprint failed for OKP: %v", err)
287
-
if thumbprint == "" {
288
-
t.Error("Expected non-empty thumbprint for OKP key")
292
-
func TestCalculateJWKThumbprint_UnsupportedKeyType(t *testing.T) {
293
-
jwk := map[string]interface{}{
297
-
_, err := CalculateJWKThumbprint(jwk)
299
-
t.Error("Expected error for unsupported key type, got nil")
301
-
if err != nil && !contains(err.Error(), "unsupported JWK key type") {
302
-
t.Errorf("Expected error about unsupported key type, got: %v", err)
306
-
func TestCalculateJWKThumbprint_CanonicalJSON(t *testing.T) {
307
-
// RFC 7638 requires lexicographic ordering of keys in canonical JSON
308
-
// This test verifies that the canonical JSON is correctly ordered
310
-
jwk := map[string]interface{}{
317
-
// The canonical JSON should be: {"crv":"P-256","kty":"EC","x":"x-coord","y":"y-coord"}
318
-
// (lexicographically ordered: crv, kty, x, y)
320
-
canonical := map[string]string{
327
-
canonicalJSON, err := json.Marshal(canonical)
329
-
t.Fatalf("Failed to marshal canonical JSON: %v", err)
332
-
expectedHash := sha256.Sum256(canonicalJSON)
333
-
expectedThumbprint := base64.RawURLEncoding.EncodeToString(expectedHash[:])
335
-
actualThumbprint, err := CalculateJWKThumbprint(jwk)
337
-
t.Fatalf("CalculateJWKThumbprint failed: %v", err)
340
-
if actualThumbprint != expectedThumbprint {
341
-
t.Errorf("Thumbprint doesn't match expected canonical JSON hash\nExpected: %s\nGot: %s",
342
-
expectedThumbprint, actualThumbprint)
346
-
// === DPoP Proof Verification Tests ===
348
-
func TestVerifyDPoPProof_Valid(t *testing.T) {
349
-
verifier := NewDPoPVerifier()
350
-
key := generateTestES256Key(t)
353
-
uri := "https://api.example.com/resource"
355
-
jti := uuid.New().String()
357
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
359
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
361
-
t.Fatalf("VerifyDPoPProof failed for valid proof: %v", err)
365
-
t.Fatal("Expected non-nil proof result")
368
-
if result.Claims.HTTPMethod != method {
369
-
t.Errorf("Expected method %s, got %s", method, result.Claims.HTTPMethod)
372
-
if result.Claims.HTTPURI != uri {
373
-
t.Errorf("Expected URI %s, got %s", uri, result.Claims.HTTPURI)
376
-
if result.Claims.ID != jti {
377
-
t.Errorf("Expected jti %s, got %s", jti, result.Claims.ID)
380
-
if result.Thumbprint != key.thumbprint {
381
-
t.Errorf("Expected thumbprint %s, got %s", key.thumbprint, result.Thumbprint)
385
-
func TestVerifyDPoPProof_InvalidSignature(t *testing.T) {
386
-
verifier := NewDPoPVerifier()
387
-
key := generateTestES256Key(t)
388
-
wrongKey := generateTestES256Key(t)
391
-
uri := "https://api.example.com/resource"
393
-
jti := uuid.New().String()
395
-
// Create proof with one key
396
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
398
-
// Parse and modify to use wrong key's JWK in header (signature won't match)
399
-
parts := splitJWT(proof)
400
-
header := parseJWTHeader(t, parts[0])
401
-
header["jwk"] = wrongKey.jwk
402
-
modifiedHeader := encodeJSON(t, header)
403
-
tamperedProof := modifiedHeader + "." + parts[1] + "." + parts[2]
405
-
_, err := verifier.VerifyDPoPProof(tamperedProof, method, uri)
407
-
t.Error("Expected error for invalid signature, got nil")
409
-
if err != nil && !contains(err.Error(), "signature verification failed") {
410
-
t.Errorf("Expected signature verification error, got: %v", err)
414
-
func TestVerifyDPoPProof_WrongHTTPMethod(t *testing.T) {
415
-
verifier := NewDPoPVerifier()
416
-
key := generateTestES256Key(t)
419
-
wrongMethod := "GET"
420
-
uri := "https://api.example.com/resource"
422
-
jti := uuid.New().String()
424
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
426
-
_, err := verifier.VerifyDPoPProof(proof, wrongMethod, uri)
428
-
t.Error("Expected error for HTTP method mismatch, got nil")
430
-
if err != nil && !contains(err.Error(), "htm mismatch") {
431
-
t.Errorf("Expected htm mismatch error, got: %v", err)
435
-
func TestVerifyDPoPProof_WrongURI(t *testing.T) {
436
-
verifier := NewDPoPVerifier()
437
-
key := generateTestES256Key(t)
440
-
uri := "https://api.example.com/resource"
441
-
wrongURI := "https://api.example.com/different"
443
-
jti := uuid.New().String()
445
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
447
-
_, err := verifier.VerifyDPoPProof(proof, method, wrongURI)
449
-
t.Error("Expected error for URI mismatch, got nil")
451
-
if err != nil && !contains(err.Error(), "htu mismatch") {
452
-
t.Errorf("Expected htu mismatch error, got: %v", err)
456
-
func TestVerifyDPoPProof_URIWithQuery(t *testing.T) {
457
-
// URI comparison should strip query and fragment
458
-
verifier := NewDPoPVerifier()
459
-
key := generateTestES256Key(t)
462
-
baseURI := "https://api.example.com/resource"
463
-
uriWithQuery := baseURI + "?param=value"
465
-
jti := uuid.New().String()
467
-
proof := createDPoPProof(t, key, method, baseURI, iat, jti)
469
-
// Should succeed because query is stripped
470
-
_, err := verifier.VerifyDPoPProof(proof, method, uriWithQuery)
472
-
t.Fatalf("VerifyDPoPProof failed for URI with query: %v", err)
476
-
func TestVerifyDPoPProof_URIWithFragment(t *testing.T) {
477
-
// URI comparison should strip query and fragment
478
-
verifier := NewDPoPVerifier()
479
-
key := generateTestES256Key(t)
482
-
baseURI := "https://api.example.com/resource"
483
-
uriWithFragment := baseURI + "#section"
485
-
jti := uuid.New().String()
487
-
proof := createDPoPProof(t, key, method, baseURI, iat, jti)
489
-
// Should succeed because fragment is stripped
490
-
_, err := verifier.VerifyDPoPProof(proof, method, uriWithFragment)
492
-
t.Fatalf("VerifyDPoPProof failed for URI with fragment: %v", err)
496
-
func TestVerifyDPoPProof_ExpiredProof(t *testing.T) {
497
-
verifier := NewDPoPVerifier()
498
-
key := generateTestES256Key(t)
501
-
uri := "https://api.example.com/resource"
502
-
// Proof issued 10 minutes ago (exceeds default MaxProofAge of 5 minutes)
503
-
iat := time.Now().Add(-10 * time.Minute)
504
-
jti := uuid.New().String()
506
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
508
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
510
-
t.Error("Expected error for expired proof, got nil")
512
-
if err != nil && !contains(err.Error(), "too old") {
513
-
t.Errorf("Expected 'too old' error, got: %v", err)
517
-
func TestVerifyDPoPProof_FutureProof(t *testing.T) {
518
-
verifier := NewDPoPVerifier()
519
-
key := generateTestES256Key(t)
522
-
uri := "https://api.example.com/resource"
523
-
// Proof issued 1 minute in the future (exceeds MaxClockSkew)
524
-
iat := time.Now().Add(1 * time.Minute)
525
-
jti := uuid.New().String()
527
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
529
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
531
-
t.Error("Expected error for future proof, got nil")
533
-
if err != nil && !contains(err.Error(), "in the future") {
534
-
t.Errorf("Expected 'in the future' error, got: %v", err)
538
-
func TestVerifyDPoPProof_WithinClockSkew(t *testing.T) {
539
-
verifier := NewDPoPVerifier()
540
-
key := generateTestES256Key(t)
543
-
uri := "https://api.example.com/resource"
544
-
// Proof issued 15 seconds in the future (within MaxClockSkew of 30s)
545
-
iat := time.Now().Add(15 * time.Second)
546
-
jti := uuid.New().String()
548
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
550
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
552
-
t.Fatalf("VerifyDPoPProof failed for proof within clock skew: %v", err)
556
-
func TestVerifyDPoPProof_MissingJti(t *testing.T) {
557
-
verifier := NewDPoPVerifier()
558
-
key := generateTestES256Key(t)
561
-
uri := "https://api.example.com/resource"
564
-
claims := &DPoPClaims{
565
-
RegisteredClaims: jwt.RegisteredClaims{
567
-
IssuedAt: jwt.NewNumericDate(iat),
569
-
HTTPMethod: method,
573
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
574
-
token.Header["typ"] = "dpop+jwt"
575
-
token.Header["jwk"] = key.jwk
577
-
proof, err := token.SignedString(key.privateKey)
579
-
t.Fatalf("Failed to create test proof: %v", err)
582
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
584
-
t.Error("Expected error for missing jti, got nil")
586
-
if err != nil && !contains(err.Error(), "missing jti") {
587
-
t.Errorf("Expected missing jti error, got: %v", err)
591
-
func TestVerifyDPoPProof_MissingTypHeader(t *testing.T) {
592
-
verifier := NewDPoPVerifier()
593
-
key := generateTestES256Key(t)
596
-
uri := "https://api.example.com/resource"
598
-
jti := uuid.New().String()
600
-
claims := &DPoPClaims{
601
-
RegisteredClaims: jwt.RegisteredClaims{
603
-
IssuedAt: jwt.NewNumericDate(iat),
605
-
HTTPMethod: method,
609
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
610
-
// Don't set typ header
611
-
token.Header["jwk"] = key.jwk
613
-
proof, err := token.SignedString(key.privateKey)
615
-
t.Fatalf("Failed to create test proof: %v", err)
618
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
620
-
t.Error("Expected error for missing typ header, got nil")
622
-
if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
623
-
t.Errorf("Expected typ header error, got: %v", err)
627
-
func TestVerifyDPoPProof_WrongTypHeader(t *testing.T) {
628
-
verifier := NewDPoPVerifier()
629
-
key := generateTestES256Key(t)
632
-
uri := "https://api.example.com/resource"
634
-
jti := uuid.New().String()
636
-
claims := &DPoPClaims{
637
-
RegisteredClaims: jwt.RegisteredClaims{
639
-
IssuedAt: jwt.NewNumericDate(iat),
641
-
HTTPMethod: method,
645
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
646
-
token.Header["typ"] = "JWT" // Wrong typ
647
-
token.Header["jwk"] = key.jwk
649
-
proof, err := token.SignedString(key.privateKey)
651
-
t.Fatalf("Failed to create test proof: %v", err)
654
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
656
-
t.Error("Expected error for wrong typ header, got nil")
658
-
if err != nil && !contains(err.Error(), "typ must be 'dpop+jwt'") {
659
-
t.Errorf("Expected typ header error, got: %v", err)
663
-
func TestVerifyDPoPProof_MissingJWK(t *testing.T) {
664
-
verifier := NewDPoPVerifier()
665
-
key := generateTestES256Key(t)
668
-
uri := "https://api.example.com/resource"
670
-
jti := uuid.New().String()
672
-
claims := &DPoPClaims{
673
-
RegisteredClaims: jwt.RegisteredClaims{
675
-
IssuedAt: jwt.NewNumericDate(iat),
677
-
HTTPMethod: method,
681
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
682
-
token.Header["typ"] = "dpop+jwt"
683
-
// Don't include JWK
685
-
proof, err := token.SignedString(key.privateKey)
687
-
t.Fatalf("Failed to create test proof: %v", err)
690
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
692
-
t.Error("Expected error for missing jwk header, got nil")
694
-
if err != nil && !contains(err.Error(), "missing jwk") {
695
-
t.Errorf("Expected missing jwk error, got: %v", err)
699
-
func TestVerifyDPoPProof_CustomTimeSettings(t *testing.T) {
700
-
verifier := &DPoPVerifier{
701
-
MaxClockSkew: 1 * time.Minute,
702
-
MaxProofAge: 10 * time.Minute,
704
-
key := generateTestES256Key(t)
707
-
uri := "https://api.example.com/resource"
708
-
// Proof issued 50 seconds in the future (within custom MaxClockSkew)
709
-
iat := time.Now().Add(50 * time.Second)
710
-
jti := uuid.New().String()
712
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
714
-
_, err := verifier.VerifyDPoPProof(proof, method, uri)
716
-
t.Fatalf("VerifyDPoPProof failed with custom time settings: %v", err)
720
-
func TestVerifyDPoPProof_HTTPMethodCaseInsensitive(t *testing.T) {
721
-
// HTTP method comparison should be case-insensitive per spec
722
-
verifier := NewDPoPVerifier()
723
-
key := generateTestES256Key(t)
726
-
uri := "https://api.example.com/resource"
728
-
jti := uuid.New().String()
730
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
732
-
// Verify with uppercase method
733
-
_, err := verifier.VerifyDPoPProof(proof, "POST", uri)
735
-
t.Fatalf("VerifyDPoPProof failed for case-insensitive method: %v", err)
739
-
// === Token Binding Verification Tests ===
741
-
func TestVerifyTokenBinding_Matching(t *testing.T) {
742
-
verifier := NewDPoPVerifier()
743
-
key := generateTestES256Key(t)
746
-
uri := "https://api.example.com/resource"
748
-
jti := uuid.New().String()
750
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
752
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
754
-
t.Fatalf("VerifyDPoPProof failed: %v", err)
757
-
// Verify token binding with matching thumbprint
758
-
err = verifier.VerifyTokenBinding(result, key.thumbprint)
760
-
t.Fatalf("VerifyTokenBinding failed for matching thumbprint: %v", err)
764
-
func TestVerifyTokenBinding_Mismatch(t *testing.T) {
765
-
verifier := NewDPoPVerifier()
766
-
key := generateTestES256Key(t)
767
-
wrongKey := generateTestES256Key(t)
770
-
uri := "https://api.example.com/resource"
772
-
jti := uuid.New().String()
774
-
proof := createDPoPProof(t, key, method, uri, iat, jti)
776
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
778
-
t.Fatalf("VerifyDPoPProof failed: %v", err)
781
-
// Verify token binding with wrong thumbprint
782
-
err = verifier.VerifyTokenBinding(result, wrongKey.thumbprint)
784
-
t.Error("Expected error for thumbprint mismatch, got nil")
786
-
if err != nil && !contains(err.Error(), "thumbprint mismatch") {
787
-
t.Errorf("Expected thumbprint mismatch error, got: %v", err)
791
-
// === ExtractCnfJkt Tests ===
793
-
func TestExtractCnfJkt_Valid(t *testing.T) {
794
-
expectedJkt := "test-thumbprint-123"
796
-
Confirmation: map[string]interface{}{
797
-
"jkt": expectedJkt,
801
-
jkt, err := ExtractCnfJkt(claims)
803
-
t.Fatalf("ExtractCnfJkt failed for valid claims: %v", err)
806
-
if jkt != expectedJkt {
807
-
t.Errorf("Expected jkt %s, got %s", expectedJkt, jkt)
811
-
func TestExtractCnfJkt_MissingCnf(t *testing.T) {
816
-
_, err := ExtractCnfJkt(claims)
818
-
t.Error("Expected error for missing cnf, got nil")
820
-
if err != nil && !contains(err.Error(), "missing cnf claim") {
821
-
t.Errorf("Expected missing cnf error, got: %v", err)
825
-
func TestExtractCnfJkt_NilCnf(t *testing.T) {
830
-
_, err := ExtractCnfJkt(claims)
832
-
t.Error("Expected error for nil cnf, got nil")
834
-
if err != nil && !contains(err.Error(), "missing cnf claim") {
835
-
t.Errorf("Expected missing cnf error, got: %v", err)
839
-
func TestExtractCnfJkt_MissingJkt(t *testing.T) {
841
-
Confirmation: map[string]interface{}{
846
-
_, err := ExtractCnfJkt(claims)
848
-
t.Error("Expected error for missing jkt, got nil")
850
-
if err != nil && !contains(err.Error(), "missing jkt") {
851
-
t.Errorf("Expected missing jkt error, got: %v", err)
855
-
func TestExtractCnfJkt_EmptyJkt(t *testing.T) {
857
-
Confirmation: map[string]interface{}{
862
-
_, err := ExtractCnfJkt(claims)
864
-
t.Error("Expected error for empty jkt, got nil")
866
-
if err != nil && !contains(err.Error(), "missing jkt") {
867
-
t.Errorf("Expected missing jkt error, got: %v", err)
871
-
func TestExtractCnfJkt_WrongType(t *testing.T) {
873
-
Confirmation: map[string]interface{}{
874
-
"jkt": 123, // Not a string
878
-
_, err := ExtractCnfJkt(claims)
880
-
t.Error("Expected error for wrong type jkt, got nil")
882
-
if err != nil && !contains(err.Error(), "missing jkt") {
883
-
t.Errorf("Expected missing jkt error, got: %v", err)
887
-
// === Helper Functions for Tests ===
889
-
// splitJWT splits a JWT into its three parts
890
-
func splitJWT(token string) []string {
892
-
token[:strings.IndexByte(token, '.')],
893
-
token[strings.IndexByte(token, '.')+1 : strings.LastIndexByte(token, '.')],
894
-
token[strings.LastIndexByte(token, '.')+1:],
898
-
// parseJWTHeader parses a base64url-encoded JWT header
899
-
func parseJWTHeader(t *testing.T, encoded string) map[string]interface{} {
901
-
decoded, err := base64.RawURLEncoding.DecodeString(encoded)
903
-
t.Fatalf("Failed to decode header: %v", err)
906
-
var header map[string]interface{}
907
-
if err := json.Unmarshal(decoded, &header); err != nil {
908
-
t.Fatalf("Failed to unmarshal header: %v", err)
914
-
// encodeJSON encodes a value to base64url-encoded JSON
915
-
func encodeJSON(t *testing.T, v interface{}) string {
917
-
data, err := json.Marshal(v)
919
-
t.Fatalf("Failed to marshal JSON: %v", err)
921
-
return base64.RawURLEncoding.EncodeToString(data)
924
-
// === ES256K (secp256k1) Test Helpers ===
926
-
// testES256KKey holds a test ES256K key pair using indigo
927
-
type testES256KKey struct {
928
-
privateKey indigoCrypto.PrivateKey
929
-
publicKey indigoCrypto.PublicKey
930
-
jwk map[string]interface{}
934
-
// generateTestES256KKey generates a test ES256K (secp256k1) key pair and JWK
935
-
func generateTestES256KKey(t *testing.T) *testES256KKey {
938
-
privateKey, err := indigoCrypto.GeneratePrivateKeyK256()
940
-
t.Fatalf("Failed to generate ES256K test key: %v", err)
943
-
publicKey, err := privateKey.PublicKey()
945
-
t.Fatalf("Failed to get public key from ES256K private key: %v", err)
948
-
// Get the JWK representation
949
-
jwkStruct, err := publicKey.JWK()
951
-
t.Fatalf("Failed to get JWK from ES256K public key: %v", err)
953
-
jwk := map[string]interface{}{
954
-
"kty": jwkStruct.KeyType,
955
-
"crv": jwkStruct.Curve,
960
-
// Calculate thumbprint
961
-
thumbprint, err := CalculateJWKThumbprint(jwk)
963
-
t.Fatalf("Failed to calculate ES256K thumbprint: %v", err)
966
-
return &testES256KKey{
967
-
privateKey: privateKey,
968
-
publicKey: publicKey,
970
-
thumbprint: thumbprint,
974
-
// createES256KDPoPProof creates a DPoP proof JWT using ES256K for testing
975
-
func createES256KDPoPProof(t *testing.T, key *testES256KKey, method, uri string, iat time.Time, jti string) string {
979
-
claims := map[string]interface{}{
987
-
header := map[string]interface{}{
993
-
// Encode header and claims
994
-
headerJSON, err := json.Marshal(header)
996
-
t.Fatalf("Failed to marshal header: %v", err)
998
-
claimsJSON, err := json.Marshal(claims)
1000
-
t.Fatalf("Failed to marshal claims: %v", err)
1003
-
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
1004
-
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
1006
-
// Sign with indigo
1007
-
signingInput := headerB64 + "." + claimsB64
1008
-
signature, err := key.privateKey.HashAndSign([]byte(signingInput))
1010
-
t.Fatalf("Failed to sign ES256K proof: %v", err)
1013
-
signatureB64 := base64.RawURLEncoding.EncodeToString(signature)
1014
-
return signingInput + "." + signatureB64
1017
-
// === ES256K Tests ===
1019
-
func TestVerifyDPoPProof_ES256K_Valid(t *testing.T) {
1020
-
verifier := NewDPoPVerifier()
1021
-
key := generateTestES256KKey(t)
1024
-
uri := "https://api.example.com/resource"
1026
-
jti := uuid.New().String()
1028
-
proof := createES256KDPoPProof(t, key, method, uri, iat, jti)
1030
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
1032
-
t.Fatalf("VerifyDPoPProof failed for valid ES256K proof: %v", err)
1035
-
if result == nil {
1036
-
t.Fatal("Expected non-nil proof result")
1039
-
if result.Claims.HTTPMethod != method {
1040
-
t.Errorf("Expected method %s, got %s", method, result.Claims.HTTPMethod)
1043
-
if result.Claims.HTTPURI != uri {
1044
-
t.Errorf("Expected URI %s, got %s", uri, result.Claims.HTTPURI)
1047
-
if result.Thumbprint != key.thumbprint {
1048
-
t.Errorf("Expected thumbprint %s, got %s", key.thumbprint, result.Thumbprint)
1052
-
func TestVerifyDPoPProof_ES256K_InvalidSignature(t *testing.T) {
1053
-
verifier := NewDPoPVerifier()
1054
-
key := generateTestES256KKey(t)
1055
-
wrongKey := generateTestES256KKey(t)
1058
-
uri := "https://api.example.com/resource"
1060
-
jti := uuid.New().String()
1062
-
// Create proof with one key
1063
-
proof := createES256KDPoPProof(t, key, method, uri, iat, jti)
1065
-
// Tamper by replacing JWK with wrong key
1066
-
parts := splitJWT(proof)
1067
-
header := parseJWTHeader(t, parts[0])
1068
-
header["jwk"] = wrongKey.jwk
1069
-
modifiedHeader := encodeJSON(t, header)
1070
-
tamperedProof := modifiedHeader + "." + parts[1] + "." + parts[2]
1072
-
_, err := verifier.VerifyDPoPProof(tamperedProof, method, uri)
1074
-
t.Error("Expected error for invalid ES256K signature, got nil")
1076
-
if err != nil && !contains(err.Error(), "signature verification failed") {
1077
-
t.Errorf("Expected signature verification error, got: %v", err)
1081
-
func TestCalculateJWKThumbprint_ES256K(t *testing.T) {
1082
-
// Test thumbprint calculation for secp256k1 keys
1083
-
key := generateTestES256KKey(t)
1085
-
thumbprint, err := CalculateJWKThumbprint(key.jwk)
1087
-
t.Fatalf("CalculateJWKThumbprint failed for ES256K: %v", err)
1090
-
if thumbprint == "" {
1091
-
t.Error("Expected non-empty thumbprint for ES256K key")
1094
-
// Verify it's valid base64url
1095
-
_, err = base64.RawURLEncoding.DecodeString(thumbprint)
1097
-
t.Errorf("ES256K thumbprint is not valid base64url: %v", err)
1100
-
// Verify length (SHA-256 produces 32 bytes = 43 base64url chars)
1101
-
if len(thumbprint) != 43 {
1102
-
t.Errorf("Expected ES256K thumbprint length 43, got %d", len(thumbprint))
1106
-
// === Algorithm-Curve Binding Tests ===
1108
-
func TestVerifyDPoPProof_AlgorithmCurveMismatch_ES256KWithP256Key(t *testing.T) {
1109
-
verifier := NewDPoPVerifier()
1110
-
key := generateTestES256Key(t) // P-256 key
1113
-
uri := "https://api.example.com/resource"
1115
-
jti := uuid.New().String()
1117
-
// Create a proof claiming ES256K but using P-256 key
1118
-
claims := &DPoPClaims{
1119
-
RegisteredClaims: jwt.RegisteredClaims{
1121
-
IssuedAt: jwt.NewNumericDate(iat),
1123
-
HTTPMethod: method,
1127
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
1128
-
token.Header["typ"] = "dpop+jwt"
1129
-
token.Header["alg"] = "ES256K" // Claim ES256K
1130
-
token.Header["jwk"] = key.jwk // But use P-256 key
1132
-
proof, err := token.SignedString(key.privateKey)
1134
-
t.Fatalf("Failed to create test proof: %v", err)
1137
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
1139
-
t.Error("Expected error for ES256K algorithm with P-256 curve, got nil")
1141
-
if err != nil && !contains(err.Error(), "requires curve secp256k1") {
1142
-
t.Errorf("Expected curve mismatch error, got: %v", err)
1146
-
func TestVerifyDPoPProof_AlgorithmCurveMismatch_ES256WithSecp256k1Key(t *testing.T) {
1147
-
verifier := NewDPoPVerifier()
1148
-
key := generateTestES256KKey(t) // secp256k1 key
1151
-
uri := "https://api.example.com/resource"
1153
-
jti := uuid.New().String()
1156
-
claims := map[string]interface{}{
1158
-
"iat": iat.Unix(),
1163
-
// Build header claiming ES256 but using secp256k1 key
1164
-
header := map[string]interface{}{
1165
-
"typ": "dpop+jwt",
1166
-
"alg": "ES256", // Claim ES256
1167
-
"jwk": key.jwk, // But use secp256k1 key
1170
-
headerJSON, _ := json.Marshal(header)
1171
-
claimsJSON, _ := json.Marshal(claims)
1173
-
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
1174
-
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
1176
-
signingInput := headerB64 + "." + claimsB64
1177
-
signature, err := key.privateKey.HashAndSign([]byte(signingInput))
1179
-
t.Fatalf("Failed to sign: %v", err)
1182
-
proof := signingInput + "." + base64.RawURLEncoding.EncodeToString(signature)
1184
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
1186
-
t.Error("Expected error for ES256 algorithm with secp256k1 curve, got nil")
1188
-
if err != nil && !contains(err.Error(), "requires curve P-256") {
1189
-
t.Errorf("Expected curve mismatch error, got: %v", err)
1193
-
// === exp/nbf Validation Tests ===
1195
-
func TestVerifyDPoPProof_ExpiredWithExpClaim(t *testing.T) {
1196
-
verifier := NewDPoPVerifier()
1197
-
key := generateTestES256Key(t)
1200
-
uri := "https://api.example.com/resource"
1201
-
iat := time.Now().Add(-2 * time.Minute)
1202
-
exp := time.Now().Add(-1 * time.Minute) // Expired 1 minute ago
1203
-
jti := uuid.New().String()
1205
-
claims := &DPoPClaims{
1206
-
RegisteredClaims: jwt.RegisteredClaims{
1208
-
IssuedAt: jwt.NewNumericDate(iat),
1209
-
ExpiresAt: jwt.NewNumericDate(exp),
1211
-
HTTPMethod: method,
1215
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
1216
-
token.Header["typ"] = "dpop+jwt"
1217
-
token.Header["jwk"] = key.jwk
1219
-
proof, err := token.SignedString(key.privateKey)
1221
-
t.Fatalf("Failed to create test proof: %v", err)
1224
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
1226
-
t.Error("Expected error for expired proof with exp claim, got nil")
1228
-
if err != nil && !contains(err.Error(), "expired") {
1229
-
t.Errorf("Expected expiration error, got: %v", err)
1233
-
func TestVerifyDPoPProof_NotYetValidWithNbfClaim(t *testing.T) {
1234
-
verifier := NewDPoPVerifier()
1235
-
key := generateTestES256Key(t)
1238
-
uri := "https://api.example.com/resource"
1240
-
nbf := time.Now().Add(5 * time.Minute) // Not valid for another 5 minutes
1241
-
jti := uuid.New().String()
1243
-
claims := &DPoPClaims{
1244
-
RegisteredClaims: jwt.RegisteredClaims{
1246
-
IssuedAt: jwt.NewNumericDate(iat),
1247
-
NotBefore: jwt.NewNumericDate(nbf),
1249
-
HTTPMethod: method,
1253
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
1254
-
token.Header["typ"] = "dpop+jwt"
1255
-
token.Header["jwk"] = key.jwk
1257
-
proof, err := token.SignedString(key.privateKey)
1259
-
t.Fatalf("Failed to create test proof: %v", err)
1262
-
_, err = verifier.VerifyDPoPProof(proof, method, uri)
1264
-
t.Error("Expected error for not-yet-valid proof with nbf claim, got nil")
1266
-
if err != nil && !contains(err.Error(), "not valid before") {
1267
-
t.Errorf("Expected not-before error, got: %v", err)
1271
-
func TestVerifyDPoPProof_ValidWithExpClaimInFuture(t *testing.T) {
1272
-
verifier := NewDPoPVerifier()
1273
-
key := generateTestES256Key(t)
1276
-
uri := "https://api.example.com/resource"
1278
-
exp := time.Now().Add(5 * time.Minute) // Valid for 5 more minutes
1279
-
jti := uuid.New().String()
1281
-
claims := &DPoPClaims{
1282
-
RegisteredClaims: jwt.RegisteredClaims{
1284
-
IssuedAt: jwt.NewNumericDate(iat),
1285
-
ExpiresAt: jwt.NewNumericDate(exp),
1287
-
HTTPMethod: method,
1291
-
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
1292
-
token.Header["typ"] = "dpop+jwt"
1293
-
token.Header["jwk"] = key.jwk
1295
-
proof, err := token.SignedString(key.privateKey)
1297
-
t.Fatalf("Failed to create test proof: %v", err)
1300
-
result, err := verifier.VerifyDPoPProof(proof, method, uri)
1302
-
t.Fatalf("VerifyDPoPProof failed for valid proof with exp in future: %v", err)
1305
-
if result == nil {
1306
-
t.Error("Expected non-nil result for valid proof")