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