diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/jwcrypto/src | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/jwcrypto/src')
-rw-r--r-- | third_party/rust/jwcrypto/src/ec.rs | 198 | ||||
-rw-r--r-- | third_party/rust/jwcrypto/src/error.rs | 25 | ||||
-rw-r--r-- | third_party/rust/jwcrypto/src/lib.rs | 243 |
3 files changed, 466 insertions, 0 deletions
diff --git a/third_party/rust/jwcrypto/src/ec.rs b/third_party/rust/jwcrypto/src/ec.rs new file mode 100644 index 0000000000..acea12cbbc --- /dev/null +++ b/third_party/rust/jwcrypto/src/ec.rs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::{ + error::{JwCryptoError, Result}, + Algorithm, CompactJwe, DecryptionParameters, EncryptionAlgorithm, EncryptionParameters, + JweHeader, Jwk, JwkKeyParameters, +}; +use rc_crypto::{ + aead, + agreement::{self, EphemeralKeyPair, InputKeyMaterial, UnparsedPublicKey}, + digest, rand, +}; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct ECKeysParameters { + pub crv: String, + pub x: String, + pub y: String, +} + +pub(crate) fn encrypt_to_jwe( + data: &[u8], + encryption_params: EncryptionParameters, +) -> Result<CompactJwe> { + let EncryptionParameters::ECDH_ES { enc, peer_jwk } = encryption_params; + let local_key_pair = EphemeralKeyPair::generate(&agreement::ECDH_P256)?; + let local_public_key = extract_pub_key_jwk(&local_key_pair)?; + let JwkKeyParameters::EC(ref ec_key_params) = peer_jwk.key_parameters; + let protected_header = JweHeader { + kid: peer_jwk.kid.clone(), + alg: Algorithm::ECDH_ES, + enc, + epk: Some(local_public_key), + apu: None, + apv: None, + }; + + let secret = derive_shared_secret(&protected_header, local_key_pair, &ec_key_params)?; + + let encryption_algorithm = match protected_header.enc { + EncryptionAlgorithm::A256GCM => &aead::AES_256_GCM, + }; + let sealing_key = aead::SealingKey::new(encryption_algorithm, &secret.as_ref())?; + let additional_data = serde_json::to_string(&protected_header)?; + let additional_data = + base64::encode_config(additional_data.as_bytes(), base64::URL_SAFE_NO_PAD); + let additional_data = additional_data.as_bytes(); + let aad = aead::Aad::from(additional_data); + let mut iv: Vec<u8> = vec![0; 12]; + rand::fill(&mut iv)?; + let nonce = aead::Nonce::try_assume_unique_for_key(encryption_algorithm, &iv)?; + let mut encrypted = aead::seal(&sealing_key, nonce, aad, data)?; + + let tag_idx = encrypted.len() - encryption_algorithm.tag_len(); + let auth_tag = encrypted.split_off(tag_idx); + let ciphertext = encrypted; + + Ok(CompactJwe::new( + Some(protected_header), + None, + Some(iv), + ciphertext, + Some(auth_tag), + )?) +} + +pub(crate) fn decrypt_jwe( + jwe: &CompactJwe, + decryption_params: DecryptionParameters, +) -> Result<String> { + let DecryptionParameters::ECDH_ES { local_key_pair } = decryption_params; + + let protected_header = jwe + .protected_header()? + .ok_or_else(|| JwCryptoError::IllegalState("protected_header must be present."))?; + if protected_header.alg != Algorithm::ECDH_ES { + return Err(JwCryptoError::IllegalState("alg mismatch.")); + } + + // Part 1: Reconstruct the secret. + let peer_jwk = protected_header + .epk + .as_ref() + .ok_or_else(|| JwCryptoError::IllegalState("epk not present"))?; + let JwkKeyParameters::EC(ref ec_key_params) = peer_jwk.key_parameters; + let secret = derive_shared_secret(&protected_header, local_key_pair, &ec_key_params)?; + + // Part 2: decrypt the payload + if jwe.encrypted_key()?.is_some() { + return Err(JwCryptoError::IllegalState( + "The Encrypted Key must be empty.", + )); + } + let encryption_algorithm = match protected_header.enc { + EncryptionAlgorithm::A256GCM => &aead::AES_256_GCM, + }; + let auth_tag = jwe + .auth_tag()? + .ok_or_else(|| JwCryptoError::IllegalState("auth_tag must be present."))?; + if auth_tag.len() != encryption_algorithm.tag_len() { + return Err(JwCryptoError::IllegalState( + "The auth tag must be 16 bytes long.", + )); + } + let iv = jwe + .iv()? + .ok_or_else(|| JwCryptoError::IllegalState("iv must be present."))?; + let opening_key = aead::OpeningKey::new(&encryption_algorithm, &secret.as_ref())?; + let ciphertext_and_tag: Vec<u8> = [jwe.ciphertext()?, auth_tag].concat(); + let nonce = aead::Nonce::try_assume_unique_for_key(&encryption_algorithm, &iv)?; + let aad = aead::Aad::from(jwe.protected_header_raw().as_bytes()); + let plaintext = aead::open(&opening_key, nonce, aad, &ciphertext_and_tag)?; + Ok(String::from_utf8(plaintext.to_vec())?) +} + +fn derive_shared_secret( + protected_header: &JweHeader, + local_key_pair: EphemeralKeyPair, + peer_key: &ECKeysParameters, +) -> Result<digest::Digest> { + let (private_key, _) = local_key_pair.split(); + let peer_public_key_raw_bytes = public_key_from_ec_params(peer_key)?; + let peer_public_key = UnparsedPublicKey::new(&agreement::ECDH_P256, &peer_public_key_raw_bytes); + // Note: We don't support key-wrapping, but if we did `algorithm_id` would be `alg` instead. + let algorithm_id = protected_header.enc.algorithm_id(); + let ikm = private_key.agree(&peer_public_key)?; + let apu = protected_header.apu.as_deref().unwrap_or_default(); + let apv = protected_header.apv.as_deref().unwrap_or_default(); + get_secret_from_ikm(ikm, &apu, &apv, &algorithm_id) +} + +fn public_key_from_ec_params(jwk: &ECKeysParameters) -> Result<Vec<u8>> { + let x = base64::decode_config(&jwk.x, base64::URL_SAFE_NO_PAD)?; + let y = base64::decode_config(&jwk.y, base64::URL_SAFE_NO_PAD)?; + if jwk.crv != "P-256" { + return Err(JwCryptoError::PartialImplementation( + "Only P-256 curves are supported.", + )); + } + if x.len() != (256 / 8) { + return Err(JwCryptoError::IllegalState("X must be 32 bytes long.")); + } + if y.len() != (256 / 8) { + return Err(JwCryptoError::IllegalState("Y must be 32 bytes long.")); + } + let mut peer_pub_key: Vec<u8> = vec![0x04]; + peer_pub_key.extend_from_slice(&x); + peer_pub_key.extend_from_slice(&y); + Ok(peer_pub_key) +} + +fn get_secret_from_ikm( + ikm: InputKeyMaterial, + apu: &str, + apv: &str, + alg: &str, +) -> Result<digest::Digest> { + let secret = ikm.derive(|z| { + let mut buf: Vec<u8> = vec![]; + // ConcatKDF (1 iteration since keyLen <= hashLen). + // See rfc7518 section 4.6 for reference. + buf.extend_from_slice(&1u32.to_be_bytes()); + buf.extend_from_slice(&z); + // otherinfo + buf.extend_from_slice(&(alg.len() as u32).to_be_bytes()); + buf.extend_from_slice(alg.as_bytes()); + buf.extend_from_slice(&(apu.len() as u32).to_be_bytes()); + buf.extend_from_slice(apu.as_bytes()); + buf.extend_from_slice(&(apv.len() as u32).to_be_bytes()); + buf.extend_from_slice(apv.as_bytes()); + buf.extend_from_slice(&256u32.to_be_bytes()); + digest::digest(&digest::SHA256, &buf) + })?; + Ok(secret) +} + +pub fn extract_pub_key_jwk(key_pair: &EphemeralKeyPair) -> Result<Jwk> { + let pub_key_bytes = key_pair.public_key().to_bytes()?; + // Uncompressed form (see SECG SEC1 section 2.3.3). + // First byte is 4, then 32 bytes for x, and 32 bytes for y. + assert_eq!(pub_key_bytes.len(), 1 + 32 + 32); + assert_eq!(pub_key_bytes[0], 0x04); + let x = Vec::from(&pub_key_bytes[1..33]); + let x = base64::encode_config(&x, base64::URL_SAFE_NO_PAD); + let y = Vec::from(&pub_key_bytes[33..]); + let y = base64::encode_config(&y, base64::URL_SAFE_NO_PAD); + Ok(Jwk { + kid: None, + key_parameters: JwkKeyParameters::EC(ECKeysParameters { + crv: "P-256".to_owned(), + x, + y, + }), + }) +} diff --git a/third_party/rust/jwcrypto/src/error.rs b/third_party/rust/jwcrypto/src/error.rs new file mode 100644 index 0000000000..cc12067996 --- /dev/null +++ b/third_party/rust/jwcrypto/src/error.rs @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use thiserror::Error; + +pub(crate) type Result<T> = std::result::Result<T, JwCryptoError>; + +#[derive(Error, Debug)] +pub enum JwCryptoError { + #[error("Deserialization error")] + DeserializationError, + #[error("Illegal state error: {0}")] + IllegalState(&'static str), + #[error("Partial implementation error: {0}")] + PartialImplementation(&'static str), + #[error("Base64 decode error: {0}")] + Base64Decode(#[from] base64::DecodeError), + #[error("Crypto error: {0}")] + CryptoError(#[from] rc_crypto::Error), + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), + #[error("UTF8 decode error: {0}")] + UTF8DecodeError(#[from] std::string::FromUtf8Error), +} diff --git a/third_party/rust/jwcrypto/src/lib.rs b/third_party/rust/jwcrypto/src/lib.rs new file mode 100644 index 0000000000..aa3bb086d5 --- /dev/null +++ b/third_party/rust/jwcrypto/src/lib.rs @@ -0,0 +1,243 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Theorically, everything done in this crate could and should be done in a JWT library. +//! However, none of the existing rust JWT libraries can handle ECDH-ES encryption, and API choices +//! made by their authors make it difficult to add this feature. +//! In the past, we chose cjose to do that job, but it added three C dependencies to build and link +//! against: jansson, openssl and cjose itself. + +pub use error::JwCryptoError; +use error::Result; +use rc_crypto::agreement::EphemeralKeyPair; +use serde_derive::{Deserialize, Serialize}; +use std::str::FromStr; + +pub mod ec; +mod error; + +pub enum EncryptionParameters<'a> { + // ECDH-ES in Direct Key Agreement mode. + #[allow(non_camel_case_types)] + ECDH_ES { + enc: EncryptionAlgorithm, + peer_jwk: &'a Jwk, + }, +} + +pub enum DecryptionParameters { + // ECDH-ES in Direct Key Agreement mode. + #[allow(non_camel_case_types)] + ECDH_ES { local_key_pair: EphemeralKeyPair }, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +enum Algorithm { + #[serde(rename = "ECDH-ES")] + #[allow(non_camel_case_types)] + ECDH_ES, +} +#[derive(Serialize, Deserialize, Debug)] +pub enum EncryptionAlgorithm { + A256GCM, +} + +impl EncryptionAlgorithm { + fn algorithm_id(&self) -> &'static str { + match self { + Self::A256GCM => "A256GCM", + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +struct JweHeader { + alg: Algorithm, + enc: EncryptionAlgorithm, + #[serde(skip_serializing_if = "Option::is_none")] + kid: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + epk: Option<Jwk>, + #[serde(skip_serializing_if = "Option::is_none")] + apu: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + apv: Option<String>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Jwk { + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option<String>, + #[serde(flatten)] + pub key_parameters: JwkKeyParameters, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "kty")] +pub enum JwkKeyParameters { + EC(ec::ECKeysParameters), +} + +#[derive(Debug)] +pub struct CompactJwe { + jwe_segments: Vec<String>, +} + +impl CompactJwe { + // A builder pattern would be nicer, but this will do for now. + fn new( + protected_header: Option<JweHeader>, + encrypted_key: Option<Vec<u8>>, + iv: Option<Vec<u8>>, + ciphertext: Vec<u8>, + auth_tag: Option<Vec<u8>>, + ) -> Result<Self> { + let protected_header = protected_header + .as_ref() + .map(|h| serde_json::to_string(&h)) + .transpose()? + .map(|h| base64::encode_config(&h, base64::URL_SAFE_NO_PAD)) + .unwrap_or_default(); + let encrypted_key = encrypted_key + .as_ref() + .map(|k| base64::encode_config(&k, base64::URL_SAFE_NO_PAD)) + .unwrap_or_default(); + let iv = iv + .as_ref() + .map(|iv| base64::encode_config(&iv, base64::URL_SAFE_NO_PAD)) + .unwrap_or_default(); + let ciphertext = base64::encode_config(&ciphertext, base64::URL_SAFE_NO_PAD); + let auth_tag = auth_tag + .as_ref() + .map(|t| base64::encode_config(&t, base64::URL_SAFE_NO_PAD)) + .unwrap_or_default(); + let jwe_segments = vec![protected_header, encrypted_key, iv, ciphertext, auth_tag]; + Ok(Self { jwe_segments }) + } + + fn protected_header(&self) -> Result<Option<JweHeader>> { + Ok(self + .try_deserialize_base64_segment(0)? + .map(|s| serde_json::from_slice(&s)) + .transpose()?) + } + + fn protected_header_raw(&self) -> &str { + &self.jwe_segments[0] + } + + fn encrypted_key(&self) -> Result<Option<Vec<u8>>> { + self.try_deserialize_base64_segment(1) + } + + fn iv(&self) -> Result<Option<Vec<u8>>> { + self.try_deserialize_base64_segment(2) + } + + fn ciphertext(&self) -> Result<Vec<u8>> { + Ok(self + .try_deserialize_base64_segment(3)? + .ok_or_else(|| JwCryptoError::IllegalState("Ciphertext is empty"))?) + } + + fn auth_tag(&self) -> Result<Option<Vec<u8>>> { + self.try_deserialize_base64_segment(4) + } + + fn try_deserialize_base64_segment(&self, index: usize) -> Result<Option<Vec<u8>>> { + Ok(match self.jwe_segments[index].is_empty() { + true => None, + false => Some(base64::decode_config( + &self.jwe_segments[index], + base64::URL_SAFE_NO_PAD, + )?), + }) + } +} + +impl FromStr for CompactJwe { + type Err = JwCryptoError; + fn from_str(str: &str) -> Result<Self> { + let jwe_segments: Vec<String> = str.split('.').map(|s| s.to_owned()).collect(); + if jwe_segments.len() != 5 { + return Err(JwCryptoError::DeserializationError); + } + Ok(Self { jwe_segments }) + } +} + +impl ToString for CompactJwe { + fn to_string(&self) -> String { + assert!(self.jwe_segments.len() == 5); + self.jwe_segments.join(".") + } +} + +/// Encrypt and serialize data in the JWE compact form. +pub fn encrypt_to_jwe(data: &[u8], encryption_params: EncryptionParameters) -> Result<String> { + let jwe = match encryption_params { + EncryptionParameters::ECDH_ES { .. } => ec::encrypt_to_jwe(data, encryption_params)?, + }; + Ok(jwe.to_string()) +} + +/// Deserialize and decrypt data in the JWE compact form. +pub fn decrypt_jwe(jwe: &str, decryption_params: DecryptionParameters) -> Result<String> { + let jwe = jwe.parse()?; + Ok(match decryption_params { + DecryptionParameters::ECDH_ES { .. } => ec::decrypt_jwe(&jwe, decryption_params)?, + }) +} + +#[test] +fn test_encrypt_decrypt_jwe_ecdh_es() { + use rc_crypto::agreement; + let key_pair = EphemeralKeyPair::generate(&agreement::ECDH_P256).unwrap(); + let jwk = ec::extract_pub_key_jwk(&key_pair).unwrap(); + let data = b"The big brown fox jumped over... What?"; + let encrypted = encrypt_to_jwe( + data, + EncryptionParameters::ECDH_ES { + enc: EncryptionAlgorithm::A256GCM, + peer_jwk: &jwk, + }, + ) + .unwrap(); + let decrypted = decrypt_jwe( + &encrypted, + DecryptionParameters::ECDH_ES { + local_key_pair: key_pair, + }, + ) + .unwrap(); + assert_eq!(decrypted, std::str::from_utf8(data).unwrap()); +} + +#[test] +fn test_compact_jwe_roundtrip() { + let mut iv = [0u8; 16]; + rc_crypto::rand::fill(&mut iv).unwrap(); + let mut ciphertext = [0u8; 243]; + rc_crypto::rand::fill(&mut ciphertext).unwrap(); + let mut auth_tag = [0u8; 16]; + rc_crypto::rand::fill(&mut auth_tag).unwrap(); + let jwe = CompactJwe::new( + Some(JweHeader { + alg: Algorithm::ECDH_ES, + enc: EncryptionAlgorithm::A256GCM, + kid: None, + epk: None, + apu: None, + apv: None, + }), + None, + Some(iv.to_vec()), + ciphertext.to_vec(), + Some(auth_tag.to_vec()), + ) + .unwrap(); + let compacted = jwe.to_string(); + let jwe2: CompactJwe = compacted.parse().unwrap(); + assert_eq!(jwe.jwe_segments, jwe2.jwe_segments); +} |