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_kid_from_token(token_str: &str) -> Result<KeyId, Error> {
195 let footer_bytes = extract_footer_from_token(token_str)?;
196 extract_kid_from_untrusted_footer(&footer_bytes)
197}
198
199pub(crate) fn extract_kid_from_untrusted_footer(footer_bytes: &[u8]) -> Result<KeyId, Error> {
201 if footer_bytes.is_empty() {
202 return Err(TokenError::MissingFooter.into());
203 }
204
205 let footer_str =
206 std::str::from_utf8(footer_bytes).map_err(|_| TokenError::InvalidFooter)?;
207
208 let footer_json: JsonValue =
209 serde_json::from_str(footer_str).map_err(|_| TokenError::InvalidFooter)?;
210
211 let kid = footer_json
212 .get("kid")
213 .and_then(|v| v.as_str())
214 .ok_or(TokenError::MissingClaim("kid"))?
215 .to_owned();
216
217 Ok(KeyId(kid))
218}
219
220pub(crate) fn extract_footer_from_token(token_str: &str) -> Result<Vec<u8>, Error> {
224 let rest = token_str
225 .strip_prefix(TOKEN_PREFIX)
226 .ok_or(TokenError::InvalidFormat)?;
227
228 let (_payload, footer_b64) = rest
229 .rsplit_once('.')
230 .ok_or(TokenError::InvalidFormat)?;
231
232 if footer_b64.is_empty() {
233 return Ok(Vec::new());
234 }
235
236 URL_SAFE_NO_PAD
237 .decode(footer_b64)
238 .map_err(|_| TokenError::InvalidFooter.into())
239}
240
241#[cfg(test)]
242#[allow(clippy::unwrap_used)]
243mod tests {
244 use super::*;
245 use static_assertions::assert_impl_all;
246
247 assert_impl_all!(PublicKey: Send, Sync);
248 assert_impl_all!(VerifiedClaims: Send, Sync);
249
250 #[test]
253 fn parse_valid_hex_key() {
254 let hex = "a".repeat(64);
256 let key = parse_public_key_hex(&hex).unwrap();
257 assert_eq!(key.as_bytes().len(), 32);
258 }
259
260 #[test]
261 fn parse_invalid_hex() {
262 let result = parse_public_key_hex("not-hex");
263 assert!(result.is_err());
264 }
265
266 #[test]
267 fn parse_wrong_length() {
268 let hex = "ab".repeat(16);
270 let result = parse_public_key_hex(&hex);
271 assert!(result.is_err());
272 let err_msg = result.unwrap_err().to_string();
273 assert!(err_msg.contains("invalid key length"));
274 }
275
276 fn generate_test_token(
279 issuer: &str,
280 audience: &str,
281 ) -> (PublicKey, String) {
282 use pasetors::claims::Claims;
283 use pasetors::footer::Footer;
284 use pasetors::keys::{AsymmetricKeyPair, Generate};
285
286 let kp = AsymmetricKeyPair::<V4>::generate().unwrap();
287
288 let mut claims = Claims::new().unwrap();
289 claims.issuer(issuer).unwrap();
290 claims.audience(audience).unwrap();
291 claims.subject("test-sub").unwrap();
292
293 let footer_json = serde_json::json!({"kid": "test-key-1"}).to_string();
294 let mut footer = Footer::new();
295 footer.parse_string(&footer_json).unwrap();
296
297 let token =
298 pasetors::public::sign(&kp.secret, &claims, Some(&footer), None).unwrap();
299
300 let pk_bytes = kp.public.as_bytes();
301 let hex = hex::encode(pk_bytes);
302 let public_key = parse_public_key_hex(&hex).unwrap();
303
304 (public_key, token)
305 }
306
307 #[test]
308 fn verify_valid_token() {
309 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
310
311 let claims =
312 verify_v4_public_access_token(&pk, &token, "accounts.ppoppo.com", "ppoppo/*").unwrap();
313
314 assert_eq!(claims.iss(), "accounts.ppoppo.com");
315 assert_eq!(claims.aud(), "ppoppo/*");
316 assert_eq!(claims.sub(), Some("test-sub"));
317 }
318
319 #[test]
320 fn verify_wrong_issuer() {
321 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
322
323 let result = verify_v4_public_access_token(&pk, &token, "wrong-issuer", "ppoppo/*");
324 assert!(result.is_err());
325 let err_msg = result.unwrap_err().to_string();
326 assert!(err_msg.contains("iss"));
327 }
328
329 #[test]
330 fn verify_wrong_audience() {
331 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
332
333 let result =
334 verify_v4_public_access_token(&pk, &token, "accounts.ppoppo.com", "wrong-aud");
335 assert!(result.is_err());
336 let err_msg = result.unwrap_err().to_string();
337 assert!(err_msg.contains("aud"));
338 }
339
340 #[test]
341 fn verify_wrong_key_fails() {
342 let (_pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
343
344 let different_hex = "bb".repeat(32);
346 let wrong_pk = parse_public_key_hex(&different_hex).unwrap();
347
348 let result =
349 verify_v4_public_access_token(&wrong_pk, &token, "accounts.ppoppo.com", "ppoppo/*");
350 assert!(result.is_err());
351 }
352
353 #[test]
354 fn verify_invalid_format() {
355 let hex = "aa".repeat(32);
356 let pk = parse_public_key_hex(&hex).unwrap();
357
358 let result = verify_v4_public_access_token(&pk, "not-a-token", "iss", "aud");
359 assert!(matches!(
360 result,
361 Err(Error::Token(TokenError::InvalidFormat))
362 ));
363 }
364
365 #[test]
368 fn extract_kid_from_valid_token() {
369 let (_pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
370
371 let kid = extract_kid_from_token(&token).unwrap();
372 assert_eq!(kid.to_string(), "test-key-1");
373 }
374
375 #[test]
376 fn extract_kid_invalid_format() {
377 let result = extract_kid_from_token("invalid");
378 assert!(result.is_err());
379 }
380
381 #[test]
384 fn verified_claims_accessors() {
385 let (pk, token) = generate_test_token("accounts.ppoppo.com", "ppoppo/*");
386
387 let claims =
388 verify_v4_public_access_token(&pk, &token, "accounts.ppoppo.com", "ppoppo/*").unwrap();
389
390 assert!(claims.get_claim("iss").is_some());
391 assert!(claims.get_claim("nonexistent").is_none());
392 assert!(claims.as_json().is_object());
393 }
394}