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