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