forked from
smokesignal.events/smokesignal
Fork i18n + search + filtering- v0.2
1use base64::{engine::general_purpose, Engine as _};
2use jwt::{Claims, Header};
3use p256::{
4 ecdsa::{
5 signature::{Signer, Verifier},
6 Signature, SigningKey, VerifyingKey,
7 },
8 PublicKey, SecretKey,
9};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use crate::encoding::ToBase64;
13use crate::jose_errors::JoseError;
14
15/// Signs a JWT token with the provided secret key, header, and claims
16///
17/// Creates a JSON Web Token (JWT) by:
18/// 1. Base64URL encoding the header and claims
19/// 2. Signing the encoded header and claims with the secret key
20/// 3. Returning the complete JWT (header.claims.signature)
21pub fn mint_token(
22 secret_key: &SecretKey,
23 header: &Header,
24 claims: &Claims,
25) -> Result<String, JoseError> {
26 // Encode header and claims to base64url
27 let header = header
28 .to_base64()
29 .map_err(|_| JoseError::SigningKeyNotFound)?;
30 let claims = claims
31 .to_base64()
32 .map_err(|_| JoseError::SigningKeyNotFound)?;
33 let content = format!("{}.{}", header, claims);
34
35 // Create signature
36 let signing_key = SigningKey::from(secret_key.clone());
37 let signature: Signature = signing_key
38 .try_sign(content.as_bytes())
39 .map_err(JoseError::SigningFailed)?;
40
41 // Return complete JWT
42 Ok(format!(
43 "{}.{}",
44 content,
45 general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes())
46 ))
47}
48
49/// Verifies a JWT token's signature and validates its claims
50///
51/// Performs the following validations:
52/// 1. Checks token format is valid (three parts separated by periods)
53/// 2. Decodes header and claims from base64url format
54/// 3. Verifies the token signature using the provided public key
55/// 4. Validates token expiration (if provided in claims)
56/// 5. Validates token not-before time (if provided in claims)
57/// 6. Returns the decoded claims if all validation passes
58pub fn verify_token(token: &str, public_key: &PublicKey) -> Result<Claims, JoseError> {
59 // Split token into its parts
60 let parts: Vec<&str> = token.split('.').collect();
61 if parts.len() != 3 {
62 return Err(JoseError::InvalidTokenFormat);
63 }
64
65 let encoded_header = parts[0];
66 let encoded_claims = parts[1];
67 let encoded_signature = parts[2];
68
69 // Decode header
70 let header_bytes = general_purpose::URL_SAFE_NO_PAD
71 .decode(encoded_header)
72 .map_err(|_| JoseError::InvalidHeader)?;
73
74 let header: Header =
75 serde_json::from_slice(&header_bytes).map_err(|_| JoseError::InvalidHeader)?;
76
77 // Verify algorithm matches what we expect
78 // We only support ES256 for now
79 if header.algorithm.as_deref() != Some("ES256") {
80 return Err(JoseError::UnsupportedAlgorithm);
81 }
82
83 // Decode claims
84 let claims_bytes = general_purpose::URL_SAFE_NO_PAD
85 .decode(encoded_claims)
86 .map_err(|_| JoseError::InvalidClaims)?;
87
88 let claims: Claims =
89 serde_json::from_slice(&claims_bytes).map_err(|_| JoseError::InvalidClaims)?;
90
91 // Decode signature
92 let signature_bytes = general_purpose::URL_SAFE_NO_PAD
93 .decode(encoded_signature)
94 .map_err(|_| JoseError::InvalidSignature)?;
95
96 let signature =
97 Signature::try_from(signature_bytes.as_slice()).map_err(|_| JoseError::InvalidSignature)?;
98
99 // Verify signature
100 let verifying_key = VerifyingKey::from(public_key);
101 let content = format!("{}.{}", encoded_header, encoded_claims);
102
103 verifying_key
104 .verify(content.as_bytes(), &signature)
105 .map_err(|_| JoseError::SignatureVerificationFailed)?;
106
107 // Get current timestamp for validation
108 let now = SystemTime::now()
109 .duration_since(UNIX_EPOCH)
110 .map_err(|_| JoseError::SystemTimeError)?
111 .as_secs();
112
113 // Validate expiration time if present
114 if let Some(exp) = claims.jose.expiration {
115 if now >= exp {
116 return Err(JoseError::TokenExpired);
117 }
118 }
119
120 // Validate not-before time if present
121 if let Some(nbf) = claims.jose.not_before {
122 if now < nbf {
123 return Err(JoseError::TokenNotYetValid);
124 }
125 }
126
127 // Return validated claims
128 Ok(claims)
129}
130
131pub mod jwk {
132 use elliptic_curve::JwkEcKey;
133 use p256::SecretKey;
134 use rand::rngs::OsRng;
135 use serde::{Deserialize, Serialize};
136
137 #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
138 pub struct WrappedJsonWebKey {
139 #[serde(skip_serializing_if = "Option::is_none", default)]
140 pub kid: Option<String>,
141
142 #[serde(skip_serializing_if = "Option::is_none", default)]
143 pub alg: Option<String>,
144
145 #[serde(flatten)]
146 pub jwk: JwkEcKey,
147 }
148
149 #[derive(Serialize, Deserialize, Clone)]
150 pub struct WrappedJsonWebKeySet {
151 pub keys: Vec<WrappedJsonWebKey>,
152 }
153
154 pub fn generate() -> WrappedJsonWebKey {
155 let secret_key = SecretKey::random(&mut OsRng);
156
157 let kid = ulid::Ulid::new().to_string();
158
159 WrappedJsonWebKey {
160 kid: Some(kid),
161 alg: Some("ES256".to_string()),
162 jwk: secret_key.to_jwk(),
163 }
164 }
165}
166
167pub mod jwt {
168
169 use std::collections::BTreeMap;
170
171 use elliptic_curve::JwkEcKey;
172 use serde::{Deserialize, Serialize};
173
174 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
175 pub struct Header {
176 #[serde(rename = "alg", skip_serializing_if = "Option::is_none")]
177 pub algorithm: Option<String>,
178
179 #[serde(rename = "kid", skip_serializing_if = "Option::is_none")]
180 pub key_id: Option<String>,
181
182 #[serde(rename = "typ", skip_serializing_if = "Option::is_none")]
183 pub type_: Option<String>,
184
185 #[serde(rename = "jwk", skip_serializing_if = "Option::is_none")]
186 pub json_web_key: Option<JwkEcKey>,
187 }
188
189 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
190 pub struct Claims {
191 #[serde(flatten)]
192 pub jose: JoseClaims,
193 #[serde(flatten)]
194 pub private: BTreeMap<String, serde_json::Value>,
195 }
196
197 impl Claims {
198 pub fn new(jose: JoseClaims) -> Self {
199 Claims {
200 jose,
201 private: BTreeMap::new(),
202 }
203 }
204 }
205
206 pub type SecondsSinceEpoch = u64;
207
208 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
209 pub struct JoseClaims {
210 #[serde(rename = "iss", skip_serializing_if = "Option::is_none")]
211 pub issuer: Option<String>,
212
213 #[serde(rename = "sub", skip_serializing_if = "Option::is_none")]
214 pub subject: Option<String>,
215
216 #[serde(rename = "aud", skip_serializing_if = "Option::is_none")]
217 pub audience: Option<String>,
218
219 #[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
220 pub expiration: Option<SecondsSinceEpoch>,
221
222 #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
223 pub not_before: Option<SecondsSinceEpoch>,
224
225 #[serde(rename = "iat", skip_serializing_if = "Option::is_none")]
226 pub issued_at: Option<SecondsSinceEpoch>,
227
228 #[serde(rename = "jti", skip_serializing_if = "Option::is_none")]
229 pub json_web_token_id: Option<String>,
230
231 #[serde(rename = "htm", skip_serializing_if = "Option::is_none")]
232 pub http_method: Option<String>,
233
234 #[serde(rename = "htu", skip_serializing_if = "Option::is_none")]
235 pub http_uri: Option<String>,
236
237 #[serde(rename = "nonce", skip_serializing_if = "Option::is_none")]
238 pub nonce: Option<String>,
239
240 #[serde(rename = "ath", skip_serializing_if = "Option::is_none")]
241 pub auth: Option<String>,
242 }
243}