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