From 29ad0d721deb5a23c3b66bc513a74a5ea9345a50 Mon Sep 17 00:00:00 2001 From: Thomas Gideon Date: Mon, 13 Feb 2017 18:40:07 -0500 Subject: [PATCH] Initial re-factor --- .gitignore | 2 + Cargo.toml | 17 +++ LICENSE | 22 ++++ README.md | 10 ++ examples/custom_claims.rs | 50 ++++++++ examples/hs256.rs | 44 +++++++ examples/privateKey.pem | 27 ++++ examples/publicKey.pub | 9 ++ examples/rs256.rs | 57 ++++++++ src/claims.rs | 97 ++++++++++++++ src/crypt.rs | 47 +++++++ src/error.rs | 23 ++++ src/header.rs | 74 +++++++++++ src/lib.rs | 264 ++++++++++++++++++++++++++++++++++++++ 14 files changed, 743 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/custom_claims.rs create mode 100644 examples/hs256.rs create mode 100644 examples/privateKey.pem create mode 100644 examples/publicKey.pub create mode 100644 examples/rs256.rs create mode 100644 src/claims.rs create mode 100644 src/crypt.rs create mode 100644 src/error.rs create mode 100644 src/header.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9561059 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "medallion" +version = "0.0.1" +authors = ["Thomas Gideon "] +description = "JWT library for rust using serde, serde_json and openssl" +homepage = "http://github.com/commandline/meiallion" +repository = "http://github.com/commandline/medallion" +readme = "README.md" +keywords = ["JWT", "token", "web"] +license = "MIT" + +[dependencies] +base64 = "0.4" +openssl = "0.9" +serde = "0.9" +serde_json = "0.9" +serde_derive = "0.9" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..064671d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2015 Michael Yang + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6de567c --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# JWT + +A JWT library for rust using serde, serde_json and openssl. + +## Usage + +The library provides a `Token` type that wraps a header and claims. The claims can be any type that implements the `Component` trait, which is automatically implemented for types that implement the `Sized`, `Encodable`, +and `Decodable` traits. Header can be any type that implements `Component` and `Header`. `Header` ensures that the required algorithm is available for signing and verification. `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, and `RS512` are supported. See the examples. + +This library was originally forked from @mikkyang's rust-jwt. diff --git a/examples/custom_claims.rs b/examples/custom_claims.rs new file mode 100644 index 0000000..7e766bf --- /dev/null +++ b/examples/custom_claims.rs @@ -0,0 +1,50 @@ +extern crate crypto; +extern crate jwt; +extern crate rustc_serialize; + +use std::default::Default; +use jwt::{ + DefaultHeader, + Token, +}; + +#[derive(Default, RustcDecodable, RustcEncodable)] +struct Custom { + sub: String, + rhino: bool, +} + +fn new_token(user_id: &str, password: &str) -> Option { + // Dummy auth + if password != "password" { + return None + } + + let header: DefaultHeader = Default::default(); + let claims = Custom { + sub: user_id.into(), + rhino: true, + ..Default::default() + }; + let token = Token::new(header, claims); + + token.signed(b"secret_key").ok() +} + +fn login(token: &str) -> Option { + let token = Token::::parse(token).unwrap(); + + if token.verify(b"secret_key") { + Some(token.claims.sub) + } else { + None + } +} + +fn main() { + let token = new_token("Michael Yang", "password").unwrap(); + + let logged_in_user = login(&*token).unwrap(); + + assert_eq!(logged_in_user, "Michael Yang"); +} diff --git a/examples/hs256.rs b/examples/hs256.rs new file mode 100644 index 0000000..b259342 --- /dev/null +++ b/examples/hs256.rs @@ -0,0 +1,44 @@ +extern crate crypto; +extern crate jwt; + +use std::default::Default; +use jwt::{ + DefaultHeader, + Registered, + Token, +}; + +fn new_token(user_id: &str, password: &str) -> Option { + // Dummy auth + if password != "password" { + return None + } + + let header: DefaultHeader = Default::default(); + let claims = Registered { + iss: Some("mikkyang.com".into()), + sub: Some(user_id.into()), + ..Default::default() + }; + let token = Token::new(header, claims); + + token.signed(b"secret_key").ok() +} + +fn login(token: &str) -> Option { + let token = Token::::parse(token).unwrap(); + + if token.verify(b"secret_key") { + token.claims.sub + } else { + None + } +} + +fn main() { + let token = new_token("Michael Yang", "password").unwrap(); + + let logged_in_user = login(&*token).unwrap(); + + assert_eq!(logged_in_user, "Michael Yang"); +} diff --git a/examples/privateKey.pem b/examples/privateKey.pem new file mode 100644 index 0000000..5009652 --- /dev/null +++ b/examples/privateKey.pem @@ -0,0 +1,27 @@ +-----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 new file mode 100644 index 0000000..0672fc0 --- /dev/null +++ b/examples/publicKey.pub @@ -0,0 +1,9 @@ +-----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 new file mode 100644 index 0000000..ebca509 --- /dev/null +++ b/examples/rs256.rs @@ -0,0 +1,57 @@ +extern crate crypto; +extern crate jwt; + +use std::default::Default; +use std::fs::File; +use std::io::{Error, Read}; +use jwt::{ + Algorithm, + DefaultHeader, + Registered, + Token, +}; + +fn load_key(keypath: &str) -> Result { + let mut key_file = try!(File::open(keypath)); + let mut key = String::new(); + try!(key_file.read_to_string(&mut key)); + Ok(key) +} + +fn new_token(user_id: &str, password: &str) -> Option { + // Dummy auth + if password != "password" { + return None + } + + let header: DefaultHeader = DefaultHeader { + alg: Algorithm::RS256, + ..Default::default() + }; + let claims = Registered { + iss: Some("mikkyang.com".into()), + sub: Some(user_id.into()), + ..Default::default() + }; + let token = Token::new(header, claims); + + token.signed(load_key("./privateKey.pem").unwrap().as_bytes()).ok() +} + +fn login(token: &str) -> Option { + let token = Token::::parse(token).unwrap(); + + if token.verify(load_key("./publicKey.pub").unwrap().as_bytes()) { + token.claims.sub + } else { + None + } +} + +fn main() { + let token = new_token("Michael Yang", "password").unwrap(); + + let logged_in_user = login(&*token).unwrap(); + + assert_eq!(logged_in_user, "Michael Yang"); +} diff --git a/src/claims.rs b/src/claims.rs new file mode 100644 index 0000000..cf2fc7c --- /dev/null +++ b/src/claims.rs @@ -0,0 +1,97 @@ +use base64::{decode, encode_config, URL_SAFE}; +use Component; +use error::Error; +use serde::{Deserialize, Serialize}; +use serde_json; +use serde_json::value::{from_value, to_value, Map, Value}; + +#[derive(Debug, Default, PartialEq)] +pub struct Claims { + pub reg: Registered, + pub private: Value +} + +#[derive(Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Registered { + pub iss: Option, + pub sub: Option, + pub aud: Option, + pub exp: Option, + pub nbf: Option, + pub iat: Option, + pub jti: Option, +} + +/// JWT Claims. Registered claims are directly accessible via the `Registered` +/// struct embedded, while private fields are a map that contains `Json` +/// values. +impl Claims { + pub fn new(reg: Registered, private: Value) -> Claims { + Claims { + reg: reg, + private: private + } + } +} + +impl Component for Claims { + fn from_base64(raw: &str) -> Result { + let data = try!(decode(raw)); + let reg_claims: Registered = try!(serde_json::from_slice(&data)); + + let pri_claims: Value = try!(serde_json::from_slice(&data)); + + + Ok(Claims{ + reg: reg_claims, + private: pri_claims + }) + } + + fn to_base64(&self) -> Result { + let mut value = try!(serde_json::to_value(&self.reg)); + let mut obj_value = &value.as_object_mut().unwrap(); + // TODO iterate private claims and add to JSON Map + //let mut pri_value = self.private.as_object_mut().unwrap(); + + //obj_value.extend(pri_value.into_iter()); + + let s = try!(serde_json::to_string(&obj_value)); + let enc = encode_config((&*s).as_bytes(), URL_SAFE); + Ok(enc) + } +} + +#[cfg(test)] +mod tests { + use std::default::Default; + use claims::{Claims, Registered}; + use Component; + + #[test] + fn from_base64() { + let enc = "ew0KICAiaXNzIjogIm1pa2t5YW5nLmNvbSIsDQogICJleHAiOiAxMzAyMzE5MTAwLA0KICAibmFtZSI6ICJNaWNoYWVsIFlhbmciLA0KICAiYWRtaW4iOiB0cnVlDQp9"; + let claims = Claims::from_base64(enc).unwrap(); + + assert_eq!(claims.reg.iss.unwrap(), "mikkyang.com"); + assert_eq!(claims.reg.exp.unwrap(), 1302319100); + } + + #[test] + fn multiple_types() { + let enc = "ew0KICAiaXNzIjogIm1pa2t5YW5nLmNvbSIsDQogICJleHAiOiAxMzAyMzE5MTAwLA0KICAibmFtZSI6ICJNaWNoYWVsIFlhbmciLA0KICAiYWRtaW4iOiB0cnVlDQp9"; + let claims = Registered::from_base64(enc).unwrap(); + + assert_eq!(claims.iss.unwrap(), "mikkyang.com"); + assert_eq!(claims.exp.unwrap(), 1302319100); + } + + #[test] + fn roundtrip() { + let mut claims: Claims = Default::default(); + claims.reg.iss = Some("mikkyang.com".into()); + claims.reg.exp = Some(1302319100); + let enc = claims.to_base64().unwrap(); + assert_eq!(claims, Claims::from_base64(&*enc).unwrap()); + } +} diff --git a/src/crypt.rs b/src/crypt.rs new file mode 100644 index 0000000..6a117cf --- /dev/null +++ b/src/crypt.rs @@ -0,0 +1,47 @@ +use base64::{decode, encode_config, URL_SAFE}; +use openssl::hash::MessageDigest; +use openssl::memcmp; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::sign::{Signer, Verifier}; + +pub fn sign(data: &str, key: &[u8], digest: MessageDigest) -> String { + let secret_key = PKey::hmac(key).unwrap(); + + let mut signer = Signer::new(digest, &secret_key).unwrap(); + signer.update(data.as_bytes()).unwrap(); + + let mac = signer.finish().unwrap(); + encode_config(&mac, URL_SAFE) +} + +pub fn sign_rsa(data: &str, key: &[u8], digest: MessageDigest) -> String { + let private_key = Rsa::private_key_from_pem(key).unwrap(); + let pkey = PKey::from_rsa(private_key).unwrap(); + + let mut signer = Signer::new(digest, &pkey).unwrap(); + signer.update(data.as_bytes()).unwrap(); + let sig = signer.finish().unwrap(); + encode_config(&sig, URL_SAFE) +} + +pub fn verify(target: &str, data: &str, key: &[u8], digest: MessageDigest) -> bool { + let target_bytes: Vec = decode(target).unwrap(); + let secret_key = PKey::hmac(key).unwrap(); + + let mut signer = Signer::new(digest, &secret_key).unwrap(); + signer.update(data.as_bytes()).unwrap(); + + let mac = signer.finish().unwrap(); + + memcmp::eq(&mac, &target_bytes) +} + +pub fn verify_rsa(signature: &str, data: &str, key: &[u8], digest: MessageDigest) -> bool { + let signature_bytes: Vec = decode(signature).unwrap(); + let public_key = Rsa::public_key_from_pem(key).unwrap(); + let pkey = PKey::from_rsa(public_key).unwrap(); + let mut verifier = Verifier::new(digest, &pkey).unwrap(); + verifier.update(data.as_bytes()).unwrap(); + verifier.finish(&signature_bytes).unwrap() +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..5523fab --- /dev/null +++ b/src/error.rs @@ -0,0 +1,23 @@ +use base64::Base64Error; +use serde_json; +use std::string::FromUtf8Error; + +#[derive(Debug)] +pub enum Error { + Format, + Utf8(FromUtf8Error), + Base64(Base64Error), + JSON(serde_json::Error), +} + +macro_rules! error_wrap { + ($f: ty, $e: expr) => { + impl From<$f> for Error { + fn from(f: $f) -> Error { $e(f) } + } + } +} + +error_wrap!(FromUtf8Error, Error::Utf8); +error_wrap!(Base64Error, Error::Base64); +error_wrap!(serde_json::Error, Error::JSON); diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..b78f613 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,74 @@ +use std::default::Default; +use Header; + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct DefaultHeader { + pub typ: Option, + pub kid: Option, + pub alg: Algorithm, +} + + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum HeaderType { + JWT, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub enum Algorithm { + HS256, + HS384, + HS512, + RS256, + RS384, + RS512 +} + +impl Default for DefaultHeader { + fn default() -> DefaultHeader { + DefaultHeader { + typ: Some(HeaderType::JWT), + kid: None, + alg: Algorithm::HS256, + } + } +} + +impl Header for DefaultHeader { + fn alg(&self) -> &Algorithm { + &(self.alg) + } +} + +#[cfg(test)] +mod tests { + use Component; + use header::{ + Algorithm, + DefaultHeader, + HeaderType, + }; + + #[test] + fn from_base64() { + let enc = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let header = DefaultHeader::from_base64(enc).unwrap(); + + assert_eq!(header.typ.unwrap(), HeaderType::JWT); + assert_eq!(header.alg, Algorithm::HS256); + + + let enc = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFLU0YzZyJ9"; + let header = DefaultHeader::from_base64(enc).unwrap(); + + assert_eq!(header.kid.unwrap(), "1KSF3g".to_string()); + assert_eq!(header.alg, Algorithm::RS256); + } + + #[test] + fn roundtrip() { + let header: DefaultHeader = Default::default(); + let enc = Component::to_base64(&header).unwrap(); + assert_eq!(header, DefaultHeader::from_base64(&*enc).unwrap()); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f6c0529 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,264 @@ +extern crate base64; +extern crate openssl; +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +use base64::{decode, encode_config, URL_SAFE}; +use openssl::hash::MessageDigest; +use serde::{Serialize, Deserialize}; +pub use error::Error; +pub use header::DefaultHeader; +pub use header::Algorithm; +pub use claims::Claims; +pub use claims::Registered; + +pub mod error; +pub mod header; +pub mod claims; +mod crypt; + +#[derive(Debug, Default)] +pub struct Token + where H: Component, C: Component { + raw: Option, + pub header: H, + pub claims: C, +} + +pub trait Header { + fn alg(&self) -> &header::Algorithm; +} + +pub trait Component: Sized { + fn from_base64(raw: &str) -> Result; + fn to_base64(&self) -> Result; +} + +impl Component for T + where T: Serialize + Deserialize + Sized { + + /// Parse from a string. + fn from_base64(raw: &str) -> Result { + let data = try!(decode(raw)); + let s = try!(String::from_utf8(data)); + Ok(try!(serde_json::from_str(&*s))) + } + + /// Encode to a string. + fn to_base64(&self) -> Result { + let s = try!(serde_json::to_string(&self)); + let enc = encode_config((&*s).as_bytes(), URL_SAFE); + Ok(enc) + } +} + +impl Token + where H: Component + Header, C: Component { + pub fn new(header: H, claims: C) -> Token { + Token { + raw: None, + header: header, + claims: claims, + } + } + + /// Parse a token from a string. + pub fn parse(raw: &str) -> Result, Error> { + let pieces: Vec<_> = raw.split('.').collect(); + + Ok(Token { + raw: Some(raw.into()), + header: try!(Component::from_base64(pieces[0])), + claims: try!(Component::from_base64(pieces[1])), + }) + } + + /// Verify a from_base64 token with a key and the token's specific algorithm + pub fn verify(&self, key: &[u8]) -> bool { + match self.header.alg() { + &Algorithm::HS256 => self.verify_hmac(key, MessageDigest::sha256()), + &Algorithm::HS384 => self.verify_hmac(key, MessageDigest::sha384()), + &Algorithm::HS512 => self.verify_hmac(key, MessageDigest::sha512()), + &Algorithm::RS256 => self.verify_rsa(key, MessageDigest::sha256()), + &Algorithm::RS384 => self.verify_rsa(key, MessageDigest::sha384()), + &Algorithm::RS512 => self.verify_rsa(key, MessageDigest::sha512()), + } + } + + fn verify_hmac(&self, key: &[u8], digest: MessageDigest) -> bool { + let raw = match self.raw { + Some(ref s) => s, + None => return false, + }; + + let pieces: Vec<_> = raw.rsplitn(2, '.').collect(); + let sig = pieces[0]; + let data = pieces[1]; + + crypt::verify(sig, data, key, digest) + } + + fn verify_rsa(&self, key: &[u8], digest: MessageDigest) -> bool { + let raw = match self.raw { + Some(ref s) => s, + None => return false, + }; + + let pieces: Vec<_> = raw.rsplitn(2, '.').collect(); + let sig = pieces[0]; + let data = pieces[1]; + + crypt::verify_rsa(sig, data, key, digest) + } + + /// Generate the signed token from a key and the specific algorithm + pub fn signed(&self, key: &[u8]) -> Result { + match self.header.alg() { + &Algorithm::HS256 => self.signed_hmac(key, MessageDigest::sha256()), + &Algorithm::HS384 => self.signed_hmac(key, MessageDigest::sha384()), + &Algorithm::HS512 => self.signed_hmac(key, MessageDigest::sha512()), + &Algorithm::RS256 => self.signed_rsa(key, MessageDigest::sha256()), + &Algorithm::RS384 => self.signed_rsa(key, MessageDigest::sha384()), + &Algorithm::RS512 => self.signed_rsa(key, MessageDigest::sha512()), + } + } + + fn signed_hmac(&self, key: &[u8], digest: MessageDigest) -> Result { + let header = try!(Component::to_base64(&self.header)); + let claims = try!(self.claims.to_base64()); + let data = format!("{}.{}", header, claims); + + let sig = crypt::sign(&*data, key, digest); + Ok(format!("{}.{}", data, sig)) + } + + fn signed_rsa(&self, key: &[u8], digest: MessageDigest) -> Result { + let header = try!(Component::to_base64(&self.header)); + let claims = try!(self.claims.to_base64()); + let data = format!("{}.{}", header, claims); + + let sig = crypt::sign_rsa(&*data, key, digest); + Ok(format!("{}.{}", data, sig)) + } +} + +impl PartialEq for Token + where H: Component + PartialEq, C: Component + PartialEq{ + fn eq(&self, other: &Token) -> bool { + self.header == other.header && + self.claims == other.claims + } +} + +#[cfg(test)] +mod tests { + use crypt::{ + sign, + sign_rsa, + verify, + verify_rsa + }; + use Claims; + use Token; + use header::Algorithm::{HS256,RS512}; + use header::DefaultHeader; + use std::io::{Error, Read}; + use std::fs::File; + use openssl::hash::MessageDigest; + + #[test] + pub fn sign_data() { + let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"; + let real_sig = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + let data = format!("{}.{}", header, claims); + + let sig = sign(&*data, "secret".as_bytes(), MessageDigest::sha256()); + + assert_eq!(sig, real_sig); + } + + #[test] + pub fn sign_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_key("./examples/privateKey.pem").unwrap(); + + let sig = sign_rsa(&*data, key.as_bytes(), MessageDigest::sha256()); + + assert_eq!(sig.trim(), real_sig); + } + + #[test] + pub fn verify_data() { + let header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + let claims = "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9"; + let target = "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + let data = format!("{}.{}", header, claims); + + assert!(verify(target, &*data, "secret".as_bytes(), MessageDigest::sha256())); + } + + #[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_key("./examples/publicKey.pub").unwrap(); + assert!(verify_rsa(&real_sig, &*data, key.as_bytes(), MessageDigest::sha256())); + } + + #[test] + pub fn raw_data() { + let raw = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + let token = Token::::parse(raw).unwrap(); + + { + assert_eq!(token.header.alg, HS256); + } + assert!(token.verify("secret".as_bytes())); + } + + #[test] + pub fn roundtrip() { + let token: Token = Default::default(); + let key = "secret".as_bytes(); + let raw = token.signed(key).unwrap(); + let same = Token::parse(&*raw).unwrap(); + + assert_eq!(token, same); + assert!(same.verify(key)); + } + + #[test] + pub fn roundtrip_rsa() { + let token: Token = Token { + header: DefaultHeader { + alg: RS512, + ..Default::default() + }, + ..Default::default() + }; + let private_key = load_key("./examples/privateKey.pem").unwrap(); + let raw = token.signed(private_key.as_bytes()).unwrap(); + let same = Token::parse(&*raw).unwrap(); + + assert_eq!(token, same); + let public_key = load_key("./examples/publicKey.pub").unwrap(); + assert!(same.verify(public_key.as_bytes())); + } + + fn load_key(keypath: &str) -> Result { + let mut key_file = try!(File::open(keypath)); + let mut key = String::new(); + try!(key_file.read_to_string(&mut key)); + Ok(key) + } +}