this repo has no description
1use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 2use bspds::oauth::dpop::{DPoPVerifier, compute_jwk_thumbprint, DPoPJwk}; 3use chrono::Utc; 4use serde_json::json; 5 6fn create_dpop_proof( 7 method: &str, 8 uri: &str, 9 nonce: Option<&str>, 10 ath: Option<&str>, 11 iat_offset_secs: i64, 12) -> String { 13 use p256::ecdsa::{SigningKey, Signature, signature::Signer}; 14 15 let signing_key = SigningKey::random(&mut rand::thread_rng()); 16 let verifying_key = signing_key.verifying_key(); 17 let point = verifying_key.to_encoded_point(false); 18 19 let x = URL_SAFE_NO_PAD.encode(point.x().unwrap()); 20 let y = URL_SAFE_NO_PAD.encode(point.y().unwrap()); 21 22 let jwk = json!({ 23 "kty": "EC", 24 "crv": "P-256", 25 "x": x, 26 "y": y 27 }); 28 29 let header = json!({ 30 "typ": "dpop+jwt", 31 "alg": "ES256", 32 "jwk": jwk 33 }); 34 35 let mut payload = json!({ 36 "jti": format!("unique-{}", Utc::now().timestamp_nanos_opt().unwrap_or(0)), 37 "htm": method, 38 "htu": uri, 39 "iat": Utc::now().timestamp() + iat_offset_secs 40 }); 41 42 if let Some(n) = nonce { 43 payload["nonce"] = json!(n); 44 } 45 46 if let Some(a) = ath { 47 payload["ath"] = json!(a); 48 } 49 50 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 51 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 52 53 let signing_input = format!("{}.{}", header_b64, payload_b64); 54 let signature: Signature = signing_key.sign(signing_input.as_bytes()); 55 let signature_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes()); 56 57 format!("{}.{}", signing_input, signature_b64) 58} 59 60#[test] 61fn test_dpop_nonce_generation() { 62 let secret = b"test-dpop-secret-32-bytes-long!!"; 63 let verifier = DPoPVerifier::new(secret); 64 65 let nonce1 = verifier.generate_nonce(); 66 let nonce2 = verifier.generate_nonce(); 67 68 assert!(!nonce1.is_empty()); 69 assert!(!nonce2.is_empty()); 70} 71 72#[test] 73fn test_dpop_nonce_validation_success() { 74 let secret = b"test-dpop-secret-32-bytes-long!!"; 75 let verifier = DPoPVerifier::new(secret); 76 77 let nonce = verifier.generate_nonce(); 78 let result = verifier.validate_nonce(&nonce); 79 80 assert!(result.is_ok(), "Valid nonce should pass: {:?}", result); 81} 82 83#[test] 84fn test_dpop_nonce_wrong_secret() { 85 let secret1 = b"test-dpop-secret-32-bytes-long!!"; 86 let secret2 = b"different-secret-32-bytes-long!!"; 87 88 let verifier1 = DPoPVerifier::new(secret1); 89 let verifier2 = DPoPVerifier::new(secret2); 90 91 let nonce = verifier1.generate_nonce(); 92 let result = verifier2.validate_nonce(&nonce); 93 94 assert!(result.is_err(), "Nonce from different secret should fail"); 95} 96 97#[test] 98fn test_dpop_nonce_invalid_format() { 99 let secret = b"test-dpop-secret-32-bytes-long!!"; 100 let verifier = DPoPVerifier::new(secret); 101 102 assert!(verifier.validate_nonce("invalid").is_err()); 103 assert!(verifier.validate_nonce("").is_err()); 104 assert!(verifier.validate_nonce("!!!not-base64!!!").is_err()); 105} 106 107#[test] 108fn test_jwk_thumbprint_ec_p256() { 109 let jwk = DPoPJwk { 110 kty: "EC".to_string(), 111 crv: Some("P-256".to_string()), 112 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()), 113 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()), 114 }; 115 116 let thumbprint = compute_jwk_thumbprint(&jwk); 117 assert!(thumbprint.is_ok()); 118 119 let tp = thumbprint.unwrap(); 120 assert!(!tp.is_empty()); 121 assert!(tp.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_')); 122} 123 124#[test] 125fn test_jwk_thumbprint_ec_secp256k1() { 126 let jwk = DPoPJwk { 127 kty: "EC".to_string(), 128 crv: Some("secp256k1".to_string()), 129 x: Some("some_x_value".to_string()), 130 y: Some("some_y_value".to_string()), 131 }; 132 133 let thumbprint = compute_jwk_thumbprint(&jwk); 134 assert!(thumbprint.is_ok()); 135} 136 137#[test] 138fn test_jwk_thumbprint_okp_ed25519() { 139 let jwk = DPoPJwk { 140 kty: "OKP".to_string(), 141 crv: Some("Ed25519".to_string()), 142 x: Some("some_x_value".to_string()), 143 y: None, 144 }; 145 146 let thumbprint = compute_jwk_thumbprint(&jwk); 147 assert!(thumbprint.is_ok()); 148} 149 150#[test] 151fn test_jwk_thumbprint_missing_crv() { 152 let jwk = DPoPJwk { 153 kty: "EC".to_string(), 154 crv: None, 155 x: Some("x".to_string()), 156 y: Some("y".to_string()), 157 }; 158 159 let thumbprint = compute_jwk_thumbprint(&jwk); 160 assert!(thumbprint.is_err()); 161} 162 163#[test] 164fn test_jwk_thumbprint_missing_x() { 165 let jwk = DPoPJwk { 166 kty: "EC".to_string(), 167 crv: Some("P-256".to_string()), 168 x: None, 169 y: Some("y".to_string()), 170 }; 171 172 let thumbprint = compute_jwk_thumbprint(&jwk); 173 assert!(thumbprint.is_err()); 174} 175 176#[test] 177fn test_jwk_thumbprint_missing_y_for_ec() { 178 let jwk = DPoPJwk { 179 kty: "EC".to_string(), 180 crv: Some("P-256".to_string()), 181 x: Some("x".to_string()), 182 y: None, 183 }; 184 185 let thumbprint = compute_jwk_thumbprint(&jwk); 186 assert!(thumbprint.is_err()); 187} 188 189#[test] 190fn test_jwk_thumbprint_unsupported_key_type() { 191 let jwk = DPoPJwk { 192 kty: "RSA".to_string(), 193 crv: None, 194 x: None, 195 y: None, 196 }; 197 198 let thumbprint = compute_jwk_thumbprint(&jwk); 199 assert!(thumbprint.is_err()); 200} 201 202#[test] 203fn test_jwk_thumbprint_deterministic() { 204 let jwk = DPoPJwk { 205 kty: "EC".to_string(), 206 crv: Some("P-256".to_string()), 207 x: Some("WbbXrPhtCg66wuF0NLhzXxF5PFzNZ7wNJm9M_1pCcXY".to_string()), 208 y: Some("DubR6_2kU1H5EYhbcNpYZGy1EY6GEKKxv6PYx8VW0rA".to_string()), 209 }; 210 211 let tp1 = compute_jwk_thumbprint(&jwk).unwrap(); 212 let tp2 = compute_jwk_thumbprint(&jwk).unwrap(); 213 214 assert_eq!(tp1, tp2, "Thumbprint should be deterministic"); 215} 216 217#[test] 218fn test_dpop_proof_invalid_format() { 219 let secret = b"test-dpop-secret-32-bytes-long!!"; 220 let verifier = DPoPVerifier::new(secret); 221 222 let result = verifier.verify_proof("not.enough.parts", "POST", "https://example.com", None); 223 assert!(result.is_err()); 224 225 let result = verifier.verify_proof("invalid", "POST", "https://example.com", None); 226 assert!(result.is_err()); 227} 228 229#[test] 230fn test_dpop_proof_invalid_typ() { 231 let secret = b"test-dpop-secret-32-bytes-long!!"; 232 let verifier = DPoPVerifier::new(secret); 233 234 let header = json!({ 235 "typ": "JWT", 236 "alg": "ES256", 237 "jwk": { 238 "kty": "EC", 239 "crv": "P-256", 240 "x": "x", 241 "y": "y" 242 } 243 }); 244 245 let payload = json!({ 246 "jti": "unique", 247 "htm": "POST", 248 "htu": "https://example.com", 249 "iat": Utc::now().timestamp() 250 }); 251 252 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); 253 let payload_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()); 254 let proof = format!("{}.{}.sig", header_b64, payload_b64); 255 256 let result = verifier.verify_proof(&proof, "POST", "https://example.com", None); 257 assert!(result.is_err()); 258} 259 260#[test] 261fn test_dpop_proof_method_mismatch() { 262 let secret = b"test-dpop-secret-32-bytes-long!!"; 263 let verifier = DPoPVerifier::new(secret); 264 265 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0); 266 267 let result = verifier.verify_proof(&proof, "GET", "https://example.com/token", None); 268 assert!(result.is_err()); 269} 270 271#[test] 272fn test_dpop_proof_uri_mismatch() { 273 let secret = b"test-dpop-secret-32-bytes-long!!"; 274 let verifier = DPoPVerifier::new(secret); 275 276 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0); 277 278 let result = verifier.verify_proof(&proof, "POST", "https://other.com/token", None); 279 assert!(result.is_err()); 280} 281 282#[test] 283fn test_dpop_proof_iat_too_old() { 284 let secret = b"test-dpop-secret-32-bytes-long!!"; 285 let verifier = DPoPVerifier::new(secret); 286 287 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, -600); 288 289 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None); 290 assert!(result.is_err()); 291} 292 293#[test] 294fn test_dpop_proof_iat_future() { 295 let secret = b"test-dpop-secret-32-bytes-long!!"; 296 let verifier = DPoPVerifier::new(secret); 297 298 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 600); 299 300 let result = verifier.verify_proof(&proof, "POST", "https://example.com/token", None); 301 assert!(result.is_err()); 302} 303 304#[test] 305fn test_dpop_proof_ath_mismatch() { 306 let secret = b"test-dpop-secret-32-bytes-long!!"; 307 let verifier = DPoPVerifier::new(secret); 308 309 let proof = create_dpop_proof( 310 "GET", 311 "https://example.com/resource", 312 None, 313 Some("wrong_hash"), 314 0, 315 ); 316 317 let result = verifier.verify_proof( 318 &proof, 319 "GET", 320 "https://example.com/resource", 321 Some("correct_hash"), 322 ); 323 assert!(result.is_err()); 324} 325 326#[test] 327fn test_dpop_proof_missing_ath_when_required() { 328 let secret = b"test-dpop-secret-32-bytes-long!!"; 329 let verifier = DPoPVerifier::new(secret); 330 331 let proof = create_dpop_proof("GET", "https://example.com/resource", None, None, 0); 332 333 let result = verifier.verify_proof( 334 &proof, 335 "GET", 336 "https://example.com/resource", 337 Some("expected_hash"), 338 ); 339 assert!(result.is_err()); 340} 341 342#[test] 343fn test_dpop_proof_uri_ignores_query_params() { 344 let secret = b"test-dpop-secret-32-bytes-long!!"; 345 let verifier = DPoPVerifier::new(secret); 346 347 let proof = create_dpop_proof("POST", "https://example.com/token", None, None, 0); 348 349 let result = verifier.verify_proof( 350 &proof, 351 "POST", 352 "https://example.com/token?foo=bar", 353 None, 354 ); 355 356 assert!(result.is_ok(), "Query params should be ignored: {:?}", result); 357}