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(×tamp_bytes);
66 let hash = hasher.finalize();
67
68 let mut nonce_data = Vec::with_capacity(8 + 16);
69 nonce_data.extend_from_slice(×tamp_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(×tamp_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}