CSRF protection using HMAC-signed state tokens (RFC 5869, RFC 2104)
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

csrf: refactor tests to modular suite structure

Extract test runner to test.ml with RNG initialization.
Rename test_csrf.ml to export suite instead of running directly.

+15 -48
+1 -1
test/dune
··· 1 1 (test 2 - (name test_csrf) 2 + (name test) 3 3 (libraries csrf kdf.hkdf alcotest crypto-rng.unix))
+3
test/test.ml
··· 1 + let () = 2 + Crypto_rng_unix.use_default (); 3 + Alcotest.run "csrf" Test_csrf.suite
+11 -47
test/test_csrf.ml
··· 1 1 (** Tests for CSRF module *) 2 2 3 - (** Generate a random hex state for testing *) 4 3 let generate_state () = 5 4 let bytes = Crypto_rng.generate 16 in 6 5 let hex = Bytes.create 32 in ··· 15 14 16 15 let test_hkdf_key_derivation () = 17 16 let secret = "test-secret-key-12345-must-be-long-enough" in 18 - 19 - (* Derive HMAC key using HKDF *) 20 17 let prk = Hkdf.extract ~hash:`SHA256 ~salt:"" secret in 21 18 let hmac_key = Hkdf.expand ~hash:`SHA256 ~prk ~info:"csrf-hmac-v1" 32 in 22 - 23 - (* Derive a different key with different context *) 24 19 let other_key = Hkdf.expand ~hash:`SHA256 ~prk ~info:"other-context-v1" 32 in 25 - 26 - (* Verify key has correct length (32 bytes for SHA-256 HMAC) *) 27 20 Alcotest.(check int) "HMAC key length" 32 (String.length hmac_key); 28 - 29 - (* Verify keys are different (context separation works) *) 30 21 Alcotest.(check bool) "keys are distinct" true (hmac_key <> other_key); 31 - 32 - (* Verify keys are deterministic (same input = same output) *) 33 22 let hmac_key2 = Hkdf.expand ~hash:`SHA256 ~prk ~info:"csrf-hmac-v1" 32 in 34 23 Alcotest.(check bool) "HMAC key is deterministic" true (hmac_key = hmac_key2); 35 - 36 - (* Verify different secrets produce different keys *) 37 24 let secret2 = "different-secret-key-12345-must-be-long" in 38 25 let prk2 = Hkdf.extract ~hash:`SHA256 ~salt:"" secret2 in 39 26 let hmac_key3 = Hkdf.expand ~hash:`SHA256 ~prk:prk2 ~info:"csrf-hmac-v1" 32 in ··· 43 30 let test_csrf_signing () = 44 31 let secret = "test-secret-key-12345-must-be-long-enough" in 45 32 let state = generate_state () in 46 - 47 - (* Test signing and verification *) 48 33 let signed_state = Csrf.sign_state ~secret state in 49 34 Alcotest.(check bool) 50 35 "signed state contains dot separator" true 51 36 (String.contains signed_state '.'); 52 - 53 - (* Test valid verification *) 54 37 let verified = Csrf.verify_state ~secret signed_state in 55 38 Alcotest.(check (option string)) 56 39 "verify valid signed state" (Some state) verified; 57 - 58 - (* Test invalid signature *) 59 40 let tampered_signed = 60 41 state ^ ".deadbeef1234567890abcdef1234567890abcdef1234567890abcdef12345678" 61 42 in 62 43 let tampered_verified = Csrf.verify_state ~secret tampered_signed in 63 44 Alcotest.(check (option string)) 64 45 "reject tampered signature" None tampered_verified; 65 - 66 - (* Test wrong secret *) 67 46 let wrong_secret_verified = 68 47 Csrf.verify_state ~secret:"wrong-secret" signed_state 69 48 in 70 49 Alcotest.(check (option string)) 71 50 "reject wrong secret" None wrong_secret_verified; 72 - 73 - (* Test malformed input (no dot) *) 74 51 let malformed_verified = Csrf.verify_state ~secret "nodothere" in 75 52 Alcotest.(check (option string)) 76 53 "reject malformed (no dot)" None malformed_verified; 77 - 78 - (* Test state containing dots (should work - signature is after last dot) *) 79 54 let dotted_state = "foo.bar.baz" in 80 55 let dotted_signed = Csrf.sign_state ~secret dotted_state in 81 56 let dotted_verified = Csrf.verify_state ~secret dotted_signed in 82 57 Alcotest.(check (option string)) 83 58 "verify state with dots" (Some dotted_state) dotted_verified; 84 - 85 - (* Test empty state *) 86 59 let empty_signed = Csrf.sign_state ~secret "" in 87 60 let empty_verified = Csrf.verify_state ~secret empty_signed in 88 61 Alcotest.(check (option string)) "verify empty state" (Some "") empty_verified 89 62 90 63 let test_length_limit () = 91 64 let secret = "test-secret" in 92 - 93 - (* Create a state that would exceed the limit when signed *) 94 65 let long_state = String.make 200 'x' in 95 66 let signed = Csrf.sign_state ~secret long_state in 96 - 97 - (* Should be rejected due to length *) 98 67 Alcotest.(check bool) 99 68 "signed long state exceeds limit" true 100 69 (String.length signed > Csrf.max_signed_state_length); 101 - 102 70 let verified = Csrf.verify_state ~secret signed in 103 71 Alcotest.(check (option string)) "reject oversized signed state" None verified; 104 - 105 - (* Test state that fits within limit *) 106 72 let short_state = String.make 100 'y' in 107 73 let short_signed = Csrf.sign_state ~secret short_state in 108 74 let short_verified = Csrf.verify_state ~secret short_signed in 109 75 Alcotest.(check (option string)) 110 76 "accept reasonably sized state" (Some short_state) short_verified 111 77 112 - let () = 113 - Crypto_rng_unix.use_default (); 114 - Alcotest.run "csrf" 115 - [ 116 - ( "CSRF", 117 - [ 118 - Alcotest.test_case "state signing and verification" `Quick 119 - test_csrf_signing; 120 - Alcotest.test_case "HKDF key derivation" `Quick 121 - test_hkdf_key_derivation; 122 - Alcotest.test_case "length limit protection" `Quick test_length_limit; 123 - ] ); 124 - ] 78 + let suite = 79 + [ 80 + ( "signing", 81 + [ 82 + Alcotest.test_case "state signing and verification" `Quick 83 + test_csrf_signing; 84 + Alcotest.test_case "length limit protection" `Quick test_length_limit; 85 + ] ); 86 + ( "hkdf", 87 + [ Alcotest.test_case "key derivation" `Quick test_hkdf_key_derivation ] ); 88 + ]