···
···
174
+
// === HS256 Verification Tests ===
176
+
// mockJWKSFetcher is a mock implementation of JWKSFetcher for testing
177
+
type mockJWKSFetcher struct {
178
+
publicKey interface{}
182
+
func (m *mockJWKSFetcher) FetchPublicKey(ctx context.Context, issuer, token string) (interface{}, error) {
183
+
return m.publicKey, m.err
186
+
func createHS256Token(t *testing.T, subject, issuer, secret string, expiry time.Duration) string {
189
+
RegisteredClaims: jwt.RegisteredClaims{
192
+
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)),
193
+
IssuedAt: jwt.NewNumericDate(time.Now()),
195
+
Scope: "atproto transition:generic",
197
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
198
+
tokenString, err := token.SignedString([]byte(secret))
200
+
t.Fatalf("Failed to create test token: %v", err)
205
+
func TestVerifyJWT_HS256_Valid(t *testing.T) {
206
+
// Setup: Configure environment for HS256 verification
207
+
secret := "test-jwt-secret-key-12345"
208
+
issuer := "https://pds.coves.social"
210
+
ResetJWTConfigForTesting()
211
+
os.Setenv("PDS_JWT_SECRET", secret)
212
+
os.Setenv("HS256_ISSUERS", issuer)
214
+
os.Unsetenv("PDS_JWT_SECRET")
215
+
os.Unsetenv("HS256_ISSUERS")
216
+
ResetJWTConfigForTesting()
219
+
tokenString := createHS256Token(t, "did:plc:test123", issuer, secret, 1*time.Hour)
222
+
claims, err := VerifyJWT(context.Background(), tokenString, &mockJWKSFetcher{})
224
+
t.Fatalf("VerifyJWT failed for valid HS256 token: %v", err)
227
+
if claims.Subject != "did:plc:test123" {
228
+
t.Errorf("Expected subject 'did:plc:test123', got '%s'", claims.Subject)
230
+
if claims.Issuer != issuer {
231
+
t.Errorf("Expected issuer '%s', got '%s'", issuer, claims.Issuer)
235
+
func TestVerifyJWT_HS256_WrongSecret(t *testing.T) {
236
+
// Setup: Configure environment with one secret, sign with another
237
+
issuer := "https://pds.coves.social"
239
+
ResetJWTConfigForTesting()
240
+
os.Setenv("PDS_JWT_SECRET", "correct-secret")
241
+
os.Setenv("HS256_ISSUERS", issuer)
243
+
os.Unsetenv("PDS_JWT_SECRET")
244
+
os.Unsetenv("HS256_ISSUERS")
245
+
ResetJWTConfigForTesting()
248
+
// Create token with wrong secret
249
+
tokenString := createHS256Token(t, "did:plc:test123", issuer, "wrong-secret", 1*time.Hour)
251
+
// Verify should fail
252
+
_, err := VerifyJWT(context.Background(), tokenString, &mockJWKSFetcher{})
254
+
t.Error("Expected error for HS256 token with wrong secret, got nil")
258
+
func TestVerifyJWT_HS256_SecretNotConfigured(t *testing.T) {
259
+
// Setup: Whitelist issuer but don't configure secret
260
+
issuer := "https://pds.coves.social"
262
+
ResetJWTConfigForTesting()
263
+
os.Unsetenv("PDS_JWT_SECRET") // Ensure secret is not set
264
+
os.Setenv("HS256_ISSUERS", issuer)
266
+
os.Unsetenv("HS256_ISSUERS")
267
+
ResetJWTConfigForTesting()
270
+
tokenString := createHS256Token(t, "did:plc:test123", issuer, "any-secret", 1*time.Hour)
272
+
// Verify should fail with descriptive error
273
+
_, err := VerifyJWT(context.Background(), tokenString, &mockJWKSFetcher{})
275
+
t.Error("Expected error when PDS_JWT_SECRET not configured, got nil")
277
+
if err != nil && !contains(err.Error(), "PDS_JWT_SECRET not configured") {
278
+
t.Errorf("Expected error about PDS_JWT_SECRET not configured, got: %v", err)
282
+
// === Algorithm Confusion Attack Prevention Tests ===
284
+
func TestVerifyJWT_AlgorithmConfusionAttack_HS256WithNonWhitelistedIssuer(t *testing.T) {
285
+
// SECURITY TEST: This tests the algorithm confusion attack prevention
286
+
// An attacker tries to use HS256 with an issuer that should use RS256/ES256
288
+
ResetJWTConfigForTesting()
289
+
os.Setenv("PDS_JWT_SECRET", "some-secret")
290
+
os.Setenv("HS256_ISSUERS", "https://trusted.example.com") // Different from token issuer
292
+
os.Unsetenv("PDS_JWT_SECRET")
293
+
os.Unsetenv("HS256_ISSUERS")
294
+
ResetJWTConfigForTesting()
297
+
// Create HS256 token with non-whitelisted issuer (simulating attack)
298
+
tokenString := createHS256Token(t, "did:plc:attacker", "https://victim-pds.example.com", "some-secret", 1*time.Hour)
300
+
// Verify should fail because issuer is not in HS256 whitelist
301
+
_, err := VerifyJWT(context.Background(), tokenString, &mockJWKSFetcher{})
303
+
t.Error("SECURITY VULNERABILITY: HS256 token accepted for non-whitelisted issuer")
305
+
if err != nil && !contains(err.Error(), "not in HS256_ISSUERS whitelist") {
306
+
t.Errorf("Expected error about HS256 not allowed for issuer, got: %v", err)
310
+
func TestVerifyJWT_AlgorithmConfusionAttack_EmptyWhitelist(t *testing.T) {
311
+
// SECURITY TEST: When no issuers are whitelisted for HS256, all HS256 tokens should be rejected
313
+
ResetJWTConfigForTesting()
314
+
os.Setenv("PDS_JWT_SECRET", "some-secret")
315
+
os.Unsetenv("HS256_ISSUERS") // Empty whitelist
317
+
os.Unsetenv("PDS_JWT_SECRET")
318
+
ResetJWTConfigForTesting()
321
+
tokenString := createHS256Token(t, "did:plc:test123", "https://any-pds.example.com", "some-secret", 1*time.Hour)
323
+
// Verify should fail because no issuers are whitelisted for HS256
324
+
_, err := VerifyJWT(context.Background(), tokenString, &mockJWKSFetcher{})
326
+
t.Error("SECURITY VULNERABILITY: HS256 token accepted with empty issuer whitelist")
330
+
func TestVerifyJWT_IssuerRequiresHS256ButTokenUsesRS256(t *testing.T) {
331
+
// Test that issuer whitelisted for HS256 rejects tokens claiming to use RS256
332
+
issuer := "https://pds.coves.social"
334
+
ResetJWTConfigForTesting()
335
+
os.Setenv("PDS_JWT_SECRET", "test-secret")
336
+
os.Setenv("HS256_ISSUERS", issuer)
338
+
os.Unsetenv("PDS_JWT_SECRET")
339
+
os.Unsetenv("HS256_ISSUERS")
340
+
ResetJWTConfigForTesting()
343
+
// Create RS256-signed token (can't actually sign without RSA key, but we can test the header check)
345
+
RegisteredClaims: jwt.RegisteredClaims{
346
+
Subject: "did:plc:test123",
348
+
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
351
+
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
352
+
// This will create an invalid signature but valid header structure
353
+
// The test should fail at algorithm check, not signature verification
354
+
tokenString, _ := token.SignedString([]byte("dummy-key"))
356
+
if tokenString != "" {
357
+
_, err := VerifyJWT(context.Background(), tokenString, &mockJWKSFetcher{})
359
+
t.Error("Expected error when HS256 issuer receives non-HS256 token")
364
+
// === ParseJWTHeader Tests ===
366
+
func TestParseJWTHeader_Valid(t *testing.T) {
367
+
tokenString := createHS256Token(t, "did:plc:test123", "https://test.example.com", "secret", 1*time.Hour)
369
+
header, err := ParseJWTHeader(tokenString)
371
+
t.Fatalf("ParseJWTHeader failed: %v", err)
374
+
if header.Alg != AlgorithmHS256 {
375
+
t.Errorf("Expected alg '%s', got '%s'", AlgorithmHS256, header.Alg)
379
+
func TestParseJWTHeader_WithBearerPrefix(t *testing.T) {
380
+
tokenString := createHS256Token(t, "did:plc:test123", "https://test.example.com", "secret", 1*time.Hour)
382
+
header, err := ParseJWTHeader("Bearer " + tokenString)
384
+
t.Fatalf("ParseJWTHeader failed with Bearer prefix: %v", err)
387
+
if header.Alg != AlgorithmHS256 {
388
+
t.Errorf("Expected alg '%s', got '%s'", AlgorithmHS256, header.Alg)
392
+
func TestParseJWTHeader_InvalidFormat(t *testing.T) {
393
+
testCases := []struct {
397
+
{"empty string", ""},
398
+
{"single part", "abc"},
399
+
{"two parts", "abc.def"},
400
+
{"too many parts", "a.b.c.d"},
403
+
for _, tc := range testCases {
404
+
t.Run(tc.name, func(t *testing.T) {
405
+
_, err := ParseJWTHeader(tc.input)
407
+
t.Errorf("Expected error for invalid JWT format '%s', got nil", tc.input)
413
+
// === shouldUseHS256 and isHS256IssuerWhitelisted Tests ===
415
+
func TestIsHS256IssuerWhitelisted_Whitelisted(t *testing.T) {
416
+
ResetJWTConfigForTesting()
417
+
os.Setenv("HS256_ISSUERS", "https://pds1.example.com,https://pds2.example.com")
419
+
os.Unsetenv("HS256_ISSUERS")
420
+
ResetJWTConfigForTesting()
423
+
if !isHS256IssuerWhitelisted("https://pds1.example.com") {
424
+
t.Error("Expected pds1 to be whitelisted")
426
+
if !isHS256IssuerWhitelisted("https://pds2.example.com") {
427
+
t.Error("Expected pds2 to be whitelisted")
431
+
func TestIsHS256IssuerWhitelisted_NotWhitelisted(t *testing.T) {
432
+
ResetJWTConfigForTesting()
433
+
os.Setenv("HS256_ISSUERS", "https://pds1.example.com")
435
+
os.Unsetenv("HS256_ISSUERS")
436
+
ResetJWTConfigForTesting()
439
+
if isHS256IssuerWhitelisted("https://attacker.example.com") {
440
+
t.Error("Expected non-whitelisted issuer to return false")
444
+
func TestIsHS256IssuerWhitelisted_EmptyWhitelist(t *testing.T) {
445
+
ResetJWTConfigForTesting()
446
+
os.Unsetenv("HS256_ISSUERS")
447
+
defer ResetJWTConfigForTesting()
449
+
if isHS256IssuerWhitelisted("https://any.example.com") {
450
+
t.Error("Expected false when whitelist is empty (safe default)")
454
+
func TestIsHS256IssuerWhitelisted_WhitespaceHandling(t *testing.T) {
455
+
ResetJWTConfigForTesting()
456
+
os.Setenv("HS256_ISSUERS", " https://pds1.example.com , https://pds2.example.com ")
458
+
os.Unsetenv("HS256_ISSUERS")
459
+
ResetJWTConfigForTesting()
462
+
if !isHS256IssuerWhitelisted("https://pds1.example.com") {
463
+
t.Error("Expected whitespace-trimmed issuer to be whitelisted")
467
+
// === shouldUseHS256 Tests (kid-based logic) ===
469
+
func TestShouldUseHS256_WithKid_AlwaysFalse(t *testing.T) {
470
+
// Tokens with kid should NEVER use HS256, regardless of issuer whitelist
471
+
ResetJWTConfigForTesting()
472
+
os.Setenv("HS256_ISSUERS", "https://whitelisted.example.com")
474
+
os.Unsetenv("HS256_ISSUERS")
475
+
ResetJWTConfigForTesting()
478
+
header := &JWTHeader{
479
+
Alg: AlgorithmHS256,
480
+
Kid: "some-key-id", // Has kid
483
+
// Even whitelisted issuer should not use HS256 if token has kid
484
+
if shouldUseHS256(header, "https://whitelisted.example.com") {
485
+
t.Error("Tokens with kid should never use HS256 (supports federation)")
489
+
func TestShouldUseHS256_WithoutKid_WhitelistedIssuer(t *testing.T) {
490
+
ResetJWTConfigForTesting()
491
+
os.Setenv("HS256_ISSUERS", "https://my-pds.example.com")
493
+
os.Unsetenv("HS256_ISSUERS")
494
+
ResetJWTConfigForTesting()
497
+
header := &JWTHeader{
498
+
Alg: AlgorithmHS256,
502
+
if !shouldUseHS256(header, "https://my-pds.example.com") {
503
+
t.Error("Token without kid from whitelisted issuer should use HS256")
507
+
func TestShouldUseHS256_WithoutKid_NotWhitelisted(t *testing.T) {
508
+
ResetJWTConfigForTesting()
509
+
os.Setenv("HS256_ISSUERS", "https://my-pds.example.com")
511
+
os.Unsetenv("HS256_ISSUERS")
512
+
ResetJWTConfigForTesting()
515
+
header := &JWTHeader{
516
+
Alg: AlgorithmHS256,
520
+
if shouldUseHS256(header, "https://external-pds.example.com") {
521
+
t.Error("Token without kid from non-whitelisted issuer should NOT use HS256")
526
+
func contains(s, substr string) bool {
527
+
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
530
+
func containsHelper(s, substr string) bool {
531
+
for i := 0; i <= len(s)-len(substr); i++ {
532
+
if s[i:i+len(substr)] == substr {