this repo has no description
at main 17 kB view raw
1use base64::Engine; 2use base64::engine::general_purpose::URL_SAFE_NO_PAD; 3use chrono::Utc; 4use serde::{Deserialize, Serialize}; 5use sha2::{Digest, Sha256}; 6 7use crate::OAuthError; 8use tranquil_types::{DPoPProofId, JwkThumbprint}; 9 10const DPOP_NONCE_VALIDITY_SECS: i64 = 300; 11const DPOP_MAX_AGE_SECS: i64 = 300; 12 13#[derive(Debug, Clone)] 14pub struct DPoPVerifyResult { 15 pub jkt: JwkThumbprint, 16 pub jti: DPoPProofId, 17} 18 19#[derive(Debug, Clone, Serialize, Deserialize)] 20pub struct DPoPProofHeader { 21 pub typ: String, 22 pub alg: String, 23 pub jwk: DPoPJwk, 24} 25 26#[derive(Debug, Clone, Serialize, Deserialize)] 27pub struct DPoPJwk { 28 pub kty: String, 29 #[serde(skip_serializing_if = "Option::is_none")] 30 pub crv: Option<String>, 31 #[serde(skip_serializing_if = "Option::is_none")] 32 pub x: Option<String>, 33 #[serde(skip_serializing_if = "Option::is_none")] 34 pub y: Option<String>, 35} 36 37#[derive(Debug, Clone, Serialize, Deserialize)] 38pub struct DPoPProofPayload { 39 pub jti: String, 40 pub htm: String, 41 pub htu: String, 42 pub iat: i64, 43 #[serde(skip_serializing_if = "Option::is_none")] 44 pub ath: Option<String>, 45 #[serde(skip_serializing_if = "Option::is_none")] 46 pub nonce: Option<String>, 47} 48 49pub struct DPoPVerifier { 50 secret: Vec<u8>, 51} 52 53impl DPoPVerifier { 54 pub fn new(secret: &[u8]) -> Self { 55 Self { 56 secret: secret.to_vec(), 57 } 58 } 59 60 pub fn generate_nonce(&self) -> String { 61 let timestamp = Utc::now().timestamp(); 62 let timestamp_bytes = timestamp.to_be_bytes(); 63 let mut hasher = Sha256::new(); 64 hasher.update(&self.secret); 65 hasher.update(timestamp_bytes); 66 let hash = hasher.finalize(); 67 let mut nonce_data = Vec::with_capacity(8 + 16); 68 nonce_data.extend_from_slice(&timestamp_bytes); 69 nonce_data.extend_from_slice(&hash[..16]); 70 URL_SAFE_NO_PAD.encode(&nonce_data) 71 } 72 73 pub fn validate_nonce(&self, nonce: &str) -> Result<(), OAuthError> { 74 let nonce_bytes = URL_SAFE_NO_PAD 75 .decode(nonce) 76 .map_err(|_| OAuthError::InvalidDpopProof("Invalid nonce encoding".to_string()))?; 77 if nonce_bytes.len() < 24 { 78 return Err(OAuthError::InvalidDpopProof( 79 "Invalid nonce length".to_string(), 80 )); 81 } 82 let timestamp_bytes: [u8; 8] = nonce_bytes[..8] 83 .try_into() 84 .map_err(|_| OAuthError::InvalidDpopProof("Invalid nonce".to_string()))?; 85 let timestamp = i64::from_be_bytes(timestamp_bytes); 86 let now = Utc::now().timestamp(); 87 if now - timestamp > DPOP_NONCE_VALIDITY_SECS { 88 return Err(OAuthError::UseDpopNonce(self.generate_nonce())); 89 } 90 let mut hasher = Sha256::new(); 91 hasher.update(&self.secret); 92 hasher.update(timestamp_bytes); 93 let expected_hash = hasher.finalize(); 94 if nonce_bytes[8..24] != expected_hash[..16] { 95 return Err(OAuthError::InvalidDpopProof( 96 "Invalid nonce signature".to_string(), 97 )); 98 } 99 Ok(()) 100 } 101 102 pub fn verify_proof( 103 &self, 104 dpop_header: &str, 105 http_method: &str, 106 http_uri: &str, 107 access_token_hash: Option<&str>, 108 ) -> Result<DPoPVerifyResult, OAuthError> { 109 let parts: Vec<&str> = dpop_header.split('.').collect(); 110 if parts.len() != 3 { 111 return Err(OAuthError::InvalidDpopProof( 112 "Invalid DPoP proof format".to_string(), 113 )); 114 } 115 let header_json = URL_SAFE_NO_PAD 116 .decode(parts[0]) 117 .map_err(|_| OAuthError::InvalidDpopProof("Invalid header encoding".to_string()))?; 118 let payload_json = URL_SAFE_NO_PAD 119 .decode(parts[1]) 120 .map_err(|_| OAuthError::InvalidDpopProof("Invalid payload encoding".to_string()))?; 121 let header: DPoPProofHeader = serde_json::from_slice(&header_json) 122 .map_err(|_| OAuthError::InvalidDpopProof("Invalid header JSON".to_string()))?; 123 let payload: DPoPProofPayload = serde_json::from_slice(&payload_json) 124 .map_err(|_| OAuthError::InvalidDpopProof("Invalid payload JSON".to_string()))?; 125 if header.typ != "dpop+jwt" { 126 return Err(OAuthError::InvalidDpopProof( 127 "Invalid typ claim".to_string(), 128 )); 129 } 130 if !matches!(header.alg.as_str(), "ES256" | "ES384" | "ES512" | "EdDSA") { 131 return Err(OAuthError::InvalidDpopProof( 132 "Unsupported algorithm".to_string(), 133 )); 134 } 135 if payload.htm.to_uppercase() != http_method.to_uppercase() { 136 return Err(OAuthError::InvalidDpopProof( 137 "HTTP method mismatch".to_string(), 138 )); 139 } 140 let proof_uri = payload.htu.split('?').next().unwrap_or(&payload.htu); 141 let request_uri = http_uri.split('?').next().unwrap_or(http_uri); 142 if proof_uri != request_uri { 143 return Err(OAuthError::InvalidDpopProof( 144 "HTTP URI mismatch".to_string(), 145 )); 146 } 147 let now = Utc::now().timestamp(); 148 if (now - payload.iat).abs() > DPOP_MAX_AGE_SECS { 149 return Err(OAuthError::InvalidDpopProof( 150 "Proof too old or from the future".to_string(), 151 )); 152 } 153 if let Some(nonce) = &payload.nonce { 154 self.validate_nonce(nonce)?; 155 } 156 if let Some(expected_ath) = access_token_hash { 157 match &payload.ath { 158 Some(ath) if ath == expected_ath => {} 159 Some(_) => { 160 return Err(OAuthError::InvalidDpopProof( 161 "Access token hash mismatch".to_string(), 162 )); 163 } 164 None => { 165 return Err(OAuthError::InvalidDpopProof( 166 "Missing access token hash".to_string(), 167 )); 168 } 169 } 170 } 171 let signature_bytes = URL_SAFE_NO_PAD 172 .decode(parts[2]) 173 .map_err(|_| OAuthError::InvalidDpopProof("Invalid signature encoding".to_string()))?; 174 let signing_input = format!("{}.{}", parts[0], parts[1]); 175 verify_dpop_signature( 176 &header.alg, 177 &header.jwk, 178 signing_input.as_bytes(), 179 &signature_bytes, 180 )?; 181 let jkt = compute_jwk_thumbprint(&header.jwk)?; 182 Ok(DPoPVerifyResult { 183 jkt: jkt.into(), 184 jti: payload.jti.clone().into(), 185 }) 186 } 187} 188 189fn verify_dpop_signature( 190 alg: &str, 191 jwk: &DPoPJwk, 192 message: &[u8], 193 signature: &[u8], 194) -> Result<(), OAuthError> { 195 match alg { 196 "ES256" => verify_es256(jwk, message, signature), 197 "ES384" => verify_es384(jwk, message, signature), 198 "EdDSA" => verify_eddsa(jwk, message, signature), 199 _ => Err(OAuthError::InvalidDpopProof(format!( 200 "Unsupported algorithm: {}", 201 alg 202 ))), 203 } 204} 205 206fn verify_es256(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 207 use p256::ecdsa::signature::Verifier; 208 use p256::ecdsa::{Signature, VerifyingKey}; 209 use p256::elliptic_curve::sec1::FromEncodedPoint; 210 use p256::{AffinePoint, EncodedPoint}; 211 let crv = jwk 212 .crv 213 .as_ref() 214 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv for ES256".to_string()))?; 215 if crv != "P-256" { 216 return Err(OAuthError::InvalidDpopProof(format!( 217 "Invalid curve for ES256: {}", 218 crv 219 ))); 220 } 221 let x_decoded = URL_SAFE_NO_PAD 222 .decode( 223 jwk.x 224 .as_ref() 225 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x coordinate".to_string()))?, 226 ) 227 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 228 let y_decoded = URL_SAFE_NO_PAD 229 .decode( 230 jwk.y 231 .as_ref() 232 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing y coordinate".to_string()))?, 233 ) 234 .map_err(|_| OAuthError::InvalidDpopProof("Invalid y encoding".to_string()))?; 235 let mut x_bytes = [0u8; 32]; 236 let mut y_bytes = [0u8; 32]; 237 if x_decoded.len() > 32 || y_decoded.len() > 32 { 238 return Err(OAuthError::InvalidDpopProof( 239 "EC coordinate too long".to_string(), 240 )); 241 } 242 x_bytes[32 - x_decoded.len()..].copy_from_slice(&x_decoded); 243 y_bytes[32 - y_decoded.len()..].copy_from_slice(&y_decoded); 244 let point = EncodedPoint::from_affine_coordinates((&x_bytes).into(), (&y_bytes).into(), false); 245 let affine_opt: Option<AffinePoint> = AffinePoint::from_encoded_point(&point).into(); 246 let affine = 247 affine_opt.ok_or_else(|| OAuthError::InvalidDpopProof("Invalid EC point".to_string()))?; 248 let verifying_key = VerifyingKey::from_affine(affine) 249 .map_err(|_| OAuthError::InvalidDpopProof("Invalid verifying key".to_string()))?; 250 let sig = Signature::from_slice(signature) 251 .map_err(|_| OAuthError::InvalidDpopProof("Invalid signature format".to_string()))?; 252 verifying_key 253 .verify(message, &sig) 254 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 255} 256 257fn verify_es384(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 258 use p384::ecdsa::signature::Verifier; 259 use p384::ecdsa::{Signature, VerifyingKey}; 260 use p384::elliptic_curve::sec1::FromEncodedPoint; 261 use p384::{AffinePoint, EncodedPoint}; 262 let crv = jwk 263 .crv 264 .as_ref() 265 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv for ES384".to_string()))?; 266 if crv != "P-384" { 267 return Err(OAuthError::InvalidDpopProof(format!( 268 "Invalid curve for ES384: {}", 269 crv 270 ))); 271 } 272 let x_decoded = URL_SAFE_NO_PAD 273 .decode( 274 jwk.x 275 .as_ref() 276 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x coordinate".to_string()))?, 277 ) 278 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 279 let y_decoded = URL_SAFE_NO_PAD 280 .decode( 281 jwk.y 282 .as_ref() 283 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing y coordinate".to_string()))?, 284 ) 285 .map_err(|_| OAuthError::InvalidDpopProof("Invalid y encoding".to_string()))?; 286 let mut x_bytes = [0u8; 48]; 287 let mut y_bytes = [0u8; 48]; 288 if x_decoded.len() > 48 || y_decoded.len() > 48 { 289 return Err(OAuthError::InvalidDpopProof( 290 "EC coordinate too long".to_string(), 291 )); 292 } 293 x_bytes[48 - x_decoded.len()..].copy_from_slice(&x_decoded); 294 y_bytes[48 - y_decoded.len()..].copy_from_slice(&y_decoded); 295 let point = EncodedPoint::from_affine_coordinates((&x_bytes).into(), (&y_bytes).into(), false); 296 let affine_opt: Option<AffinePoint> = AffinePoint::from_encoded_point(&point).into(); 297 let affine = 298 affine_opt.ok_or_else(|| OAuthError::InvalidDpopProof("Invalid EC point".to_string()))?; 299 let verifying_key = VerifyingKey::from_affine(affine) 300 .map_err(|_| OAuthError::InvalidDpopProof("Invalid verifying key".to_string()))?; 301 let sig = Signature::from_slice(signature) 302 .map_err(|_| OAuthError::InvalidDpopProof("Invalid signature format".to_string()))?; 303 verifying_key 304 .verify(message, &sig) 305 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 306} 307 308fn verify_eddsa(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 309 use ed25519_dalek::{Signature, VerifyingKey}; 310 let crv = jwk 311 .crv 312 .as_ref() 313 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv for EdDSA".to_string()))?; 314 if crv != "Ed25519" { 315 return Err(OAuthError::InvalidDpopProof(format!( 316 "Invalid curve for EdDSA: {}", 317 crv 318 ))); 319 } 320 let x_bytes = URL_SAFE_NO_PAD 321 .decode( 322 jwk.x 323 .as_ref() 324 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x coordinate".to_string()))?, 325 ) 326 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 327 let key_bytes: [u8; 32] = x_bytes 328 .try_into() 329 .map_err(|_| OAuthError::InvalidDpopProof("Invalid Ed25519 key length".to_string()))?; 330 let verifying_key = VerifyingKey::from_bytes(&key_bytes) 331 .map_err(|_| OAuthError::InvalidDpopProof("Invalid Ed25519 key".to_string()))?; 332 let sig_bytes: [u8; 64] = signature.try_into().map_err(|_| { 333 OAuthError::InvalidDpopProof("Invalid Ed25519 signature length".to_string()) 334 })?; 335 let sig = Signature::from_bytes(&sig_bytes); 336 verifying_key 337 .verify_strict(message, &sig) 338 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 339} 340 341pub fn compute_jwk_thumbprint(jwk: &DPoPJwk) -> Result<String, OAuthError> { 342 let canonical = match jwk.kty.as_str() { 343 "EC" => { 344 let crv = jwk 345 .crv 346 .as_ref() 347 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?; 348 let x = jwk 349 .x 350 .as_ref() 351 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?; 352 let y = jwk 353 .y 354 .as_ref() 355 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing y".to_string()))?; 356 format!(r#"{{"crv":"{}","kty":"EC","x":"{}","y":"{}"}}"#, crv, x, y) 357 } 358 "OKP" => { 359 let crv = jwk 360 .crv 361 .as_ref() 362 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?; 363 let x = jwk 364 .x 365 .as_ref() 366 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?; 367 format!(r#"{{"crv":"{}","kty":"OKP","x":"{}"}}"#, crv, x) 368 } 369 _ => { 370 return Err(OAuthError::InvalidDpopProof( 371 "Unsupported key type".to_string(), 372 )); 373 } 374 }; 375 let mut hasher = Sha256::new(); 376 hasher.update(canonical.as_bytes()); 377 let hash = hasher.finalize(); 378 Ok(URL_SAFE_NO_PAD.encode(hash)) 379} 380 381pub fn compute_access_token_hash(access_token: &str) -> String { 382 let mut hasher = Sha256::new(); 383 hasher.update(access_token.as_bytes()); 384 let hash = hasher.finalize(); 385 URL_SAFE_NO_PAD.encode(hash) 386} 387 388#[cfg(test)] 389mod tests { 390 use super::*; 391 392 #[test] 393 fn test_nonce_generation_and_validation() { 394 let secret = b"test-secret-key-32-bytes-long!!!"; 395 let verifier = DPoPVerifier::new(secret); 396 let nonce = verifier.generate_nonce(); 397 assert!(verifier.validate_nonce(&nonce).is_ok()); 398 } 399 400 #[test] 401 fn test_jwk_thumbprint_ec() { 402 let jwk = DPoPJwk { 403 kty: "EC".to_string(), 404 crv: Some("P-256".to_string()), 405 x: Some("test_x".to_string()), 406 y: Some("test_y".to_string()), 407 }; 408 let thumbprint = compute_jwk_thumbprint(&jwk).unwrap(); 409 assert!(!thumbprint.is_empty()); 410 } 411 412 #[test] 413 fn test_es256_short_coordinate_no_panic() { 414 let short_31_bytes = vec![0x42u8; 31]; 415 let short_30_bytes = vec![0x42u8; 30]; 416 let x_b64 = URL_SAFE_NO_PAD.encode(&short_31_bytes); 417 let y_b64 = URL_SAFE_NO_PAD.encode(&short_30_bytes); 418 let jwk = DPoPJwk { 419 kty: "EC".to_string(), 420 crv: Some("P-256".to_string()), 421 x: Some(x_b64), 422 y: Some(y_b64), 423 }; 424 let result = verify_es256(&jwk, b"test", &[0u8; 64]); 425 assert!(result.is_err(), "Invalid coordinates should return error, not panic"); 426 } 427 428 #[test] 429 fn test_es256_valid_key_with_trimmed_coordinates() { 430 use p256::ecdsa::{SigningKey, signature::Signer}; 431 use p256::elliptic_curve::rand_core::OsRng; 432 433 let signing_key = SigningKey::random(&mut OsRng); 434 let verifying_key = signing_key.verifying_key(); 435 let point = verifying_key.to_encoded_point(false); 436 let x_bytes = point.x().unwrap(); 437 let y_bytes = point.y().unwrap(); 438 let x_trimmed: Vec<u8> = x_bytes.iter().copied().skip_while(|&b| b == 0).collect(); 439 let y_trimmed: Vec<u8> = y_bytes.iter().copied().skip_while(|&b| b == 0).collect(); 440 let x_b64 = URL_SAFE_NO_PAD.encode(&x_trimmed); 441 let y_b64 = URL_SAFE_NO_PAD.encode(&y_trimmed); 442 let jwk = DPoPJwk { 443 kty: "EC".to_string(), 444 crv: Some("P-256".to_string()), 445 x: Some(x_b64), 446 y: Some(y_b64), 447 }; 448 let message = b"test message for signature verification"; 449 let signature: p256::ecdsa::Signature = signing_key.sign(message); 450 let result = verify_es256(&jwk, message, signature.to_bytes().as_slice()); 451 assert!( 452 result.is_ok(), 453 "Should verify signature with trimmed coordinates (x={}, y={}): {:?}", 454 x_trimmed.len(), 455 y_trimmed.len(), 456 result 457 ); 458 } 459}