Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@ once_cell = "1.21"
openssl = { version = "0.10", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
sha2 = { version = "0.10", optional = true }
# RustCrypto backend dependencies
p256 = { version = "0.13", optional = true, features = ["ecdh", "std"] }
aes-gcm = { version = "0.10", optional = true }
rand_core = { version = "0.6", optional = true, features = ["std"] }

[features]
default = ["backend-openssl", "serializable-keys"]
serializable-keys = ["serde"]
backend-openssl = ["openssl", "lazy_static", "hkdf", "sha2"]
backend-rustcrypto = ["p256", "aes-gcm", "hkdf", "sha2", "rand_core"]
backend-test-helper = []

[package.metadata.release]
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,32 @@ These restrictions might be lifted in future, if it turns out that we need them.

## Cryptographic backends

This crate is designed to use pluggable backend implementations of low-level crypto primitives. different crypto
backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported.
This crate is designed to use pluggable backend implementations of low-level crypto primitives.

Two backends are currently supported:

* **OpenSSL** (default): Uses the [openssl](https://github.com/sfackler/rust-openssl) crate. This is the default backend and provides excellent performance, but requires OpenSSL to be installed on the system.
* **RustCrypto**: Uses pure-Rust implementations from the [RustCrypto](https://github.com/RustCrypto) project. This backend has no C dependencies and works well with MUSL and static linking scenarios (e.g., Docker Alpine images).

### Using the RustCrypto backend

To use the RustCrypto backend instead of OpenSSL:

```toml
[dependencies]
ece = { version = "2.4", default-features = false, features = ["backend-rustcrypto", "serializable-keys"] }
```

### Using both backends

You can enable both backends simultaneously if needed:

```toml
[dependencies]
ece = { version = "2.4", features = ["backend-rustcrypto"] }
```

When both backends are enabled, OpenSSL takes precedence by default. The backends are fully interoperable - keys and ciphertext generated with one backend can be used with the other.

## Release process

Expand Down
10 changes: 8 additions & 2 deletions src/crypto/holder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct SetCryptographerError(());
///
/// This is a convenience wrapper over [`set_cryptographer`],
/// but takes a `Box<dyn Cryptographer>` instead.
#[cfg(not(feature = "backend-openssl"))]
#[cfg(not(any(feature = "backend-openssl", feature = "backend-rustcrypto")))]
pub fn set_boxed_cryptographer(c: Box<dyn Cryptographer>) -> Result<(), SetCryptographerError> {
// Just leak the Box. It wouldn't be freed as a `static` anyway, and we
// never allow this to be re-assigned (so it's not a meaningful memory leak).
Expand Down Expand Up @@ -45,6 +45,12 @@ fn autoinit_crypto() {
let _ = set_cryptographer(&super::openssl::OpensslCryptographer);
}

#[cfg(not(feature = "backend-openssl"))]
#[cfg(all(feature = "backend-rustcrypto", not(feature = "backend-openssl")))]
#[inline]
fn autoinit_crypto() {
let _ = set_cryptographer(&super::rustcrypto::RustCryptoCryptographer);
}

#[cfg(not(any(feature = "backend-openssl", feature = "backend-rustcrypto")))]
#[inline]
fn autoinit_crypto() {}
35 changes: 34 additions & 1 deletion src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use std::any::Any;
pub(crate) mod holder;
#[cfg(feature = "backend-openssl")]
mod openssl;
#[cfg(feature = "backend-rustcrypto")]
mod rustcrypto;

#[cfg(not(feature = "backend-openssl"))]
#[cfg(not(any(feature = "backend-openssl", feature = "backend-rustcrypto")))]
pub use holder::{set_boxed_cryptographer, set_cryptographer};

pub trait RemotePublicKey: Send + Sync + 'static {
Expand Down Expand Up @@ -167,3 +169,34 @@ mod tests {
test_cryptographer(super::openssl::OpensslCryptographer);
}
}

#[cfg(all(test, feature = "backend-rustcrypto", not(feature = "backend-openssl")))]
mod rustcrypto_tests {
use super::*;

#[test]
fn test_rustcrypto_cryptographer() {
test_cryptographer(super::rustcrypto::RustCryptoCryptographer);
}
}

#[cfg(all(test, feature = "backend-openssl", feature = "backend-rustcrypto"))]
mod interop_tests {
use super::*;

#[test]
fn test_backend_interop() {
let openssl_crypto = super::openssl::OpensslCryptographer;
let rustcrypto_crypto = super::rustcrypto::RustCryptoCryptographer;

// Generate key with OpenSSL, import to RustCrypto
let key = openssl_crypto.generate_ephemeral_keypair().unwrap();
let components = key.raw_components().unwrap();
rustcrypto_crypto.import_key_pair(&components).unwrap();

// Generate key with RustCrypto, import to OpenSSL
let key = rustcrypto_crypto.generate_ephemeral_keypair().unwrap();
let components = key.raw_components().unwrap();
openssl_crypto.import_key_pair(&components).unwrap();
}
}
233 changes: 233 additions & 0 deletions src/crypto/rustcrypto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
/* 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/. */

// Silence deprecation warnings from generic-array < 1.0 used by aes-gcm 0.10
// This will be resolved when upgrading to aes-gcm 0.11+ (currently RC)
#![allow(deprecated)]

use crate::{
crypto::{Cryptographer, EcKeyComponents, LocalKeyPair, RemotePublicKey},
error::*,
};
use aes_gcm::{
aead::{Aead, KeyInit},
Aes128Gcm, Nonce,
};
use hkdf::Hkdf;
use p256::{
ecdh::diffie_hellman,
elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint},
EncodedPoint, PublicKey, SecretKey,
};
use rand_core::OsRng;
use sha2::Sha256;
use std::{any::Any, fmt};

// Types and methods may appear unused when both backends are enabled,
// but they're required by the Cryptographer trait implementation
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct RustCryptoRemotePublicKey {
public_key: PublicKey,
raw_pub_key: Vec<u8>,
}

#[allow(dead_code)]
impl RustCryptoRemotePublicKey {
fn from_raw(raw: &[u8]) -> Result<Self> {
let encoded_point = EncodedPoint::from_bytes(raw).map_err(|_| Error::InvalidKeyLength)?;
let public_key = PublicKey::from_encoded_point(&encoded_point)
.into_option()
.ok_or(Error::InvalidKeyLength)?;
Ok(RustCryptoRemotePublicKey {
public_key,
raw_pub_key: raw.to_vec(),
})
}

pub(crate) fn public_key(&self) -> &PublicKey {
&self.public_key
}
}

impl RemotePublicKey for RustCryptoRemotePublicKey {
fn as_raw(&self) -> Result<Vec<u8>> {
Ok(self.raw_pub_key.clone())
}

fn as_any(&self) -> &dyn Any {
self
}
}

#[allow(dead_code)]
#[derive(Clone)]
pub struct RustCryptoLocalKeyPair {
secret_key: SecretKey,
}

impl fmt::Debug for RustCryptoLocalKeyPair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:?}",
base64::Engine::encode(
&base64::engine::general_purpose::URL_SAFE,
self.secret_key.to_bytes()
)
)
}
}

#[allow(dead_code)]
impl RustCryptoLocalKeyPair {
/// Generate a random local key pair using p256's RNG.
fn generate_random() -> Result<Self> {
let secret_key = SecretKey::random(&mut OsRng);
Ok(RustCryptoLocalKeyPair { secret_key })
}

fn from_raw_components(components: &EcKeyComponents) -> Result<Self> {
// Verify the public key matches the private key
let private_bytes = components.private_key();
if private_bytes.len() != 32 {
return Err(Error::InvalidKeyLength);
}

let secret_key =
SecretKey::from_slice(private_bytes).map_err(|_| Error::InvalidKeyLength)?;

// Verify the public key component matches
let derived_public = secret_key.public_key();
let derived_raw = derived_public.to_encoded_point(false);

if derived_raw.as_bytes() != components.public_key() {
return Err(Error::InvalidKeyLength);
}

Ok(RustCryptoLocalKeyPair { secret_key })
}

pub(crate) fn secret_key(&self) -> &SecretKey {
&self.secret_key
}
}

impl LocalKeyPair for RustCryptoLocalKeyPair {
/// Export the public key component in the binary uncompressed point representation.
fn pub_as_raw(&self) -> Result<Vec<u8>> {
let public_key = self.secret_key.public_key();
let encoded = public_key.to_encoded_point(false);
Ok(encoded.as_bytes().to_vec())
}

fn raw_components(&self) -> Result<EcKeyComponents> {
let private_key = self.secret_key.to_bytes();
let public_key = self.pub_as_raw()?;
Ok(EcKeyComponents::new(private_key.to_vec(), public_key))
}

fn as_any(&self) -> &dyn Any {
self
}
}

#[allow(dead_code)]
pub struct RustCryptoCryptographer;

impl Cryptographer for RustCryptoCryptographer {
fn generate_ephemeral_keypair(&self) -> Result<Box<dyn LocalKeyPair>> {
Ok(Box::new(RustCryptoLocalKeyPair::generate_random()?))
}

fn import_key_pair(&self, components: &EcKeyComponents) -> Result<Box<dyn LocalKeyPair>> {
Ok(Box::new(RustCryptoLocalKeyPair::from_raw_components(
components,
)?))
}

fn import_public_key(&self, raw: &[u8]) -> Result<Box<dyn RemotePublicKey>> {
Ok(Box::new(RustCryptoRemotePublicKey::from_raw(raw)?))
}

fn compute_ecdh_secret(
&self,
remote: &dyn RemotePublicKey,
local: &dyn LocalKeyPair,
) -> Result<Vec<u8>> {
let local_any = local.as_any();
let local = local_any
.downcast_ref::<RustCryptoLocalKeyPair>()
.ok_or(Error::CryptoError)?;

let remote_any = remote.as_any();
let remote = remote_any
.downcast_ref::<RustCryptoRemotePublicKey>()
.ok_or(Error::CryptoError)?;

// Perform ECDH using the diffie_hellman function
let shared_secret = diffie_hellman(
local.secret_key.to_nonzero_scalar(),
remote.public_key.as_affine(),
);

Ok(shared_secret.raw_secret_bytes().to_vec())
}

fn hkdf_sha256(&self, salt: &[u8], secret: &[u8], info: &[u8], len: usize) -> Result<Vec<u8>> {
let (_, hk) = Hkdf::<Sha256>::extract(Some(salt), secret);
let mut okm = vec![0u8; len];
hk.expand(info, &mut okm).map_err(|_| Error::CryptoError)?;
Ok(okm)
}

fn aes_gcm_128_encrypt(&self, key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
if key.len() != 16 {
return Err(Error::CryptoError);
}
if iv.len() != 12 {
return Err(Error::CryptoError);
}

let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::CryptoError)?;
let nonce = Nonce::from_slice(iv);

// AES-GCM encrypt returns [ciphertext || tag]
let ciphertext = cipher
.encrypt(nonce, data)
.map_err(|_| Error::CryptoError)?;

Ok(ciphertext)
}

fn aes_gcm_128_decrypt(
&self,
key: &[u8],
iv: &[u8],
ciphertext_and_tag: &[u8],
) -> Result<Vec<u8>> {
if key.len() != 16 {
return Err(Error::CryptoError);
}
if iv.len() != 12 {
return Err(Error::CryptoError);
}

let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::CryptoError)?;
let nonce = Nonce::from_slice(iv);

// aes-gcm crate expects [ciphertext || tag] format
let plaintext = cipher
.decrypt(nonce, ciphertext_and_tag)
.map_err(|_| Error::CryptoError)?;

Ok(plaintext)
}

fn random_bytes(&self, dest: &mut [u8]) -> Result<()> {
use rand_core::RngCore;
OsRng.fill_bytes(dest);
Ok(())
}
}
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ pub enum Error {
#[cfg(feature = "backend-openssl")]
#[error("OpenSSL error: {0}")]
OpenSSLError(#[from] openssl::error::ErrorStack),

#[cfg(feature = "backend-rustcrypto")]
#[error("RustCrypto error: {0}")]
RustCryptoError(String),
}
7 changes: 5 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ pub fn decrypt(components: &EcKeyComponents, auth: &[u8], data: &[u8]) -> Result

/// Generate a pair of keys; useful for writing tests.
///
#[cfg(all(test, feature = "backend-openssl"))]
#[cfg(test)]
fn generate_keys() -> Result<(Box<dyn LocalKeyPair>, Box<dyn LocalKeyPair>)> {
let cryptographer = crypto::holder::get_cryptographer();
let local_key = cryptographer.generate_ephemeral_keypair()?;
let remote_key = cryptographer.generate_ephemeral_keypair()?;
Ok((local_key, remote_key))
}

#[cfg(all(test, feature = "backend-openssl"))]
#[cfg(test)]
mod aes128gcm_tests {
use super::common::ECE_TAG_LENGTH;
use super::*;
Expand Down Expand Up @@ -293,7 +293,10 @@ mod aes128gcm_tests {
"8115f4988b8c392a7bacb43c8f1ac5650000001241041994483c541e9bc39a6af03ff713aa7745c284e138a42a2435b797b20c4b698cf5118b4f8555317c190eabebfab749c164d3f6bdebe0d441719131a357d8890a13c4dbd4b16ff3dd5a83f7c91ad6e040ac42730a7f0b3cd3245e9f8d6ff31c751d410cfd"
).unwrap_err();
match err {
#[cfg(feature = "backend-openssl")]
Error::OpenSSLError(_) => {}
#[cfg(feature = "backend-rustcrypto")]
Error::CryptoError => {}
_ => panic!("Unexpected error {:?}", err),
};
}
Expand Down