Heavily customized version of smokesignal - https://whtwnd.com/kayrozen.com/3lpwe4ymowg2t
at main 437 lines 16 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::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}