1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use pasetors::claims::ClaimsValidationRules;
4use pasetors::keys::AsymmetricPublicKey;
5use pasetors::token::UntrustedToken;
6use pasetors::version4::V4;
7use pasetors::{Public, public};
8use serde_json::Value as JsonValue;
9use time::OffsetDateTime;
10use time::format_description::well_known::Rfc3339;
11
12use crate::error::{Error, TokenError};
13use crate::types::KeyId;
14
15const TOKEN_PREFIX: &str = "v4.public.";
16const ED25519_PUBLIC_KEY_SIZE: usize = 32;
17
18#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct PublicKey {
24 bytes: [u8; ED25519_PUBLIC_KEY_SIZE],
25}
26
27impl PublicKey {
28 #[must_use]
30 pub fn as_bytes(&self) -> &[u8; ED25519_PUBLIC_KEY_SIZE] {
31 &self.bytes
32 }
33}
34
35impl TryFrom<&crate::well_known::WellKnownPasetoKey> for PublicKey {
36 type Error = Error;
37
38 fn try_from(key: &crate::well_known::WellKnownPasetoKey) -> Result<Self, Error> {
39 parse_public_key_hex(&key.public_key_hex)
40 }
41}
42
43pub fn parse_public_key_hex(public_key_hex: &str) -> Result<PublicKey, Error> {
49 let bytes: [u8; ED25519_PUBLIC_KEY_SIZE] = hex::decode(public_key_hex)
50 .map_err(|e| TokenError::VerificationFailed(format!("invalid hex: {e}")))?
51 .try_into()
52 .map_err(|v: Vec<u8>| {
53 TokenError::VerificationFailed(format!(
54 "invalid key length: expected {ED25519_PUBLIC_KEY_SIZE}, got {}",
55 v.len()
56 ))
57 })?;
58 Ok(PublicKey { bytes })
59}
60
61#[derive(Debug, Clone)]
66pub struct VerifiedClaims {
67 iss: String,
68 aud: String,
69 inner: JsonValue,
70}
71
72impl VerifiedClaims {
73 #[must_use]
75 pub fn iss(&self) -> &str {
76 &self.iss
77 }
78
79 #[must_use]
81 pub fn aud(&self) -> &str {
82 &self.aud
83 }
84
85 #[must_use]
87 pub fn sub(&self) -> Option<&str> {
88 self.inner.get("sub").and_then(|v| v.as_str())
89 }
90
91 #[must_use]
93 pub fn get_claim(&self, key: &str) -> Option<&JsonValue> {
94 self.inner.get(key)
95 }
96
97 #[must_use]
99 pub fn as_json(&self) -> &JsonValue {
100 &self.inner
101 }
102}
103
104pub fn verify_v4_public_access_token(
111 public_key: &PublicKey,
112 token_str: &str,
113 expected_issuer: &str,
114 expected_audience: &str,
115) -> Result<VerifiedClaims, Error> {
116 if !token_str.starts_with(TOKEN_PREFIX) {
117 return Err(TokenError::InvalidFormat.into());
118 }
119
120 let pk = AsymmetricPublicKey::<V4>::from(&public_key.bytes[..])
121 .map_err(|e| TokenError::VerificationFailed(e.to_string()))?;
122
123 let validation_rules = ClaimsValidationRules::new();
124
125 let untrusted_token = UntrustedToken::<Public, V4>::try_from(token_str)
126 .map_err(|e| TokenError::VerificationFailed(e.to_string()))?;
127
128 let trusted_token = public::verify(&pk, &untrusted_token, &validation_rules, None, None)
129 .map_err(|e| TokenError::VerificationFailed(e.to_string()))?;
130
131 let payload = trusted_token
132 .payload_claims()
133 .ok_or(TokenError::MissingPayload)?;
134 let payload_str = payload
135 .to_string()
136 .map_err(|e| TokenError::VerificationFailed(e.to_string()))?;
137 let json_value: JsonValue = serde_json::from_str(&payload_str)
138 .map_err(|e| TokenError::VerificationFailed(e.to_string()))?;
139
140 if let Some(exp_str) = json_value.get("exp").and_then(|v| v.as_str()) {
142 let exp_time = OffsetDateTime::parse(exp_str, &Rfc3339)
143 .map_err(|e| TokenError::VerificationFailed(format!("invalid exp format: {e}")))?;
144 if exp_time < OffsetDateTime::now_utc() {
145 return Err(TokenError::Expired.into());
146 }
147 }
148
149 if let Some(nbf_str) = json_value.get("nbf").and_then(|v| v.as_str()) {
151 let nbf_time = OffsetDateTime::parse(nbf_str, &Rfc3339)
152 .map_err(|e| TokenError::VerificationFailed(format!("invalid nbf format: {e}")))?;
153 if nbf_time > OffsetDateTime::now_utc() {
154 return Err(TokenError::VerificationFailed("token not yet valid (nbf)".into()).into());
155 }
156 }
157
158 let iss = validate_claim(&json_value, "iss", expected_issuer)?;
159 let aud = validate_claim(&json_value, "aud", expected_audience)?;
160
161 Ok(VerifiedClaims {
162 iss,
163 aud,
164 inner: json_value,
165 })
166}
167
168fn validate_claim(
170 claims: &JsonValue,
171 key: &'static str,
172 expected: &str,
173) -> Result<String, TokenError> {
174 let actual = claims
175 .get(key)
176 .and_then(|v| v.as_str())
177 .ok_or(TokenError::MissingClaim(key))?;
178 if actual != expected {
179 return Err(TokenError::ClaimMismatch {
180 claim: key,
181 expected: expected.to_string(),
182 actual: actual.to_string(),
183 });
184 }
185 Ok(actual.to_string())
186}
187
188pub fn extract_unverified_kid(token_str: &str) -> Result<KeyId, Error> {
207 let footer_bytes = extract_footer_from_token(token_str)?;
208 extract_kid_from_untrusted_footer(&footer_bytes)
209}
210
211pub fn verify_v4_with_keyset(
230 keyset: &crate::well_known::WellKnownPasetoDocument,
231 token_str: &str,
232 expected_issuer: &str,
233 expected_audience: &str,
234) -> Result<VerifiedClaims, Error> {
235 let kid = extract_unverified_kid(token_str)?;
236
237 let key_meta = keyset
238 .keys
239 .iter()
240 .find(|k| k.kid == kid)
241 .ok_or_else(|| TokenError::VerificationFailed(format!("kid '{kid}' not in keyset")))?;
242
243 if key_meta.status == crate::well_known::WellKnownKeyStatus::Revoked {
244 return Err(TokenError::VerificationFailed(format!("kid '{kid}' is revoked")).into());
245 }
246
247 let public_key = PublicKey::try_from(key_meta)?;
248 verify_v4_public_access_token(&public_key, token_str, expected_issuer, expected_audience)
249}
250
251pub(crate) fn extract_kid_from_untrusted_footer(footer_bytes: &[u8]) -> Result<KeyId, Error> {
253 if footer_bytes.is_empty() {
254 return Err(TokenError::MissingFooter.into());
255 }
256
257 let footer_str = std::str::from_utf8(footer_bytes).map_err(|_| TokenError::InvalidFooter)?;
258
259 let footer_json: JsonValue =
260 serde_json::from_str(footer_str).map_err(|_| TokenError::InvalidFooter)?;
261
262 let kid = footer_json
263 .get("kid")
264 .and_then(|v| v.as_str())
265 .ok_or(TokenError::MissingClaim("kid"))?
266 .to_owned();
267
268 Ok(KeyId(kid))
269}
270
271pub(crate) fn extract_footer_from_token(token_str: &str) -> Result<Vec<u8>, Error> {
275 let rest = token_str
276 .strip_prefix(TOKEN_PREFIX)
277 .ok_or(TokenError::InvalidFormat)?;
278
279 let (_payload, footer_b64) = rest.rsplit_once('.').ok_or(TokenError::InvalidFormat)?;
280
281 if footer_b64.is_empty() {
282 return Ok(Vec::new());
283 }
284
285 URL_SAFE_NO_PAD
286 .decode(footer_b64)
287 .map_err(|_| TokenError::InvalidFooter.into())
288}
289
290#[cfg(test)]
291#[allow(clippy::unwrap_used)]
292mod tests {
293 use super::*;
294 use static_assertions::assert_impl_all;
295
296 assert_impl_all!(PublicKey: Send, Sync);
297 assert_impl_all!(VerifiedClaims: Send, Sync);
298
299 #[test]
302 fn parse_valid_hex_key() {
303 let hex = "a".repeat(64);
305 let key = parse_public_key_hex(&hex).unwrap();
306 assert_eq!(key.as_bytes().len(), 32);
307 }
308
309 #[test]
310 fn parse_invalid_hex() {
311 let result = parse_public_key_hex("not-hex");
312 assert!(result.is_err());
313 }
314
315 #[test]
316 fn parse_wrong_length() {
317 let hex = "ab".repeat(16);
319 let result = parse_public_key_hex(&hex);
320 assert!(result.is_err());
321 let err_msg = result.unwrap_err().to_string();
322 assert!(err_msg.contains("invalid key length"));
323 }
324
325 fn generate_test_token(issuer: &str, audience: &str) -> (PublicKey, String) {
328 use pasetors::claims::Claims;
329 use pasetors::footer::Footer;
330 use pasetors::keys::{AsymmetricKeyPair, Generate};
331
332 let kp = AsymmetricKeyPair::<V4>::generate().unwrap();
333
334 let mut claims = Claims::new().unwrap();
335 claims.issuer(issuer).unwrap();
336 claims.audience(audience).unwrap();
337 claims.subject("test-sub").unwrap();
338
339 let footer_json = serde_json::json!({"kid": "test-key-1"}).to_string();
340 let mut footer = Footer::new();
341 footer.parse_string(&footer_json).unwrap();
342
343 let token = pasetors::public::sign(&kp.secret, &claims, Some(&footer), None).unwrap();
344
345 let pk_bytes = kp.public.as_bytes();
346 let hex = hex::encode(pk_bytes);
347 let public_key = parse_public_key_hex(&hex).unwrap();
348
349 (public_key, token)
350 }
351
352 #[test]
353 fn verify_valid_token() {
354 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
355
356 let claims =
357 verify_v4_public_access_token(&pk, &token, "accounts.ppoppo.com", "ppoppo/*").unwrap();
358
359 assert_eq!(claims.iss(), "accounts.ppoppo.com");
360 assert_eq!(claims.aud(), "ppoppo/*");
361 assert_eq!(claims.sub(), Some("test-sub"));
362 }
363
364 #[test]
365 fn verify_wrong_issuer() {
366 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
367
368 let result = verify_v4_public_access_token(&pk, &token, "wrong-issuer", "ppoppo/*");
369 assert!(result.is_err());
370 let err_msg = result.unwrap_err().to_string();
371 assert!(err_msg.contains("iss"));
372 }
373
374 #[test]
375 fn verify_wrong_audience() {
376 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
377
378 let result = verify_v4_public_access_token(&pk, &token, "accounts.ppoppo.com", "wrong-aud");
379 assert!(result.is_err());
380 let err_msg = result.unwrap_err().to_string();
381 assert!(err_msg.contains("aud"));
382 }
383
384 #[test]
385 fn verify_wrong_key_fails() {
386 let (_pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
387
388 let different_hex = "bb".repeat(32);
390 let wrong_pk = parse_public_key_hex(&different_hex).unwrap();
391
392 let result =
393 verify_v4_public_access_token(&wrong_pk, &token, "accounts.ppoppo.com", "ppoppo/*");
394 assert!(result.is_err());
395 }
396
397 #[test]
398 fn verify_invalid_format() {
399 let hex = "aa".repeat(32);
400 let pk = parse_public_key_hex(&hex).unwrap();
401
402 let result = verify_v4_public_access_token(&pk, "not-a-token", "iss", "aud");
403 assert!(matches!(
404 result,
405 Err(Error::Token(TokenError::InvalidFormat))
406 ));
407 }
408
409 #[test]
412 fn extract_kid_from_valid_token() {
413 let (_pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
414
415 let kid = extract_unverified_kid(&token).unwrap();
416 assert_eq!(kid.to_string(), "test-key-1");
417 }
418
419 #[test]
420 fn extract_kid_invalid_format() {
421 let result = extract_unverified_kid("invalid");
422 assert!(result.is_err());
423 }
424
425 fn keyset_with(pk: &PublicKey, kid: &str, status: crate::well_known::WellKnownKeyStatus) -> crate::well_known::WellKnownPasetoDocument {
428 use crate::well_known::{WellKnownPasetoDocument, WellKnownPasetoKey};
429 WellKnownPasetoDocument {
430 issuer: "accounts.ppoppo.com".into(),
431 version: "v4.public".into(),
432 keys: vec![WellKnownPasetoKey {
433 kid: KeyId(kid.into()),
434 public_key_hex: hex::encode(pk.as_bytes()),
435 status,
436 created_at: time::OffsetDateTime::now_utc(),
437 }],
438 cache_ttl_seconds: 3600,
439 }
440 }
441
442 #[test]
443 fn verify_with_keyset_active_key_succeeds() {
444 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
445 let keyset = keyset_with(&pk, "test-key-1", crate::well_known::WellKnownKeyStatus::Active);
446
447 let claims = verify_v4_with_keyset(&keyset, &token, "accounts.ppoppo.com", "ppoppo/*").unwrap();
448 assert_eq!(claims.iss(), "accounts.ppoppo.com");
449 }
450
451 #[test]
452 fn verify_with_keyset_retiring_key_succeeds() {
453 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
455 let keyset = keyset_with(&pk, "test-key-1", crate::well_known::WellKnownKeyStatus::Retiring);
456
457 let result = verify_v4_with_keyset(&keyset, &token, "accounts.ppoppo.com", "ppoppo/*");
458 assert!(result.is_ok(), "retiring keys should still verify: {result:?}");
459 }
460
461 #[test]
462 fn verify_with_keyset_revoked_key_fails() {
463 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
464 let keyset = keyset_with(&pk, "test-key-1", crate::well_known::WellKnownKeyStatus::Revoked);
465
466 let result = verify_v4_with_keyset(&keyset, &token, "accounts.ppoppo.com", "ppoppo/*");
467 assert!(result.is_err(), "revoked key MUST fail verification");
468 assert!(result.unwrap_err().to_string().contains("revoked"));
469 }
470
471 #[test]
472 fn verify_with_keyset_unknown_kid_fails() {
473 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
474 let keyset = keyset_with(&pk, "different-kid", crate::well_known::WellKnownKeyStatus::Active);
475
476 let result = verify_v4_with_keyset(&keyset, &token, "accounts.ppoppo.com", "ppoppo/*");
477 assert!(result.is_err());
478 assert!(result.unwrap_err().to_string().contains("not in keyset"));
479 }
480
481 #[test]
484 fn verified_claims_accessors() {
485 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
486
487 let claims =
488 verify_v4_public_access_token(&pk, &token, "accounts.ppoppo.com", "ppoppo/*").unwrap();
489
490 assert!(claims.get_claim("iss").is_some());
491 assert!(claims.get_claim("nonexistent").is_none());
492 assert!(claims.as_json().is_object());
493 }
494}