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