Refactor API
This commit is contained in:
		
							
								
								
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -40,7 +40,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" | ||||
|  | ||||
| [[package]] | ||||
| name = "basic-jwt" | ||||
| version = "0.1.0" | ||||
| version = "0.2.0" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "elliptic-curve", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "basic-jwt" | ||||
| version = "0.1.0" | ||||
| version = "0.2.0" | ||||
| edition = "2021" | ||||
| authors = ["Pierre Hubert <pierre.git@communiquons.org>"] | ||||
| description = "Basic JWT signing and verification library" | ||||
| @@ -17,4 +17,4 @@ serde = { version = "1.0.198", features = ["derive"] } | ||||
| anyhow = "1.0.82" | ||||
| elliptic-curve = { version = "0.13.8", features = ["pkcs8", "pem"] } | ||||
| p384 = { version = "0.13.0", features = ["ecdsa", "pkcs8", "pem"] } | ||||
| jsonwebtoken = "9.3.0" | ||||
| jsonwebtoken = "9.3.0" | ||||
|   | ||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @@ -10,13 +10,16 @@ Basic usage: | ||||
| ```rust | ||||
| let claims = ...; // note : claims must be serializable | ||||
|  | ||||
| // Generate a key pair. Public and private key are both serializable | ||||
| let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); | ||||
| // Generate a key pair. Private and public key are both serializable | ||||
| let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap(); | ||||
| let pub_key = priv_key.to_public_key().unwrap(); | ||||
|  | ||||
| // Create a JWT for the given claims (note: standard claims: sub, iss, ...) are not | ||||
| // automatically added if they are missing | ||||
| let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!"); | ||||
| let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!"); | ||||
|  | ||||
| // Validate signed JWT | ||||
| let claims_out = validate_jwt::<Claims>(&pub_key, &jwt).expect("Failed to validate JWT!"); | ||||
| let claims_out = pub_key | ||||
|             .validate_jwt::<Claims>(&jwt) | ||||
|             .expect("Failed to validate JWT!"); | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										83
									
								
								src/lib.rs
									
									
									
									
									
								
							
							
						
						
									
										83
									
								
								src/lib.rs
									
									
									
									
									
								
							| @@ -5,41 +5,54 @@ use p384::pkcs8::{EncodePrivateKey, LineEnding}; | ||||
| use rand::rngs::OsRng; | ||||
| use serde::de::DeserializeOwned; | ||||
| use serde::Serialize; | ||||
| use std::str::FromStr; | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq)] | ||||
| #[serde(tag = "alg")] | ||||
| pub enum TokenPubKey { | ||||
| pub enum JWTPublicKey { | ||||
|     /// ECDSA with SHA2-384 variant | ||||
|     ES384 { r#pub: String }, | ||||
|     ES384 { | ||||
|         #[serde(rename = "pub")] | ||||
|         public: String, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] | ||||
| #[serde(tag = "alg")] | ||||
| pub enum TokenPrivKey { | ||||
| pub enum JWTPrivateKey { | ||||
|     ES384 { r#priv: String }, | ||||
| } | ||||
|  | ||||
| /// Generate a new ES384 keypair | ||||
| pub fn generate_ec384_keypair() -> anyhow::Result<(TokenPubKey, TokenPrivKey)> { | ||||
|     let signing_key = SigningKey::random(&mut OsRng); | ||||
|     let priv_pem = signing_key | ||||
|         .to_pkcs8_der()? | ||||
|         .to_pem("PRIVATE KEY", LineEnding::LF)? | ||||
|         .to_string(); | ||||
| impl JWTPrivateKey { | ||||
|     /// Generate a new ES384 signing key | ||||
|     pub fn generate_ec384_signing_key() -> anyhow::Result<Self> { | ||||
|         let signing_key = SigningKey::random(&mut OsRng); | ||||
|         let priv_pem = signing_key | ||||
|             .to_pkcs8_der()? | ||||
|             .to_pem("PRIVATE KEY", LineEnding::LF)? | ||||
|             .to_string(); | ||||
|  | ||||
|     let pub_key = VerifyingKey::from(signing_key); | ||||
|     let pub_pem = pub_key.to_public_key_pem(LineEnding::LF)?; | ||||
|         Ok(Self::ES384 { r#priv: priv_pem }) | ||||
|     } | ||||
|  | ||||
|     /// Get associated public key | ||||
|     pub fn to_public_key(&self) -> anyhow::Result<JWTPublicKey> { | ||||
|         match self { | ||||
|             JWTPrivateKey::ES384 { r#priv } => { | ||||
|                 let signing_key = SigningKey::from_str(r#priv)?; | ||||
|  | ||||
|                 let pub_key = VerifyingKey::from(signing_key); | ||||
|                 let pub_pem = pub_key.to_public_key_pem(LineEnding::LF)?; | ||||
|  | ||||
|                 Ok(JWTPublicKey::ES384 { public: pub_pem }) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(( | ||||
|         TokenPubKey::ES384 { r#pub: pub_pem }, | ||||
|         TokenPrivKey::ES384 { r#priv: priv_pem }, | ||||
|     )) | ||||
| } | ||||
| impl TokenPrivKey { | ||||
|     /// Sign a JWT | ||||
|     pub fn sign_jwt<C: Serialize>(&self, claims: &C) -> anyhow::Result<String> { | ||||
|         match self { | ||||
|             TokenPrivKey::ES384 { r#priv } => { | ||||
|             JWTPrivateKey::ES384 { r#priv } => { | ||||
|                 let encoding_key = EncodingKey::from_ec_pem(r#priv.as_bytes())?; | ||||
|  | ||||
|                 Ok(jsonwebtoken::encode( | ||||
| @@ -52,12 +65,12 @@ impl TokenPrivKey { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TokenPubKey { | ||||
| impl JWTPublicKey { | ||||
|     /// Validate a given JWT | ||||
|     pub fn validate_jwt<E: DeserializeOwned>(&self, jwt: &str) -> anyhow::Result<E> { | ||||
|         match self { | ||||
|             TokenPubKey::ES384 { r#pub } => { | ||||
|                 let decoding_key = DecodingKey::from_ec_pem(r#pub.as_bytes())?; | ||||
|             JWTPublicKey::ES384 { public } => { | ||||
|                 let decoding_key = DecodingKey::from_ec_pem(public.as_bytes())?; | ||||
|  | ||||
|                 let validation = Validation::new(Algorithm::ES384); | ||||
|                 Ok(jsonwebtoken::decode::<E>(jwt, &decoding_key, &validation)?.claims) | ||||
| @@ -68,9 +81,9 @@ impl TokenPubKey { | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::generate_ec384_keypair; | ||||
|     use std::time::{SystemTime, UNIX_EPOCH}; | ||||
|  | ||||
|     use crate::JWTPrivateKey; | ||||
|     use serde::{Deserialize, Serialize}; | ||||
|  | ||||
|     fn time() -> u64 { | ||||
| @@ -97,7 +110,9 @@ mod test { | ||||
|  | ||||
|     #[test] | ||||
|     fn jwt_encode_sign_verify_valid() { | ||||
|         let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); | ||||
|         let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap(); | ||||
|         let pub_key = priv_key.to_public_key().unwrap(); | ||||
|  | ||||
|         let claims = Claims::default(); | ||||
|         let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!"); | ||||
|         let claims_out = pub_key | ||||
| @@ -109,8 +124,12 @@ mod test { | ||||
|  | ||||
|     #[test] | ||||
|     fn jwt_encode_sign_verify_invalid_key() { | ||||
|         let (_pub_key, priv_key) = generate_ec384_keypair().unwrap(); | ||||
|         let (pub_key_2, _priv_key_2) = generate_ec384_keypair().unwrap(); | ||||
|         let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap(); | ||||
|         let pub_key_2 = JWTPrivateKey::generate_ec384_signing_key() | ||||
|             .unwrap() | ||||
|             .to_public_key() | ||||
|             .unwrap(); | ||||
|  | ||||
|         let claims = Claims::default(); | ||||
|         let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!"); | ||||
|         pub_key_2 | ||||
| @@ -120,7 +139,9 @@ mod test { | ||||
|  | ||||
|     #[test] | ||||
|     fn jwt_verify_random_string() { | ||||
|         let (pub_key, _priv_key) = generate_ec384_keypair().unwrap(); | ||||
|         let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap(); | ||||
|         let pub_key = priv_key.to_public_key().unwrap(); | ||||
|  | ||||
|         pub_key | ||||
|             .validate_jwt::<Claims>("random_string") | ||||
|             .expect_err("JWT should not have validated!"); | ||||
| @@ -128,7 +149,9 @@ mod test { | ||||
|  | ||||
|     #[test] | ||||
|     fn jwt_expired() { | ||||
|         let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); | ||||
|         let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap(); | ||||
|         let pub_key = priv_key.to_public_key().unwrap(); | ||||
|  | ||||
|         let claims = Claims { | ||||
|             exp: time() - 100, | ||||
|             ..Default::default() | ||||
| @@ -141,7 +164,9 @@ mod test { | ||||
|  | ||||
|     #[test] | ||||
|     fn jwt_invalid_signature() { | ||||
|         let (pub_key, priv_key) = generate_ec384_keypair().unwrap(); | ||||
|         let priv_key = JWTPrivateKey::generate_ec384_signing_key().unwrap(); | ||||
|         let pub_key = priv_key.to_public_key().unwrap(); | ||||
|  | ||||
|         let claims = Claims::default(); | ||||
|         let jwt = priv_key.sign_jwt(&claims).expect("Failed to sign JWT!"); | ||||
|         pub_key | ||||
|   | ||||
		Reference in New Issue
	
	Block a user