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