From 42824a133e41d5372654fe6b9a9883b628b719a8 Mon Sep 17 00:00:00 2001 From: Thomas Gideon Date: Thu, 10 Aug 2017 09:39:35 -0400 Subject: [PATCH] Update to serde 1.0 (#4) * Try updating service to 1.0 * Strip bounds from structs * Clean bounds from Default impl * Remove pem files, clean up some documentation * Add badges, categories --- Cargo.toml | 8 +++++++- examples/custom_claims.rs | 7 ++++++- examples/custom_headers.rs | 8 +++++++- examples/hs256.rs | 6 +++++- examples/privateKey.pem | 27 --------------------------- examples/publicKey.pub | 9 --------- examples/rs256.rs | 35 ++++++++++++++++------------------- src/crypt.rs | 31 ++++++------------------------- src/error.rs | 5 +++++ src/header.rs | 1 + src/lib.rs | 20 ++++++++++++-------- 11 files changed, 65 insertions(+), 92 deletions(-) delete mode 100644 examples/privateKey.pem delete mode 100644 examples/publicKey.pub diff --git a/Cargo.toml b/Cargo.toml index 6fcb9fa..e5b3a80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "medallion" -version = "2.1.1" +version = "2.2.1" authors = ["Thomas Gideon "] description = "JWT library for rust using serde, serde_json and openssl" homepage = "http://github.com/commandline/medallion" @@ -8,6 +8,8 @@ repository = "http://github.com/commandline/medallion" documentation = "https://commandline.github.io/medallion/" readme = "README.md" keywords = ["JWT", "token", "web", "JSON", "RSA"] +categories = ["cryptography", "authentication", "web-programming", +"data-structures"] license = "MIT" [dependencies] @@ -17,3 +19,7 @@ serde = "1.0" serde_json = "1.0" serde_derive = "1.0" time = "0.1" + +[badges] +travis-ci = { repository = "https://travis-ci.org/commandline/medallion", branch = "master" } + diff --git a/examples/custom_claims.rs b/examples/custom_claims.rs index c14cd9e..70d4f55 100644 --- a/examples/custom_claims.rs +++ b/examples/custom_claims.rs @@ -16,13 +16,18 @@ struct Custom { } fn new_token(user_id: &str, password: &str) -> Option { - // Dummy auth + // dummy auth, in a real application using something like openidconnect, this would be some + // specific authentication scheme that takes place first then the JWT is generated as part of + // sucess and signed with the provider's private key so other services can validate trust for + // the claims in the token if password != "password" { return None; } let header: Header<()> = Default::default(); let payload = Payload { + // custom claims will be application specific, they may come from open standards such as + // openidconnect where they may be referred to as registered claims claims: Some(Custom { user_id: user_id.into(), rhino: true, diff --git a/examples/custom_headers.rs b/examples/custom_headers.rs index baebf54..dd7a84e 100644 --- a/examples/custom_headers.rs +++ b/examples/custom_headers.rs @@ -15,12 +15,18 @@ struct Custom { } fn new_token(sub: &str, password: &str) -> Option { - // Dummy auth + // dummy auth, in a real application using something like openidconnect, this would be some + // specific authentication scheme that takes place first then the JWT is generated as part of + // sucess and signed with the provider's private key so other services can validate trust for + // the claims in the token if password != "password" { return None; } let header = Header { + // customer headers generally are about the token itself, like here describing the type of + // token, as opposed to claims which are about the authenticated user or some output of + // the authentication process headers: Some(Custom { typ: "JWT".into(), ..Default::default() }), ..Default::default() }; diff --git a/examples/hs256.rs b/examples/hs256.rs index b84fc8a..92d1d1d 100644 --- a/examples/hs256.rs +++ b/examples/hs256.rs @@ -4,7 +4,10 @@ use std::default::Default; use medallion::{Header, DefaultPayload, DefaultToken}; fn new_token(user_id: &str, password: &str) -> Option { - // Dummy auth + // dummy auth, in a real application using something like openidconnect, this would be some + // specific authentication scheme that takes place first then the JWT is generated as part of + // sucess and signed with the provider's private key so other services can validate trust for + // the claims in the token if password != "password" { return None; } @@ -24,6 +27,7 @@ fn new_token(user_id: &str, password: &str) -> Option { fn login(token: &str) -> Option { let token: DefaultToken<()> = DefaultToken::parse(token).unwrap(); + // the key for HMAC is some secret known to trusted/trusting parties if token.verify(b"secret_key").unwrap() { token.payload.sub } else { diff --git a/examples/privateKey.pem b/examples/privateKey.pem deleted file mode 100644 index 5009652..0000000 --- a/examples/privateKey.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAqfGTaNo/PBWs3frGcNpboXCh2/NV2edFwkXMnEjGVw9tyLEu -62Tv/LD+hHGQ/EtRfy+iueamtqwRnpeQDnThdYgc7a1nfava7j7ecUCJNnjCJSBT -8qWX5U7lsFrxm2NEhGIMgO40wQRxFylb4izxrloBvFKr+XtfgIY5CaUTaKdvZ2mo -OXgzZ5kC8Qr7SASpEJiMRM432N9QDTTg3kv23htec+FzSucEdJHrkIxr2LjHAmtm -XcT8vRikpN2GThbRUwjLEW3YsWJSTn9GGzF2XZ2rRhdDMviPPjjYPH7Sau2ROnk8 -sgGMtEzZNS7i/Lm5qsAoPw85mnnnTvJ6ArId2QIDAQABAoIBADPz4S+lwL0alz1J -Q88OQgLpjuHR0wYJeL76XaHNcaz9z38SA5j8w40JgtV0bnFiiSiLpICWbZLcqYpF -JUn2G1K16LoUT9YQap5448HVi9z2L8vvxRoh23zDkN5H/yKUx0Z9PvtPVxtGw1fk -Ue2j9cJqS6uJzn83YyvEXL2BFJziS1mQ7W6raD9fWTBFOlcNwR2+djlECEvxoMcf -SQqYa5oBUFWFmaJdCjOxqQxNRgWFxPEVlAz62PUoC1WUoP3uKAg5mvKhH/PjNSGl -9K6JhpVgvlBfVUu7dWGAmaUZOLu8l6EUO9MWXDfk10qFKo1bZT9orrARHESPzL2H -PPzd3J0CgYEA0TyJxaSVVfWqoSm8yYTbczaRuJ89S104pOzV3UjpLS8DWClFhbl0 -fYhXGgWIbu3QKksmJ2m9fWm7WbkcYzBjSJOuMFi3actA/glMzZ+DRP+v/154gDR0 -4/5Etwv+mI+xJqDFGThelC1I7qKtwiyiz85zslW/zDDrn2Yz9OE01S8CgYEAz+zm -Qtp88FiSnllzZSM8Hnzey3j4CDk73R2izBrECTI4FE+EPHu6w7wmNiRRTSNjc1F2 -4qPz/95fUWqwH3ES0UgArFN8vBaoWaoQDmWZGG0ao9Pr6tWDl0DZKrz63jCtCKn7 -7/bhcxZXYKIPzWgRQhEsZndmcsvARqPVrecmC3cCgYEAofGdIJ/2BYYS/pHzUHXH -9DB0MNTu9/m68cts68yWzSXqDL5E1O9pPg/ceoN1yYW+7D0l1rN8uiivnQ4s7ohx -D6dd1oWT0ApEz1obW7ruOuU7LwfLdE8leaE/Rf2+nA37Ks6cPpzmdwFlxW2b1wH9 -MaG04n6D6GKku8a6x/nWjnkCgYEAwSnAMNNxxodCjsFjJs45B8nR4Q2cv2cMajsi -BqPHAxQYbSYCH36C31xn01yh+xupRHSmEZ9nCom325dVz5/ob2yI048sDkCuXb5T -9EwGkl6ppRE31o5NFbM1DTNLjCeEWMwyNZgRki1rN2bXb2gCwHHb4cWC85q+IeIK -nOhku7kCgYAfj3vj/Wc/xTKkZREevgqLm4+B2Sgl5lLaV7OF8jtXEv2mmmmun3Xd -r5V2dvvsBQ1D5DQmGw+ObICgdox9BViqG+2PYBWAUfAWyDZaaSEfo72L/1RDdsAR -ldUh5fpbdCNl4cz9I2Tysl54pTKMCCH+zj10w+0g5TuNlEZCX/p7qA== ------END RSA PRIVATE KEY----- diff --git a/examples/publicKey.pub b/examples/publicKey.pub deleted file mode 100644 index 0672fc0..0000000 --- a/examples/publicKey.pub +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqfGTaNo/PBWs3frGcNpb -oXCh2/NV2edFwkXMnEjGVw9tyLEu62Tv/LD+hHGQ/EtRfy+iueamtqwRnpeQDnTh -dYgc7a1nfava7j7ecUCJNnjCJSBT8qWX5U7lsFrxm2NEhGIMgO40wQRxFylb4izx -rloBvFKr+XtfgIY5CaUTaKdvZ2moOXgzZ5kC8Qr7SASpEJiMRM432N9QDTTg3kv2 -3htec+FzSucEdJHrkIxr2LjHAmtmXcT8vRikpN2GThbRUwjLEW3YsWJSTn9GGzF2 -XZ2rRhdDMviPPjjYPH7Sau2ROnk8sgGMtEzZNS7i/Lm5qsAoPw85mnnnTvJ6ArId -2QIDAQAB ------END PUBLIC KEY----- diff --git a/examples/rs256.rs b/examples/rs256.rs index 6c70991..c67f947 100644 --- a/examples/rs256.rs +++ b/examples/rs256.rs @@ -1,24 +1,20 @@ extern crate medallion; +extern crate openssl; use std::default::Default; -use std::fs::File; -use std::io::{Error, Read}; +use openssl::rsa; use medallion::{Algorithm, Header, DefaultPayload, DefaultToken}; -fn load_pem(keypath: &str) -> Result { - let mut key_file = File::open(keypath)?; - let mut key = String::new(); - key_file.read_to_string(&mut key)?; - Ok(key) -} - -fn new_token(user_id: &str, password: &str) -> Option { - // Dummy auth +fn new_token(private_key: &[u8], user_id: &str, password: &str) -> Option { + // dummy auth, in a real application using something like openidconnect, this would be some + // specific authentication scheme that takes place first then the JWT is generated as part of + // sucess and signed with the provider's private key so other services can validate trust for + // the claims in the token if password != "password" { return None; } - // can satisfy Header's generic parameter with an empty type + // can satisfy Header's type parameter with an empty tuple let header: Header<()> = Header { alg: Algorithm::RS256, ..Default::default() }; let payload: DefaultPayload = DefaultPayload { iss: Some("example.com".into()), @@ -27,15 +23,13 @@ fn new_token(user_id: &str, password: &str) -> Option { }; let token = DefaultToken::new(header, payload); - // this key was generated explicitly for these examples and is not used anywhere else - token.sign(load_pem("./privateKey.pem").unwrap().as_bytes()).ok() + token.sign(private_key).ok() } -fn login(token: &str) -> Option { +fn login(public_key: &[u8], token: &str) -> Option { let token: DefaultToken<()> = DefaultToken::parse(token).unwrap(); - // this key was generated explicitly for these examples and is not used anywhere else - if token.verify(load_pem("./publicKey.pub").unwrap().as_bytes()).unwrap() { + if token.verify(public_key).unwrap() { token.payload.sub } else { None @@ -43,9 +37,12 @@ fn login(token: &str) -> Option { } fn main() { - let token = new_token("Random User", "password").unwrap(); + // alternatively can read .pem files from fs or fetch from a server or... + let keypair = rsa::Rsa::generate(2048).unwrap(); - let logged_in_user = login(&*token).unwrap(); + let token = new_token(&keypair.private_key_to_pem().unwrap(), "Random User", "password").unwrap(); + + let logged_in_user = login(&keypair.public_key_to_pem().unwrap(), &*token).unwrap(); assert_eq!(logged_in_user, "Random User"); } diff --git a/src/crypt.rs b/src/crypt.rs index 6bc1118..1de80cb 100644 --- a/src/crypt.rs +++ b/src/crypt.rs @@ -73,8 +73,7 @@ fn verify_rsa(signature: &str, data: &str, key: &[u8], digest: MessageDigest) -> #[cfg(test)] pub mod tests { use header::Algorithm; - use std::io::{Error, Read}; - use std::fs::File; + use openssl; use super::{sign, verify}; #[test] @@ -90,17 +89,17 @@ pub mod tests { } #[test] - pub fn sign_data_rsa() { + pub fn sign_and_verify_data_rsa() { let header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"; let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"; - let real_sig = "nXdpIkFQYZXZ0VlJjHmAc5_aewHCCJpT5jP1fpexUCF_9m3NxlC7uYNXAl6NKno520oh9wVT4VV_vmPeEin7BnnoIJNPcImWcUzkYpLTrDBntiF9HCuqFaniuEVzlf8dVlRJgo8QxhmUZEjyDFjPZXZxPlPV1LD6hrtItxMKZbh1qoNY3OL7Mwo-WuSRQ0mmKj-_y3weAmx_9EaTLY639uD8-o5iZxIIf85U4e55Wdp-C9FJ4RxyHpjgoG8p87IbChfleSdWcZL3NZuxjRCHVWgS1uYG0I-LqBWpWyXnJ1zk6-w4tfxOYpZFMOIyq4tY2mxJQ78Kvcu8bTO7UdI7iA"; + let data = format!("{}.{}", header, claims); - let key = load_pem("./examples/privateKey.pem").unwrap(); + let keypair = openssl::rsa::Rsa::generate(2048).unwrap(); - let sig = sign(&*data, key.as_bytes(), &Algorithm::RS256); + let sig = sign(&*data, &keypair.private_key_to_pem().unwrap(), &Algorithm::RS256).unwrap(); - assert_eq!(sig.unwrap(), real_sig); + assert!(verify(&sig, &*data, &keypair.public_key_to_pem().unwrap(), &Algorithm::RS256).unwrap()); } #[test] @@ -112,22 +111,4 @@ pub mod tests { assert!(verify(target, &*data, "secret".as_bytes(), &Algorithm::HS256).unwrap()); } - - #[test] - pub fn verify_data_rsa() { - let header = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"; - let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"; - let real_sig = "nXdpIkFQYZXZ0VlJjHmAc5_aewHCCJpT5jP1fpexUCF_9m3NxlC7uYNXAl6NKno520oh9wVT4VV_vmPeEin7BnnoIJNPcImWcUzkYpLTrDBntiF9HCuqFaniuEVzlf8dVlRJgo8QxhmUZEjyDFjPZXZxPlPV1LD6hrtItxMKZbh1qoNY3OL7Mwo-WuSRQ0mmKj-_y3weAmx_9EaTLY639uD8-o5iZxIIf85U4e55Wdp-C9FJ4RxyHpjgoG8p87IbChfleSdWcZL3NZuxjRCHVWgS1uYG0I-LqBWpWyXnJ1zk6-w4tfxOYpZFMOIyq4tY2mxJQ78Kvcu8bTO7UdI7iA"; - let data = format!("{}.{}", header, claims); - - let key = load_pem("./examples/publicKey.pub").unwrap(); - assert!(verify(&real_sig, &*data, key.as_bytes(), &Algorithm::RS256).unwrap()); - } - - pub fn load_pem(keypath: &str) -> Result { - let mut key_file = File::open(keypath)?; - let mut key = String::new(); - key_file.read_to_string(&mut key)?; - Ok(key) - } } diff --git a/src/error.rs b/src/error.rs index b1578e7..7cbaeef 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,10 +7,15 @@ use openssl::error::ErrorStack; #[derive(Debug)] pub enum Error { + /// Custom, Medallion specific errors. Custom(String), + /// String encoding errors. Utf8(FromUtf8Error), + /// Base64 encoding or decoding errors. Base64(Base64Error), + /// JSON parsing or stringifying errors. JSON(serde_json::Error), + /// Errors from RSA operations. Crypto(ErrorStack), } diff --git a/src/header.rs b/src/header.rs index 102efad..465589c 100644 --- a/src/header.rs +++ b/src/header.rs @@ -31,6 +31,7 @@ pub enum Algorithm { } impl Header { + /// Decode from base64. pub fn from_base64(raw: &str) -> Result> { let data = decode_config(raw, URL_SAFE_NO_PAD)?; let own: Header = serde_json::from_slice(&data)?; diff --git a/src/lib.rs b/src/lib.rs index 1301ee6..7a5886c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,11 @@ #![crate_name = "medallion"] #![crate_type = "lib"] #![doc(html_root_url = "https://commandline.github.io/medallion/")] +///! A crate for working with JSON WebTokens that use OpenSSL for RSA signing and encryption and +///! serde and serde_json for JSON encoding and decoding. +///! +///! Tries to support the standard uses for JWTs while providing reasonable ways to extend, +///! primarily by adding custom headers and claims to tokens. extern crate base64; extern crate openssl; extern crate serde; @@ -23,8 +28,8 @@ mod crypt; pub type Result = std::result::Result; -/// A convenient type that bins the same type parameter for the custom claims, an empty tuple, as -/// DefaultPayload so that the two aliases may be used together to reduce boilerplate when not +/// A convenient type that binds the same type parameter for the custom claims, an empty tuple, as +/// DefaultPayload so that the two aliases may be used together to reduce boilerplate when no /// custom claims are needed. pub type DefaultToken = Token; @@ -54,7 +59,7 @@ impl Token let pieces: Vec<_> = raw.split('.').collect(); Ok(Token { - raw: Some(raw.into()), + raw: Some(raw.to_owned()), header: Header::from_base64(pieces[0])?, payload: Payload::from_base64(pieces[1])?, }) @@ -98,7 +103,7 @@ impl PartialEq for Token #[cfg(test)] mod tests { use {DefaultPayload, DefaultToken, Header}; - use crypt::tests::load_pem; + use openssl; use std::default::Default; use time::{self, Duration, Tm}; use super::Algorithm::{HS256, RS512}; @@ -158,15 +163,14 @@ mod tests { #[test] pub fn roundtrip_rsa() { + let rsa_keypair = openssl::rsa::Rsa::generate(2048).unwrap(); let header: Header<()> = Header { alg: RS512, ..Default::default() }; let token = DefaultToken { header: header, ..Default::default() }; - let private_key = load_pem("./examples/privateKey.pem").unwrap(); - let raw = token.sign(private_key.as_bytes()).unwrap(); + let raw = token.sign(&rsa_keypair.private_key_to_pem().unwrap()).unwrap(); let same = DefaultToken::parse(&*raw).unwrap(); assert_eq!(token, same); - let public_key = load_pem("./examples/publicKey.pub").unwrap(); - assert!(same.verify(public_key.as_bytes()).unwrap()); + assert!(same.verify(&rsa_keypair.public_key_to_pem().unwrap()).unwrap()); } fn create_for_range(nbf: Tm, exp: Tm) -> DefaultToken<()> {