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; 8use crate::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_bytes = 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_bytes = 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 point = EncodedPoint::from_affine_coordinates( 236 x_bytes.as_slice().into(), 237 y_bytes.as_slice().into(), 238 false, 239 ); 240 let affine_opt: Option<AffinePoint> = AffinePoint::from_encoded_point(&point).into(); 241 let affine = 242 affine_opt.ok_or_else(|| OAuthError::InvalidDpopProof("Invalid EC point".to_string()))?; 243 let verifying_key = VerifyingKey::from_affine(affine) 244 .map_err(|_| OAuthError::InvalidDpopProof("Invalid verifying key".to_string()))?; 245 let sig = Signature::from_slice(signature) 246 .map_err(|_| OAuthError::InvalidDpopProof("Invalid signature format".to_string()))?; 247 verifying_key 248 .verify(message, &sig) 249 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 250} 251 252fn verify_es384(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 253 use p384::ecdsa::signature::Verifier; 254 use p384::ecdsa::{Signature, VerifyingKey}; 255 use p384::elliptic_curve::sec1::FromEncodedPoint; 256 use p384::{AffinePoint, EncodedPoint}; 257 let crv = jwk 258 .crv 259 .as_ref() 260 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv for ES384".to_string()))?; 261 if crv != "P-384" { 262 return Err(OAuthError::InvalidDpopProof(format!( 263 "Invalid curve for ES384: {}", 264 crv 265 ))); 266 } 267 let x_bytes = URL_SAFE_NO_PAD 268 .decode( 269 jwk.x 270 .as_ref() 271 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x coordinate".to_string()))?, 272 ) 273 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 274 let y_bytes = URL_SAFE_NO_PAD 275 .decode( 276 jwk.y 277 .as_ref() 278 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing y coordinate".to_string()))?, 279 ) 280 .map_err(|_| OAuthError::InvalidDpopProof("Invalid y encoding".to_string()))?; 281 let point = EncodedPoint::from_affine_coordinates( 282 x_bytes.as_slice().into(), 283 y_bytes.as_slice().into(), 284 false, 285 ); 286 let affine_opt: Option<AffinePoint> = AffinePoint::from_encoded_point(&point).into(); 287 let affine = 288 affine_opt.ok_or_else(|| OAuthError::InvalidDpopProof("Invalid EC point".to_string()))?; 289 let verifying_key = VerifyingKey::from_affine(affine) 290 .map_err(|_| OAuthError::InvalidDpopProof("Invalid verifying key".to_string()))?; 291 let sig = Signature::from_slice(signature) 292 .map_err(|_| OAuthError::InvalidDpopProof("Invalid signature format".to_string()))?; 293 verifying_key 294 .verify(message, &sig) 295 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 296} 297 298fn verify_eddsa(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 299 use ed25519_dalek::{Signature, VerifyingKey}; 300 let crv = jwk 301 .crv 302 .as_ref() 303 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv for EdDSA".to_string()))?; 304 if crv != "Ed25519" { 305 return Err(OAuthError::InvalidDpopProof(format!( 306 "Invalid curve for EdDSA: {}", 307 crv 308 ))); 309 } 310 let x_bytes = URL_SAFE_NO_PAD 311 .decode( 312 jwk.x 313 .as_ref() 314 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x coordinate".to_string()))?, 315 ) 316 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 317 let key_bytes: [u8; 32] = x_bytes 318 .try_into() 319 .map_err(|_| OAuthError::InvalidDpopProof("Invalid Ed25519 key length".to_string()))?; 320 let verifying_key = VerifyingKey::from_bytes(&key_bytes) 321 .map_err(|_| OAuthError::InvalidDpopProof("Invalid Ed25519 key".to_string()))?; 322 let sig_bytes: [u8; 64] = signature.try_into().map_err(|_| { 323 OAuthError::InvalidDpopProof("Invalid Ed25519 signature length".to_string()) 324 })?; 325 let sig = Signature::from_bytes(&sig_bytes); 326 verifying_key 327 .verify_strict(message, &sig) 328 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 329} 330 331pub fn compute_jwk_thumbprint(jwk: &DPoPJwk) -> Result<String, OAuthError> { 332 let canonical = match jwk.kty.as_str() { 333 "EC" => { 334 let crv = jwk 335 .crv 336 .as_ref() 337 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?; 338 let x = jwk 339 .x 340 .as_ref() 341 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?; 342 let y = jwk 343 .y 344 .as_ref() 345 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing y".to_string()))?; 346 format!(r#"{{"crv":"{}","kty":"EC","x":"{}","y":"{}"}}"#, crv, x, y) 347 } 348 "OKP" => { 349 let crv = jwk 350 .crv 351 .as_ref() 352 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?; 353 let x = jwk 354 .x 355 .as_ref() 356 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?; 357 format!(r#"{{"crv":"{}","kty":"OKP","x":"{}"}}"#, crv, x) 358 } 359 _ => { 360 return Err(OAuthError::InvalidDpopProof( 361 "Unsupported key type".to_string(), 362 )); 363 } 364 }; 365 let mut hasher = Sha256::new(); 366 hasher.update(canonical.as_bytes()); 367 let hash = hasher.finalize(); 368 Ok(URL_SAFE_NO_PAD.encode(hash)) 369} 370 371pub fn compute_access_token_hash(access_token: &str) -> String { 372 let mut hasher = Sha256::new(); 373 hasher.update(access_token.as_bytes()); 374 let hash = hasher.finalize(); 375 URL_SAFE_NO_PAD.encode(hash) 376} 377 378#[cfg(test)] 379mod tests { 380 use super::*; 381 382 #[test] 383 fn test_nonce_generation_and_validation() { 384 let secret = b"test-secret-key-32-bytes-long!!!"; 385 let verifier = DPoPVerifier::new(secret); 386 let nonce = verifier.generate_nonce(); 387 assert!(verifier.validate_nonce(&nonce).is_ok()); 388 } 389 390 #[test] 391 fn test_jwk_thumbprint_ec() { 392 let jwk = DPoPJwk { 393 kty: "EC".to_string(), 394 crv: Some("P-256".to_string()), 395 x: Some("test_x".to_string()), 396 y: Some("test_y".to_string()), 397 }; 398 let thumbprint = compute_jwk_thumbprint(&jwk).unwrap(); 399 assert!(!thumbprint.is_empty()); 400 } 401}