Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
88 changes: 85 additions & 3 deletions wacore/libsignal/src/core/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,15 +516,28 @@ impl PrivateKey {

pub fn calculate_agreement(&self, their_key: &PublicKey) -> Result<[u8; 32], CurveError> {
match (&self.key, their_key.key) {
// The single place the agreement is decided, so a consumer that
// supplies the primitive replaces it for every caller at once.
(PrivateKeyData::DjbPrivateKey { key, .. }, PublicKeyData::DjbPublicKey(pub_key)) => {
// Use from_bytes_without_cache since agreement doesn't need the Edwards cache
let private_key = curve25519::PrivateKey::from_bytes_without_cache(*key);
Ok(private_key.calculate_agreement(&pub_key))
Ok(crate::crypto::x25519_agreement(key, &pub_key))
}
}
}
}

/// This crate's own X25519 agreement, and the only copy of that path: it is
/// what [`SignalCryptoProvider::x25519_agreement`] runs by default.
///
/// [`SignalCryptoProvider::x25519_agreement`]: crate::crypto::SignalCryptoProvider::x25519_agreement
pub(crate) fn x25519_agreement(
private_key: &[u8; curve25519::PRIVATE_KEY_LENGTH],
their_public_key: &[u8; curve25519::PUBLIC_KEY_LENGTH],
) -> [u8; 32] {
// from_bytes_without_cache because agreement never reads the Edwards cache.
curve25519::PrivateKey::from_bytes_without_cache(*private_key)
.calculate_agreement(their_public_key)
}

impl TryFrom<&[u8]> for PrivateKey {
type Error = CurveError;

Expand Down Expand Up @@ -799,6 +812,75 @@ mod tests {
}
}

fn key_bytes(hex_key: &str) -> [u8; 32] {
let mut out = [0u8; 32];
hex::decode_to_slice(hex_key, &mut out).expect("32 hex-encoded bytes");
out
}

/// RFC 7748 section 6.1. With no provider installed the agreement runs the
/// library's own path, so this pins the bytes that routing must preserve.
#[test]
fn rfc7748_agreement_vector() {
let alice_private = PrivateKey::deserialize(&key_bytes(
"77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a",
))
.expect("alice private key");
let bob_private = PrivateKey::deserialize(&key_bytes(
"5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb",
))
.expect("bob private key");
let alice_public = PublicKey::from_djb_public_key_bytes(&key_bytes(
"8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a",
))
.expect("alice public key");
let bob_public = PublicKey::from_djb_public_key_bytes(&key_bytes(
"de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f",
))
.expect("bob public key");
let expected =
key_bytes("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");

assert_eq!(alice_private.public_key().expect("derive"), alice_public);
assert_eq!(bob_private.public_key().expect("derive"), bob_public);
assert_eq!(
alice_private.calculate_agreement(&bob_public).expect("dh"),
expected
);
assert_eq!(
bob_private.calculate_agreement(&alice_public).expect("dh"),
expected
);
}

/// A key of the wrong length never reaches the agreement at all.
#[test]
fn agreement_rejects_wrong_length_keys() {
assert!(matches!(
PrivateKey::deserialize(&[0x42; 31]),
Err(CurveError::BadKeyLength(KeyType::Djb, 31))
));
assert!(matches!(
PublicKey::from_djb_public_key_bytes(&[0x42; 33]),
Err(CurveError::BadKeyLength(KeyType::Djb, 33))
));
}

/// Signing state is expensive and the agreement has no use for it; routing
/// must not start building it behind our back.
#[test]
fn agreement_leaves_the_signing_cache_cold() {
let mut csprng = rng();
let alice = KeyPair::generate(&mut csprng);
let bob = KeyPair::generate(&mut csprng);

alice
.calculate_agreement(&bob.public_key)
.expect("agreement");

assert!(!alice.private_key.has_warm_signing_cache());
}

#[test]
fn test_key_agreement_works_without_signing() {
let mut csprng = rng();
Expand Down
7 changes: 7 additions & 0 deletions wacore/libsignal/src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ pub fn hmac_sha256_two_part(key: &[u8], first: &[u8], second: &[u8]) -> [u8; 32]
provider::provider().hmac_sha256_two_part(key, first, second)
}

/// X25519 key agreement over raw 32-byte keys. Delegates to the active
/// [`SignalCryptoProvider`], whose default is this crate's own implementation.
#[inline]
pub fn x25519_agreement(private_key: &[u8; 32], their_public_key: &[u8; 32]) -> [u8; 32] {
provider::provider().x25519_agreement(private_key, their_public_key)
}

/// In-place AES-256-GCM seal. On entry `buffer` holds the plaintext; on return
/// it holds `ciphertext || tag` (length grown by 16). Zero allocations with
/// the default [`RustCryptoProvider`].
Expand Down
106 changes: 104 additions & 2 deletions wacore/libsignal/src/crypto/provider.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
//! Pluggable Signal crypto provider.
//!
//! Default uses RustCrypto (soft). Override via [`set_crypto_provider`] to
//! delegate hot-path primitives to a faster backend (e.g. `node:crypto`
//! over a WASM bridge). Must be set before any crypto call.
//! delegate the primitives to another backend: AES-256-CBC, AES-256-GCM,
//! HMAC-SHA256, the transport AEAD and the X25519 agreement. Must be set
//! before any crypto call, key agreement included.

use std::sync::OnceLock;

Expand Down Expand Up @@ -220,6 +221,17 @@ pub trait SignalCryptoProvider: Send + Sync + 'static {
self.hmac_sha256(key, &input)
}

/// X25519 key agreement over the raw 32-byte keys as this crate stores
/// them; an implementation clamps the private key itself, as X25519
/// requires (clamping is idempotent, so repeating it is safe). Overriding
/// this replaces the primitive for every agreement the crate performs.
///
/// The default is this crate's own implementation, so a provider that
/// leaves it alone keeps today's result byte for byte.
fn x25519_agreement(&self, private_key: &[u8; 32], their_public_key: &[u8; 32]) -> [u8; 32] {
Comment thread
jlucaso1 marked this conversation as resolved.
Outdated
crate::core::curve::x25519_agreement(private_key, their_public_key)
}
Comment thread
jlucaso1 marked this conversation as resolved.
Outdated

/// In-place AES-256-GCM seal. On entry `buffer` holds the plaintext; on
/// return it holds `ciphertext || tag` (length grown by 16).
///
Expand Down Expand Up @@ -574,6 +586,96 @@ mod tests {
);
}

/// A provider written before the agreement joined the trait: it implements
/// only the required methods and must keep agreeing keys correctly.
struct SymmetricOnlyProvider;

impl SignalCryptoProvider for SymmetricOnlyProvider {
fn aes_256_cbc_encrypt(
&self,
key: &[u8; 32],
iv: &[u8; 16],
plaintext: &[u8],
out: &mut Vec<u8>,
) -> Result<(), CryptoProviderError> {
RustCryptoProvider.aes_256_cbc_encrypt(key, iv, plaintext, out)
}

fn aes_256_cbc_decrypt(
&self,
key: &[u8; 32],
iv: &[u8; 16],
ciphertext: &[u8],
out: &mut Vec<u8>,
) -> Result<(), CryptoProviderError> {
RustCryptoProvider.aes_256_cbc_decrypt(key, iv, ciphertext, out)
}

fn aes_256_gcm_encrypt(
&self,
key: &[u8; 32],
nonce: &[u8; 12],
aad: &[u8],
plaintext: &[u8],
out: &mut Vec<u8>,
) -> Result<(), CryptoProviderError> {
RustCryptoProvider.aes_256_gcm_encrypt(key, nonce, aad, plaintext, out)
}

fn aes_256_gcm_decrypt(
&self,
key: &[u8; 32],
nonce: &[u8; 12],
aad: &[u8],
ciphertext_with_tag: &[u8],
out: &mut Vec<u8>,
) -> Result<(), CryptoProviderError> {
RustCryptoProvider.aes_256_gcm_decrypt(key, nonce, aad, ciphertext_with_tag, out)
}

fn hmac_sha256(&self, key: &[u8], input: &[u8]) -> [u8; 32] {
RustCryptoProvider.hmac_sha256(key, input)
}
}

/// RFC 7748 section 6.1, reached through the trait default.
#[test]
fn provider_without_agreement_override_keeps_the_pure_path() {
let alice_private = [
0x77, 0x07, 0x6d, 0x0a, 0x73, 0x18, 0xa5, 0x7d, 0x3c, 0x16, 0xc1, 0x72, 0x51, 0xb2,
0x66, 0x45, 0xdf, 0x4c, 0x2f, 0x87, 0xeb, 0xc0, 0x99, 0x2a, 0xb1, 0x77, 0xfb, 0xa5,
0x1d, 0xb9, 0x2c, 0x2a,
];
let bob_public = [
0xde, 0x9e, 0xdb, 0x7d, 0x7b, 0x7d, 0xc1, 0xb4, 0xd3, 0x5b, 0x61, 0xc2, 0xec, 0xe4,
0x35, 0x37, 0x3f, 0x83, 0x43, 0xc8, 0x5b, 0x78, 0x67, 0x4d, 0xad, 0xfc, 0x7e, 0x14,
0x6f, 0x88, 0x2b, 0x4f,
];
let expected = [
0x4a, 0x5d, 0x9d, 0x5b, 0xa4, 0xce, 0x2d, 0xe1, 0x72, 0x8e, 0x3b, 0xf4, 0x80, 0x35,
0x0f, 0x25, 0xe0, 0x7e, 0x21, 0xc9, 0x47, 0xd1, 0x9e, 0x33, 0x76, 0xf0, 0x9b, 0x3c,
0x1e, 0x16, 0x17, 0x42,
];

assert_eq!(
SymmetricOnlyProvider.x25519_agreement(&alice_private, &bob_public),
expected
);
assert_eq!(
RustCryptoProvider.x25519_agreement(&alice_private, &bob_public),
expected
);
}

/// A public key of low order agrees to the all-zero secret; the primitive
/// reports it as bytes rather than failing, and callers must not read that
/// as a usable secret.
#[test]
fn provider_agreement_with_low_order_point_is_all_zero() {
let agreement = RustCryptoProvider.x25519_agreement(&[0x42; 32], &[0u8; 32]);
assert_eq!(agreement, [0u8; 32]);
}

/// NIST SP 800-38D Test Case 14: all-zero key/nonce, 128-bit plaintext.
#[test]
fn rust_provider_gcm_nist_tc14() {
Expand Down
Loading