diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 12:41:41 +0000 |
commit | 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch) | |
tree | bdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/pasetors/src/version4.rs | |
parent | Releasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff) | |
download | rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip |
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/pasetors/src/version4.rs')
-rw-r--r-- | vendor/pasetors/src/version4.rs | 983 |
1 files changed, 983 insertions, 0 deletions
diff --git a/vendor/pasetors/src/version4.rs b/vendor/pasetors/src/version4.rs new file mode 100644 index 000000000..858b4b9bc --- /dev/null +++ b/vendor/pasetors/src/version4.rs @@ -0,0 +1,983 @@ +#![cfg_attr(docsrs, doc(cfg(feature = "v4")))] + +use core::convert::TryFrom; +use core::marker::PhantomData; + +use crate::common::{encode_b64, validate_footer_untrusted_token}; +use crate::errors::Error; +use crate::keys::{ + AsymmetricKeyPair, AsymmetricPublicKey, AsymmetricSecretKey, Generate, SymmetricKey, +}; +use crate::pae; +use crate::token::{Local, Public, TrustedToken, UntrustedToken}; +use crate::version::private::Version; +use alloc::string::String; +use alloc::vec::Vec; +use blake2b::SecretKey as AuthKey; +use ed25519_compact::{KeyPair, PublicKey, SecretKey, Seed, Signature}; +use orion::hazardous::mac::blake2b; +use orion::hazardous::mac::blake2b::Blake2b; +use orion::hazardous::stream::xchacha20; +use subtle::ConstantTimeEq; +use xchacha20::Nonce as EncNonce; +use xchacha20::SecretKey as EncKey; + +#[derive(Debug, PartialEq, Eq, Clone)] +/// Version 4 of the PASETO spec. +pub struct V4; + +impl Version for V4 { + const LOCAL_KEY: usize = 32; + const SECRET_KEY: usize = 32 + Self::PUBLIC_KEY; // Seed || PK + const PUBLIC_KEY: usize = 32; + const PUBLIC_SIG: usize = 64; + const LOCAL_NONCE: usize = 32; + const LOCAL_TAG: usize = 32; + const PUBLIC_HEADER: &'static str = "v4.public."; + const LOCAL_HEADER: &'static str = "v4.local."; + #[cfg(feature = "paserk")] + const PASERK_ID: usize = 44; + + fn validate_local_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::LOCAL_KEY { + return Err(Error::Key); + } + + Ok(()) + } + + fn validate_secret_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::SECRET_KEY { + return Err(Error::Key); + } + + let seed = Seed::from_slice(&key_bytes[..32]).map_err(|_| Error::Key)?; + let kp = KeyPair::from_seed(seed); + + if !bool::from(kp.pk.as_slice().ct_eq(&key_bytes[32..])) { + return Err(Error::Key); + } + + Ok(()) + } + + fn validate_public_key(key_bytes: &[u8]) -> Result<(), Error> { + if key_bytes.len() != Self::PUBLIC_KEY { + return Err(Error::Key); + } + + Ok(()) + } +} + +impl TryFrom<&AsymmetricSecretKey<V4>> for AsymmetricPublicKey<V4> { + type Error = Error; + + fn try_from(value: &AsymmetricSecretKey<V4>) -> Result<Self, Self::Error> { + AsymmetricPublicKey::<V4>::from(&value.as_bytes()[32..]) + } +} + +impl Generate<AsymmetricKeyPair<V4>, V4> for AsymmetricKeyPair<V4> { + fn generate() -> Result<AsymmetricKeyPair<V4>, Error> { + let key_pair = KeyPair::generate(); + + let secret = AsymmetricSecretKey::<V4>::from(key_pair.sk.as_ref()) + .map_err(|_| Error::KeyGeneration)?; + let public = AsymmetricPublicKey::<V4>::from(key_pair.pk.as_ref()) + .map_err(|_| Error::KeyGeneration)?; + + Ok(Self { public, secret }) + } +} + +impl Generate<SymmetricKey<V4>, V4> for SymmetricKey<V4> { + fn generate() -> Result<SymmetricKey<V4>, Error> { + let mut rng_bytes = vec![0u8; V4::LOCAL_KEY]; + V4::validate_local_key(&rng_bytes)?; + getrandom::getrandom(&mut rng_bytes)?; + + Ok(Self { + bytes: rng_bytes, + phantom: PhantomData, + }) + } +} + +/// PASETO v4 public tokens. +pub struct PublicToken; + +impl PublicToken { + /// The header and purpose for the public token: `v4.public.`. + pub const HEADER: &'static str = "v4.public."; + + /// Create a public token. + pub fn sign( + secret_key: &AsymmetricSecretKey<V4>, + message: &[u8], + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + if message.is_empty() { + return Err(Error::EmptyPayload); + } + + let sk = SecretKey::from_slice(secret_key.as_bytes()).map_err(|_| Error::Key)?; + + let f = footer.unwrap_or(&[]); + let i = implicit_assert.unwrap_or(&[]); + let m2 = pae::pae(&[Self::HEADER.as_bytes(), message, f, i])?; + let sig = sk.sign(m2, None); + + let mut m_sig: Vec<u8> = Vec::from(message); + m_sig.extend_from_slice(sig.as_ref()); + + let token_no_footer = format!("{}{}", Self::HEADER, encode_b64(m_sig)?); + + if f.is_empty() { + Ok(token_no_footer) + } else { + Ok(format!("{}.{}", token_no_footer, encode_b64(f)?)) + } + } + + /// Verify a public token. + /// + /// If `footer.is_none()`, then it will be validated but not compared to a known value. + /// If `footer.is_some()`, then it will be validated AND compared to the known value. + pub fn verify( + public_key: &AsymmetricPublicKey<V4>, + token: &UntrustedToken<Public, V4>, + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + validate_footer_untrusted_token(token, footer)?; + + let f = token.untrusted_footer(); + let i = implicit_assert.unwrap_or(&[]); + let sm = token.untrusted_message(); + let m = token.untrusted_payload(); + let s = sm[m.len()..m.len() + V4::PUBLIC_SIG].as_ref(); + + let m2 = pae::pae(&[Self::HEADER.as_bytes(), m, f, i])?; + let pk: PublicKey = PublicKey::from_slice(public_key.as_bytes()).map_err(|_| Error::Key)?; + + debug_assert!(s.len() == V4::PUBLIC_SIG); + // If the below fails, it is an invalid signature. + let sig = Signature::from_slice(s).map_err(|_| Error::TokenValidation)?; + + if pk.verify(m2, &sig).is_ok() { + TrustedToken::_new(Self::HEADER, m, f, i) + } else { + Err(Error::TokenValidation) + } + } +} + +/// PASETO v4 local tokens. +pub struct LocalToken; + +impl LocalToken { + /// The header and purpose for the local token: `v4.local.`. + pub const HEADER: &'static str = "v4.local."; + + /// Domain separator for key-splitting the encryption key (21 in length as bytes). + const DOMAIN_SEPARATOR_ENC: &'static str = "paseto-encryption-key"; + + /// Domain separator for key-splitting the authentication key (24 in length as bytes). + const DOMAIN_SEPARATOR_AUTH: &'static str = "paseto-auth-key-for-aead"; + + const M1_LEN: usize = V4::LOCAL_NONCE + Self::DOMAIN_SEPARATOR_ENC.as_bytes().len(); + const M2_LEN: usize = V4::LOCAL_NONCE + Self::DOMAIN_SEPARATOR_AUTH.as_bytes().len(); + + /// Split the user-provided secret key into keys used for encryption and authentication. + fn key_split(sk: &[u8], n: &[u8]) -> Result<(EncKey, EncNonce, AuthKey), Error> { + debug_assert_eq!(n.len(), V4::LOCAL_NONCE); + debug_assert_eq!(sk.len(), V4::LOCAL_KEY); + + let mut m1 = [0u8; Self::M1_LEN]; + m1[..21].copy_from_slice(Self::DOMAIN_SEPARATOR_ENC.as_bytes()); + m1[21..].copy_from_slice(n); + + let mut m2 = [0u8; Self::M2_LEN]; + m2[..24].copy_from_slice(Self::DOMAIN_SEPARATOR_AUTH.as_bytes()); + m2[24..].copy_from_slice(n); + + let sk = blake2b::SecretKey::from_slice(sk).unwrap(); + let mut b2_ctx = Blake2b::new(&sk, 56).unwrap(); + b2_ctx.update(&m1).unwrap(); + let tmp = b2_ctx.finalize().unwrap(); + let enc_key = EncKey::from_slice(&tmp.unprotected_as_bytes()[..32]).unwrap(); + let n2 = EncNonce::from_slice(&tmp.unprotected_as_bytes()[32..]).unwrap(); + + b2_ctx = Blake2b::new(&sk, V4::LOCAL_TAG).unwrap(); + b2_ctx.update(&m2).unwrap(); + let auth_key = + AuthKey::from_slice(b2_ctx.finalize().unwrap().unprotected_as_bytes()).unwrap(); + + Ok((enc_key, n2, auth_key)) + } + + /// Encrypt and authenticate a message using nonce directly. + pub(crate) fn encrypt_with_nonce( + secret_key: &SymmetricKey<V4>, + nonce: &[u8], + message: &[u8], + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + debug_assert_eq!(nonce.len(), V4::LOCAL_NONCE); + let f = footer.unwrap_or(&[]); + let i = implicit_assert.unwrap_or(&[]); + + let (enc_key, n2, auth_key) = Self::key_split(secret_key.as_bytes(), nonce)?; + + let mut ciphertext = vec![0u8; message.len()]; + xchacha20::encrypt(&enc_key, &n2, 0, message, &mut ciphertext) + .map_err(|_| Error::Encryption)?; + let pre_auth = pae::pae(&[Self::HEADER.as_bytes(), nonce, ciphertext.as_slice(), f, i])?; + + let mut b2_ctx = Blake2b::new(&auth_key, V4::LOCAL_TAG).unwrap(); + b2_ctx + .update(pre_auth.as_slice()) + .map_err(|_| Error::Encryption)?; + let tag = b2_ctx.finalize().map_err(|_| Error::Encryption)?; + + // nonce and tag lengths are both 32, so obviously safe to op::add + let concat_len: usize = match (nonce.len() + tag.len()).checked_add(ciphertext.len()) { + Some(len) => len, + None => return Err(Error::Encryption), + }; + let mut concat = vec![0u8; concat_len]; + concat[..32].copy_from_slice(nonce); + concat[32..32 + ciphertext.len()].copy_from_slice(ciphertext.as_slice()); + concat[concat_len - V4::LOCAL_TAG..].copy_from_slice(tag.unprotected_as_bytes()); + + let token_no_footer = format!("{}{}", Self::HEADER, encode_b64(concat)?); + + if f.is_empty() { + Ok(token_no_footer) + } else { + Ok(format!("{}.{}", token_no_footer, encode_b64(f)?)) + } + } + + /// Create a local token. + pub fn encrypt( + secret_key: &SymmetricKey<V4>, + message: &[u8], + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<String, Error> { + if message.is_empty() { + return Err(Error::EmptyPayload); + } + + let mut n = [0u8; V4::LOCAL_NONCE]; + getrandom::getrandom(&mut n)?; + + Self::encrypt_with_nonce(secret_key, &n, message, footer, implicit_assert) + } + + #[allow(clippy::many_single_char_names)] // The single-char names match those in the spec + /// Verify and decrypt a local token. + /// + /// If `footer.is_none()`, then it will be validated but not compared to a known value. + /// If `footer.is_some()`, then it will be validated AND compared to the known value. + pub fn decrypt( + secret_key: &SymmetricKey<V4>, + token: &UntrustedToken<Local, V4>, + footer: Option<&[u8]>, + implicit_assert: Option<&[u8]>, + ) -> Result<TrustedToken, Error> { + validate_footer_untrusted_token(token, footer)?; + + let f = token.untrusted_footer(); + let i = implicit_assert.unwrap_or(&[]); + let nc = token.untrusted_message(); + + let mut n: [u8; 32] = [0u8; V4::LOCAL_NONCE]; + n.copy_from_slice(nc[..V4::LOCAL_NONCE].as_ref()); + let c = token.untrusted_payload(); + let t = nc[nc.len() - V4::LOCAL_TAG..].as_ref(); + + let (enc_key, n2, auth_key) = Self::key_split(secret_key.as_bytes(), &n)?; + + let pre_auth = pae::pae(&[Self::HEADER.as_bytes(), n.as_ref(), c, f, i])?; + let expected_tag = blake2b::Tag::from_slice(t).map_err(|_| Error::TokenValidation)?; + blake2b::Blake2b::verify(&expected_tag, &auth_key, 32, pre_auth.as_slice()) + .map_err(|_| Error::TokenValidation)?; + + let mut out = vec![0u8; c.len()]; + xchacha20::decrypt(&enc_key, &n2, 0, c, &mut out).map_err(|_| Error::TokenValidation)?; + + TrustedToken::_new(Self::HEADER, &out, f, i) + } +} + +#[cfg(test)] +#[cfg(feature = "std")] +mod test_vectors { + + use hex; + + use super::*; + use core::convert::TryFrom; + use std::fs::File; + use std::io::BufReader; + + use crate::claims::Claims; + use crate::common::tests::*; + + fn test_local(test: &PasetoTest) { + debug_assert!(test.nonce.is_some()); + debug_assert!(test.key.is_some()); + + let sk = + SymmetricKey::<V4>::from(&hex::decode(test.key.as_ref().unwrap()).unwrap()).unwrap(); + + let nonce = hex::decode(test.nonce.as_ref().unwrap()).unwrap(); + let footer: Option<&[u8]> = if test.footer.as_bytes().is_empty() { + None + } else { + Some(test.footer.as_bytes()) + }; + let implicit_assert = test.implicit_assertion.as_bytes(); + + // payload is null when we expect failure + if test.expect_fail { + if let Ok(ut) = UntrustedToken::<Local, V4>::try_from(&test.token) { + assert!(LocalToken::decrypt(&sk, &ut, footer, Some(implicit_assert)).is_err()); + } + + return; + } + + let message = test.payload.as_ref().unwrap().as_str().unwrap(); + + let actual = LocalToken::encrypt_with_nonce( + &sk, + &nonce, + message.as_bytes(), + footer, + Some(implicit_assert), + ) + .unwrap(); + assert_eq!(actual, test.token, "Failed {:?}", test.name); + + let ut = UntrustedToken::<Local, V4>::try_from(&test.token).unwrap(); + let trusted = LocalToken::decrypt(&sk, &ut, footer, Some(implicit_assert)).unwrap(); + assert_eq!(trusted.payload(), message, "Failed {:?}", test.name); + assert_eq!(trusted.footer(), test.footer.as_bytes()); + assert_eq!(trusted.header(), LocalToken::HEADER); + assert_eq!(trusted.implicit_assert(), implicit_assert); + + let parsed_claims = Claims::from_bytes(trusted.payload().as_bytes()).unwrap(); + let test_vector_claims = serde_json::from_str::<Payload>(message).unwrap(); + + assert_eq!( + parsed_claims.get_claim("data").unwrap().as_str().unwrap(), + test_vector_claims.data, + ); + assert_eq!( + parsed_claims.get_claim("exp").unwrap().as_str().unwrap(), + test_vector_claims.exp, + ); + } + + fn test_public(test: &PasetoTest) { + debug_assert!(test.public_key.is_some()); + debug_assert!(test.secret_key.is_some()); + + let sk = AsymmetricSecretKey::<V4>::from( + &hex::decode(test.secret_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + let pk = AsymmetricPublicKey::<V4>::from( + &hex::decode(test.public_key.as_ref().unwrap()).unwrap(), + ) + .unwrap(); + let footer: Option<&[u8]> = if test.footer.as_bytes().is_empty() { + None + } else { + Some(test.footer.as_bytes()) + }; + let implicit_assert = test.implicit_assertion.as_bytes(); + + // payload is null when we expect failure + if test.expect_fail { + if let Ok(ut) = UntrustedToken::<Public, V4>::try_from(&test.token) { + assert!(PublicToken::verify(&pk, &ut, footer, Some(implicit_assert)).is_err()); + } + + return; + } + + let message = test.payload.as_ref().unwrap().as_str().unwrap(); + + let actual = + PublicToken::sign(&sk, message.as_bytes(), footer, Some(implicit_assert)).unwrap(); + assert_eq!(actual, test.token, "Failed {:?}", test.name); + let ut = UntrustedToken::<Public, V4>::try_from(&test.token).unwrap(); + + let trusted = PublicToken::verify(&pk, &ut, footer, Some(implicit_assert)).unwrap(); + assert_eq!(trusted.payload(), message); + assert_eq!(trusted.footer(), test.footer.as_bytes()); + assert_eq!(trusted.header(), PublicToken::HEADER); + assert_eq!(trusted.implicit_assert(), implicit_assert); + } + + #[test] + fn run_test_vectors() { + let path = "./test_vectors/v4.json"; + let file = File::open(path).unwrap(); + let reader = BufReader::new(file); + let tests: TestFile = serde_json::from_reader(reader).unwrap(); + + for t in tests.tests { + // v4.public + if t.public_key.is_some() { + test_public(&t); + } + // v4.local + if t.nonce.is_some() { + test_local(&t); + } + } + } +} + +#[cfg(test)] +mod test_tokens { + use super::*; + use crate::common::decode_b64; + use crate::keys::{AsymmetricKeyPair, Generate, SymmetricKey}; + use crate::token::UntrustedToken; + use core::convert::TryFrom; + + const TEST_LOCAL_SK_BYTES: [u8; 32] = [ + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, + 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, + ]; + + pub(crate) const TEST_SK_BYTES: [u8; 64] = [ + 180, 203, 251, 67, 223, 76, 226, 16, 114, 125, 149, 62, 74, 113, 51, 7, 250, 25, 187, 125, + 159, 133, 4, 20, 56, 217, 225, 27, 148, 42, 55, 116, 30, 185, 219, 187, 188, 4, 124, 3, + 253, 112, 96, 78, 0, 113, 240, 152, 126, 22, 178, 139, 117, 114, 37, 193, 31, 0, 65, 93, + 14, 32, 177, 162, + ]; + + const TEST_PK_BYTES: [u8; 32] = [ + 30, 185, 219, 187, 188, 4, 124, 3, 253, 112, 96, 78, 0, 113, 240, 152, 126, 22, 178, 139, + 117, 114, 37, 193, 31, 0, 65, 93, 14, 32, 177, 162, + ]; + + const MESSAGE: &str = + "{\"data\":\"this is a signed message\",\"exp\":\"2022-01-01T00:00:00+00:00\"}"; + const FOOTER: &str = "{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"; + const VALID_PUBLIC_TOKEN: &str = "v4.public.eyJkYXRhIjoidGhpcyBpcyBhIHNpZ25lZCBtZXNzYWdlIiwiZXhwIjoiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9v3Jt8mx_TdM2ceTGoqwrh4yDFn0XsHvvV_D0DtwQxVrJEBMl0F2caAdgnpKlt4p7xBnx1HcO-SPo8FPp214HDw.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + const VALID_LOCAL_TOKEN: &str = "v4.local.32VIErrEkmY4JVILovbmfPXKW9wT1OdQepjMTC_MOtjA4kiqw7_tcaOM5GNEcnTxl60WkwMsYXw6FSNb_UdJPXjpzm0KW9ojM5f4O2mRvE2IcweP-PRdoHjd5-RHCiExR1IK6t4x-RMNXtQNbz7FvFZ_G-lFpk5RG3EOrwDL6CgDqcerSQ.eyJraWQiOiJ6VmhNaVBCUDlmUmYyc25FY1Q3Z0ZUaW9lQTlDT2NOeTlEZmdMMVc2MGhhTiJ9"; + + #[test] + fn test_gen_keypair() { + let kp = AsymmetricKeyPair::<V4>::generate().unwrap(); + + let token = PublicToken::sign(&kp.secret, MESSAGE.as_bytes(), None, None).unwrap(); + + let ut = UntrustedToken::<Public, V4>::try_from(&token).unwrap(); + assert!(PublicToken::verify(&kp.public, &ut, None, None).is_ok()); + } + + #[test] + fn test_untrusted_token_usage() { + // Local + let sk = SymmetricKey::<V4>::generate().unwrap(); + let token = + LocalToken::encrypt(&sk, MESSAGE.as_bytes(), Some(FOOTER.as_bytes()), None).unwrap(); + + let untrusted_token = UntrustedToken::<Local, V4>::try_from(token.as_str()).unwrap(); + let _ = LocalToken::decrypt( + &sk, + &untrusted_token, + Some(untrusted_token.untrusted_footer()), + None, + ) + .unwrap(); + + // Public + let kp = AsymmetricKeyPair::<V4>::generate().unwrap(); + let token = PublicToken::sign( + &kp.secret, + MESSAGE.as_bytes(), + Some(FOOTER.as_bytes()), + None, + ) + .unwrap(); + + let untrusted_token = UntrustedToken::<Public, V4>::try_from(token.as_str()).unwrap(); + assert!( + PublicToken::verify(&kp.public, &untrusted_token, Some(FOOTER.as_bytes()), None) + .is_ok() + ); + } + + #[test] + fn test_roundtrip_local() { + let sk = SymmetricKey::<V4>::generate().unwrap(); + let message = "token payload"; + + let token = LocalToken::encrypt(&sk, message.as_bytes(), None, None).unwrap(); + let ut = UntrustedToken::<Local, V4>::try_from(&token).unwrap(); + let trusted_token = LocalToken::decrypt(&sk, &ut, None, None).unwrap(); + + assert_eq!(trusted_token.payload(), message); + } + + #[test] + fn test_roundtrip_public() { + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + + let token = PublicToken::sign(&test_sk, MESSAGE.as_bytes(), None, None).unwrap(); + let ut = UntrustedToken::<Public, V4>::try_from(&token).unwrap(); + + assert!(PublicToken::verify(&test_pk, &ut, None, None).is_ok()); + } + + #[test] + fn footer_logic() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let message = + b"{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + + // We create a token with Some(footer) and with None + let actual_some = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, Some(FOOTER.as_bytes()), None).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, None, None).unwrap(), + ) + .unwrap(); + + // token = Some(footer) = validate and compare + // token = None(footer) = validate only + + // We should be able to validate with None if created with Some() (excludes constant-time + // comparison with known value) + assert!(PublicToken::verify(&test_pk, &actual_some, None, None).is_ok()); + // We should be able to validate with Some() if created with Some() + assert!(PublicToken::verify(&test_pk, &actual_some, Some(FOOTER.as_bytes()), None).is_ok()); + // We should NOT be able to validate with Some() if created with None + assert!( + PublicToken::verify(&test_pk, &actual_none, Some(FOOTER.as_bytes()), None).is_err() + ); + + let actual_some = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, Some(FOOTER.as_bytes()), None).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, None, None).unwrap(), + ) + .unwrap(); + + // They don't equal because the nonce is random. So we only check decryption. + assert!(LocalToken::decrypt(&test_local_sk, &actual_some, None, None).is_ok()); + assert!( + LocalToken::decrypt(&test_local_sk, &actual_some, Some(FOOTER.as_bytes()), None) + .is_ok() + ); + assert!( + LocalToken::decrypt(&test_local_sk, &actual_none, Some(FOOTER.as_bytes()), None) + .is_err() + ); + } + + #[test] + fn implicit_none_some_empty_is_same() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let message = + b"{\"data\":\"this is a signed message\",\"exp\":\"2019-01-01T00:00:00+00:00\"}"; + let implicit = b""; + + let actual_some = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, None, Some(implicit)).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Public, V4>::try_from( + &PublicToken::sign(&test_sk, message, None, None).unwrap(), + ) + .unwrap(); + assert_eq!(actual_some, actual_none); + + assert!(PublicToken::verify(&test_pk, &actual_none, None, Some(implicit)).is_ok()); + assert!(PublicToken::verify(&test_pk, &actual_some, None, None).is_ok()); + + let actual_some = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, None, Some(implicit)).unwrap(), + ) + .unwrap(); + let actual_none = UntrustedToken::<Local, V4>::try_from( + &LocalToken::encrypt(&test_local_sk, message, None, None).unwrap(), + ) + .unwrap(); + // They don't equal because the nonce is random. So we only check decryption. + + assert!(LocalToken::decrypt(&test_local_sk, &actual_none, None, Some(implicit)).is_ok()); + assert!(LocalToken::decrypt(&test_local_sk, &actual_some, None, None).is_ok()); + } + + #[test] + // NOTE: See https://github.com/paseto-standard/paseto-spec/issues/17 + fn empty_payload() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + let test_sk = AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::sign(&test_sk, b"", None, None).unwrap_err(), + Error::EmptyPayload + ); + assert_eq!( + LocalToken::encrypt(&test_local_sk, b"", None, None).unwrap_err(), + Error::EmptyPayload + ); + } + + #[test] + fn err_on_modified_footer() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.replace("kid", "mid").as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.replace("kid", "mid").as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_wrong_implicit_assert() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + assert!(PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .is_ok()); + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + Some(b"WRONG IMPLICIT") + ) + .unwrap_err(), + Error::TokenValidation + ); + assert!(LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .is_ok()); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + Some(b"WRONG IMPLICIT") + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_footer_in_token_none_supplied() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(b""), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(b""), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_no_footer_in_token_some_supplied() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let invalid_public: String = format!( + "{}.{}.{}", + split_public[0], split_public[1], split_public[2] + ); + + let split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let invalid_local: String = + format!("{}.{}.{}", split_local[0], split_local[1], split_local[2]); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_signature() { + let test_pk = AsymmetricPublicKey::<V4>::from(&TEST_PK_BYTES).unwrap(); + + let mut split_public = VALID_PUBLIC_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_sig = decode_b64(split_public[2]).unwrap(); + bad_sig.copy_within(0..32, 32); + let tmp = encode_b64(bad_sig).unwrap(); + split_public[2] = &tmp; + let invalid_public: String = format!( + "{}.{}.{}.{}", + split_public[0], split_public[1], split_public[2], split_public[3] + ); + + assert_eq!( + PublicToken::verify( + &test_pk, + &UntrustedToken::<Public, V4>::try_from(&invalid_public).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_tag() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_tag = decode_b64(split_local[2]).unwrap(); + let tlen = bad_tag.len(); + bad_tag.copy_within(0..16, tlen - 16); + let tmp = encode_b64(bad_tag).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_ciphertext() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_ct = decode_b64(split_local[2]).unwrap(); + let ctlen = bad_ct.len(); + bad_ct.copy_within((ctlen - 16)..ctlen, 24); + let tmp = encode_b64(bad_ct).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_modified_nonce() { + let test_local_sk = SymmetricKey::<V4>::from(&TEST_LOCAL_SK_BYTES).unwrap(); + + let mut split_local = VALID_LOCAL_TOKEN.split('.').collect::<Vec<&str>>(); + let mut bad_nonce = decode_b64(split_local[2]).unwrap(); + let nlen = bad_nonce.len(); + bad_nonce.copy_within((nlen - 24)..nlen, 0); + let tmp = encode_b64(bad_nonce).unwrap(); + split_local[2] = &tmp; + let invalid_local: String = format!( + "{}.{}.{}.{}", + split_local[0], split_local[1], split_local[2], split_local[3] + ); + + assert_eq!( + LocalToken::decrypt( + &test_local_sk, + &UntrustedToken::<Local, V4>::try_from(&invalid_local).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_invalid_public_secret_key() { + let bad_pk = AsymmetricPublicKey::<V4>::from(&[0u8; 32]).unwrap(); + + assert_eq!( + PublicToken::verify( + &bad_pk, + &UntrustedToken::<Public, V4>::try_from(VALID_PUBLIC_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } + + #[test] + fn err_on_invalid_shared_secret_key() { + let bad_local_sk = SymmetricKey::<V4>::from(&[0u8; 32]).unwrap(); + + assert_eq!( + LocalToken::decrypt( + &bad_local_sk, + &UntrustedToken::<Local, V4>::try_from(VALID_LOCAL_TOKEN).unwrap(), + Some(FOOTER.as_bytes()), + None + ) + .unwrap_err(), + Error::TokenValidation + ); + } +} + +#[cfg(test)] +mod test_keys { + use super::*; + use crate::version4::test_tokens::TEST_SK_BYTES; + + #[test] + fn test_symmetric_gen() { + let randomv = SymmetricKey::<V4>::generate().unwrap(); + assert_ne!(randomv.as_bytes(), &[0u8; 32]); + } + + #[test] + fn test_invalid_sizes() { + assert!(AsymmetricSecretKey::<V4>::from(&[1u8; 63]).is_err()); + assert!(AsymmetricSecretKey::<V4>::from(&TEST_SK_BYTES).is_ok()); + assert!(AsymmetricSecretKey::<V4>::from(&[1u8; 65]).is_err()); + + assert!(AsymmetricPublicKey::<V4>::from(&[1u8; 31]).is_err()); + assert!(AsymmetricPublicKey::<V4>::from(&[1u8; 32]).is_ok()); + assert!(AsymmetricPublicKey::<V4>::from(&[1u8; 33]).is_err()); + + assert!(SymmetricKey::<V4>::from(&[0u8; 31]).is_err()); + assert!(SymmetricKey::<V4>::from(&[0u8; 32]).is_ok()); + assert!(SymmetricKey::<V4>::from(&[0u8; 33]).is_err()); + } + + #[test] + fn try_from_secret_to_public() { + let kpv4 = AsymmetricKeyPair::<V4>::generate().unwrap(); + let pubv4 = AsymmetricPublicKey::<V4>::try_from(&kpv4.secret).unwrap(); + assert_eq!(pubv4.as_bytes(), kpv4.public.as_bytes()); + assert_eq!(pubv4, kpv4.public); + assert_eq!(&kpv4.secret.as_bytes()[32..], pubv4.as_bytes()); + } + + #[test] + fn test_trait_impls() { + let debug = format!("{:?}", SymmetricKey::<V4>::generate().unwrap()); + assert_eq!(debug, "SymmetricKey {***OMITTED***}"); + + let randomv = SymmetricKey::<V4>::generate().unwrap(); + let zero = SymmetricKey::<V4>::from(&[0u8; V4::LOCAL_KEY]).unwrap(); + assert_ne!(randomv, zero); + + let debug = format!("{:?}", AsymmetricKeyPair::<V4>::generate().unwrap().secret); + assert_eq!(debug, "AsymmetricSecretKey {***OMITTED***}"); + + let random1 = AsymmetricKeyPair::<V4>::generate().unwrap(); + let random2 = AsymmetricKeyPair::<V4>::generate().unwrap(); + assert_ne!(random1.secret, random2.secret); + } + + #[test] + fn test_clone() { + let sk = SymmetricKey::<V4>::generate().unwrap(); + assert_eq!(sk, sk.clone()); + + let kp = AsymmetricKeyPair::<V4>::generate().unwrap(); + assert_eq!(kp.secret, kp.secret.clone()); + assert_eq!(kp.public, kp.public.clone()); + } +} |