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