i18n+filtering fork - fluent-templates v2
at main 367 lines 12 kB view raw
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}