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