forked from
smokesignal.events/smokesignal
i18n+filtering fork - fluent-templates v2
1//! # JOSE Module
2//!
3//! JSON Object Signing and Encryption (JOSE) implementation for secure token management and cryptographic operations.
4//!
5//! This module provides a comprehensive implementation of JOSE standards including JSON Web Tokens (JWT),
6//! JSON Web Signatures (JWS), and JSON Web Keys (JWK). It's designed specifically for AT Protocol
7//! authentication and secure communication between services.
8//!
9//! ## Architecture
10//!
11//! The JOSE implementation is organized around several key components:
12//!
13//! ### Core Components
14//! - **JWT (JSON Web Tokens)** - Token creation, signing, and verification
15//! - **JWS (JSON Web Signatures)** - Digital signatures for data integrity
16//! - **JWK (JSON Web Keys)** - Cryptographic key management and serialization
17//! - **Key Management** - Secure key storage and rotation
18//!
19//! ## Features
20//!
21//! ### Token Management
22//! - **Token Creation** - Generate JWTs with custom claims and headers
23//! - **Token Signing** - ECDSA P-256 signing for security and performance
24//! - **Token Verification** - Cryptographic verification of token authenticity
25//! - **Token Parsing** - Safe parsing and validation of incoming tokens
26//!
27//! ### Cryptographic Operations
28//! - **ECDSA P-256** - Elliptic Curve Digital Signature Algorithm with P-256 curve
29//! - **Base64URL Encoding** - URL-safe base64 encoding for web compatibility
30//! - **Key Derivation** - Secure key generation and derivation
31//! - **Signature Verification** - Robust signature validation
32//!
33//! ### Security Features
34//! - **Constant Time Operations** - Protection against timing attacks
35//! - **Secure Random Generation** - Cryptographically secure randomness
36//! - **Key Rotation Support** - Infrastructure for key lifecycle management
37//! - **Algorithm Validation** - Strict algorithm verification to prevent attacks
38//!
39//! ## Supported Standards
40//!
41//! ### JSON Web Token (JWT) - RFC 7519
42//! - Header-Claims-Signature structure
43//! - Registered and custom claims support
44//! - Expiration and not-before validation
45//! - Issuer and audience verification
46//!
47//! ### JSON Web Signature (JWS) - RFC 7515
48//! - ECDSA with P-256 curve (ES256)
49//! - Detached signature support
50//! - Protected and unprotected headers
51//! - Multiple signature support
52//!
53//! ### JSON Web Key (JWK) - RFC 7517
54//! - Elliptic Curve keys (EC)
55//! - Key identification and rotation
56//! - Public key distribution
57//! - Key usage constraints
58//!
59//! ## AT Protocol Integration
60//!
61//! The JOSE implementation is specifically designed for AT Protocol requirements:
62//! - **DPoP Tokens** - Demonstration of Proof-of-Possession tokens
63//! - **Access Tokens** - OAuth 2.0 access token signing and verification
64//! - **ID Tokens** - OpenID Connect identity tokens
65//! - **Service Authentication** - Inter-service authentication tokens
66//!
67//! ## Example Usage
68//!
69//! ### Creating and Signing a JWT
70//! ```rust,no_run
71//! use smokesignal::jose::{mint_token, jwt::{Header, Claims}};
72//! use p256::SecretKey;
73//! use std::time::{SystemTime, UNIX_EPOCH};
74//!
75//! async fn create_jwt() -> anyhow::Result<String> {
76//! // Generate a signing key
77//! let secret_key = SecretKey::random(&mut rand::thread_rng());
78//!
79//! // Create header
80//! let header = Header {
81//! alg: "ES256".to_string(),
82//! typ: Some("JWT".to_string()),
83//! kid: Some("key-1".to_string()),
84//! };
85//!
86//! // Create claims
87//! let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
88//! let claims = Claims {
89//! iss: Some("smokesignal".to_string()),
90//! sub: Some("user123".to_string()),
91//! aud: Some("api.example.com".to_string()),
92//! exp: Some(now + 3600), // 1 hour expiry
93//! iat: Some(now),
94//! ..Default::default()
95//! };
96//!
97//! // Sign and create JWT
98//! let token = mint_token(&secret_key, &header, &claims)?;
99//! Ok(token)
100//! }
101//! ```
102//!
103//! ### Verifying a JWT
104//! ```rust,no_run
105//! use smokesignal::jose::{verify_token, jwk::JsonWebKey};
106//! use p256::PublicKey;
107//!
108//! async fn verify_jwt(token: &str, public_key: &PublicKey) -> anyhow::Result<bool> {
109//! // Verify token signature and claims
110//! let is_valid = verify_token(token, public_key)?;
111//! Ok(is_valid)
112//! }
113//! ```
114//!
115//! ## Security Considerations
116//!
117//! When using JOSE for cryptographic operations:
118//! - Always use cryptographically secure random number generation
119//! - Implement proper key rotation and lifecycle management
120//! - Validate all token claims including expiration and audience
121//! - Use constant-time comparison for sensitive operations
122//! - Store private keys securely and never expose them
123//! - Implement proper error handling without leaking information
124
125use base64::{engine::general_purpose, Engine as _};
126use jwt::{Claims, Header};
127use p256::{
128 ecdsa::{
129 signature::{Signer, Verifier},
130 Signature, SigningKey, VerifyingKey,
131 },
132 PublicKey, SecretKey,
133};
134use std::time::{SystemTime, UNIX_EPOCH};
135
136use crate::encoding::ToBase64;
137use crate::jose_errors::JoseError;
138
139/// Signs a JWT token with the provided secret key, header, and claims
140///
141/// Creates a JSON Web Token (JWT) by:
142/// 1. Base64URL encoding the header and claims
143/// 2. Signing the encoded header and claims with the secret key
144/// 3. Returning the complete JWT (header.claims.signature)
145pub fn mint_token(
146 secret_key: &SecretKey,
147 header: &Header,
148 claims: &Claims,
149) -> Result<String, JoseError> {
150 // Encode header and claims to base64url
151 let header = header
152 .to_base64()
153 .map_err(|_| JoseError::SigningKeyNotFound)?;
154 let claims = claims
155 .to_base64()
156 .map_err(|_| JoseError::SigningKeyNotFound)?;
157 let content = format!("{}.{}", header, claims);
158
159 // Create signature
160 let signing_key = SigningKey::from(secret_key.clone());
161 let signature: Signature = signing_key
162 .try_sign(content.as_bytes())
163 .map_err(JoseError::SigningFailed)?;
164
165 // Return complete JWT
166 Ok(format!(
167 "{}.{}",
168 content,
169 general_purpose::URL_SAFE_NO_PAD.encode(signature.to_bytes())
170 ))
171}
172
173/// Verifies a JWT token's signature and validates its claims
174///
175/// Performs the following validations:
176/// 1. Checks token format is valid (three parts separated by periods)
177/// 2. Decodes header and claims from base64url format
178/// 3. Verifies the token signature using the provided public key
179/// 4. Validates token expiration (if provided in claims)
180/// 5. Validates token not-before time (if provided in claims)
181/// 6. Returns the decoded claims if all validation passes
182pub fn verify_token(token: &str, public_key: &PublicKey) -> Result<Claims, JoseError> {
183 // Split token into its parts
184 let parts: Vec<&str> = token.split('.').collect();
185 if parts.len() != 3 {
186 return Err(JoseError::InvalidTokenFormat);
187 }
188
189 let encoded_header = parts[0];
190 let encoded_claims = parts[1];
191 let encoded_signature = parts[2];
192
193 // Decode header
194 let header_bytes = general_purpose::URL_SAFE_NO_PAD
195 .decode(encoded_header)
196 .map_err(|_| JoseError::InvalidHeader)?;
197
198 let header: Header =
199 serde_json::from_slice(&header_bytes).map_err(|_| JoseError::InvalidHeader)?;
200
201 // Verify algorithm matches what we expect
202 // We only support ES256 for now
203 if header.algorithm.as_deref() != Some("ES256") {
204 return Err(JoseError::UnsupportedAlgorithm);
205 }
206
207 // Decode claims
208 let claims_bytes = general_purpose::URL_SAFE_NO_PAD
209 .decode(encoded_claims)
210 .map_err(|_| JoseError::InvalidClaims)?;
211
212 let claims: Claims =
213 serde_json::from_slice(&claims_bytes).map_err(|_| JoseError::InvalidClaims)?;
214
215 // Decode signature
216 let signature_bytes = general_purpose::URL_SAFE_NO_PAD
217 .decode(encoded_signature)
218 .map_err(|_| JoseError::InvalidSignature)?;
219
220 let signature =
221 Signature::try_from(signature_bytes.as_slice()).map_err(|_| JoseError::InvalidSignature)?;
222
223 // Verify signature
224 let verifying_key = VerifyingKey::from(public_key);
225 let content = format!("{}.{}", encoded_header, encoded_claims);
226
227 verifying_key
228 .verify(content.as_bytes(), &signature)
229 .map_err(|_| JoseError::SignatureVerificationFailed)?;
230
231 // Get current timestamp for validation
232 let now = SystemTime::now()
233 .duration_since(UNIX_EPOCH)
234 .map_err(|_| JoseError::SystemTimeError)?
235 .as_secs();
236
237 // Validate expiration time if present
238 if let Some(exp) = claims.jose.expiration {
239 if now >= exp {
240 return Err(JoseError::TokenExpired);
241 }
242 }
243
244 // Validate not-before time if present
245 if let Some(nbf) = claims.jose.not_before {
246 if now < nbf {
247 return Err(JoseError::TokenNotYetValid);
248 }
249 }
250
251 // Return validated claims
252 Ok(claims)
253}
254
255pub mod jwk {
256 use elliptic_curve::JwkEcKey;
257 use p256::SecretKey;
258 use rand::rngs::OsRng;
259 use serde::{Deserialize, Serialize};
260
261 #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
262 pub struct WrappedJsonWebKey {
263 #[serde(skip_serializing_if = "Option::is_none", default)]
264 pub kid: Option<String>,
265
266 #[serde(skip_serializing_if = "Option::is_none", default)]
267 pub alg: Option<String>,
268
269 #[serde(flatten)]
270 pub jwk: JwkEcKey,
271 }
272
273 #[derive(Serialize, Deserialize, Clone)]
274 pub struct WrappedJsonWebKeySet {
275 pub keys: Vec<WrappedJsonWebKey>,
276 }
277
278 pub fn generate() -> WrappedJsonWebKey {
279 let secret_key = SecretKey::random(&mut OsRng);
280
281 let kid = ulid::Ulid::new().to_string();
282
283 WrappedJsonWebKey {
284 kid: Some(kid),
285 alg: Some("ES256".to_string()),
286 jwk: secret_key.to_jwk(),
287 }
288 }
289}
290
291pub mod jwt {
292
293 use std::collections::BTreeMap;
294
295 use elliptic_curve::JwkEcKey;
296 use serde::{Deserialize, Serialize};
297
298 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
299 pub struct Header {
300 #[serde(rename = "alg", skip_serializing_if = "Option::is_none")]
301 pub algorithm: Option<String>,
302
303 #[serde(rename = "kid", skip_serializing_if = "Option::is_none")]
304 pub key_id: Option<String>,
305
306 #[serde(rename = "typ", skip_serializing_if = "Option::is_none")]
307 pub type_: Option<String>,
308
309 #[serde(rename = "jwk", skip_serializing_if = "Option::is_none")]
310 pub json_web_key: Option<JwkEcKey>,
311 }
312
313 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
314 pub struct Claims {
315 #[serde(flatten)]
316 pub jose: JoseClaims,
317 #[serde(flatten)]
318 pub private: BTreeMap<String, serde_json::Value>,
319 }
320
321 impl Claims {
322 pub fn new(jose: JoseClaims) -> Self {
323 Claims {
324 jose,
325 private: BTreeMap::new(),
326 }
327 }
328 }
329
330 pub type SecondsSinceEpoch = u64;
331
332 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
333 pub struct JoseClaims {
334 #[serde(rename = "iss", skip_serializing_if = "Option::is_none")]
335 pub issuer: Option<String>,
336
337 #[serde(rename = "sub", skip_serializing_if = "Option::is_none")]
338 pub subject: Option<String>,
339
340 #[serde(rename = "aud", skip_serializing_if = "Option::is_none")]
341 pub audience: Option<String>,
342
343 #[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
344 pub expiration: Option<SecondsSinceEpoch>,
345
346 #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
347 pub not_before: Option<SecondsSinceEpoch>,
348
349 #[serde(rename = "iat", skip_serializing_if = "Option::is_none")]
350 pub issued_at: Option<SecondsSinceEpoch>,
351
352 #[serde(rename = "jti", skip_serializing_if = "Option::is_none")]
353 pub json_web_token_id: Option<String>,
354
355 #[serde(rename = "htm", skip_serializing_if = "Option::is_none")]
356 pub http_method: Option<String>,
357
358 #[serde(rename = "htu", skip_serializing_if = "Option::is_none")]
359 pub http_uri: Option<String>,
360
361 #[serde(rename = "nonce", skip_serializing_if = "Option::is_none")]
362 pub nonce: Option<String>,
363
364 #[serde(rename = "ath", skip_serializing_if = "Option::is_none")]
365 pub auth: Option<String>,
366 }
367}