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(×tamp_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}