Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
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::sec1::ToEncodedPoint;
257 use p256::{
258 SecretKey as P256SecretKeyInternal,
259 };
260 use rand::rngs::OsRng;
261 use serde::{Deserialize, Serialize};
262 use sha2::{Digest, Sha256};
263
264 use crate::jose_errors::JoseError;
265
266 // Renamed to JsonWebKey to match atrium-oauth expectations and common naming.
267 #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
268 pub struct JsonWebKey {
269 #[serde(skip_serializing_if = "Option::is_none", default)]
270 pub kid: Option<String>,
271
272 #[serde(skip_serializing_if = "Option::is_none", default)]
273 pub alg: Option<String>,
274
275 // Standard JWK fields for an EC key
276 #[serde(rename = "kty")]
277 pub kty: String, // Key Type, e.g., "EC"
278 #[serde(rename = "crv")]
279 pub crv: String, // Curve, e.g., "P-256"
280 #[serde(rename = "x")]
281 pub x: String, // X Coordinate, base64urlUInt
282 #[serde(rename = "y")]
283 pub y: String, // Y Coordinate, base64urlUInt
284 #[serde(skip_serializing_if = "Option::is_none", rename = "d", default)]
285 pub d: Option<String>, // ECC Private Key, base64urlUInt (only for private keys)
286 }
287
288 // This struct might be used if you are directly embedding the JsonWebKey
289 // into another structure that needs a wrapper, like in OAuthRequestParams.
290 // If JsonWebKey itself is sufficient, this might be redundant.
291 #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
292 pub struct WrappedJsonWebKey {
293 #[serde(flatten)]
294 pub jwk: JsonWebKey,
295 }
296
297 #[derive(Serialize, Deserialize, Clone)]
298 pub struct WrappedJsonWebKeySet {
299 pub keys: Vec<WrappedJsonWebKey>,
300 }
301
302 /// Generates a new P-256 key pair and returns it as a JsonWebKey (including private key component).
303 pub fn generate_p256_jwk() -> JsonWebKey {
304 let secret_key = P256SecretKeyInternal::random(&mut OsRng);
305 let public_key = secret_key.public_key();
306 let kid = ulid::Ulid::new().to_string();
307
308 // Coordinates are base64urlUInt encoded.
309 // The `elliptic-curve` crate's `to_encoded_point` with `false` for uncompressed
310 // gives a byte string starting with 0x04, followed by x and then y.
311 // We need to extract x and y and base64url encode them separately.
312 let point_bytes = public_key.to_encoded_point(false).as_bytes().to_vec();
313 // Assuming P-256, x and y are 32 bytes each, after the 0x04 prefix.
314 let x_bytes = &point_bytes[1..33];
315 let y_bytes = &point_bytes[33..65];
316
317 let d_bytes = secret_key.to_bytes(); // This is the scalar for the private key
318
319 JsonWebKey {
320 kid: Some(kid),
321 alg: Some("ES256".to_string()),
322 kty: "EC".to_string(),
323 crv: "P-256".to_string(),
324 x: base64_url::encode(x_bytes),
325 y: base64_url::encode(y_bytes),
326 d: Some(base64_url::encode(&d_bytes)),
327 }
328 }
329
330 /// Calculates the SHA-256 JWK Thumbprint as a base64url encoded string.
331 /// RFC 7638: https://tools.ietf.org/html/rfc7638
332 pub fn calculate_jwk_thumbprint_sha256_base64url(jwk: &JsonWebKey) -> Result<String, JoseError> {
333 // For EC keys, the thumbprint input is a JSON object with ONLY kty, crv, x, y, sorted alphabetically.
334 // {"crv":"P-256","kty":"EC","x":"...","y":"..."}
335 // Ensure these fields are present.
336 if jwk.kty != "EC" {
337 return Err(JoseError::InvalidKeyFormat("JWK must be an EC key for this thumbprint method.".to_string()));
338 }
339 // Construct the JSON string for hashing according to RFC 7638, Section 3.2.
340 // The members MUST be sorted lexicographically by the names of the members.
341 let thumbprint_input = format!(
342 r#"{{\"crv\":\"{}\",\"kty\":\"{}\",\"x\":\"{}\",\"y\":\"{}\"}}"#,
343 jwk.crv, jwk.kty, jwk.x, jwk.y
344 );
345
346 let mut hasher = Sha256::new();
347 hasher.update(thumbprint_input.as_bytes());
348 let digest = hasher.finalize();
349 Ok(base64_url::encode(&digest))
350 }
351
352 /// Generates a new P-256 key pair and returns it as a WrappedJsonWebKey.
353 /// This function is used by the crypto binary for key generation.
354 pub fn generate() -> WrappedJsonWebKey {
355 WrappedJsonWebKey {
356 jwk: generate_p256_jwk()
357 }
358 }
359}
360
361pub mod jwt {
362
363 use std::collections::BTreeMap;
364
365 use elliptic_curve::JwkEcKey;
366 use serde::{Deserialize, Serialize};
367
368 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
369 pub struct Header {
370 #[serde(rename = "alg", skip_serializing_if = "Option::is_none")]
371 pub algorithm: Option<String>,
372
373 #[serde(rename = "kid", skip_serializing_if = "Option::is_none")]
374 pub key_id: Option<String>,
375
376 #[serde(rename = "typ", skip_serializing_if = "Option::is_none")]
377 pub type_: Option<String>,
378
379 #[serde(rename = "jwk", skip_serializing_if = "Option::is_none")]
380 pub json_web_key: Option<JwkEcKey>,
381 }
382
383 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
384 pub struct Claims {
385 #[serde(flatten)]
386 pub jose: JoseClaims,
387 #[serde(flatten)]
388 pub private: BTreeMap<String, serde_json::Value>,
389 }
390
391 impl Claims {
392 pub fn new(jose: JoseClaims) -> Self {
393 Claims {
394 jose,
395 private: BTreeMap::new(),
396 }
397 }
398 }
399
400 pub type SecondsSinceEpoch = u64;
401
402 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
403 pub struct JoseClaims {
404 #[serde(rename = "iss", skip_serializing_if = "Option::is_none")]
405 pub issuer: Option<String>,
406
407 #[serde(rename = "sub", skip_serializing_if = "Option::is_none")]
408 pub subject: Option<String>,
409
410 #[serde(rename = "aud", skip_serializing_if = "Option::is_none")]
411 pub audience: Option<String>,
412
413 #[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
414 pub expiration: Option<SecondsSinceEpoch>,
415
416 #[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
417 pub not_before: Option<SecondsSinceEpoch>,
418
419 #[serde(rename = "iat", skip_serializing_if = "Option::is_none")]
420 pub issued_at: Option<SecondsSinceEpoch>,
421
422 #[serde(rename = "jti", skip_serializing_if = "Option::is_none")]
423 pub json_web_token_id: Option<String>,
424
425 #[serde(rename = "htm", skip_serializing_if = "Option::is_none")]
426 pub http_method: Option<String>,
427
428 #[serde(rename = "htu", skip_serializing_if = "Option::is_none")]
429 pub http_uri: Option<String>,
430
431 #[serde(rename = "nonce", skip_serializing_if = "Option::is_none")]
432 pub nonce: Option<String>,
433
434 #[serde(rename = "ath", skip_serializing_if = "Option::is_none")]
435 pub auth: Option<String>,
436 }
437}