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}; 6use super::OAuthError; 7const DPOP_NONCE_VALIDITY_SECS: i64 = 300; 8const DPOP_MAX_AGE_SECS: i64 = 300; 9#[derive(Debug, Clone)] 10pub struct DPoPVerifyResult { 11 pub jkt: String, 12 pub jti: String, 13} 14#[derive(Debug, Clone, Serialize, Deserialize)] 15pub struct DPoPProofHeader { 16 pub typ: String, 17 pub alg: String, 18 pub jwk: DPoPJwk, 19} 20#[derive(Debug, Clone, Serialize, Deserialize)] 21pub struct DPoPJwk { 22 pub kty: String, 23 #[serde(skip_serializing_if = "Option::is_none")] 24 pub crv: Option<String>, 25 #[serde(skip_serializing_if = "Option::is_none")] 26 pub x: Option<String>, 27 #[serde(skip_serializing_if = "Option::is_none")] 28 pub y: Option<String>, 29} 30#[derive(Debug, Clone, Serialize, Deserialize)] 31pub struct DPoPProofPayload { 32 pub jti: String, 33 pub htm: String, 34 pub htu: String, 35 pub iat: i64, 36 #[serde(skip_serializing_if = "Option::is_none")] 37 pub ath: Option<String>, 38 #[serde(skip_serializing_if = "Option::is_none")] 39 pub nonce: Option<String>, 40} 41pub struct DPoPVerifier { 42 secret: Vec<u8>, 43} 44impl DPoPVerifier { 45 pub fn new(secret: &[u8]) -> Self { 46 Self { 47 secret: secret.to_vec(), 48 } 49 } 50 pub fn generate_nonce(&self) -> String { 51 let timestamp = Utc::now().timestamp(); 52 let timestamp_bytes = timestamp.to_be_bytes(); 53 let mut hasher = Sha256::new(); 54 hasher.update(&self.secret); 55 hasher.update(&timestamp_bytes); 56 let hash = hasher.finalize(); 57 let mut nonce_data = Vec::with_capacity(8 + 16); 58 nonce_data.extend_from_slice(&timestamp_bytes); 59 nonce_data.extend_from_slice(&hash[..16]); 60 URL_SAFE_NO_PAD.encode(&nonce_data) 61 } 62 pub fn validate_nonce(&self, nonce: &str) -> Result<(), OAuthError> { 63 let nonce_bytes = URL_SAFE_NO_PAD 64 .decode(nonce) 65 .map_err(|_| OAuthError::InvalidDpopProof("Invalid nonce encoding".to_string()))?; 66 if nonce_bytes.len() < 24 { 67 return Err(OAuthError::InvalidDpopProof("Invalid nonce length".to_string())); 68 } 69 let timestamp_bytes: [u8; 8] = nonce_bytes[..8] 70 .try_into() 71 .map_err(|_| OAuthError::InvalidDpopProof("Invalid nonce".to_string()))?; 72 let timestamp = i64::from_be_bytes(timestamp_bytes); 73 let now = Utc::now().timestamp(); 74 if now - timestamp > DPOP_NONCE_VALIDITY_SECS { 75 return Err(OAuthError::UseDpopNonce(self.generate_nonce())); 76 } 77 let mut hasher = Sha256::new(); 78 hasher.update(&self.secret); 79 hasher.update(&timestamp_bytes); 80 let expected_hash = hasher.finalize(); 81 if nonce_bytes[8..24] != expected_hash[..16] { 82 return Err(OAuthError::InvalidDpopProof("Invalid nonce signature".to_string())); 83 } 84 Ok(()) 85 } 86 pub fn verify_proof( 87 &self, 88 dpop_header: &str, 89 http_method: &str, 90 http_uri: &str, 91 access_token_hash: Option<&str>, 92 ) -> Result<DPoPVerifyResult, OAuthError> { 93 let parts: Vec<&str> = dpop_header.split('.').collect(); 94 if parts.len() != 3 { 95 return Err(OAuthError::InvalidDpopProof("Invalid DPoP proof format".to_string())); 96 } 97 let header_json = URL_SAFE_NO_PAD 98 .decode(parts[0]) 99 .map_err(|_| OAuthError::InvalidDpopProof("Invalid header encoding".to_string()))?; 100 let payload_json = URL_SAFE_NO_PAD 101 .decode(parts[1]) 102 .map_err(|_| OAuthError::InvalidDpopProof("Invalid payload encoding".to_string()))?; 103 let header: DPoPProofHeader = serde_json::from_slice(&header_json) 104 .map_err(|_| OAuthError::InvalidDpopProof("Invalid header JSON".to_string()))?; 105 let payload: DPoPProofPayload = serde_json::from_slice(&payload_json) 106 .map_err(|_| OAuthError::InvalidDpopProof("Invalid payload JSON".to_string()))?; 107 if header.typ != "dpop+jwt" { 108 return Err(OAuthError::InvalidDpopProof("Invalid typ claim".to_string())); 109 } 110 if !matches!(header.alg.as_str(), "ES256" | "ES384" | "ES512" | "EdDSA") { 111 return Err(OAuthError::InvalidDpopProof("Unsupported algorithm".to_string())); 112 } 113 if payload.htm.to_uppercase() != http_method.to_uppercase() { 114 return Err(OAuthError::InvalidDpopProof("HTTP method mismatch".to_string())); 115 } 116 let proof_uri = payload.htu.split('?').next().unwrap_or(&payload.htu); 117 let request_uri = http_uri.split('?').next().unwrap_or(http_uri); 118 if proof_uri != request_uri { 119 return Err(OAuthError::InvalidDpopProof("HTTP URI mismatch".to_string())); 120 } 121 let now = Utc::now().timestamp(); 122 if (now - payload.iat).abs() > DPOP_MAX_AGE_SECS { 123 return Err(OAuthError::InvalidDpopProof("Proof too old or from the future".to_string())); 124 } 125 if let Some(nonce) = &payload.nonce { 126 self.validate_nonce(nonce)?; 127 } 128 if let Some(expected_ath) = access_token_hash { 129 match &payload.ath { 130 Some(ath) if ath == expected_ath => {} 131 Some(_) => { 132 return Err(OAuthError::InvalidDpopProof( 133 "Access token hash mismatch".to_string(), 134 )); 135 } 136 None => { 137 return Err(OAuthError::InvalidDpopProof( 138 "Missing access token hash".to_string(), 139 )); 140 } 141 } 142 } 143 let signature_bytes = URL_SAFE_NO_PAD 144 .decode(parts[2]) 145 .map_err(|_| OAuthError::InvalidDpopProof("Invalid signature encoding".to_string()))?; 146 let signing_input = format!("{}.{}", parts[0], parts[1]); 147 verify_dpop_signature(&header.alg, &header.jwk, signing_input.as_bytes(), &signature_bytes)?; 148 let jkt = compute_jwk_thumbprint(&header.jwk)?; 149 Ok(DPoPVerifyResult { 150 jkt, 151 jti: payload.jti.clone(), 152 }) 153 } 154} 155fn verify_dpop_signature( 156 alg: &str, 157 jwk: &DPoPJwk, 158 message: &[u8], 159 signature: &[u8], 160) -> Result<(), OAuthError> { 161 match alg { 162 "ES256" => verify_es256(jwk, message, signature), 163 "ES384" => verify_es384(jwk, message, signature), 164 "EdDSA" => verify_eddsa(jwk, message, signature), 165 _ => Err(OAuthError::InvalidDpopProof(format!( 166 "Unsupported algorithm: {}", 167 alg 168 ))), 169 } 170} 171fn verify_es256(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 172 use p256::ecdsa::signature::Verifier; 173 use p256::ecdsa::{Signature, VerifyingKey}; 174 use p256::elliptic_curve::sec1::FromEncodedPoint; 175 use p256::{AffinePoint, EncodedPoint}; 176 let crv = jwk.crv.as_ref().ok_or_else(|| { 177 OAuthError::InvalidDpopProof("Missing crv for ES256".to_string()) 178 })?; 179 if crv != "P-256" { 180 return Err(OAuthError::InvalidDpopProof(format!( 181 "Invalid curve for ES256: {}", 182 crv 183 ))); 184 } 185 let x_bytes = URL_SAFE_NO_PAD 186 .decode(jwk.x.as_ref().ok_or_else(|| { 187 OAuthError::InvalidDpopProof("Missing x coordinate".to_string()) 188 })?) 189 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 190 let y_bytes = URL_SAFE_NO_PAD 191 .decode(jwk.y.as_ref().ok_or_else(|| { 192 OAuthError::InvalidDpopProof("Missing y coordinate".to_string()) 193 })?) 194 .map_err(|_| OAuthError::InvalidDpopProof("Invalid y encoding".to_string()))?; 195 let point = EncodedPoint::from_affine_coordinates( 196 x_bytes.as_slice().into(), 197 y_bytes.as_slice().into(), 198 false, 199 ); 200 let affine_opt: Option<AffinePoint> = AffinePoint::from_encoded_point(&point).into(); 201 let affine = affine_opt 202 .ok_or_else(|| OAuthError::InvalidDpopProof("Invalid EC point".to_string()))?; 203 let verifying_key = VerifyingKey::from_affine(affine) 204 .map_err(|_| OAuthError::InvalidDpopProof("Invalid verifying key".to_string()))?; 205 let sig = Signature::from_slice(signature) 206 .map_err(|_| OAuthError::InvalidDpopProof("Invalid signature format".to_string()))?; 207 verifying_key 208 .verify(message, &sig) 209 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 210} 211fn verify_es384(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 212 use p384::ecdsa::signature::Verifier; 213 use p384::ecdsa::{Signature, VerifyingKey}; 214 use p384::elliptic_curve::sec1::FromEncodedPoint; 215 use p384::{AffinePoint, EncodedPoint}; 216 let crv = jwk.crv.as_ref().ok_or_else(|| { 217 OAuthError::InvalidDpopProof("Missing crv for ES384".to_string()) 218 })?; 219 if crv != "P-384" { 220 return Err(OAuthError::InvalidDpopProof(format!( 221 "Invalid curve for ES384: {}", 222 crv 223 ))); 224 } 225 let x_bytes = URL_SAFE_NO_PAD 226 .decode(jwk.x.as_ref().ok_or_else(|| { 227 OAuthError::InvalidDpopProof("Missing x coordinate".to_string()) 228 })?) 229 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 230 let y_bytes = URL_SAFE_NO_PAD 231 .decode(jwk.y.as_ref().ok_or_else(|| { 232 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 = affine_opt 242 .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} 251fn verify_eddsa(jwk: &DPoPJwk, message: &[u8], signature: &[u8]) -> Result<(), OAuthError> { 252 use ed25519_dalek::{Signature, VerifyingKey}; 253 let crv = jwk.crv.as_ref().ok_or_else(|| { 254 OAuthError::InvalidDpopProof("Missing crv for EdDSA".to_string()) 255 })?; 256 if crv != "Ed25519" { 257 return Err(OAuthError::InvalidDpopProof(format!( 258 "Invalid curve for EdDSA: {}", 259 crv 260 ))); 261 } 262 let x_bytes = URL_SAFE_NO_PAD 263 .decode(jwk.x.as_ref().ok_or_else(|| { 264 OAuthError::InvalidDpopProof("Missing x coordinate".to_string()) 265 })?) 266 .map_err(|_| OAuthError::InvalidDpopProof("Invalid x encoding".to_string()))?; 267 let key_bytes: [u8; 32] = x_bytes.try_into().map_err(|_| { 268 OAuthError::InvalidDpopProof("Invalid Ed25519 key length".to_string()) 269 })?; 270 let verifying_key = VerifyingKey::from_bytes(&key_bytes) 271 .map_err(|_| OAuthError::InvalidDpopProof("Invalid Ed25519 key".to_string()))?; 272 let sig_bytes: [u8; 64] = signature.try_into().map_err(|_| { 273 OAuthError::InvalidDpopProof("Invalid Ed25519 signature length".to_string()) 274 })?; 275 let sig = Signature::from_bytes(&sig_bytes); 276 verifying_key 277 .verify_strict(message, &sig) 278 .map_err(|_| OAuthError::InvalidDpopProof("Signature verification failed".to_string())) 279} 280pub fn compute_jwk_thumbprint(jwk: &DPoPJwk) -> Result<String, OAuthError> { 281 let canonical = match jwk.kty.as_str() { 282 "EC" => { 283 let crv = jwk 284 .crv 285 .as_ref() 286 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?; 287 let x = jwk 288 .x 289 .as_ref() 290 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?; 291 let y = jwk 292 .y 293 .as_ref() 294 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing y".to_string()))?; 295 format!( 296 r#"{{"crv":"{}","kty":"EC","x":"{}","y":"{}"}}"#, 297 crv, x, y 298 ) 299 } 300 "OKP" => { 301 let crv = jwk 302 .crv 303 .as_ref() 304 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing crv".to_string()))?; 305 let x = jwk 306 .x 307 .as_ref() 308 .ok_or_else(|| OAuthError::InvalidDpopProof("Missing x".to_string()))?; 309 format!(r#"{{"crv":"{}","kty":"OKP","x":"{}"}}"#, crv, x) 310 } 311 _ => { 312 return Err(OAuthError::InvalidDpopProof( 313 "Unsupported key type".to_string(), 314 )); 315 } 316 }; 317 let mut hasher = Sha256::new(); 318 hasher.update(canonical.as_bytes()); 319 let hash = hasher.finalize(); 320 Ok(URL_SAFE_NO_PAD.encode(&hash)) 321} 322pub fn compute_access_token_hash(access_token: &str) -> String { 323 let mut hasher = Sha256::new(); 324 hasher.update(access_token.as_bytes()); 325 let hash = hasher.finalize(); 326 URL_SAFE_NO_PAD.encode(&hash) 327} 328#[cfg(test)] 329mod tests { 330 use super::*; 331 #[test] 332 fn test_nonce_generation_and_validation() { 333 let secret = b"test-secret-key-32-bytes-long!!!"; 334 let verifier = DPoPVerifier::new(secret); 335 let nonce = verifier.generate_nonce(); 336 assert!(verifier.validate_nonce(&nonce).is_ok()); 337 } 338 #[test] 339 fn test_jwk_thumbprint_ec() { 340 let jwk = DPoPJwk { 341 kty: "EC".to_string(), 342 crv: Some("P-256".to_string()), 343 x: Some("test_x".to_string()), 344 y: Some("test_y".to_string()), 345 }; 346 let thumbprint = compute_jwk_thumbprint(&jwk).unwrap(); 347 assert!(!thumbprint.is_empty()); 348 } 349}