1+ use std:: time:: Duration ;
2+
13use base64:: { engine:: general_purpose:: URL_SAFE_NO_PAD as BASE64URL , Engine } ;
24use chrono:: Utc ;
35use p256:: ecdsa:: { SigningKey , VerifyingKey } ;
46use rand:: RngCore ;
57
68use crate :: crypto;
79use crate :: error:: Error ;
10+ use crate :: nonce:: NonceStore ;
811use crate :: types:: mutual:: { Challenge , Response } ;
912
1013const NONCE_EXPIRY_SECS : i64 = 60 ;
@@ -39,6 +42,19 @@ pub fn verify_response(
3942 response : & Response ,
4043 challenge : & Challenge ,
4144 verifying_key : & VerifyingKey ,
45+ ) -> Result < bool , Error > {
46+ verify_response_with_nonce_store ( response, challenge, verifying_key, None )
47+ }
48+
49+ /// Verify a challenge response with optional nonce deduplication.
50+ ///
51+ /// When a `nonce_store` is provided, the response nonce is checked against
52+ /// previously seen nonces to prevent replay attacks within the validity window.
53+ pub fn verify_response_with_nonce_store (
54+ response : & Response ,
55+ challenge : & Challenge ,
56+ verifying_key : & VerifyingKey ,
57+ nonce_store : Option < & dyn NonceStore > ,
4258) -> Result < bool , Error > {
4359 // Check nonce matches
4460 if response. nonce != challenge. nonce {
@@ -56,6 +72,14 @@ pub fn verify_response(
5672 }
5773 }
5874
75+ // Check nonce deduplication if a store is provided
76+ if let Some ( store) = nonce_store {
77+ let fresh = store. check_and_record ( & response. nonce , Duration :: from_secs ( 60 ) ) ?;
78+ if !fresh {
79+ return Err ( Error :: Jwt ( "Nonce has already been used" . to_string ( ) ) ) ;
80+ }
81+ }
82+
5983 // Verify signature over the nonce
6084 crypto:: verify_bytes (
6185 verifying_key,
@@ -146,4 +170,41 @@ mod tests {
146170 Some ( "eyJ...test-jwt" . to_string( ) )
147171 ) ;
148172 }
173+
174+ #[ test]
175+ fn test_verify_with_nonce_store ( ) {
176+ let kp = crypto:: generate_key_pair ( ) . unwrap ( ) ;
177+ let sk = crypto:: load_signing_key ( & kp. private_key_pem ) . unwrap ( ) ;
178+ let vk = crypto:: load_verifying_key ( & kp. public_key_pem ) . unwrap ( ) ;
179+
180+ let store = crate :: nonce:: InMemoryNonceStore :: new ( ) ;
181+ let challenge = create_challenge ( None ) ;
182+ let response = create_response ( & challenge, & sk, "test-key" ) ;
183+
184+ // First verification should succeed.
185+ let valid =
186+ verify_response_with_nonce_store ( & response, & challenge, & vk, Some ( & store) ) . unwrap ( ) ;
187+ assert ! ( valid) ;
188+
189+ // Second verification with the same nonce should fail (replay).
190+ let result = verify_response_with_nonce_store ( & response, & challenge, & vk, Some ( & store) ) ;
191+ assert ! ( result. is_err( ) , "Replayed nonce should be rejected" ) ;
192+ }
193+
194+ #[ test]
195+ fn test_verify_without_nonce_store ( ) {
196+ let kp = crypto:: generate_key_pair ( ) . unwrap ( ) ;
197+ let sk = crypto:: load_signing_key ( & kp. private_key_pem ) . unwrap ( ) ;
198+ let vk = crypto:: load_verifying_key ( & kp. public_key_pem ) . unwrap ( ) ;
199+
200+ let challenge = create_challenge ( None ) ;
201+ let response = create_response ( & challenge, & sk, "test-key" ) ;
202+
203+ // Without a nonce store, same nonce can be verified multiple times.
204+ let valid1 = verify_response ( & response, & challenge, & vk) . unwrap ( ) ;
205+ assert ! ( valid1) ;
206+
207+ let valid2 = verify_response ( & response, & challenge, & vk) . unwrap ( ) ;
208+ assert ! ( valid2) ;
209+ }
149210}
0 commit comments