diff --git a/CLAUDE.md b/CLAUDE.md index 908439d61..84c6ff660 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,7 +104,7 @@ The workspace has four top-level kinds of member: ### The `core` / `core-test-framework` / `factory` spine -- `crypto/core` defines the abstract traits (`Hash`, `KDF`, `MAC`, `KEM`, `Signature`, `RNG`, `Algorithm`, `HashAlgParams`, …), error enums (`HashError`, `KDFError`, `KEMError`, `MACError`, `RNGError`, `SignatureError`), and the `KeyMaterial` / `KeyType` wrapper that all sensitive byte buffers are required to use (see `Secret` super-trait requirement in QUALITY_AND_STYLE.md). +- `crypto/core` defines the abstract traits (`Hash`, `KDF`, `MAC`, `KEM`, `Signature`, `RNG`, `Algorithm`, `HashAlgParams`, …), error enums (`HashError`, `KDFError`, `KEMError`, `MACError`, `RNGError`, `SignatureError`), and the `KeyMaterial` / `KeyType` wrapper for key bytes. Sensitive data is held in `bouncycastle_utils::secret::Secret`, which `KeyMaterial` is built on (see the sensitive-data rule under "APIs" in QUALITY_AND_STYLE.md). - `crypto/core-test-framework` contains the shared per-trait test suite (`hash.rs`, `kdf.rs`, `kem.rs`, `mac.rs`, `signature.rs`). New implementations of a core trait must be exercised through this framework — it's how trait conformance and error-condition coverage stay consistent across implementations. - `crypto/factory` provides enum-based string-name factories (`HashFactory`, `KDFFactory`, `MACFactory`, `RNGFactory`, `XOFFactory`). Each factory enum impls the underlying trait so it can be used transparently as that primitive, and each impls `AlgorithmFactory` (`new(name)`, `default_128_bit()`, `default_256_bit()`). When adding a new primitive that fits an existing trait, register it in the corresponding factory. diff --git a/Cargo.toml b/Cargo.toml index 11e1f13cc..af1f019f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,12 @@ bouncycastle-base64 = { path = "./crypto/base64" } bouncycastle-cipher = { path = "./crypto/cipher" } bouncycastle-core = { path = "crypto/core" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" } +bouncycastle-ed = { path = "crypto/ed" } bouncycastle-factory = { path = "./crypto/factory" } bouncycastle-hex = { path = "./crypto/hex" } bouncycastle-hkdf = { path = "./crypto/hkdf" } bouncycastle-hmac = { path = "./crypto/hmac" } +bouncycastle-hpke = { path = "./crypto/hpke" } bouncycastle-mlkem = { path = "./crypto/mlkem" } bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory" } bouncycastle-mldsa = { path = "./crypto/mldsa" } @@ -52,10 +54,12 @@ bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-cipher.workspace = true bouncycastle-core.workspace = true +bouncycastle-ed.workspace = true bouncycastle-factory.workspace = true bouncycastle-hex.workspace = true bouncycastle-hkdf.workspace = true bouncycastle-hmac.workspace = true +bouncycastle-hpke.workspace = true bouncycastle-mldsa.workspace = true bouncycastle-mldsa-lowmemory.workspace = true bouncycastle-mlkem.workspace = true diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index 458177530..2574a072f 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -20,7 +20,7 @@ a cache and resume later. ## Tests -All crates must have tests in `src/tests`. Part of writing code that treats future maintainers as malicious is that all +All crates must have tests in `tests/`. Part of writing code that treats future maintainers as malicious is that all functions that form part of the public interface should have their expected behaviour fully constrained with tests. In other words, any behaviour change of the library that could cause a change in a calling application should also cause a test in bc-rust to fail. An excellent tool for achieving this is `cargo mutants` which must be run on every crate and @@ -99,7 +99,8 @@ subsequent calls can be made to this object (as opposed to the usual pattern of `do_update(&self, ...)`). These tricks go a long way to reducing fallibility since now in general there is no (or very very little) object state to track and return errors about. -Any struct that holds sensitive data must impl the `core::Secret` trait and all associated super-traits. +Any struct that holds sensitive data must keep it in a `bouncycastle_utils::secret::Secret` field (or in a +`KeyMaterial`, which is built on it), so that it is zeroized on drop and redacted from `Debug` and `Display`. A primitive whose safe use depends on the caller composing it correctly -- a raw block permutation, a raw keystream -- lives under a `hazmat` module in its crate or sub-module, never at the crate root or next to the safe API; diff --git a/cli/src/main.rs b/cli/src/main.rs index d4872f25d..e445355ea 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -16,6 +16,7 @@ mod rng_cmd; mod sha2_cmd; mod sha3_cmd; mod sm3_cmd; +mod x25519_cmd; use crate::mac_cmd::HMACVariant; use crate::mldsa_cmd::MLDSAAction; @@ -1737,6 +1738,28 @@ enum Subcommands { /// Output the hashes in hex format. x: bool, }, + + /// The X25519 key agreement algorithm (RFC 7748), and the DHKEM(X25519, HKDF-SHA256) key + /// encapsulation built on it (RFC 9180). + X25519 { + action: x25519_cmd::X25519Action, + + #[arg(long)] + /// The private key file (in hex or binary) for agree and decaps + skfile: Option, + + #[arg(long)] + /// The public key file (in hex or binary) for agree and encaps + pkfile: Option, + + #[arg(long)] + /// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from. + ctfile: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, } fn main() { @@ -2043,6 +2066,9 @@ fn run() { Some(Subcommands::TUPLEHASH256 { length, elements, customization, x }) => { sha3_cmd::tuplehash_cmd(256, *length, elements, customization, *x); } + Some(Subcommands::X25519 { action, skfile, pkfile, ctfile, x }) => { + x25519_cmd::x25519_cmd(action, skfile, pkfile, ctfile, *x); + } None => { eprintln!("No command provided. See -h") } diff --git a/cli/src/x25519_cmd.rs b/cli/src/x25519_cmd.rs new file mode 100644 index 000000000..58f5de0ac --- /dev/null +++ b/cli/src/x25519_cmd.rs @@ -0,0 +1,160 @@ +use crate::helpers::{ + read_from_file, read_from_file_or_stdin, write_bytes_or_hex, write_bytes_or_hex_to_file, +}; +use bouncycastle::core::key_material::KeyMaterialTrait; +use bouncycastle::core::traits::{ + KEMDecapsulator, KEMEncapsulator, KeyAgreement, KeyAgreementPrivateKey, KeyAgreementPublicKey, +}; +use bouncycastle::ed::x25519::{POINT_LEN, X25519, X25519PrivateKey, X25519PublicKey}; +use clap::ValueEnum; +use std::process::exit; + +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum X25519Action { + /// Generate and output a new private key. + Keygen, + /// Output the public key of a private key read from skfile or stdin. + /// Accepts either binary or hex. + PkFromSk, + /// Accepts a sk (from skfile or stdin) and a pk (from pkfile), and checks that they match. + CheckConsistency, + /// Perform an X25519 key agreement between a private key (from skfile or stdin) and the + /// peer's public key (from pkfile), and output the shared secret to stdout. + /// The output is the raw X25519 shared secret: derive keys from it with a KDF, such as the + /// hkdf-sha256 subcommand, rather than using it as a key directly. + Agree, + /// Perform a DHKEM(X25519, HKDF-SHA256) encapsulation (RFC 9180 Sec 4.1) with a public key, + /// and output the shared secret and the ciphertext (the encapsulated key). + /// If a ctfile is provided, then the shared secret is output to stdout in either binary or hex + /// and the ciphertext to file also in binary or hex. + /// If no ctfile is provided then ct followed by ss are output to stdout in hex, separated by a newline. + /// The shared secret is always output to stdout to prevent it + /// staying in a file longer than it should and causing a vulnerability. + Encaps, + /// Perform a DHKEM(X25519, HKDF-SHA256) decapsulation with a private key and ciphertext, and + /// output the shared secret to stdout either in binary or hex. + /// The private key is read from skfile, and the ciphertext from ctfile or stdin, in binary or hex. + Decaps, +} + +pub(crate) fn x25519_cmd( + action: &X25519Action, + skfile: &Option, + pkfile: &Option, + ctfile: &Option, + output_hex: bool, +) { + match action { + X25519Action::Keygen => { + let (_pk, sk) = X25519::keygen().unwrap(); + write_bytes_or_hex(&sk.encode(), output_hex); + } + X25519Action::PkFromSk => { + let sk = parse_sk(&read_from_file_or_stdin(skfile)); + write_bytes_or_hex(&sk.pk().encode(), output_hex); + } + X25519Action::CheckConsistency => { + let sk = parse_sk(&read_from_file_or_stdin(skfile)); + let pk = parse_pk(&read_pkfile(pkfile)); + + match X25519::keypair_consistency_check(&pk, &sk) { + Ok(_) => { + crate::helpers::println_stdout("SUCCESS: pk and sk match."); + } + Err(_) => { + eprintln!("FAILURE: pk and sk do not match."); + exit(-1); + } + } + } + X25519Action::Agree => { + let sk = parse_sk(&read_from_file_or_stdin(skfile)); + let pk = parse_pk(&read_pkfile(pkfile)); + + let ss = match X25519::key_agreement(&pk, &sk) { + Ok(ss) => ss, + Err(e) => { + eprintln!("Error: {:?}", e); + exit(-1); + } + }; + write_bytes_or_hex(ss.as_ref(), output_hex); + } + X25519Action::Encaps => { + let pk = parse_pk(&read_from_file_or_stdin(pkfile)); + + let (ss, ct) = match X25519::encaps(&pk) { + Ok(r) => r, + Err(e) => { + eprintln!("Error: {:?}", e); + exit(-1); + } + }; + + if ctfile.is_some() { + // write ct to file and ss to stdout + write_bytes_or_hex_to_file(&ct, ctfile.as_ref().unwrap(), output_hex); + write_bytes_or_hex(ss.ref_to_bytes(), output_hex); + } else { + // write both to stdout in hex, separated by a newline. + write_bytes_or_hex(&ct, true); + crate::helpers::write_stdout(b"\n"); + write_bytes_or_hex(ss.ref_to_bytes(), true); + } + } + X25519Action::Decaps => { + let sk = match skfile { + Some(f) => parse_sk(&read_from_file(f)), + None => { + eprintln!("Error: no skfile provided."); + exit(-1); + } + }; + + let ct = read_from_file_or_stdin(ctfile); + if ct.len() != POINT_LEN { + eprintln!("Error: ciphertext is not the correct length."); + exit(-1); + } + + let ss = match X25519::decaps(&sk, &ct) { + Ok(ss) => ss, + Err(e) => { + eprintln!("Error: {:?}", e); + exit(-1); + } + }; + write_bytes_or_hex(ss.ref_to_bytes(), output_hex); + } + } +} + +fn read_pkfile(pkfile: &Option) -> Vec { + match pkfile { + Some(f) => read_from_file(f), + None => { + eprintln!("Error: no pkfile provided."); + exit(-1); + } + } +} + +fn parse_sk(bytes: &[u8]) -> X25519PrivateKey { + match X25519PrivateKey::from_bytes(bytes) { + Ok(sk) => sk, + Err(_) => { + eprintln!("Error: couldn't parse the input as a 32-byte X25519 private key."); + exit(-1); + } + } +} + +fn parse_pk(bytes: &[u8]) -> X25519PublicKey { + match X25519PublicKey::from_bytes(bytes) { + Ok(pk) => pk, + Err(_) => { + eprintln!("Error: couldn't parse the input as a 32-byte X25519 public key."); + exit(-1); + } + } +} diff --git a/cli/tests/test_x25519.sh b/cli/tests/test_x25519.sh new file mode 100755 index 000000000..fdc84ad00 --- /dev/null +++ b/cli/tests/test_x25519.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +# The x25519 subcommand, end to end through the binary: X25519 key agreement (RFC 7748) and +# DHKEM(X25519, HKDF-SHA256) encapsulation (RFC 9180). +# +# The known answers: the key agreement is RFC 7748 Section 6.1; the decapsulation is RFC 9180 +# Appendix A.1.1. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# RFC 7748 Section 6.1. +ALICE_SK=77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a +ALICE_PK=8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a +BOB_SK=5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb +BOB_PK=de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f +SHARED_K=4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742 + +# RFC 9180 Appendix A.1.1. +SK_RM=4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8 +PK_RM=3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d +ENC=37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431 +SHARED_SECRET=fe0e18c9f024ce43799ae393c7e8fe8fce9d218875e8227b0187c04e7d2ea1fc + +# ---- known answers ------------------------------------------------------------------------ + +test_pk_from_sk_matches_rfc7748() { + local got + got=$(hex_out "$BC_RUST" x25519 pk-from-sk -x < <(printf '%s' "$ALICE_SK")) + assert_eq "$got" "$ALICE_PK" "RFC 7748 6.1 Alice's public key" + printf '%s' "$BOB_SK" >"$TMP/bob.sk" + got=$(hex_out "$BC_RUST" x25519 pk-from-sk --skfile "$TMP/bob.sk" -x) + assert_eq "$got" "$BOB_PK" "RFC 7748 6.1 Bob's public key" +} + +test_agree_matches_rfc7748() { + local got + printf '%s' "$ALICE_SK" >"$TMP/alice.sk" + printf '%s' "$ALICE_PK" >"$TMP/alice.pk" + printf '%s' "$BOB_SK" >"$TMP/bob.sk" + printf '%s' "$BOB_PK" >"$TMP/bob.pk" + got=$(hex_out "$BC_RUST" x25519 agree --skfile "$TMP/alice.sk" --pkfile "$TMP/bob.pk" -x) + assert_eq "$got" "$SHARED_K" "RFC 7748 6.1 K, computed by Alice" + got=$(hex_out "$BC_RUST" x25519 agree --pkfile "$TMP/alice.pk" -x <"$TMP/bob.sk") + assert_eq "$got" "$SHARED_K" "RFC 7748 6.1 K, computed by Bob with the sk on stdin" +} + +test_decaps_matches_rfc9180_a1_1() { + local got + printf '%s' "$SK_RM" >"$TMP/sk" + got=$(hex_out "$BC_RUST" x25519 pk-from-sk --skfile "$TMP/sk" -x) + assert_eq "$got" "$PK_RM" "RFC 9180 A.1.1 pkRm" + got=$(hex_out "$BC_RUST" x25519 decaps --skfile "$TMP/sk" -x < <(printf '%s' "$ENC")) + assert_eq "$got" "$SHARED_SECRET" "RFC 9180 A.1.1 shared_secret, enc on stdin" + printf '%s' "$ENC" >"$TMP/ct" + got=$(hex_out "$BC_RUST" x25519 decaps --skfile "$TMP/sk" --ctfile "$TMP/ct" -x) + assert_eq "$got" "$SHARED_SECRET" "RFC 9180 A.1.1 shared_secret, enc from --ctfile" +} + +# ---- binary and hex ----------------------------------------------------------------------- + +test_binary_keys_match_hex_keys() { + local from_hex from_bin + unhex "$ALICE_SK" >"$TMP/alice.sk" + unhex "$BOB_PK" >"$TMP/bob.pk" + from_bin=$(hex_out "$BC_RUST" x25519 agree --skfile "$TMP/alice.sk" --pkfile "$TMP/bob.pk" -x) + assert_eq "$from_bin" "$SHARED_K" "agree with binary key files" + "$BC_RUST" x25519 agree --skfile "$TMP/alice.sk" --pkfile "$TMP/bob.pk" >"$TMP/k" + assert_eq "$(hex "$TMP/k")" "$SHARED_K" "agree with binary output" + from_hex=$(hex_out "$BC_RUST" x25519 pk-from-sk -x <"$TMP/alice.sk") + assert_eq "$from_hex" "$ALICE_PK" "pk-from-sk with a binary sk on stdin" +} + +# ---- fresh keys --------------------------------------------------------------------------- + +test_keygen_outputs_fresh_keys() { + "$BC_RUST" x25519 keygen >"$TMP/sk1" + "$BC_RUST" x25519 keygen >"$TMP/sk2" + assert_size "$TMP/sk1" 32 "keygen binary private key" + assert_differs "$TMP/sk1" "$TMP/sk2" "two keygens must give different keys" + assert_eq "$(hex_out "$BC_RUST" x25519 keygen -x | wc -c | tr -d ' ')" 64 "keygen -x length" +} + +test_agree_with_fresh_keys() { + "$BC_RUST" x25519 keygen >"$TMP/a.sk" + "$BC_RUST" x25519 keygen >"$TMP/b.sk" + "$BC_RUST" x25519 pk-from-sk --skfile "$TMP/a.sk" >"$TMP/a.pk" + "$BC_RUST" x25519 pk-from-sk --skfile "$TMP/b.sk" >"$TMP/b.pk" + "$BC_RUST" x25519 agree --skfile "$TMP/a.sk" --pkfile "$TMP/b.pk" >"$TMP/k_a" + "$BC_RUST" x25519 agree --skfile "$TMP/b.sk" --pkfile "$TMP/a.pk" >"$TMP/k_b" + assert_size "$TMP/k_a" 32 "shared secret" + assert_same "$TMP/k_a" "$TMP/k_b" "both sides must agree" +} + +test_check_consistency() { + "$BC_RUST" x25519 keygen >"$TMP/sk" + "$BC_RUST" x25519 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + expect_ok "matching pair" \ + "$BC_RUST" x25519 check-consistency --skfile "$TMP/sk" --pkfile "$TMP/pk" >/dev/null + "$BC_RUST" x25519 keygen >"$TMP/other.sk" + expect_fail "mismatched pair" \ + "$BC_RUST" x25519 check-consistency --skfile "$TMP/other.sk" --pkfile "$TMP/pk" + assert_stderr_has "FAILURE" +} + +# ---- encaps / decaps ---------------------------------------------------------------------- + +test_encaps_decaps_round_trip_with_ctfile() { + "$BC_RUST" x25519 keygen >"$TMP/sk" + "$BC_RUST" x25519 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + "$BC_RUST" x25519 encaps --pkfile "$TMP/pk" --ctfile "$TMP/ct" >"$TMP/ss1" + "$BC_RUST" x25519 decaps --skfile "$TMP/sk" --ctfile "$TMP/ct" >"$TMP/ss2" + assert_size "$TMP/ct" 32 "ciphertext" + assert_size "$TMP/ss1" 32 "shared secret" + assert_same "$TMP/ss1" "$TMP/ss2" "decaps must recover the encapsulated secret" +} + +# Without --ctfile, encaps prints the ciphertext and then the shared secret, in hex, one per line. +test_encaps_to_stdout_round_trip() { + local ct ss got + "$BC_RUST" x25519 keygen >"$TMP/sk" + "$BC_RUST" x25519 pk-from-sk --skfile "$TMP/sk" | "$BC_RUST" x25519 encaps >"$TMP/out" + ct=$(sed -n 1p "$TMP/out") + ss=$(sed -n 2p "$TMP/out") + assert_eq "${#ct}" 64 "hex ciphertext length" + assert_eq "${#ss}" 64 "hex shared secret length" + got=$(hex_out "$BC_RUST" x25519 decaps --skfile "$TMP/sk" -x < <(printf '%s' "$ct")) + assert_eq "$got" "$ss" "decaps must recover the encapsulated secret" +} + +test_encaps_is_randomized() { + printf '%s' "$PK_RM" >"$TMP/pk" + "$BC_RUST" x25519 encaps --pkfile "$TMP/pk" --ctfile "$TMP/ct1" >"$TMP/ss1" + "$BC_RUST" x25519 encaps --pkfile "$TMP/pk" --ctfile "$TMP/ct2" >"$TMP/ss2" + assert_differs "$TMP/ct1" "$TMP/ct2" "two encapsulations must give different ciphertexts" + assert_differs "$TMP/ss1" "$TMP/ss2" "two encapsulations must give different secrets" +} + +# DHKEM rejects implicitly: the wrong private key gives a different secret, not an error. +test_decaps_with_wrong_key_gives_different_secret() { + local got + "$BC_RUST" x25519 keygen >"$TMP/sk" + got=$(hex_out "$BC_RUST" x25519 decaps --skfile "$TMP/sk" -x < <(printf '%s' "$ENC")) + assert_eq "${#got}" 64 "hex shared secret length" + [ "$got" != "$SHARED_SECRET" ] || fail "the wrong private key must not give the RFC secret" +} + +# ---- errors ------------------------------------------------------------------------------- + +# An all-zero public key has small order, so the shared secret would be all zeros, which RFC 9180 +# Section 7.1.4 and RFC 8446 Section 7.4.2 require rejecting. +test_low_order_public_key_is_rejected() { + printf '%s' "$ALICE_SK" >"$TMP/sk" + printf '%064d' 0 >"$TMP/zero.pk" + expect_fail "agree with an all-zero public key" \ + "$BC_RUST" x25519 agree --skfile "$TMP/sk" --pkfile "$TMP/zero.pk" + assert_stderr_has "InvalidPublicKey" + expect_fail "encaps to an all-zero public key" \ + "$BC_RUST" x25519 encaps --pkfile "$TMP/zero.pk" --ctfile "$TMP/ct" + expect_fail "decaps of an all-zero enc" \ + "$BC_RUST" x25519 decaps --skfile "$TMP/sk" --ctfile "$TMP/zero.pk" +} + +test_wrong_length_inputs_are_rejected() { + printf '%s' "$ALICE_SK" >"$TMP/sk" + printf '%s' "${BOB_PK:0:62}" >"$TMP/short.pk" + expect_fail "31-byte private key" "$BC_RUST" x25519 pk-from-sk < <(printf '%s' "${ALICE_SK:0:62}") + assert_stderr_has "private key" + expect_fail "31-byte public key" \ + "$BC_RUST" x25519 agree --skfile "$TMP/sk" --pkfile "$TMP/short.pk" + assert_stderr_has "public key" + expect_fail "31-byte ciphertext" \ + "$BC_RUST" x25519 decaps --skfile "$TMP/sk" --ctfile "$TMP/short.pk" + assert_stderr_has "ciphertext" +} + +test_missing_files_are_rejected() { + printf '%s' "$ALICE_SK" >"$TMP/sk" + expect_fail "agree without --pkfile" "$BC_RUST" x25519 agree <"$TMP/sk" + assert_stderr_has "pkfile" + expect_fail "check-consistency without --pkfile" "$BC_RUST" x25519 check-consistency <"$TMP/sk" + assert_stderr_has "pkfile" + expect_fail "decaps without --skfile" "$BC_RUST" x25519 decaps < <(printf '%s' "$ENC") + assert_stderr_has "skfile" +} + +run_all diff --git a/crypto/core-test-framework/src/key_agreement.rs b/crypto/core-test-framework/src/key_agreement.rs new file mode 100644 index 000000000..fa21d3b2f --- /dev/null +++ b/crypto/core-test-framework/src/key_agreement.rs @@ -0,0 +1,146 @@ +//! Generic behaviour tests for anything that implements [`KeyAgreement`]. + +use bouncycastle_core::errors::KeyAgreementError; +use bouncycastle_core::traits::{KeyAgreement, KeyAgreementPrivateKey, KeyAgreementPublicKey}; + +/// Instance of the test framework. +pub struct TestFrameworkKeyAgreement { + // Put any config options here +} + +impl TestFrameworkKeyAgreement { + /// + pub fn new() -> Self { + Self {} + } + + /// Test the members of trait [`KeyAgreement`]. + /// + /// Since key generation is not part of the trait, the caller supplies a + /// `keygen` function pointer (the inherent `keygen` associated function on the algorithm struct). + pub fn test_key_agreement< + PK: KeyAgreementPublicKey, + SK: KeyAgreementPrivateKey, + KAAlg: KeyAgreement, + const PK_LEN: usize, + const SK_LEN: usize, + >( + &self, + keygen: fn() -> Result<(PK, SK), KeyAgreementError>, + ) where + KAAlg::DerivedKey: AsRef<[u8]>, + { + let (pk_a, sk_a) = keygen().unwrap(); + let (pk_b, sk_b) = keygen().unwrap(); + + // Symmetry test -- basic correctness + // KA(&pk_a, &sk_b) == KA(&pk_b, &sk_a) + let k_ab = KAAlg::key_agreement(&pk_a, &sk_b).unwrap(); + let k_ba = KAAlg::key_agreement(&pk_b, &sk_a).unwrap(); + assert_eq!(k_ba.as_ref(), k_ab.as_ref()); + + // Determinism + // The same (pk, sk) twice gives the same output + let k_1 = KAAlg::key_agreement(&pk_a, &sk_b).unwrap(); + let k_2 = KAAlg::key_agreement(&pk_a, &sk_b).unwrap(); + assert_eq!(k_1.as_ref(), k_2.as_ref()); + + // Output decodes as a usable PK + let k = KAAlg::key_agreement(&pk_a, &sk_b).unwrap(); + _ = KAAlg::key_agreement(&PK::from_bytes(k.as_ref()).unwrap(), &sk_a).unwrap(); + + // Decoded keys behave the same + // KA on PK::from_bytes(&pk.encode()) and SK::from_bytes(&sk.encode()) equals KA on the originals + let k = KAAlg::key_agreement(&pk_a, &sk_b).unwrap(); + let k1 = KAAlg::key_agreement(&PK::from_bytes(&pk_a.encode()).unwrap(), &sk_b).unwrap(); + assert_eq!(k.as_ref(), k1.as_ref()); + let k2 = KAAlg::key_agreement(&pk_a, &SK::from_bytes(&sk_b.encode()).unwrap()).unwrap(); + assert_eq!(k.as_ref(), k2.as_ref()); + } +} + +/// Instance of the test framework. +pub struct TestFrameworkKeyAgreementKeys {} + +impl TestFrameworkKeyAgreementKeys { + /// + pub fn new() -> Self { + Self {} + } + + /// Test the members of traits [`KeyAgreementPublicKey`] and [`KeyAgreementPrivateKey`]. + /// + /// Since key generation is not part of either key trait, the caller supplies a + /// `keygen` function pointer (the inherent `keygen` associated function on the algorithm struct). + pub fn test_keys< + PK: KeyAgreementPublicKey, + SK: KeyAgreementPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + >( + &self, + keygen: fn() -> Result<(PK, SK), KeyAgreementError>, + ) { + let (pk, sk) = keygen().unwrap(); + + // Check encoded lengths + assert_eq!(pk.encode().len(), PK_LEN); + assert_eq!(sk.encode().len(), SK_LEN); + + // AsRef matches encode + assert_eq!(pk.as_ref(), &pk.encode()); + + // encode_out matches encode + let mut pk_buf = [0u8; PK_LEN]; + let bytes_written = pk.encode_out(&mut pk_buf); + assert_eq!(bytes_written, PK_LEN); + assert_eq!(pk_buf, pk.encode()); + + let mut sk_buf = [0u8; SK_LEN]; + let bytes_written = sk.encode_out(&mut sk_buf); + assert_eq!(bytes_written, SK_LEN); + assert_eq!(sk_buf, sk.encode()); + + // encode_out overwrites + let mut pk_buf = [0xff; PK_LEN]; + let bytes_written = pk.encode_out(&mut pk_buf); + assert_eq!(bytes_written, PK_LEN); + assert_eq!(pk_buf, pk.encode()); + + let mut sk_buf = [0xff; SK_LEN]; + let bytes_written = sk.encode_out(&mut sk_buf); + assert_eq!(bytes_written, SK_LEN); + assert_eq!(sk_buf, sk.encode()); + + // Encoding round trips + assert_eq!(PK::from_bytes(&pk.encode()).unwrap().encode(), pk.encode()); + assert_eq!(SK::from_bytes(&sk.encode()).unwrap().encode(), sk.encode()); + + // Wrong length throws Err(DecodingError(_)) + // pk too small + assert!(matches!( + PK::from_bytes(&pk.encode()[..PK_LEN - 1]), + Err(KeyAgreementError::DecodingError(_)) + )); + // sk too large + let mut pk_buf_too_large = [0xff; 1000]; + pk_buf_too_large[..PK_LEN].copy_from_slice(&pk.encode()); + assert!(matches!( + PK::from_bytes(&pk_buf_too_large), + Err(KeyAgreementError::DecodingError(_)) + )); + + // sk too small + assert!(matches!( + SK::from_bytes(&sk.encode()[..SK_LEN - 1]), + Err(KeyAgreementError::DecodingError(_)) + )); + // sk too large + let mut sk_buf_too_large = [0xff; 1000]; + sk_buf_too_large[..SK_LEN].copy_from_slice(&sk.encode()); + assert!(matches!( + SK::from_bytes(&sk_buf_too_large), + Err(KeyAgreementError::DecodingError(_)) + )); + } +} diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index 5f7ffb288..d5ed30afc 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -20,6 +20,7 @@ pub mod electronic_code_book; pub mod hash; pub mod kdf; pub mod kem; +pub mod key_agreement; pub mod key_stream; pub mod mac; pub mod signature; diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 211cb9220..9499f1651 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -73,6 +73,8 @@ pub enum KEMError { DecapsulationFailed, /// DecodingError(&'static str), + /// The public key given to encapsulation is unusable, for example a point of small order. + InvalidPublicKey(&'static str), /// KeyGenError(&'static str), /// @@ -83,6 +85,24 @@ pub enum KEMError { RNGError(RNGError), } +/// +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum KeyAgreementError { + /// + GenericError(&'static str), + /// + ConsistencyCheckFailed(&'static str), + /// A key encoding could not be decoded, for example because it is the wrong length. + DecodingError(&'static str), + /// The peer's public key is unusable for key agreement, for example a point of small order. + InvalidPublicKey(&'static str), + /// + KeyGenError(&'static str), + /// + RNGError(RNGError), +} + /// #[derive(Debug, PartialEq, Eq)] #[non_exhaustive] @@ -258,6 +278,12 @@ impl From for KEMError { } } +impl From for KeyAgreementError { + fn from(e: RNGError) -> KeyAgreementError { + Self::RNGError(e) + } +} + impl From for MACError { fn from(e: KeyMaterialError) -> MACError { Self::KeyMaterialError(e) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 4a68f9a45..1a674f477 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1084,6 +1084,53 @@ pub trait KEMPublicKey: fn from_bytes(bytes: &[u8]) -> Result; } +/// A Key Agreement algorithm, often called a Diffie-Hellman algorithm. +/// The core function is `key_agreement(&pk, &sk)`, which combines one party's public key with the +/// other party's private key, and returns the shared secret as a value of the public key type `PK`. +pub trait KeyAgreement< + PK: KeyAgreementPublicKey, + SK: KeyAgreementPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, +>: Sized +{ + /// The type of the derived key that results from performing a key agreement. + type DerivedKey: Sized; + /// Perform a key agreement. + /// Note that this returns a public key. The caller is responsible for using a [`KDF`] to + /// derive a shared symmetric key from it. + /// Alternatively, most algorithms that impl [`KeyAgreement`] should also impl + /// [`KEMEncapsulator`] / [`KEMDecapsulator`], which will include a KDF step and return a [`KeyMaterial`]. + fn key_agreement(pk: &PK, sk: &SK) -> Result; +} + +/// A public key for a KeyAgreement (Diffie-Hellman) algorithm, often denoted "pk". +/// The bound on `AsRef<[u8]>` is because a Diffie-Hellman key agreement returns a public key as +/// the shared secret value, so it must be easy to convert this to bytes in a cannonical representation +/// so that it can be fed into a [`KDF`]. +pub trait KeyAgreementPublicKey: + PartialEq + Eq + Clone + Debug + Display + Sized + AsRef<[u8]> +{ + /// Write it out to bytes in its standard encoding. + fn encode(&self) -> [u8; PK_LEN]; + /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. + fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize; + /// Read it in from bytes in its standard encoding. + fn from_bytes(bytes: &[u8]) -> Result; +} + +/// A private key for a KeyAgreement (Diffie-Hellman) algorithm, often denoted "sk" (for "secret key"). +pub trait KeyAgreementPrivateKey: Sized { + /// Write it out to bytes in its standard encoding. + fn encode(&self) -> [u8; SK_LEN]; + /// Write it out to bytes in its standard encoding. + /// The entire output buffer is zeroized before the encoding is written. + fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize; + /// Read it in from bytes in its standard encoding. + fn from_bytes(bytes: &[u8]) -> Result; +} + /// A Message Authentication Code algorithm is a keyed hash function that behaves somewhat like a symmetric signature function. /// A MAC algorithm takes in a key and some data, and produces a MAC (message authentication code) that /// can be used to verify the integrity of data. diff --git a/crypto/ed/Cargo.toml b/crypto/ed/Cargo.toml new file mode 100644 index 000000000..99ad2aff4 --- /dev/null +++ b/crypto/ed/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bouncycastle-ed" +edition.workspace = true +version.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-hpke.workspace = true +bouncycastle-rng.workspace = true +bouncycastle-sha2.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true + +[[bench]] +name = "x25519_benches" +harness = false diff --git a/crypto/ed/benches/x25519_benches.rs b/crypto/ed/benches/x25519_benches.rs new file mode 100644 index 000000000..fe710268c --- /dev/null +++ b/crypto/ed/benches/x25519_benches.rs @@ -0,0 +1,125 @@ +use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +use bouncycastle_core::traits::{KEMDecapsulator, KEMEncapsulator, KeyAgreement}; +use bouncycastle_core_test_framework::{DUMMY_SEED, FixedSeedRNG}; +use bouncycastle_ed::x25519::{X25519, X25519PrivateKey, X25519PublicKey}; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +/// The number of distinct keys each benchmark iterates over, so that the CPU does not cache one +/// input or learn its branches. +const NUM_ELEMS: usize = 32; + +/// `NUM_ELEMS` distinct 32-byte seeds, taken from `DUMMY_SEED`. +fn seeds() -> Vec { + DUMMY_SEED + .chunks(32) + .take(NUM_ELEMS) + .enumerate() + .map(|(i, chunk)| { + // The chunks of DUMMY_SEED repeat every 8; mixing in the index keeps all NUM_ELEMS + // seeds distinct. + let mut bytes: [u8; 32] = chunk.try_into().unwrap(); + bytes[0] ^= i as u8; + KeyMaterial256::from_bytes_as_type(&bytes, KeyType::Seed).unwrap() + }) + .collect() +} + +/// A key pair for each of [`seeds`]. +fn key_pairs() -> Vec<(X25519PublicKey, X25519PrivateKey)> { + seeds().iter().map(|seed| X25519::keygen_from_seed(seed).unwrap()).collect() +} + +fn bench_x25519_keygen(c: &mut Criterion) { + let mut group = c.benchmark_group("KeyGen"); + + // Set up the seeds outside of the timing loop. + let seeds = seeds(); + + group.throughput(criterion::Throughput::Elements(seeds.len() as u64)); + + group.bench_function("X25519", |b| { + b.iter(|| { + for seed in seeds.iter() { + _ = black_box(X25519::keygen_from_seed(seed)); + } + }) + }); + + group.finish(); +} + +fn bench_x25519_key_agreement(c: &mut Criterion) { + let mut group = c.benchmark_group("KeyAgreement"); + + let keys = key_pairs(); + + group.throughput(criterion::Throughput::Elements(keys.len() as u64)); + + // Each private key agrees with the next key pair's public key. + group.bench_function("X25519", |b| { + b.iter(|| { + for i in 0..keys.len() { + let pk = &keys[(i + 1) % keys.len()].0; + _ = black_box(X25519::key_agreement(pk, &keys[i].1)); + } + }) + }); + + group.finish(); +} + +fn bench_x25519_encaps(c: &mut Criterion) { + let mut group = c.benchmark_group("Encaps"); + + let keys = key_pairs(); + + // A fixed-output RNG per encapsulation, so that we're not measuring the time of the DRBG. + let mut ephemeral_seeds = [[0u8; 32]; NUM_ELEMS]; + for (i, seed) in ephemeral_seeds.iter_mut().enumerate() { + seed.fill(i as u8 + 1); + } + + group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); + + group.bench_function("DHKEM(X25519, HKDF-SHA256)", |b| { + b.iter(|| { + for i in 0..NUM_ELEMS { + let mut rng = FixedSeedRNG::new(ephemeral_seeds[i]); + _ = black_box(X25519::encaps_rng(&keys[i].0, &mut rng)); + } + }) + }); + + group.finish(); +} + +fn bench_x25519_decaps(c: &mut Criterion) { + let mut group = c.benchmark_group("Decaps"); + + let keys = key_pairs(); + + // Set up the ciphertexts outside of the timing loop. + let cts: Vec<[u8; 32]> = keys.iter().map(|(pk, _sk)| X25519::encaps(pk).unwrap().1).collect(); + + group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); + + group.bench_function("DHKEM(X25519, HKDF-SHA256)", |b| { + b.iter(|| { + for i in 0..NUM_ELEMS { + _ = black_box(X25519::decaps(&keys[i].1, &cts[i])); + } + }) + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_x25519_keygen, + bench_x25519_key_agreement, + bench_x25519_encaps, + bench_x25519_decaps, +); +criterion_main!(benches); diff --git a/crypto/ed/ed_todo.md b/crypto/ed/ed_todo.md new file mode 100644 index 000000000..d82f8355a --- /dev/null +++ b/crypto/ed/ed_todo.md @@ -0,0 +1,211 @@ +# bouncycastle-ed TODO + +Work for the `bouncycastle-ed` crate, in priority order. Each algorithm family goes through three +phases, in order, each in stacked commits: + +1. x25519 +2. x448 +3. ed25519 / ed448 + +Within each, the priority order is: + +1. **Functional**: correct output, passing the RFC and Wycheproof vectors. +2. **Traits**: implementing the appropriate traits from `crypto/core/src/traits.rs`. +3. **Polish**: docs, API ergonomics, and the extra APIs. + +--- + +# X25519 (RFC 7748) + +## Phase 1: Functional + +- [x] Add a Wycheproof suite `tests/x25519_wycheproof.rs` over `x25519_test.json`: 518 vectors, + 264 valid and 254 acceptable, 31 with an all-zero shared secret. (maybe need to be renamed / refactored to match other + crates) +- [x] Close the TODO comments in `x25519.rs` and `curve25519.rs` about RFC §5 top-bit masking and + non-canonical input; the Wycheproof suite now covers both. +- [x] Add the RFC 7748 §6.1 Diffie-Hellman vectors: Alice's and Bob's public keys and the shared + secret K. +- [x] Document `check_iterated` as the RFC 7748 §5.2 iterated test. +- [x] Label `vector_1`/`vector_2` as RFC 7748 §5.2, and drop the stale comment on `scalar_mult` + that said these vectors are not exercised. +- [x] Remove the unused imports (`bouncycastle_core`, `bouncycastle_rng`) from `x25519_tests.rs`. +- [x] Remove the commented-out `decode_hex` from `x25519_tests.rs`. +- [x] Review `FieldElement25519` in `curve25519.rs`. Ed25519 reuses it, so get it right here first: + - [x] Review every method marked `// TODO: Claude-generated, double-check`, plus + `decode_scalar_25519` and `scalar_mult` in `x25519.rs`. + - [x] Enforce the normalization precondition: typestate. `normalize` returns a + `NormalizedFieldElement25519`, which alone has `encode_255`, `is_zero`/`is_one`, `parity` + and (once restored) `are_equal`. + - [x] `normalize`'s own input bound: the value in `[0, 2^255)` (limbs may exceed 51 bits), + checked by a `debug_assert` at its top. Output range checked by `NormalizedFieldElement25519::debug_check_range`. + - [x] Identify `C_A` (486662) and `C_A24` and document them against RFC 7748. +- [x] Remove the dead code: `precompute()` is gone, and `P32`, `P64` and the methods X25519 does + not use are commented out; the crate builds with no warnings. +- [x] `cargo mutants -p bouncycastle-ed` (without Wycheproof). + +## Phase 2: Traits, Bells & Whistles + +- [x] Finish the `KeyAgreement` / `KeyAgreementPublicKey` / `KeyAgreementPrivateKey` traits in + `traits.rs`. They need: + - [x] key `from_bytes`/encoding, by analogy with `KEMPublicKey`/`KEMPrivateKey` (lengths as + const generics). The X25519 `KEMPublicKey`/`KEMPrivateKey` impls delegate to these. + - [x] the output type. Decided: `key_agreement` returns a `PK`, and the caller applies a KDF; + the KEM traits are the API that hands back a `KeyMaterial` shared secret. + - [x] an error type: `KeyAgreementError`. +- [x] Add X25519 key types: `X25519PrivateKey` (`Secret`-backed, zeroized, redacted `Debug`) and + `X25519PublicKey`, used by `X25519::key_agreement` and `X25519PrivateKey::pk`. +- [x] Add `struct X25519` (`Algorithm`). It and the key types are public through `pub mod x25519` + (no crate-root re-exports). +- [x] Implement the traits for X25519. Move the Wycheproof and RFC suites onto the trait API. +- [x] Re-enable the `agreement` round-trip test. Later removed as redundant: the RFC 7748 iterated + and Wycheproof known answers, and the KEM framework round trip, cover it. +- [x] Add a shared `KeyAgreement` conformance suite in `core-test-framework` and invoke it from + X25519. +- [x] Decide what happens on an all-zero shared secret (low-order public key): `key_agreement` + rejects it with `KeyAgreementError::InvalidPublicKey`. RFC 7748 §6.1 makes the check a MAY; + RFC 8446 §7.4.2 and RFC 9180 §7.1.4 make it a MUST. + - [x] Open: detect a small-order public key before the private-key operation, rather than + from the output. Is this even possible? Possible in constant-time? After analysis, while it's possible, there is + no reason to do this; it detects exactly the same bad public keys for no extra security. +- [x] A `KEM` impl over X25519: `KEMEncapsulator`/`KEMDecapsulator` on `X25519` as DHKEM (X25519, + HKDF-SHA256) (RFC 9180 §4.1), built on the generic `DHKEM` in the new `bouncycastle-hpke` + utility crate. Tested against RFC 9180 A.1.1/A.1.2 and the core-test-framework KEM suites. +- [x] Keygen, mirroring ML-KEM, on `X25519` with `KeyAgreementError` (added to `core`): + - [x] `keygen` (OS-seeded `HashDRBG_SHA512`) and `keygen_from_rng`; + - [x] `keygen_from_seed`, from a 32-byte `KeyMaterial256`; + - [x] `keypair_consistency_check`. No `keygen_from_seed_and_encoded`: the seed *is* the + private key encoding, so there is nothing to compare. +- [x] Wire into the cli (we are going to skip Factory): `bc-rust x25519` in `cli/src/x25519_cmd.rs`, + with keygen, pk-from-sk, check-consistency, agree, and DHKEM encaps/decaps. +- [x] `cargo mutants -p bouncycastle-ed` (without Wycheproof): 432 mutants, 327 caught, 95 unviable, + 2 timeouts (`sqr_n`'s loop counter, which never terminates: kills), 8 missed. Seven are `|` -> `^` + in `decode64_255`/`encode64_255`, where the OR'd operands have disjoint bits (equivalent). One is + the `KeyGenError` arm in the DHKEM `generate_key_pair_rng`, reachable only with an RNG whose + `fill_keymaterial_out` returns a wrongly-typed seed. Re-run on `x25519.rs` after the stored + public key and the ladder split: 158 mutants, 78 caught, 79 unviable, only that arm missed. + +## Phase 3: Docs and ergonomics + +- [x] `FieldElement25519` and its methods are `pub(crate)`. +- [x] Remove `calculate_agreement`; the tests and Wycheproof suite use `X25519::key_agreement`. +- [x] `FieldElement25519` cleanups: + - [x] `[Secret; 5]`? Block `Debug`? Prune the derive list. + - [x] In-place methods (`*_mut`, `cmov`, `cnegate`) return `()`, not `&mut Self`. + - [x] Do we need both value and `_mut` forms? No: only the value forms remain. + - [x] Construct from `[u64; 4]` / `Nat<4>` instead of `new(x0..x4)`. Moot: `new` is removed. + - [x] Rename `parity` (to `is_odd`?). + - [x] Remove the answered "why are we cloning?" TODO. +- [x] Docs: + - [x] Re-enable `#![forbid(missing_docs)]`. + - [x] Write the crate doc sections required by QUALITY_AND_STYLE.md: + - [x] Usage Examples (`lib.rs`, linking to the `x25519` module's examples); + - [x] Memory Usage (`x25519.rs` module docs); + - [x] Security Considerations (`x25519.rs` module docs). + - [x] Is XDH in a NIST document? No: RFC 7748 only. SP 800-186 specifies the curves but does not + list them for key establishment, and SP 800-56A Rev. 3 does not include XDH (`lib.rs` + `# Standards`). +- [x] Benches: criterion, plus a `mem_usage_benches` harness and a `# Memory Usage` docs section. + - [x] criterion: `benches/x25519_benches.rs` (KeyGen, KeyAgreement, Encaps, Decaps). + - [x] `mem_usage_benches/src/bench_x25519_mem_usage.rs`. Every row reads 7696 B, the harness + floor (the dynamic loader, before `main`); the operations peak at 2.2 to 3.0 kB. + - [x] memory-hygiene pass: `extract_and_expand` made `#[inline(never)]` (Encaps -656 B). + - [x] `# Memory Usage` docs section (`x25519.rs` module docs): key sizes, and the deepest frame + chain per entry point, as AES and Ascon do, since every operation is below the massif + harness floor. +- [x] `X25519PrivateKey` stores its public key (64 B in memory), computed once on keygen or decode: + `pk()` is free and Decaps no longer recomputes `pk(skR)` (about 48% faster). +- [x] Split the ladder out of `scalar_mult` into an `#[inline(never)]` `ladder`, so the field + inversion no longer runs on top of the ladder's registers: -416 B on KeyGen, KeyAgreement and + private-key decoding, no speed cost. Encaps/Decaps unchanged (HKDF sets their peak). +- [ ] Fast fixed-base `scalar_mult_base` (TODO in `x25519.rs`). Options: an Edwards comb table as + BC Java does (`Ed25519.scalarMultBaseYZ`, then u = (Z+Y)/(Z−Y); ~7.7 KB rodata, est. 2.5–3x + faster keygen, needs the Ed25519 point arithmetic), or a ladder specialised to u = 9 (no table, + est. ~10%). Probably best done with the Ed25519 phase's fixed-base comb tables. +- [x] Rethink the name of this crate: `bouncycastle-ed` is kindo the wrong name (but maybe nothing better exists?) + +--- + +# X448 (RFC 7748) + +Not started on this branch; earlier work is in `bc-rust-nursery/crates/{curve448,x448}`. Mostly a +twin of X25519 over the second field: start once X25519's Phase 2 has settled the traits and key +types, and carry each X25519 item across. Only the differences are listed here. + +## Phase 1: Functional + +- [ ] Twin `curve25519.rs` / `x25519.rs` as `curve448.rs` / `x448.rs`: field element mod + p = 2^448 − 2^224 − 1, 56-byte scalars and u-coordinates, and the same ladder. +- [ ] Differences from X25519 (RFC 7748 §5): + - [ ] `decodeScalar448` clears the two low bits and sets bit 447; no high bit to clear. + - [ ] No masking of the top bit of u: that rule is X25519-only. + - [ ] a24 = (156326 − 2) / 4 = 39081. +- [ ] Vectors: RFC 7748 §5.2 (single-call and iterated), §6.2, and Wycheproof `x448_test.json` + (510 vectors; its 12 `PublicKeyTooLong` cases have no X25519 counterpart). +- [ ] Field unit tests; `cargo mutants`. + +## Phase 2: Traits, Bells & Whistles + +- [ ] Same as X25519, for X448: key types, `KeyAgreement` impls (same all-zero-secret decision), + conformance suite, KEM if X25519 has one, keygen (56-byte seed), fixed-base scalar + multiplication, factory and CLI. + +## Phase 3: Docs and ergonomics + +- [ ] Same as X25519: docs, Wycheproof, benches and + `mem_usage_benches`. + +--- + +# Ed25519 / Ed448 (RFC 8032, FIPS 186-5) + +## Phase 1: Functional + +### Ed25519 + +- [ ] Port from `bc-rust-nursery/crates/{curve25519,ed25519}` onto the shared `FieldElement25519`: + Edwards point arithmetic, point encoding/decoding, and SHA-512 from `bouncycastle-sha2`. +- [ ] Move what X25519 shares with Ed25519 (e.g. curve constants) into `curve25519.rs`. Not + `ladder_double`: that is Montgomery u-only doubling, while Ed25519 doubles in extended + coordinates (RFC 8032 §5.1.4). +- [ ] Pure Ed25519: RFC 8032 §7.1 vectors. +- [ ] Ed25519ctx: RFC 8032 §7.2 vectors. +- [ ] Ed25519ph: RFC 8032 §7.3 vectors. +- [ ] Wycheproof `ed25519_test.json`, including the malleability and non-canonical-encoding + cases. +- [ ] `sqrt_ratio_var`: check whether it branches on secret data, and constrain its use to public + inputs (decoding during verification). +- [ ] `inv_var`: real variable-time inverse for public operands (verification); today it is the + constant-time `inv`. +- [ ] `pow_p_sub5_div8`: document the addition chain's source; consider an addition-chain helper. +- [ ] Unit tests for the point arithmetic; `cargo mutants`. + +### Ed448 + +- [ ] Port from `bc-rust-nursery/crates/{curve448,ed448}`, reusing the X448 field element and + SHAKE256 from `bouncycastle-sha3`. +- [ ] Ed448: RFC 8032 §7.4 vectors. +- [ ] Ed448ph: RFC 8032 §7.5 vectors. +- [ ] Wycheproof `ed448_test.json`. +- [ ] Unit tests; `cargo mutants`. + +## Phase 2: Traits + +- [ ] Ed25519/Ed448 key types implementing `SignaturePrivateKey` / `SignaturePublicKey`. +- [ ] `Signer` / `SignatureVerifier` for pure Ed25519/Ed448, including the context string for + Ed25519ctx and Ed448. +- [ ] `PHSigner` / `PHSignatureVerifier` for Ed25519ph / Ed448ph. +- [ ] Run the `core-test-framework` `signature.rs` suite against all variants; move the RFC and + Wycheproof suites onto the trait API. +- [ ] Factory and CLI wiring, following ML-DSA's (`cli/src/mldsa_cmd.rs`). + +## Phase 3: Docs and ergonomics + +- [ ] Deterministic keygen from a 32/57-byte seed, plus random keygen from an `RNG`. +- [ ] Expose the pre-expanded private key: the hashed seed, i.e. the clamped scalar s and the + prefix. Keygen, sign-from-expanded, and serialization, so repeated signing skips the hash. +- [ ] Precomputed / cached public-key decompression for repeated verification. +- [ ] Fixed-base comb tables for keygen and signing; batch verification if wanted. +- [ ] Docs: the crate doc sections; cite FIPS 186-5 §7 alongside RFC 8032 (verify section numbers + against a downloaded copy). +- [ ] Benches: criterion, plus `mem_usage_benches`. diff --git a/crypto/ed/src/curve25519.rs b/crypto/ed/src/curve25519.rs new file mode 100644 index 000000000..d24324519 --- /dev/null +++ b/crypto/ed/src/curve25519.rs @@ -0,0 +1,832 @@ +use bouncycastle_utils::ct::Condition; + +/// Mask for one limb: `2^51 - 1`. A [`FieldElement25519`] holds the value +/// `x0 + x1*2^51 + x2*2^102 + x3*2^153 + x4*2^204`, so `& M51` extracts a limb's own 51 bits and +/// leaves the overflow to be carried into the next limb (or, out of `x4`, folded back into `x0` +/// scaled by 19, since `2^255 == 19 mod p`). +const M51: i64 = (1 << 51) - 1; + +// /// The field prime `p = 2^255 - 19` as eight little-endian 32-bit words, least significant first. +// /// +// /// Currently referenced nowhere in the crate; [`FieldElement25519::normalize`] reduces via the +// /// `* 19` fold rather than by comparing against `p`. +// /// +// /// Scope: neither -- dead as of today. +// // TODO: remove? +// const P32: [u32; 8] = [ +// 0xFFFFFFED, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0x7FFFFFFF, +// ]; +// /// The field prime `p = 2^255 - 19` as four little-endian 64-bit words, least significant first. +// /// Note the low word is `...FFED` = `-19 mod 2^64`, and the top word has bit 63 clear, giving +// /// `2^255 - 19` rather than `2^256 - 19`. +// /// +// /// Referenced only by the commented-out `modular::mod_odd_inverse` form of `inv`, so it is dead +// /// until inversion is restored. +// /// +// /// Scope: both -- once inversion lands. +// // TODO: remove? +// const P64: [u64; 4] = +// [0xFFFFFFFFFFFFFFED, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0x7FFFFFFFFFFFFFFF]; + +/// An element of GF(p), p = 2^255 - 19: the field that curve25519's point coordinates live in. +/// +/// This is the base field, as opposed to the scalar field of integers mod the group order that +/// private keys live in. Every coordinate in this crate is one of these: the u-coordinate of +/// X25519 (RFC7748), and the x and y coordinates of Ed25519 (RFC8032). +/// +/// # Representation +/// Five signed 64-bit limbs in radix 2^51, holding the value +/// `x0 + x1*2^51 + x2*2^102 + x3*2^153 + x4*2^204` (see [`M51`]). Arithmetic is lazily reduced: +/// limbs may exceed 51 bits or go negative between operations, so the same residue mod p has many +/// representations. [`Self::normalize`] produces the canonical representation as a +/// [`NormalizedFieldElement25519`], the only type that encodes, compares and tests parity. +/// +/// # Mathematical operations implemented +/// The field operations: addition, subtraction and negation, multiplication and squaring, +/// inversion, and the square root of a ratio. Constant-time conditional move, negate, select and +/// swap. Encoding to and decoding from 32 bytes. Everything is constant time except the methods +/// suffixed `_var`, which are for public inputs only. +/// +/// # Math Background +/// The group law on elliptic curve points is point addition, where A + B yields a new point C on +/// the curve. The special case of adding a point to itself, A + A, is called "point doubling" and +/// is often implemented separately because it can be done more efficiently than the general case. +/// Repeated addition, A + A + ... + A, is written k * A for a scalar k and called "scalar +/// multiplication", again implemented separately. All three are built out of the field operations +/// on this type. +/// +/// X25519 represents a point in Montgomery form by its u-coordinate alone, which identifies a +/// point on the curve or its quadratic twist up to sign; ie it names the pair {P, -P}. Point +/// doubling and scalar multiplication are well-defined on u-coordinates, since they commute with +/// negation (k*(-A) = -(k*A)), but general point addition is not. XDH (RFC7748) only needs scalar +/// multiplication, which the Montgomery ladder in [`crate::x25519`] computes. Ed25519 uses the full +/// (x, y) coordinates on the equivalent twisted Edwards curve, where general addition is defined. +#[derive(Clone, Default)] +#[must_use] // to help catch bugs that x.sqr() actually returns a result rather than mutates x, so not using the return is a bug. +pub(crate) struct FieldElement25519([i64; 5]); + +impl FieldElement25519 { + /// Add another [`FieldElement25519`] to this one, component-wise. + /// + /// Lazily reduced: no carry propagation, so limb magnitudes grow by up to one bit and the result + /// is congruent mod p but not normalized. That is deliberate -- every consumer here funnels into + /// a [`Self::mul`], [`Self::sqr`] or [`Self::mul_u32`], each of which reduces through + /// [`Self::reduce_products`], so magnitudes never run away and an explicit carry pass is not + /// needed. + pub(crate) const fn add(&self, rhs: &Self) -> Self { + let [x0, x1, x2, x3, x4] = self.0; + let [y0, y1, y2, y3, y4] = rhs.0; + Self([x0 + y0, x1 + y1, x2 + y2, x3 + y3, x4 + y4]) + } + + /// Returns `(self + rhs, self - rhs)` in one pass over the limbs. + /// + /// Each half is the plain limb-wise [`Self::add`] / [`Self::sub`], with the same lazy-reduction + /// caveat: no carry propagation, so limbs may go negative and both results are congruent mod p + /// but not normalized. Fusing them avoids reading the limbs twice, and matches the fact that + /// the ladder always wants the sum and the difference of the same pair. + /// + /// This is Section 5 of RFC7748's `A = x_2 + z_2` / `B = x_2 - z_2` pair, and equally its + /// `C = x_3 + z_3` / `D = x_3 - z_3`. It is the workhorse of the ladder in [`crate::x25519`]. + /// + /// Note on the name: RFC7748 does not name this operation -- it writes the two assignments out + /// separately, so there is no spec name to match. + pub(crate) const fn sum_and_difference(&self, rhs: &Self) -> (Self, Self) { + let [x0, x1, x2, x3, x4] = self.0; + let [y0, y1, y2, y3, y4] = rhs.0; + ( + Self([x0 + y0, x1 + y1, x2 + y2, x3 + y3, x4 + y4]), + Self([x0 - y0, x1 - y1, x2 - y2, x3 - y3, x4 - y4]), + ) + } + + // /// Add one to self, i.e. add 1 to limb `x0`, returning a new [`FieldElement25519`]. + // /// + // /// One is representable exactly in the low limb, so no carry or fold is involved; as with + // /// [`Self::add`] the result is congruent mod p but not normalized. + // /// + // /// Scope: Ed25519 -- unused today. The birational map of Section 4.1 of RFC7748, + // /// `y = (u - 1)/(u + 1)`, is what wants `add_one` and [`Self::sub_one`]; the X25519 ladder never + // /// adds a constant. + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn add_one(&self) -> Self { + // let [x0, x1, x2, x3, x4] = self.0; + // Self([x0 + 1, x1, x2, x3, x4]) + // } + + // /// [`Self::add_one`] in place. + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) fn add_one_mut(&mut self) { + // self.0[0] += 1; + // } + + // /// Carry each limb's overflow above 51 bits into the next limb up. + // /// The overflow out of the top limb `x4` has weight `2^255`, so it wraps around + // /// into `x0` multiplied by 19, since `2^255 == 19 mod p`. + // /// + // /// This is a weak reduction: the value is unchanged mod p, but the result is not normalized -- it + // /// may be `>= p`. Use [`Self::normalize`] for the canonical representation. + // /// + // /// All five carries are taken from the input limbs at once, rather than rippling up from `x0` as + // /// in [`Self::reduce`]. That keeps the five steps independent, so the longest dependency chain is + // /// about three instructions (shift, `* 19`, add) rather than a ripple's ten or so, which an + // /// out-of-order CPU can overlap; it also lets LLVM auto-vectorize limbs 1..4. The cost is that + // /// each output limb is only bounded by `2^51` plus the carry it received: for input limbs below + // /// `2^63` in magnitude a carry lies in `[-2^12, 2^12)`, so `z1..z4` lie in + // /// `[-2^12, 2^51 + 2^12)` and `z0` in `[-19 * 2^12, 2^51 + 19 * 2^12)`. + // /// + // /// `>> 51` is an arithmetic shift, i.e. floor division by `2^51`, and `& M51` the matching + // /// non-negative remainder, so a negative limb borrows from its neighbour correctly. A borrow can + // /// leave the next limb negative, so the output limbs are not guaranteed non-negative. + // pub(crate) const fn carry(&self) -> Self { + // let [x0, x1, x2, x3, x4] = self.0; + // let c1 = x0 >> 51; + // let c2 = x1 >> 51; + // let c3 = x2 >> 51; + // let c4 = x3 >> 51; + // let c5 = x4 >> 51; + // let z0 = (x0 & M51) + c5 * 19; + // let z1 = (x1 & M51) + c1; + // let z2 = (x2 & M51) + c2; + // let z3 = (x3 & M51) + c3; + // let z4 = (x4 & M51) + c4; + // Self([z0, z1, z2, z3, z4]) + // } + + // /// Conditionally move the coordinates of x into self if `cond` is true. + // /// + // /// Constant-time. + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) fn cmov(&mut self, cond: Condition, x: &Self) { + // // *self = Self::cselect(cond, x, self); + // cond.mov(x.0[0], &mut self.0[0]); + // cond.mov(x.0[1], &mut self.0[1]); + // cond.mov(x.0[2], &mut self.0[2]); + // cond.mov(x.0[3], &mut self.0[3]); + // cond.mov(x.0[4], &mut self.0[4]); + // } + + // /// Returns a copy of lhs if `cond` is true, else a copy of rhs. + // /// + // /// Constant time. + // /// + // /// Scope: Ed25519 -- unused today; see [`Self::cmov`]. + // // TODO -- add back once we get to implementing Ed25519 + // // TODO -- do we really need this? Seems like this is equivalent to lhs.cmov(cond, rhs); Do we really need both? + // pub(crate) fn cselect(cond: Condition, lhs: &Self, rhs: &Self) -> Self { + // let [x0, x1, x2, x3, x4] = lhs.0; + // let [y0, y1, y2, y3, y4] = rhs.0; + // let z0 = cond.select(x0, y0); + // let z1 = cond.select(x1, y1); + // let z2 = cond.select(x2, y2); + // let z3 = cond.select(x3, y3); + // let z4 = cond.select(x4, y4); + // Self([z0, z1, z2, z3, z4]) + // } + + // /// Negate the components of self if `cond` is true. + // /// + // /// Constant time. + // // TODO -- add back once we get to implementing Ed25519 + // // TODO -- seems weird to negate the limbs instead of negating only the leading digit + // pub(crate) fn cnegate(&mut self, cond: Condition) { + // self.0[0] = cond.negate(self.0[0]); + // self.0[1] = cond.negate(self.0[1]); + // self.0[2] = cond.negate(self.0[2]); + // self.0[3] = cond.negate(self.0[3]); + // self.0[4] = cond.negate(self.0[4]); + // } + + /// Conditionally Swap the values of lhs and rhs if `cond` is true. + /// + /// Constant time: both limb arrays are rewritten unconditionally with a masked exchange, so + /// nothing about `cond` -- and hence nothing about the scalar bit driving it -- leaks through + /// memory access patterns or timing. + pub(crate) fn cswap(cond: Condition, lhs: &mut Self, rhs: &mut Self) { + (lhs.0[0], rhs.0[0]) = cond.swap(lhs.0[0], rhs.0[0]); + (lhs.0[1], rhs.0[1]) = cond.swap(lhs.0[1], rhs.0[1]); + (lhs.0[2], rhs.0[2]) = cond.swap(lhs.0[2], rhs.0[2]); + (lhs.0[3], rhs.0[3]) = cond.swap(lhs.0[3], rhs.0[3]); + (lhs.0[4], rhs.0[4]) = cond.swap(lhs.0[4], rhs.0[4]); + } + + /// Decodes 32 little-endian bytes as a field element, ignoring bit 255 (the most significant + /// bit of the final byte); the inverse of [`NormalizedFieldElement25519::encode_255`]. + /// + /// This is the RFC-neutral byte layout shared by both protocols. Callers own the meaning of + /// bit 255 and the policy on non-canonical input: X25519 ignores the bit and accepts values + /// `>= p` (`decode_u_coordinate` in `x25519.rs`, RFC7748 Section 5), while Ed25519 reads the bit + /// as the sign of x and rejects a y-coordinate `>= p` (RFC8032 Section 5.1.3). + /// + /// Note: Result is NOT normalized -- the value is in `[0, 2^255)`, so the 19 encodings of + /// `2^255 - 19 .. 2^255 - 1` decode to values `>= p`. Call [`Self::normalize`] before comparing + /// or encoding. + /// + /// Input: 32 bytes, read as a little-endian integer. + /// Output: The low 255 bits of that integer as a [`FieldElement25519`]. + pub(crate) const fn decode_255(bytes: &[u8; 32]) -> Self { + /// Unpacks a 256-bit little-endian integer held as four 64-bit words into the five 51-bit limbs + /// of a [`FieldElement25519`], where the represented value is + /// `z0 + z1*2^51 + z2*2^102 + z3*2^153 + z4*2^204`. Each limb is a 51-bit window of the input, + /// straddling word boundaries; the `& M51` masks make every limb non-negative and leave headroom + /// for lazily-carried arithmetic. Bit 255 of the input is dropped -- see [`Self::decode_255`]. + /// Inverse of [`NormalizedFieldElement25519::encode64_255`]. + /// Input: A 256-bit integer as four little-endian 64-bit words, least significant word first. + /// Output: Bits 0..254 of that integer as a [`FieldElement25519`]. + /// + /// Note: Result is NOT normalized. + const fn decode64_255(x: &[u64; 4]) -> FieldElement25519 { + let [x0, x1, x2, x3] = *x; + let z0 = x0 as i64 & M51; + let z1 = (x0 >> 51 | x1 << 13) as i64 & M51; + let z2 = (x1 >> 38 | x2 << 26) as i64 & M51; + let z3 = (x2 >> 25 | x3 << 39) as i64 & M51; + let z4 = (x3 >> 12) as i64 & M51; + FieldElement25519([z0, z1, z2, z3, z4]) + } + + let b = bytes; + let x0 = u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]); + let x1 = u64::from_le_bytes([b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]]); + let x2 = u64::from_le_bytes([b[16], b[17], b[18], b[19], b[20], b[21], b[22], b[23]]); + let x3 = u64::from_le_bytes([b[24], b[25], b[26], b[27], b[28], b[29], b[30], b[31]]); + decode64_255(&[x0, x1, x2, x3]) + } + + /// Multiplicative inverse in GF(p), by Fermat's little theorem: `x^(p-2) == x^-1`. + /// + /// [`Self::pow_p_sub5_div8`] returns `(x^3, x^((p-5)/8))`. Raising the second to the 8th and + /// multiplying by the first gives exponent `8 * (2^252 - 3) + 3 == 2^255 - 21 == p - 2`. + /// Costs 254 squarings and 11 multiplies. + /// + /// Constant time: a fixed addition chain, with no data-dependent branch or index. + /// + /// `inv(0) == 0`, since `0^(p-2) == 0`. That is not a case to guard against -- X25519 depends + /// on it. A small-order input point drives the ladder's `z_2` to zero, and Section 6.1 of + /// RFC7748 requires the resulting all-zero output to be detectable: "the X25519 function + /// produces that value if it operates on an input corresponding to a point with small order". + /// Returning zero here is what carries that through the final `x_2 * z_2^-1`. + /// + /// Verified against a reference modular inverse over 305 values, edge cases included. + /// + /// Scope: both x25519 and ed25519. + // TODO -- a safegcd-style `mod_odd_inverse` would be asymptotically cheaper, and P64 above is + // the constant it wants. Only worth revisiting if this shows up in a profile -- it is + // one call per scalar_mult. + pub(crate) const fn inv(&self) -> Self { + let (x_cubed, t) = self.pow_p_sub5_div8(); + t.sqr_n(3).mul(&x_cubed) + } + + /// Multiply self by rhs in GF(p), `p = 2^255 - 19`. + /// + /// Schoolbook 5x5: output limb `k` collects every `x[i] * y[j]` with `i + j == k`, plus every + /// pair with `i + j == k + 5`. The second set lands at weight `2^255` or above, and + /// `2^255 == 19 mod p`, so those terms are pre-scaled by 19 -- that is what `t1..t4` are. The 25 + /// products accumulate in `i128` (a 51-bit by 56-bit product would overflow `i64`) and + /// [`Self::reduce_products`] carries them back down to five `i64` limbs. + pub(crate) const fn mul(&self, rhs: &Self) -> Self { + #[inline(always)] + const fn m(x: i64, y: i64) -> i128 { + x as i128 * y as i128 + } + + let [x0, x1, x2, x3, x4] = self.0; + let [y0, y1, y2, y3, y4] = rhs.0; + let [t1, t2, t3, t4] = [y1 * 19, y2 * 19, y3 * 19, y4 * 19]; + + Self::reduce_products( + m(x0, y0) + m(x1, t4) + m(x2, t3) + m(x3, t2) + m(x4, t1), + m(x0, y1) + m(x1, y0) + m(x2, t4) + m(x3, t3) + m(x4, t2), + m(x0, y2) + m(x1, y1) + m(x2, y0) + m(x3, t4) + m(x4, t3), + m(x0, y3) + m(x1, y2) + m(x2, y1) + m(x3, y0) + m(x4, t4), + m(x0, y4) + m(x1, y3) + m(x2, y2) + m(x3, y1) + m(x4, y0), + ) + } + + /// Multiply self by a small unsigned scalar in GF(p). + /// + /// A single-limb multiply, so unlike [`Self::mul`] there are no wrap-around terms to pre-scale by + /// 19 -- each output limb takes exactly one product. The `19` fold still happens, but only for + /// whatever carries out of the top limb inside [`Self::reduce_products`]. + /// + /// Scope: X25519. This is the `a24 * E` of the Section 5 ladder, with + /// `a24 = (486662 - 2) / 4 = 121665`. + pub(crate) const fn mul_u32(&self, rhs: u32) -> Self { + #[inline(always)] + const fn m(x: i64, y: i64) -> i128 { + x as i128 * y as i128 + } + + let [x0, x1, x2, x3, x4] = self.0; + let y0 = rhs as i64; + + Self::reduce_products(m(x0, y0), m(x1, y0), m(x2, y0), m(x3, y0), m(x4, y0)) + } + + // /// Negates self, component-wise. + // /// + // /// Negating every limb negates the value, since the limb weights `2^(51*i)` are all positive. + // /// Lazily reduced, and note the result has negative limbs, which is legal in this representation + // /// but means it is *not* normalized -- [`Self::normalize`] must run before any encode or + // /// comparison. + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn negate(&self) -> Self { + // let [x0, x1, x2, x3, x4] = self.0; + // Self([-x0, -x1, -x2, -x3, -x4]) + // } + + /// Reduce self to the unique canonical representative of its residue class: limbs all in + /// `[0, 2^51)` and the value in `[0, p)`. + /// + /// Idempotent. + /// + /// Two [`Self::reduce`] passes, `reduce(1)` then `reduce(-1)`: the first adds 19 at the bottom + /// and the second removes it, with no matching change at the top. Neither pass alone is + /// congruent mod p, but the `+19` and `-19` cancel exactly, leaving `value - (c + c') * p` for + /// the two top-carry folds `c`, `c'`. + /// + /// The `+19` makes the first pass's top carry fire exactly when the value is `>= p`, since + /// `v >= p` if and only if `v + 19 >= 2^255`. This is what implements Section 5 of RFC7748's + /// "Implementations MUST accept non-canonical values and process them as if they had been + /// reduced modulo the field prime. The non-canonical values are 2^255 - 19 through 2^255 - 1". + /// The `normalize_edge_values` unit test pins `p - 1`, `p` and `2^255 - 1`. + /// + /// Requires the value in `[0, 2^255)`, which is what [`Self::decode_255`] produces. Individual + /// limbs may exceed 51 bits: [`Self::mul`] can return a limb of exactly `2^51` (Wycheproof + /// X25519 tcId 115 and 116), which the carry chain in [`Self::reduce`] handles. Checked by a + /// `debug_assert` in debug builds; the [`NormalizedFieldElement25519`] typestate cannot enforce + /// it. + pub(crate) const fn normalize(&self) -> NormalizedFieldElement25519 { + // Carry the limbs through without folding: the value is in [0, 2^255) exactly when the + // carried top limb is in [0, 2^51). + let [x0, x1, x2, x3, x4] = self.0; + debug_assert!( + { + let c = ((((x0 >> 51) + x1) >> 51) + x2) >> 51; + let top = ((c + x3) >> 51) + x4; + 0 <= top && top <= M51 + }, + "normalize: value out of [0, 2^255)" + ); + + NormalizedFieldElement25519(self.reduce(1).reduce(-1)) + } + + /// The multiplicative identity, 1, in canonical form. + pub(crate) const fn one() -> Self { + Self([1, 0, 0, 0, 0]) + } + + /// Returns `(x^3, x^((p-5)/8))` for `x = self`, by a fixed addition chain. + /// + /// `(p - 5) / 8 == 2^252 - 3` for `p = 2^255 - 19`, which in binary is 250 ones, a zero, then a + /// one -- the `FFFF..FD` of the comment below. The chain reaches it in 10 steps by doubling runs + /// of 1-bits. Both return values are exponent-verified. + /// + /// Beware the naming: the locals count *consecutive 1-bits*, not exponents. `x2` is `x^3` + /// (`0b11`, two ones), `x3` is `x^7`, `x5` is `x^31`, and `x250` is `x^(2^250 - 1)`. It reads + /// like an off-by-one bug and is not one. + /// + /// Both consumers need the pair: + /// * inversion, as `z.sqr_n(3).mul(&x2)` -- exponent `8 * (2^252 - 3) + 3 == 2^255 - 21 == p - 2`, + /// i.e. Fermat. + /// * [`Self::sqrt_ratio_var`], which uses `x^((p-5)/8)` directly as the `p == 5 mod 8` square-root + /// candidate. + const fn pow_p_sub5_div8(&self) -> (Self, Self) { + // z = x^((p-5)/8) = x^FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD + // (250 1s) (1 0s) (1 1s) + // Addition chain: [1] 2 3 5 10 15 25 50 75 125 [250] + + let x = self; + let x2 = x.sqr().mul(x); + let x3 = x2.sqr().mul(x); + let x5 = x3.sqr_n(2).mul(&x2); + let x10 = x5.sqr_n(5).mul(&x5); + let x15 = x10.sqr_n(5).mul(&x5); + let x25 = x15.sqr_n(10).mul(&x10); + let x50 = x25.sqr_n(25).mul(&x25); + let x75 = x50.sqr_n(25).mul(&x25); + let x125 = x75.sqr_n(50).mul(&x50); + let x250 = x125.sqr_n(125).mul(&x125); + + let z = x250.sqr_n(2).mul(x); + (x2, z) + } + + /// One carry pass that additionally subtracts `(x4 >> 51)` multiples of p and adds `19 * y` at + /// the bottom. Not a standalone reduction -- see the warning below. + /// + /// Bits at or above `2^255` (the `x4 >> 51` carry) are removed from the top and re-added at the + /// bottom scaled by 19, which subtracts a multiple of `p = 2^255 - 19`. The `y` argument is added + /// into that same fold, so it contributes `19 * y` at the bottom with *no* matching removal at + /// the top. The verified identity is: + /// + /// ```text + /// value(reduce(x, y)) == value(x) - (x4 >> 51) * p + 19 * y + /// ``` + /// + /// Warning: a single `reduce` is therefore **not** congruent to its input mod p unless `y == 0`. + /// The `19 * y` is a deliberate offset that only cancels across the pair of calls in + /// [`Self::normalize`], and this function exists to serve that pair -- it is not a general-purpose + /// reduction. + /// + /// Requires limbs inside the magnitude bound the carry chain assumes: `t5 * 19 + x0` must not + /// overflow `i64`. + const fn reduce(&self, y: i64) -> Self { + let [x0, x1, x2, x3, x4] = self.0; + + let t4 = x4 & M51; + let t5 = (x4 >> 51) + y; + + let mut c = t5 * 19; + + c += x0; + let z0 = c & M51; + c >>= 51; + c += x1; + let z1 = c & M51; + c >>= 51; + c += x2; + let z2 = c & M51; + c >>= 51; + c += x3; + let z3 = c & M51; + c >>= 51; + c += t4; + let z4 = c; + + Self([z0, z1, z2, z3, z4]) + } + + /// Carry the five `i128` product accumulators produced by [`Self::mul`], [`Self::sqr`] and + /// [`Self::mul_u32`] back down into five `i64` limbs. + /// + /// One pass: `split` peels each accumulator into its own 51 bits and the carry above them, the + /// carries move up one limb each, and the carry out of the top (`c5`, at weight `2^255`) folds + /// into limb 0 scaled by 19 because `2^255 == 19 mod p`. A second short carry chain over + /// `u0..u3` absorbs the fold. + /// + /// The result is congruent mod p with limbs near 51 bits, but is *not* normalized -- it may still + /// exceed p. Call [`Self::normalize`] before encoding or comparing. + const fn reduce_products(p0: i128, p1: i128, p2: i128, p3: i128, p4: i128) -> Self { + #[inline(always)] + const fn split(x: i128) -> (i64, i64) { + (x as i64 & M51, (x >> 51) as i64) + } + + let (t0, c1) = split(p0); + let (t1, c2) = split(p1); + let (t2, c3) = split(p2); + let (t3, c4) = split(p3); + let (t4, c5) = split(p4 + c4 as i128); + + let u0 = t0 + c5 * 19; + let u1 = t1 + c1; + let u2 = t2 + c2; + let u3 = t3 + c3; + + Self([ + u0 & M51, + (u1 & M51) + (u0 >> 51), + (u2 & M51) + (u1 >> 51), + (u3 & M51) + (u2 >> 51), + t4 + (u3 >> 51), + ]) + } + + /// Square self in GF(p). + /// + /// Same shape as [`Self::mul`], but a squaring's cross terms come in equal pairs, so pre-doubling + /// `x0..x3` into `y0..y3` lets one product stand for two and drops the count from 25 to 15. The + /// `19` fold for the `i + j >= 5` terms is folded into `t3`, `t4` as before. + pub(crate) const fn sqr(&self) -> Self { + #[inline(always)] + const fn m(x: i64, y: i64) -> i128 { + x as i128 * y as i128 + } + + let [x0, x1, x2, x3, x4] = self.0; + let [y0, y1, y2, y3] = [x0 * 2, x1 * 2, x2 * 2, x3 * 2]; + let [t3, t4] = [x3 * 19, x4 * 19]; + + Self::reduce_products( + m(x0, x0) + m(y1, t4) + m(y2, t3), + m(y0, x1) + m(y2, t4) + m(x3, t3), + m(y0, x2) + m(x1, x1) + m(y3, t4), + m(y0, x3) + m(y1, x2) + m(x4, t4), + m(y0, x4) + m(y1, x3) + m(x2, x2), + ) + } + + /// Square self `n` times, i.e. raise to the `2^n`. + /// Declared const to enable pre-computed tables to be built at compile-time. + pub(crate) const fn sqr_n(&self, mut n: usize) -> Self { + let mut z = Self(self.0); + while n > 0 { + z = z.sqr(); + n -= 1; + } + z + } + + // /// Variable-time square root of the ratio `u / v`: returns `Some(x)` with `x^2 == u / v` if that + // /// ratio is a quadratic residue, else `None`. + // /// + // /// The standard `p == 5 mod 8` method (p mod 8 is 5, checked), arranged to need no inversion: the + // /// candidate is `x = u * v^3 * (u * v^7)^((p-5)/8)`, and `v * x^2` is then compared against `+u` + // /// and `-u` to sort the three cases. If `v * x^2 == -u`, the true root is `x` times a square root + // /// of -1, which is the `ROOT_NEG_ONE` constant -- verified to square to `-1 mod p`, and to equal + // /// `2^((p-1)/4)`. + // /// + // /// Verified over 4,000 random `(u, v)`: every returned root satisfies `x^2 == u / v`, and every + // /// `None` is a genuine non-residue. + // /// + // /// Variable time by construction. + // /// That is acceptable for point decompression, which runs on public data, and it is why the + // /// name carries the `_var` suffix. + // // TODO -- double-check the variable-time assumption + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn sqrt_ratio_var(u: &Self, v: &Self) -> Option { + // let uv = u.mul(v); + // let v2 = v.sqr(); + // let uv3 = v2.mul(&uv); + // let uv7 = v2.sqr().mul(&uv3); + // + // let (_, w) = uv7.pow_p_sub5_div8(); + // let x = w.mul(&uv3); + // let vx2 = x.sqr().mul(v); + // + // let t = vx2.sub(u).normalize(); + // + // if t.is_zero().to_bool() { + // return Some(x); + // } + // + // let t = vx2.add(u).normalize(); + // if t.is_zero().to_bool() { + // const ROOT_NEG_ONE: FieldElement25519 = FieldElement25519([ + // -0x0001E4D8B5F15F50, 0x0000D5A5FC8F189E, -0x000010A16342F3A0, -0x00007A6A597FB361, + // 0x0002B8324804FC1E, + // ]); + // + // return Some(x.mul(&ROOT_NEG_ONE)); + // } + // + // None + // } + + /// Subtract rhs from self, component-wise. + /// + /// Lazily reduced, exactly as [`Self::add`]: no borrow propagation, so limbs may go negative and + /// the result is congruent mod p but not normalized. Negative limbs are fine here -- + /// [`Self::reduce_products`] and [`Self::reduce`] both use arithmetic (sign-propagating) shifts, + /// so a negative limb borrows correctly on the next reduction. + pub(crate) const fn sub(&self, rhs: &Self) -> Self { + let [x0, x1, x2, x3, x4] = self.0; + let [y0, y1, y2, y3, y4] = rhs.0; + Self([x0 - y0, x1 - y1, x2 - y2, x3 - y3, x4 - y4]) + } + + // /// Subtract one from self, i.e. subtract 1 from limb `x0`, returning a new + // /// [`FieldElement25519`]. + // /// + // /// Mirror of [`Self::add_one`]; if `x0` was zero the limb simply goes negative, which the next + // /// reduction resolves. + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn sub_one(&self) -> Self { + // let [x0, x1, x2, x3, x4] = self.0; + // Self([x0 - 1, x1, x2, x3, x4]) + // } + + /// The additive identity, 0, in canonical form. + pub(crate) const fn zero() -> Self { + Self([0; 5]) + } +} + +/// A [`FieldElement25519`] in canonical form: every limb in `[0, 2^51)` and the value in `[0, p)`. +/// The only constructor is [`FieldElement25519::normalize`]. +/// +// TODO -- impl Deref once a caller needs to continue arithmetic on a +// normalized value (e.g. Ed25519 point decompression). +#[derive(Clone)] +#[must_use] +pub(crate) struct NormalizedFieldElement25519(FieldElement25519); + +impl NormalizedFieldElement25519 { + /// Debug-only check of the invariant this type carries: every limb in `[0, 2^51)` and the + /// value below p. Every method calls it first; it compiles to nothing in release builds. + /// + /// p's limbs are `[2^51 - 19, 2^51 - 1, 2^51 - 1, 2^51 - 1, 2^51 - 1]`, so a value with every + /// limb in range is `>= p` exactly when limbs 1..4 are all `2^51 - 1` and limb 0 is at least + /// `2^51 - 19`. + /// + /// Branches on the limbs, so it is not constant time. That is acceptable only because it is + /// compiled out of release builds. + const fn debug_check_range(&self) { + let [x0, x1, x2, x3, x4] = self.0.0; + debug_assert!(0 <= x0 && x0 <= M51, "limb 0 out of [0, 2^51)"); + debug_assert!(0 <= x1 && x1 <= M51, "limb 1 out of [0, 2^51)"); + debug_assert!(0 <= x2 && x2 <= M51, "limb 2 out of [0, 2^51)"); + debug_assert!(0 <= x3 && x3 <= M51, "limb 3 out of [0, 2^51)"); + debug_assert!(0 <= x4 && x4 <= M51, "limb 4 out of [0, 2^51)"); + debug_assert!( + !(x1 == M51 && x2 == M51 && x3 == M51 && x4 == M51 && x0 >= M51 - 18), + "value >= p" + ); + } + + // /// Constant-time equality of two field elements. + // /// + // /// Compares representations, not residue classes, so it is only correct on the normalized + // /// form: a value and that value plus p have different limbs. + // /// + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn are_equal(lhs: &Self, rhs: &Self) -> Condition { + // lhs.debug_check_range(); + // rhs.debug_check_range(); + // let [x0, x1, x2, x3, x4] = lhs.0.0; + // let [y0, y1, y2, y3, y4] = rhs.0.0; + // Condition::::is_zero((x0 ^ y0) | (x1 ^ y1) | (x2 ^ y2) | (x3 ^ y3) | (x4 ^ y4)) + // } + + /// Encodes the field element as a 255-bit value in 32 little-endian bytes; the inverse of + /// [`FieldElement25519::decode_255`]. Bit 255 (the most significant bit of the last byte) is + /// always written as zero. + /// + /// Callers that need that bit to carry something else -- the + /// sign of x in an Ed25519 point encoding (Section 5.1.3 of RFC 8032) -- must OR it in after. + /// + /// Input: self; a 32-byte output buffer. + /// Output: The value written to `bytes` as a 255-bit little-endian integer. + pub(crate) fn encode_255(&self, bytes: &mut [u8; 32]) { + self.debug_check_range(); + let [w0, w1, w2, w3] = self.encode64_255(); + let (a, b, c, d) = (w0.to_le_bytes(), w1.to_le_bytes(), w2.to_le_bytes(), w3.to_le_bytes()); + // Each word fills a fixed 8-byte window, least significant word first. Every index is a + // constant into a fixed-size array, so an out-of-bounds index would be a compile error (the + // deny-by-default `unconditional_panic` lint) and no runtime bounds check survives. + *bytes = [ + a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], // + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], // + c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], // + d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], + ]; + } + + /// Packs the five 51-bit limbs into a 255-bit little-endian integer held as four 64-bit words, + /// each word taking a whole limb plus the low bits of the next. Inverse of the `decode64_255` + /// step inside [`FieldElement25519::decode_255`]. Bit 255 of the result is always zero, since + /// limb `x4` lands in bits 204..254. + /// + /// Relies on the normalized form: every limb in [0, 2^51) and the value reduced mod p. The + /// splices below are `|`, not `+`, so a limb with bits at or above 51 collides with the next + /// limb's bits instead of carrying into them, and the result is garbage. (The `x1 << 51` etc. + /// deliberately shift bits off the top of the i64 -- those are the bits the previous word + /// already carried.) + /// + /// Input: self. + /// Output: The value as four little-endian 64-bit words, least significant word first. + const fn encode64_255(&self) -> [u64; 4] { + self.debug_check_range(); + let [x0, x1, x2, x3, x4] = self.0.0; + let z0 = (x0 | x1 << 51) as u64; + let z1 = (x1 >> 13 | x2 << 38) as u64; + let z2 = (x2 >> 26 | x3 << 25) as u64; + let z3 = (x3 >> 39 | x4 << 12) as u64; + [z0, z1, z2, z3] + } + + // /// Constant-time test for the value 1: `is_zero` over `(x0 ^ 1) | x1 | x2 | x3 | x4`. + // /// + // /// Correct only on the normalized form: only the canonical representative of 1 has limbs + // /// `[1, 0, 0, 0, 0]`. + // /// + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn is_one(&self) -> Condition { + // self.debug_check_range(); + // let [x0, x1, x2, x3, x4] = self.0.0; + // let x0 = x0 ^ 1; + // Condition::::is_zero(x0 | x1 | x2 | x3 | x4) + // } + + // /// Constant-time test for the value 0: `is_zero` over the OR of all five limbs. + // /// + // /// Correct only on the normalized form: an un-normalized zero can be represented as p, whose + // /// limbs are not all zero. + // /// + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn is_zero(&self) -> Condition { + // self.debug_check_range(); + // let [x0, x1, x2, x3, x4] = self.0.0; + // Condition::::is_zero(x0 | x1 | x2 | x3 | x4) + // } + + // /// The least significant bit of the value. + // /// + // /// Correct only on the normalized form, and the reason this type exists: p is odd, so an + // /// un-normalized value in `[p, 2^255)` would report the parity of `v` rather than of `v - p` -- + // /// a wrong answer, not merely a non-canonical one. + // // TODO -- add back once we get to implementing Ed25519 + // pub(crate) const fn is_odd(&self) -> u8 { + // self.debug_check_range(); + // (self.0.0[0] & 1) as u8 + // } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Alice's public key from the X25519 test vector in Section 6.1 of RFC 7748, used here purely + /// as a fixed value for the byte <-> limb codec. It is less than p (its top byte is 0x6A), so + /// its encoding is canonical and it needs no normalization to round-trip exactly. + const KNOWN_VALUE: [u8; 32] = [ + 0x85, 0x20, 0xF0, 0x09, 0x89, 0x30, 0xA7, 0x54, // + 0x74, 0x8B, 0x7D, 0xDC, 0xB4, 0x3E, 0xF7, 0x5A, // + 0x0D, 0xBF, 0x3A, 0x0D, 0x26, 0x38, 0x1A, 0xF4, // + 0xEB, 0xA4, 0xA9, 0x8E, 0xAA, 0x9B, 0x4E, 0x6A, + ]; + + /// The same value as five 51-bit limbs: `KNOWN_LIMBS[i] == (v >> (51 * i)) & (2^51 - 1)`, where + /// v is `KNOWN_VALUE` read as a little-endian integer. Computed independently of this crate. + const KNOWN_LIMBS: [i64; 5] = + [0x7308909F02085, 0x69B8FB16E8A94, 0x4EAFC356BDCFA, 0x275FA0D1C1306, 0x6A4E9BAA8EA9A]; + + #[test] + fn decode_255_known_value() { + assert_eq!(FieldElement25519::decode_255(&KNOWN_VALUE).0, KNOWN_LIMBS); + + // Bit 255 is not part of the field element, so setting it must not change the result. + let mut bit_255_set = KNOWN_VALUE; + bit_255_set[31] |= 0x80; + assert_eq!(FieldElement25519::decode_255(&bit_255_set).0, KNOWN_LIMBS); + } + + /// p - 1 = 2^255 - 20, the largest canonical value, passes the range check. + #[test] + fn debug_check_range_accepts_p_minus_1() { + NormalizedFieldElement25519(FieldElement25519([M51 - 19, M51, M51, M51, M51])) + .debug_check_range(); + } + + /// p itself is not canonical; the range check must reject it. + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "value >= p")] + fn debug_check_range_rejects_p() { + NormalizedFieldElement25519(FieldElement25519([M51 - 18, M51, M51, M51, M51])) + .debug_check_range(); + } + + /// A limb at 2^51 is out of range even though the value is below p. + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "limb 0 out of [0, 2^51)")] + fn debug_check_range_rejects_wide_limb() { + NormalizedFieldElement25519(FieldElement25519([M51 + 1, 0, 0, 0, 0])).debug_check_range(); + } + + /// A value of `2^255` or more is outside the domain normalize is verified on. + #[test] + #[cfg(debug_assertions)] + #[should_panic(expected = "normalize: value out of [0, 2^255)")] + fn normalize_rejects_value_2_pow_255() { + let _ = FieldElement25519([0, 0, 0, 0, M51 + 1]).normalize(); + } + + /// The shape of input Wycheproof X25519 tcId 115 produces: limb 1 is exactly `2^51`, but the + /// value is `2^51 * 2^51 + (2^51 - 1) * (2^102 + 2^153) = 2^204`, well inside the domain. + #[test] + fn normalize_accepts_limb_of_2_pow_51() { + let n = FieldElement25519([0, 1 << 51, M51, M51, 0]).normalize(); + assert_eq!(n.0.0, [0, 0, 0, 0, 1]); + } + + /// The values either side of p, where normalize's `+19` / `-19` passes decide the answer. Not + /// reachable through the public API: X25519 only normalizes ladder outputs, which land in + /// `[p, 2^255)` with negligible probability. + #[test] + fn normalize_edge_values() { + // p - 1 = 2^255 - 20: already canonical, unchanged. + let n = FieldElement25519([M51 - 19, M51, M51, M51, M51]).normalize(); + assert_eq!(n.0.0, [M51 - 19, M51, M51, M51, M51]); + + // p = 2^255 - 19: reduces to 0. + let n = FieldElement25519([M51 - 18, M51, M51, M51, M51]).normalize(); + assert_eq!(n.0.0, [0, 0, 0, 0, 0]); + + // 2^255 - 1, the largest non-canonical value: reduces to 18. + let n = FieldElement25519([M51, M51, M51, M51, M51]).normalize(); + assert_eq!(n.0.0, [18, 0, 0, 0, 0]); + } + + #[test] + fn encode_255_known_value() { + // Start from a non-zero buffer so a partial write cannot pass by accident. + let mut bytes = [0xFF_u8; 32]; + FieldElement25519(KNOWN_LIMBS).normalize().encode_255(&mut bytes); + assert_eq!(bytes, KNOWN_VALUE); + } +} diff --git a/crypto/ed/src/lib.rs b/crypto/ed/src/lib.rs new file mode 100644 index 000000000..b83a66354 --- /dev/null +++ b/crypto/ed/src/lib.rs @@ -0,0 +1,27 @@ +//! The Edwards curve family includes curve25519 and curve448 which are used to construct +//! the EdDSA signature schemes Ed25519 and Ed448, and the XDH key exchange schemes X25519 and X448. +//! The crate name is something of a misnomer, since X25519 and X448 work on the curves' Montgomery +//! form rather than their Edwards form. +//! +//! # Standards +//! +//! Standardization of the Edwards Curve algorithms is spread across a number of documents: +//! * RFC7748 specifies the core elliptic curves "curve25519" and "curve448", as well as the XDH key exchange algorithms X25519 and X448 built on top of them. +//! * RFC8032 specifies the EdDSA digital signature algorithms, also built on top of curve25519 and curve448. +//! * FIPS 186-5 section 7 specifies the EdDSA digital signature algorithms, essentially re-published from RFC 8032. +//! * NIST SP 800-186 gives an excellent treatment of the mathematics of all the families of elliptic curves used in cryptography, including Edwards curves. +//! +//! Note that NIST SP 800-186 §3.2.2 specifies the curves Curve25519 and Curve448, but its Table 2 +//! does not list them for key establishment, and NIST SP 800-56A Rev. 3 does not include XDH +//! (X25519 / X448), so X25519 and X448 are not NIST-approved key agreement schemes. +//! +//! # Usage Examples +//! +//! See the [`x25519`] module for X25519 key generation and sample Alice-and-Bob key agreement schemes. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod curve25519; +pub mod x25519; diff --git a/crypto/ed/src/x25519.rs b/crypto/ed/src/x25519.rs new file mode 100644 index 000000000..2715dcb58 --- /dev/null +++ b/crypto/ed/src/x25519.rs @@ -0,0 +1,978 @@ +//! # Usage Examples +//! +//! ## Generating keys +//! +//! ```rust +//! use bouncycastle_ed::x25519::X25519; +//! +//! let (pk, sk) = X25519::keygen().unwrap(); +//! ``` +//! That will use the library's default OS-backed RNG. To supply your own randomness, use +//! [`X25519::keygen_from_rng`], or [`X25519::keygen_from_seed`] with a 32-byte seed, which is also +//! how an existing private key is imported. The seed must be a full 32-byte [`KeyMaterial256`] of +//! [`KeyType::Seed`] or [`KeyType::CryptographicRandom`], at [`SecurityStrength::_128bit`] or more. +//! +//! ```rust +//! use bouncycastle_ed::x25519::X25519; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; +//! use bouncycastle_core::hazmat::do_hazardous_operations; +//! +//! // Create a seed. +//! // We're using a do_hazardous_operations() closure to prevent the library from complaining +//! // that the key material is weak. +//! let mut seed = KeyMaterial256::new(); +//! do_hazardous_operations(&mut seed, |seed| { +//! seed.set_bytes_as_type(&[0x02; 32], KeyType::Seed) +//! }).unwrap(); +//! +//! let (pk, sk) = X25519::keygen_from_seed(&seed).unwrap(); +//! ``` +//! +//! ## X25519 key agreement +//! +//! Alice and Bob each generate a key pair and send each other their public keys. Each then combines +//! their own private key with the other's public key in +//! [`X25519::key_agreement`](X25519#method.key_agreement), and both arrive +//! at the same shared secret `K`. +//! +//! `K` is then put through a KDF (see [Security Considerations](#-security-considerations-)). Here +//! that is HKDF-SHA256, with `K` as the input key material and `Alice's public key || Bob's public +//! key` as the context. Both parties must put the public keys in the same order. +//! +//! ```rust +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; +//! use bouncycastle_core::security_strength::SecurityStrength; +//! use bouncycastle_core::traits::{KDF, KeyAgreement}; +//! use bouncycastle_ed::x25519::{X25519, X25519DerivedKey, X25519PublicKey}; +//! use bouncycastle_sha2::hkdf::HKDF_SHA256; +//! +//! // Alice and Bob each generate a key pair, and exchange public keys. +//! let (alice_pk, alice_sk) = X25519::keygen().unwrap(); +//! let (bob_pk, bob_sk) = X25519::keygen().unwrap(); +//! +//! // Each performs a key agreement from their own private key and the other's public key, +//! // each arriving at the same k. +//! // k is an X25519DerivedKey, a secret; this example derives a shared symmetric key from it. +//! // An error here means the other party's public key is unusable; abort the exchange. +//! let alice_k = X25519::key_agreement(&bob_pk, &alice_sk).expect("Bad public key"); +//! let bob_k = X25519::key_agreement(&alice_pk, &bob_sk).expect("Bad public key"); +//! +//! // RFC 7748 §6.1 recommends using a key derivation function (KDF) over K, K_A, and K_B +//! // to derive a symmetric key. This example will use HKDF_SHA256. +//! fn derive_key_rfc7748( +//! k: &X25519DerivedKey, +//! pk_A: &X25519PublicKey, +//! pk_B: &X25519PublicKey) -> KeyMaterial256 { +//! let mut ikm = +//! KeyMaterial256::from_bytes_as_type(k.as_ref(), KeyType::CryptographicRandom).unwrap(); +//! // X25519 provides 128-bit security, not the 256 bits a 32-byte key would imply. +//! // This is related to the fact that an X25519PublicKey encodes to 32 bytes, +//! // but does not contain 32 bytes of entropy. +//! ikm.set_security_strength(SecurityStrength::_128bit).unwrap(); +//! +//! let mut context = [0u8; 64]; +//! context[..32].copy_from_slice(pk_A.as_ref()); +//! context[32..].copy_from_slice(pk_B.as_ref()); +//! +//! let mut key = KeyMaterial256::new(); +//! HKDF_SHA256::new().derive_key_out(&ikm, &context, &mut key).unwrap(); +//! key +//! } +//! +//! // Each derives the symmetric key independently. +//! // Note that they have to agree on whose key goes in first; +//! // if Alice does `(alice_pk, bob_pk)` and bob does `(bob_pk, alice_pk)` then they will +//! // arrive at different output keys. +//! // Typically, this will be specified unambiguously by the protocol, such as +//! // (sender_pk, recipient_pk) since Alice and Bob are expected to know whether they +//! // are the sender or receiver. +//! let alice_key = derive_key_rfc7748(&alice_k, &alice_pk, &bob_pk); +//! let bob_key = derive_key_rfc7748(&bob_k, &alice_pk, &bob_pk); +//! +//! // Alice and Bob now hold the same 32-byte KeyMaterial256. +//! assert!(alice_key.equals(&bob_key)); +//! assert_eq!(alice_key.key_len(), 32); +//! +//! // Which has been set at 128-bit-strength. +//! assert_eq!(alice_key.key_type(), KeyType::CryptographicRandom); +//! assert_eq!(alice_key.security_strength(), SecurityStrength::_128bit); +//! ``` +//! +//! ## X25519 as a KEM +//! +//! [`X25519`] also implements [`KEMEncapsulator`] and [`KEMDecapsulator`] as DHKEM(X25519, +//! HKDF-SHA256) of RFC 9180 §4.1, the KEM used by HPKE. The sender encapsulates against the +//! recipient's public key, and gets a shared secret and a 32-byte ciphertext (an ephemeral public +//! key) to send. The shared secret is already the output of a KDF, ready to use as a key. +//! +//! ```rust +//! use bouncycastle_core::traits::{KEMDecapsulator, KEMEncapsulator}; +//! use bouncycastle_ed::x25519::X25519; +//! +//! let (pk, sk) = X25519::keygen().unwrap(); +//! +//! // The sender encapsulates against the recipient's public key... +//! let (ss, ct) = X25519::encaps(&pk).unwrap(); +//! +//! // ...and the recipient recovers the same shared secret from the ciphertext. +//! let ss1 = X25519::decaps(&sk, &ct).unwrap(); +//! assert_eq!(ss, ss1); +//! ``` +//! +//! ## Chaining together key agreement operations +//! +//! The output of a Diffie-Hellman key agreement is itself a valid public key, so key agreements can +//! be chained. +//! Below is an example of a simple three-party agreement, in which Alice, Bob and Charlie all arrive +//! at the same shared secret `k`, by combining their private key with public data from the other two. +//! The trick here is that each party does a pairwise key agreement with each other party, +//! and then another agreement with the result of the other two parties' agreements with each other. +//! This is a simple scheme to illustrate the point and obviously does not scale well to large groups. +//! +//! ```rust +//! use bouncycastle_core::traits::KeyAgreement; +//! use bouncycastle_ed::x25519::X25519; +//! +//! let (alice_pk, alice_sk) = X25519::keygen().unwrap(); +//! let (bob_pk, bob_sk) = X25519::keygen().unwrap(); +//! let (charlie_pk, charlie_sk) = X25519::keygen().unwrap(); +//! +//! // Assume that Alice, Bob, and Charlie have all exchanged public keys. +//! // Round 1: each pair computes a pairwise value. Key agreement is symmetric, so `ab_k` is +//! // computed by both Alice and Bob; only one side of each pair is shown. +//! let ab_k = X25519::key_agreement(&bob_pk, &alice_sk).expect("Bad public key"); +//! let bc_k = X25519::key_agreement(&charlie_pk, &bob_sk).expect("Bad public key"); +//! let ac_k = X25519::key_agreement(&charlie_pk, &alice_sk).expect("Bad public key"); +//! +//! // Round 2: each pairwise value is converted to a public key type and sent, in the clear, +//! // to the third party, who combines it with their own private key. +//! // Since it is no longer secret, `ab_k` must not also be used as a secret key between +//! // Alice and Bob. +//! let alice_k = X25519::key_agreement(&bc_k.as_pk(), &alice_sk).expect("Bad public key"); +//! let bob_k = X25519::key_agreement(&ac_k.as_pk(), &bob_sk).expect("Bad public key"); +//! let charlie_k = X25519::key_agreement(&ab_k.as_pk(), &charlie_sk).expect("Bad public key"); +//! +//! // The computations ran in different orders, but by the magic of +//! // commutative algebra, they all end up with the same shared secret key `k`. +//! assert_eq!(alice_k.as_ref(), bob_k.as_ref()); +//! assert_eq!(bob_k.as_ref(), charlie_k.as_ref()); +//! +//! // As in the two-party case, `k` is not uniformly random: run it through a suitable KDF scheme +//! // to derive a usable and secure symmetric key. +//! ``` +//! +//! Like any unauthenticated Diffie-Hellman, this resists only passive eavesdroppers. An active +//! attacker who can substitute public keys or pairwise values in transit can sit in the middle, so a +//! real protocol must authenticate them, for example by signing them. +//! +//! # Memory Usage +//! +//! There are no lookup tables and no heap allocation, and no state is held between calls other +//! than the keys: +//! +//! | Key Object | PK size on disk | PK size in memory | SK size on disk | SK size in memory | +//! |------------|-----------------|-------------------|-----------------|-------------------| +//! | X25519 | 32 | 32 | 32 | 64 | +//! +//! All values are in bytes. The "in memory" sizes are measured by rust's `std::mem::size_of`. +//! [`X25519PrivateKey`] holds its public key alongside the 32-byte scalar. +//! +//! Per-call stack usage, measured as the deepest frame chain below each entry point in the release +//! build (x86-64 Linux, rustc 1.99.0, return addresses included): +//! +//! | Entry point | Stack | +//! |---------------------------------|--------| +//! | `X25519::keygen_from_seed` | 1200 B | +//! | `X25519PrivateKey::from_bytes` | 1136 B | +//! | `X25519::key_agreement` | 1072 B | +//! | `X25519::encaps` / `encaps_rng` | 2368 B | +//! | `X25519::decaps` | 1872 B | +//! +//! Key generation, decoding a private key, and key agreement each run one Montgomery ladder plus +//! the field inversion. +//! The KEM figures additionally run HPKE.DHKEM and stack usage is dominated by the HKDF-SHA256 key +//! schedule. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! **A raw shared secret is not a key**. [`X25519::key_agreement`](X25519#method.key_agreement) +//! returns the shared secret `K` as an [`X25519PublicKey`]: the 32-byte encoding of the +//! u-coordinate of a point on curve25519. Those bytes are not uniformly random, and therefore not +//! suitable for direct use as a symmetric key. +//! The encoded shared key MUST be run through a KDF such as HMAC-SHA256, HKDF-SHA256, or SHAKE. +//! +//! RFC 7748 §7: "for each public key, there are several publicly computable public keys that are +//! equivalent to it, i.e., they produce the same shared secrets. Thus using a public key as an +//! identifier and knowledge of a shared secret as proof of ownership (without including the public +//! keys in the key derivation) might lead to subtle vulnerabilities". +//! This is the reason for including both public keys in the key derivation step. +//! Note that the HPKE DHKEM exposed via the [`KEMEncapsulator`] / [`KEMDecapsulator`] traits protects +//! against this. +//! +//! RFC 9180 §9.7.4: "HPKE ciphertexts are not forward secret with respect to recipient compromise +//! in any mode". +//! Anyone who later obtains a long-term DHKEM private key can decrypt everything ever sent to it. +//! This equally applies to most uses of the raw [`KeyAgreement`] interface, unless a suitable protocol +//! is layered on top. + +use bouncycastle_core::errors::{KEMError, KeyAgreementError, RNGError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, KeyAgreement, + KeyAgreementPrivateKey, KeyAgreementPublicKey, RNG, +}; +use bouncycastle_hpke::dhkem::{DHKEM, DHKEMParams}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_sha2::SHA256; +use bouncycastle_utils::ct::{Condition, ct_eq_bytes, ct_eq_zero_bytes}; +use bouncycastle_utils::secret::Secret; +use core::fmt::{self, Display, Formatter}; + +use crate::curve25519::FieldElement25519; + +// Only the field arithmetic (`FieldElement25519` in curve25519.rs) is shared with Ed25519. The +// scalar and point types below are X25519's own: an Ed25519 scalar is reduced mod the group order +// and an Ed25519 point is an encoded y-coordinate plus a sign bit (RFC 8032 §5.1.2), so neither +// fits these. + +/*** Constants ***/ + +/// Size in bytes of the encoded representation of a point on curve25519, +/// which are used as public keys. +pub const POINT_LEN: usize = 32; +/// Size in bytes of the encoded representation of a scalar in the curve25519 field, +/// which are used as private keys. +pub const SCALAR_LEN: usize = 32; + +/// String name of the X25519 algorithm. +pub const X25519_NAME: &str = "X25519"; + +/// The base point of curve25519, `U(P) = 9` (Section 4.1 of RFC7748). +const BASE_POINT: X25519PublicKey = { + let mut u = [0_u8; POINT_LEN]; + u[0] = 9; + X25519PublicKey(u) +}; + +/// The coefficient A of curve25519, `v^2 = u^3 + A*u^2 + u` (RFC 7748 §4.1). +const C_A: u32 = 486662; + +/// `(A + 2) / 4 = 121666`, the ladder's doubling constant. +/// +/// Deviation from the RFC: RFC 7748 §5 uses `a24 = (486662 - 2) / 4 = 121665` in +/// `z_2 = E * (AA + a24 * E)`. Since `E = AA - BB`, `AA + a24 * E = BB + (a24 + 1) * E`, so pairing +/// the constant with BB instead needs `a24 + 1 = (A + 2) / 4`. [`scalar_mult`] and +/// [`ladder_double`] both use that form. +const C_A24: u32 = (C_A + 2) / 4; + +/// The X25519 key agreement algorithm of RFC 7748 §5. +pub struct X25519; + +impl Algorithm for X25519 { + const ALG_NAME: &'static str = X25519_NAME; + // RFC 7748 §7: "the security level ... of curve25519 is slightly under the standard 128-bit + // level", which it argues is acceptable; it is classed here at the conventional 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl X25519 { + /// Generates a fresh key pair using the library's default RNG, seeded from the OS. + /// In environments where the default OS based RNG is not available, use instead + /// [`X25519::keygen_from_rng`] and explicitly provide an [`RNG`] implementation, or use + /// [`X25519::keygen_from_seed`] and provide the private key seed directly. + pub fn keygen() -> Result<(X25519PublicKey, X25519PrivateKey), KeyAgreementError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::keygen_from_rng(&mut os_rng) + } + + /// Generates a key pair using the provided RNG implementation. + pub fn keygen_from_rng( + rng: &mut dyn RNG, + ) -> Result<(X25519PublicKey, X25519PrivateKey), KeyAgreementError> { + if rng.security_strength() < Self::MAX_SECURITY_STRENGTH { + return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; + } + let mut seed = KeyMaterial256::new(); + rng.fill_keymaterial_out(&mut seed)?; + Self::keygen_from_seed(&seed) + } + + /// Generates a key pair from a 32-byte seed, which must have [`KeyType::Seed`] or + /// [`KeyType::CryptographicRandom`] and at least [`SecurityStrength::_128bit`]. + /// + /// RFC 7748 §6.1: "Alice generates 32 random bytes in `a[0]` to `a[31]` and transmits + /// K_A = X25519(a, 9) to Bob". The seed is used as the private key unchanged, so this is also + /// how to import an existing private key along with its public key. + pub fn keygen_from_seed( + seed: &KeyMaterial256, + ) -> Result<(X25519PublicKey, X25519PrivateKey), KeyAgreementError> { + if !(seed.key_type() == KeyType::Seed || seed.key_type() == KeyType::CryptographicRandom) + || seed.key_len() != SCALAR_LEN + { + return Err(KeyAgreementError::KeyGenError( + "Seed must be 32 bytes and KeyType::Seed or KeyType::CryptographicRandom.", + )); + } + + if seed.security_strength() < Self::MAX_SECURITY_STRENGTH { + return Err(KeyAgreementError::KeyGenError( + "Seed SecurityStrength must match algorithm security strength", + )); + } + + let mut k: Secret<[u8; SCALAR_LEN]> = Secret::new(); + // Cannot panic: the length was checked above. + k.copy_from_slice(seed.ref_to_bytes()); + let sk = X25519PrivateKey { pk: scalar_mult_base(&k), k }; + + Ok((sk.pk(), sk)) + } + + /// Checks that `pk` is the public key of `sk`, by deriving it from `sk` and comparing. + /// + /// Returns either `()` or [`KeyAgreementError::ConsistencyCheckFailed`]. + pub fn keypair_consistency_check( + pk: &X25519PublicKey, + sk: &X25519PrivateKey, + ) -> Result<(), KeyAgreementError> { + sk.consistency_check(pk) + } +} + +impl KeyAgreement for X25519 { + type DerivedKey = X25519DerivedKey; + + /// The X25519 Diffie-Hellman of RFC 7748 §6.1: computes the shared secret + /// `K = X25519(sk, pk)`, the u-coordinate of the scalar multiple of the peer's public key by + /// our private key, as its canonical 32-byte little-endian encoding. + /// + /// Alice computes `X25519(a, K_B)` and Bob `X25519(b, K_A)`, and §6.1 shows both equal + /// `X25519(a, X25519(b, 9)) = X25519(b, X25519(a, 9))`. §6.1 also says to then "use a + /// key-derivation function that includes K, K_A, and K_B to derive a symmetric key": `K` is + /// not uniformly random, so it should not be used as a key directly. + /// + /// Any 32 bytes are accepted as `pk`: per RFC 7748 §5, bit 255 is ignored and values `>= p` + /// are processed as if reduced mod p. + /// + /// # Errors + /// + /// The only error condition ([`KeyAgreementError::InvalidPublicKey`]) on key_agreement happens + /// when the input public key is part of the curve's small subgroup (which will almost always be + /// done maliciously in practice). + /// That results in an output `K` that is all zeros. Such a point + /// "will eliminate any contribution from the other party's private key" (RFC 7748 §7), so `K` is the + /// same for every private key and is known to whoever chose `pk`. + /// + /// RFC 7748 makes the check optional: both parties "MAY check, without leaking extra + /// information about the value of K, whether K is the all-zero value and abort if so". + /// This implementation always performs it and returns [`KeyAgreementError::InvalidPublicKey`] + /// if `K` is all zeros. + /// + /// The check is constant time, ORing all the bytes together as §6.1 suggests, so the only + /// information it reveals about `K` is whether it is zero. + fn key_agreement( + pk: &X25519PublicKey, + sk: &X25519PrivateKey, + ) -> Result { + let k = dh(sk, pk)?; + let mut dk = Secret::<[u8; POINT_LEN]>::new(); + dk.copy_from_slice(k.as_bytes()); + Ok(X25519DerivedKey(dk)) + } +} + +/// `X25519(sk, pk)`, rejecting an all-zero output; see [`X25519::key_agreement`]. +fn dh(sk: &X25519PrivateKey, pk: &X25519PublicKey) -> Result { + let r = scalar_mult(&sk.k, pk); + if r.is_zero() { + return Err(KeyAgreementError::InvalidPublicKey( + "Public key has small order: the shared secret is all zeros", + )); + } + Ok(r) +} + +/// An X25519 private key: a 32-byte scalar, as defined in Section 5 of RFC7748. +/// +/// Any 32 bytes are a valid scalar. The fixed bits `decodeScalar25519` sets and clears +/// ("clamping") are applied each time the scalar is used, so keys that differ only in those bits +/// are the same key, and compare equal. Held in a [`Secret`], so it is zeroized on drop and +/// redacted from `Debug`. +/// +/// The public key is computed once, when the private key is created or decoded, and stored with +/// it. +#[derive(Clone)] +pub struct X25519PrivateKey { + k: Secret<[u8; SCALAR_LEN]>, + pk: X25519PublicKey, +} + +impl X25519PrivateKey { + /// The public key, `X25519(k, 9)` (RFC 7748 §6.1). + pub fn pk(&self) -> X25519PublicKey { + self.pk + } + + /// Checks that `pk` is this private key's public key, by computing it from the scalar and + /// comparing. + /// + /// Returns either `()` or [`KeyAgreementError::ConsistencyCheckFailed`]. + pub fn consistency_check(&self, pk: &X25519PublicKey) -> Result<(), KeyAgreementError> { + if scalar_mult_base(&self.k) == *pk { + Ok(()) + } else { + Err(KeyAgreementError::ConsistencyCheckFailed("Public key does not match private key")) + } + } + + /// The key bytes, [`clamp`]ed. + fn clamped(&self) -> Secret<[u8; SCALAR_LEN]> { + let mut k = self.k.clone(); + clamp(&mut k); + k + } +} + +/// Applies `decodeScalar25519`'s fixed bits to the key bytes `k` (RFC 7748 §5): +/// +/// ```text +/// k_list[0] &= 248 +/// k_list[31] &= 127 +/// k_list[31] |= 64 +/// ``` +fn clamp(k: &mut [u8; SCALAR_LEN]) { + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; +} + +/// The encoding is the 32 key bytes, clamped (the bit operations of `decodeScalar25519`, RFC 7748 +/// §5), so keys that compare equal have the same encoding. This is also the DHKEM serialization: +/// RFC 9180 §7.1.2 says "The SerializePrivateKey() function MUST clamp its output and the +/// DeserializePrivateKey() function MUST clamp its input". +impl KeyAgreementPrivateKey for X25519PrivateKey { + fn encode(&self) -> [u8; SCALAR_LEN] { + *self.clamped() + } + + fn encode_out(&self, out: &mut [u8; SCALAR_LEN]) -> usize { + out.fill(0); + *out = *self.clamped(); + SCALAR_LEN + } + + /// Any 32 bytes are a valid private key; they are clamped on the way in. + /// + /// # Errors + /// + /// [`KeyAgreementError::DecodingError`] if `bytes` is not 32 bytes. + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; SCALAR_LEN] = bytes.try_into().map_err(|_| { + KeyAgreementError::DecodingError("An X25519 private key must be 32 bytes") + })?; + let mut k: Secret<[u8; SCALAR_LEN]> = Secret::new(); + *k = *bytes; + clamp(&mut k); + Ok(Self { pk: scalar_mult_base(&k), k }) + } +} + +/// Constant-time comparison of the clamped scalars. +impl PartialEq for X25519PrivateKey { + fn eq(&self, other: &Self) -> bool { + ct_eq_bytes(&*self.clamped(), &*other.clamped()) + } +} + +impl Eq for X25519PrivateKey {} + +/// An X25519 public value: the u-coordinate of a point on curve25519, as 32 little-endian bytes +/// (Section 5 of RFC7748). +/// +/// Any 32 bytes are accepted: Section 5 requires bit 255 to be ignored and values `>= p` to be +/// processed as if reduced mod p. Public data, so it is `Copy` and its `Debug` shows the bytes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct X25519PublicKey([u8; POINT_LEN]); + +impl X25519PublicKey { + /// The 32 encoded bytes. + pub const fn as_bytes(&self) -> &[u8; POINT_LEN] { + &self.0 + } + + /// Constant-time test for all 32 bytes being zero. + /// + /// This tests the encoding, not the value: it equals "u = 0" only for canonical encodings, + /// such as [`scalar_mult`]'s output. A non-canonical encoding of 0 (`p`, or bit 255 set) + /// returns `false`. + fn is_zero(&self) -> bool { + ct_eq_zero_bytes(&self.0) + } +} + +/// The encoding is the 32 bytes of the u-coordinate, as is (RFC 7748 §5). +impl KeyAgreementPublicKey for X25519PublicKey { + fn encode(&self) -> [u8; POINT_LEN] { + self.0 + } + + fn encode_out(&self, out: &mut [u8; POINT_LEN]) -> usize { + out.fill(0); + *out = self.0; + POINT_LEN + } + + /// Any 32 bytes are accepted: RFC 7748 §5 requires bit 255 to be ignored and values `>= p` to + /// be processed as if reduced mod p. + /// + /// # Errors + /// + /// [`KeyAgreementError::DecodingError`] if `bytes` is not 32 bytes. + fn from_bytes(bytes: &[u8]) -> Result { + let bytes: &[u8; POINT_LEN] = bytes.try_into().map_err(|_| { + KeyAgreementError::DecodingError("An X25519 public key must be 32 bytes") + })?; + Ok(Self(*bytes)) + } +} + +impl AsRef<[u8]> for X25519PublicKey { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// The 32 bytes in hex. +impl Display for X25519PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "X25519PublicKey {{ ")?; + for b in &self.0 { + write!(f, "{b:02x}")?; + } + write!(f, " }}") + } +} + +/// The shared secret `K` from [`X25519::key_agreement`](X25519#method.key_agreement). +/// This impls `AsRef<[u8]>` so the 32 bytes can be freely extracted. +/// Structurally, this is an X25519PublicKey, but it is wrapped in its own type to protect it with +/// [`Secret`]. If it needs to be treated as a public key, use [`X25519DerivedKey::as_pk`]. +/// +/// Note that this impls `From for X25519DerivedKey` so that any public key can be +/// freely converted into a DerivedKey type. +pub struct X25519DerivedKey(Secret<[u8; POINT_LEN]>); + +impl X25519DerivedKey { + /// Opt-in to treating this as a public key. + pub fn as_pk(&self) -> X25519PublicKey { + X25519PublicKey(*self.0) + } +} + +impl AsRef<[u8]> for X25519DerivedKey { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +/// `decodeScalar25519(k)` as defined in Section 5 of RFC7748: +/// +/// ```text +/// def decodeScalar25519(k): +/// k_list = [ord(b) for b in k] +/// k_list[0] &= 248 +/// k_list[31] &= 127 +/// k_list[31] |= 64 +/// return decodeLittleEndian(k_list, 255) +/// ``` +/// +/// Section 5 gives the same rule in prose: "set the three least significant bits of the first +/// byte and the most significant bit of the last to zero, set the second most significant bit of +/// the last byte to 1 and, finally, decode as little-endian. This means that the resulting +/// integer is of the form 2^254 plus eight times a value between 0 and 2^251 - 1 (inclusive)." +/// +/// That is why the ladder can start at bit 253 against a pre-swapped initial state; +/// the `8 *` is why its last three iterations collapse into three [`ladder_double`] calls with no +/// trailing conditional swap. +/// +/// Deviation from the RFC: the RFC masks bytes and then decodes, whereas this decodes into +/// four-byte words first and masks the equivalent word positions. `k_list[0] &= 248` becomes +/// `n[0] &= 0xFFFFFFF8` -- the same low three bits, since `n[0]` is the first four bytes -- and +/// `k_list[31] &= 127` / `|= 64` become `n[7] &= 0x7FFFFFFF` / `|= 0x40000000`, the same top two +/// bits, since `n[7]` is the last four bytes. +/// +/// This differs from `decodeScalar448`, which clears two low bits rather than +/// three, and sets bit 447 (Section 5). +/// +/// Input: A 32-byte little-endian scalar. +/// Output: The decoded scalar, written to `n` as eight little-endian 32-bit words. +fn decode_scalar_25519(k: &[u8; SCALAR_LEN], n: &mut [u32; 8]) { + let (words, _) = k.as_chunks::<4>(); // 32 bytes: exactly 8 chunks, no remainder + for (n_i, w) in n.iter_mut().zip(words) { + *n_i = u32::from_le_bytes(*w); + } + + n[0] &= 0xFFFFFFF8_u32; + n[7] &= 0x7FFFFFFF_u32; + n[7] |= 0x40000000_u32; +} + +/// `decodeUCoordinate(u, 255)` as defined in Section 5 of RFC7748: +/// +/// ```text +/// def decodeUCoordinate(u, bits): +/// u_list = [ord(b) for b in u] +/// # Ignore any unused bits. +/// if bits % 8: +/// u_list[-1] &= (1<<(bits%8))-1 +/// return decodeLittleEndian(u_list, bits) +/// ``` +/// +/// With `bits` = 255, `bits % 8` is 7 and the mask clears bit 255, the most significant bit of +/// the final byte. Section 5 requires exactly that: "When receiving such an array, +/// implementations of X25519 (but not X448) MUST mask the most significant bit in the final +/// byte." [`FieldElement25519::decode_255`] does the masking and the little-endian decode. +/// +/// The result is not normalized, as Section 5 also requires: "Implementations MUST accept +/// non-canonical values and process them as if they had been reduced modulo the field prime. The +/// non-canonical values are 2^255 - 19 through 2^255 - 1 for X25519". +/// +/// Input: A 32-byte little-endian u-coordinate. +/// Output: The u-coordinate as a [`FieldElement25519`], not normalized. +fn decode_u_coordinate(u: &X25519PublicKey) -> FieldElement25519 { + FieldElement25519::decode_255(&u.0) +} + +/// `encodeUCoordinate(u, 255)` as defined in Section 5 of RFC 7748. +/// The RFC's `u = u % p` step is [`FieldElement25519::normalize`]: an X25519 output must be the +/// canonical encoding of its residue class, even though non-canonical values MUST be accepted on +/// the way in (see [`decode_u_coordinate`]). Bit 255 of the final byte is written as zero, since +/// it is unused by X25519. +/// +/// Input: A u-coordinate, in any state of reduction; a 32-byte output buffer. +/// Output: The canonical 32-byte little-endian encoding of that value, written to `r`. +fn encode_u_coordinate(u: &FieldElement25519, r: &mut [u8; POINT_LEN]) { + u.normalize().encode_255(r); +} + +/// Doubles one point, held projectively as the pair `(x : z)` representing `u = x / z` +/// Updated in place. +/// +/// Keeping u as a fraction is what lets the ladder avoid a field inversion on every step: only +/// multiplications and squarings are needed here, and [`scalar_mult`] divides `x` by `z` once, at +/// the end. +/// +/// This is the doubling half of the Montgomery ladder step in Section 5 of RFC7748, used by +/// [`scalar_mult`] for the last three iterations. With `AA = (x + z)^2`, `BB = (x - z)^2` and +/// `E = AA - BB`, it writes `x = AA * BB` and `z = E * (BB + C_A24 * E)`; see [`C_A24`] for the +/// pairing with `BB`. +/// +/// X25519 only: it works on curve25519's Montgomery form and depends on its coefficient A. +/// Ed25519 doubles points on the twisted Edwards curve in extended coordinates (Section 5.1.4 of +/// RFC8032), which is a separate function. +fn ladder_double(x: &mut FieldElement25519, z: &mut FieldElement25519) { + let (mut a, mut b) = x.sum_and_difference(z); + a = a.sqr(); + b = b.sqr(); + *x = a.mul(&b); + + a = a.sub(&b); + let t = a.mul_u32(C_A24).add(&b); + *z = t.mul(&a); +} + +/// `X25519(k, u)` as defined in Section 5 of RFC7748: the Montgomery ladder, computing scalar +/// multiplication on curve25519 from a u-coordinate alone. +/// +/// The ladder is constant time with respect to `k`: the iteration count is fixed and the only +/// scalar-dependent step is [`FieldElement25519::cswap`]. +/// +/// # Modifications from the RFC +/// Modified to fold the RFC's first ladder iteration into the initial state, to replace its last +/// three iterations with [`ladder_double`] calls, and to cycle the RFC's named intermediates +/// through a fixed set of registers rather than materialise them. +/// +/// # Inputs +/// * `k`, a 32-byte scalar +/// * `u`, a 32-byte little-endian u-coordinate +/// Output: The canonical little-endian encoding of the u-coordinate of `k * u`. +fn scalar_mult(k: &[u8; SCALAR_LEN], u: &X25519PublicKey) -> X25519PublicKey { + // The decoded scalar is as secret as `k` itself. + let mut n: Secret<[u32; 8]> = Secret::new(); + decode_scalar_25519(k, &mut n); + + // Pre-condition: + // The ladder below is only correct for scalars of the form 2^254 + 8m, 0 <= m <= 2^251 - 1 + // which should be enforced by a correct implementation of decode_scalar_25519. + debug_assert_eq!(1, n[7] >> 30); + debug_assert_eq!(0, n[0] & 7); + + let x1 = decode_u_coordinate(u); + let mut x2 = FieldElement25519::zero(); + let mut z2 = FieldElement25519::zero(); + ladder(&n, &x1, &mut x2, &mut z2); + + x2 = x2.mul(&z2.inv()); + let mut out = [0_u8; POINT_LEN]; + encode_u_coordinate(&x2, &mut out); + X25519PublicKey(out) +} + +/// The Montgomery ladder of [`scalar_mult`]: writes the projective u-coordinate of `n * x1` to +/// `(x2, z2)`, for a scalar `n` decoded by [`decode_scalar_25519`]. +// #[inline(never)]: so that the ladder's variables live in a different stack frame and can be popped +// before scalar_mult's `z2.inv()` runs (which is the other stack-expensive operation). +#[inline(never)] +fn ladder( + n: &[u32; 8], + x1: &FieldElement25519, + x2: &mut FieldElement25519, + z2: &mut FieldElement25519, +) { + *x2 = x1.clone(); + *z2 = FieldElement25519::one(); + let mut x3 = FieldElement25519::one(); + let mut z3 = FieldElement25519::zero(); + + let mut t1; + let mut t2; + + let mut swap = Condition::::from_bool_const::(); + + // Deviation from the RFC: + // RFC7748 §5 runs `For t = bits-1 down to 0`, i.e. t = 254 down to 0, with the conditional + // swap at the top of the body and the arithmetic after it. This loop deviates in three ways, + // all of them consequences of the scalar's form -- see the pre-condition above: + // + // 1. Rotated body. The arithmetic comes first here and the swap last. + // 2. t = 254 is not in the loop because bit 254 is always set, so that iteration's swap is known in + // advance: the registers above are seeded with the RFC's x_2 = 1, z_2 = 0, x_3 = u, + // z_3 = 1 *after* it has been applied, and `swap` starts at k_254. So the loop starts at 253 and not 254. + // 3. t = 2, 1, 0 are not in the loop because bits 0..2 are always clear, so those iterations reduce + // to doubling the (x_2, z_2) branch and their swaps are no-ops. They become the three + // ladder_double calls after the loop, and the RFC's post-loop cswap is dropped for the same + // reason -- the debug_assert below is what checks it would have been a no-op. + for bit in (2..=253_usize).rev() { + // This section is the same core algorithm as in RFC7748 §5, but uses a different variable + // naming convention in order to more efficiently reuse memory. + // Comment style: + // , -> + + // C = x_3 + z_3, -> t1 + // D = x_3 - z_3, -> x3 + (t1, x3) = x3.sum_and_difference(&z3); + // A = x_2 + z_2, -> z3 + // B = x_2 - z_2, -> x2 + // Must follow the line above, not precede it: that one still reads the incoming z_3, + // which this one overwrites. + (z3, *x2) = x2.sum_and_difference(z2); + // CB = C * B, -> t1 + t1 = t1.mul(x2); + // DA = D * A, -> x3 + x3 = x3.mul(&z3); + // AA = A^2, -> z3 + z3 = z3.sqr(); + // BB = B^2, -> x2 + *x2 = x2.sqr(); + + // E = AA - BB, -> t2 + t2 = z3.sub(x2); + // z_2 = E * (AA + a24 * E), -> z2 + // Deviation from the RFC: written as E * (BB + C_A24 * E), an equivalent pairing of the + // constant with the other operand. See C_A24's declaration. + // The incoming z_2 died on the `sum_and_difference` above, which is what frees this slot. + *z2 = t2.mul_u32(C_A24).add(x2).mul(&t2); + // x_2 = AA * BB, -> x2 + *x2 = x2.mul(&z3); + + // x_3 = (DA + CB)^2, -> x3 + // z_3 = x_1 * (DA - CB)^2, -> z3 + // The function `sum_and_difference()` computes `(DA + CB) and (DA - CB)` is a single + // operation, allowing these lines to be combined. + // Deviation from the RFC: yields CB - DA, negated. Harmless as both lines below square it. + (x3, z3) = t1.sum_and_difference(&x3); + // (completes x_3) + x3 = x3.sqr(); + // (completes z_3) + z3 = z3.sqr().mul(&x1); + + // This differs from the ladder presentation in the RFC because it implements the + // constant-time cswap recommendation at the bottom of §5 using an all-ones/all-zeros + // mask and the Condition type. + + // k_t = (k >> t) & 1 + // > Presentation differs due to decoding k into n, and the bit-selecting Condition operator. + let k_bit = Condition::::is_bit_set(n[bit >> 5] as i64, (bit & 0x1F) as u32); + // swap ^= k_t + swap ^= k_bit; + // (x_2, x_3) = cswap(swap, x_2, x_3) + // > swaps in-place + FieldElement25519::cswap(swap, x2, &mut x3); + // (z_2, z_3) = cswap(swap, z_2, z_3) + // > swaps in-place + FieldElement25519::cswap(swap, z2, &mut z3); + // swap = k_t + swap = k_bit; + } + + debug_assert!(!swap.to_bool()); + + for _ in 0..3 { + ladder_double(x2, z2); + } +} + +fn scalar_mult_base(k: &[u8; SCALAR_LEN]) -> X25519PublicKey { + // Equivalent (but much slower) -- since the BASE_POINT never changes, you can + // precompute lookup tables for the multiples of BASE_POINT. TODO + scalar_mult(k, &BASE_POINT) +} + +/*** DHKEM ***/ + +// This section provides the machinery to promote x25519 to a KEM via RFC9180's DHKEM construction. + +/// The length of the shared secret from [`X25519`]'s KEM: Nsecret for DHKEM(X25519, HKDF-SHA256) +/// (RFC 9180 §7.1). +pub const DHKEM_SS_LEN: usize = 32; + +/// DHKEM(X25519, HKDF-SHA256) of RFC 9180 §4.1. Nenc = Npk = Ndh = 32 and Nsk = 32 (§7.1). +type X25519DHKEM = DHKEM; + +/// DHKEM(X25519, HKDF-SHA256): kem_id 0x0020, with Nenc = Npk = Nsk = 32 (RFC 9180 §7.1). +impl DHKEMParams for X25519 { + type PublicKey = X25519PublicKey; + type PrivateKey = X25519PrivateKey; + type KDFHash = SHA256; + const KEM_ID: u16 = 0x0020; + + fn generate_key_pair_rng( + rng: &mut dyn RNG, + ) -> Result<(X25519PublicKey, X25519PrivateKey), KEMError> { + // RFC 9180 §4: "GenerateKeyPair can be implemented as DeriveKeyPair(random(Nsk))"; for + // X25519, 32 random bytes are already a private key (RFC 7748 §6.1), so they are used as is. + Self::keygen_from_rng(rng).map_err(|e| match e { + KeyAgreementError::RNGError(e) => KEMError::RNGError(e), + KeyAgreementError::KeyGenError(msg) => KEMError::KeyGenError(msg), + _ => KEMError::GenericError("X25519 key generation failed"), + }) + } + + fn private_key_from_bytes(sk: &[u8; SCALAR_LEN]) -> X25519PrivateKey { + let mut k: Secret<[u8; SCALAR_LEN]> = Secret::new(); + *k = *sk; + X25519PrivateKey { pk: scalar_mult_base(&k), k } + } + + fn pk(sk: &X25519PrivateKey) -> X25519PublicKey { + sk.pk() + } + + fn dh( + sk: &X25519PrivateKey, + pk: &X25519PublicKey, + ) -> Result, KeyAgreementError> { + let mut k: Secret<[u8; POINT_LEN]> = Secret::new(); + *k = dh(sk, pk)?.0; + Ok(k) + } + + // RFC 9180 §7.1.1: "For X25519 and X448, the SerializePublicKey() and DeserializePublicKey() + // functions are the identity function". + fn serialize_public_key(pk: &X25519PublicKey) -> [u8; POINT_LEN] { + pk.0 + } + + fn deserialize_public_key(pkm: &[u8; POINT_LEN]) -> Result { + Ok(X25519PublicKey(*pkm)) + } +} + +/// X25519 as a KEM: DHKEM(X25519, HKDF-SHA256) of RFC 9180 §4.1, where the ciphertext is an +/// ephemeral public key (`enc`) and the shared secret is derived with HKDF-SHA256 from the X25519 +/// output and both public keys. +impl KEMEncapsulator for X25519 { + /// `Encap(pkR)` (RFC 9180 §4.1), drawing the ephemeral key pair from the library's default RNG, + /// seeded from the OS. + /// + /// # Errors + /// + /// [`KEMError::InvalidPublicKey`] if `pk` gives an all-zero X25519 output, as for + /// [`X25519::key_agreement`]. + fn encaps( + pk: &X25519PublicKey, + ) -> Result<(KeyMaterial, [u8; POINT_LEN]), KEMError> { + let mut os_rng = HashDRBG_SHA512::new_from_os(); + Self::encaps_rng(pk, &mut os_rng) + } + + /// `Encap(pkR)` (RFC 9180 §4.1), drawing the ephemeral key pair from `rng`. + /// + /// # Errors + /// + /// * [`KEMError::RNGError`] if `rng`'s security strength is below 128 bits. + /// * [`KEMError::InvalidPublicKey`] if `pk` gives an all-zero X25519 output, as for + /// [`X25519::key_agreement`]. + fn encaps_rng( + pk: &X25519PublicKey, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; POINT_LEN]), KEMError> { + X25519DHKEM::encap_rng(pk, rng) + } +} + +impl KEMDecapsulator for X25519 { + /// `Decap(enc, skR)` (RFC 9180 §4.1), with the ciphertext `ct` as `enc`. + /// + /// # Errors + /// + /// * [`KEMError::LengthError`] if `ct` is not 32 bytes. + /// * [`KEMError::DecapsulationFailed`] if `ct` gives an all-zero X25519 output. + fn decaps(sk: &X25519PrivateKey, ct: &[u8]) -> Result, KEMError> { + let enc: &[u8; POINT_LEN] = ct + .try_into() + .map_err(|_| KEMError::LengthError("An X25519 KEM ciphertext must be 32 bytes"))?; + X25519DHKEM::decap(enc, sk) + } +} + +/// The same clamped encoding as the [`KeyAgreementPrivateKey`] impl, which RFC 9180 §7.1.2 requires +/// of a DHKEM. +impl KEMPrivateKey for X25519PrivateKey { + fn encode(&self) -> [u8; SCALAR_LEN] { + >::encode(self) + } + + fn encode_out(&self, out: &mut [u8; SCALAR_LEN]) -> usize { + >::encode_out(self, out) + } + + /// # Errors + /// + /// [`KEMError::DecodingError`] if `bytes` is not 32 bytes. + fn from_bytes(bytes: &[u8]) -> Result { + >::from_bytes(bytes) + .map_err(|_| KEMError::DecodingError("An X25519 private key must be 32 bytes")) + } +} + +/// The same encoding as the [`KeyAgreementPublicKey`] impl: RFC 9180 §7.1.1 makes serialization the +/// identity function for X25519. +impl KEMPublicKey for X25519PublicKey { + fn encode(&self) -> [u8; POINT_LEN] { + >::encode(self) + } + + fn encode_out(&self, out: &mut [u8; POINT_LEN]) -> usize { + >::encode_out(self, out) + } + + /// # Errors + /// + /// [`KEMError::DecodingError`] if `bytes` is not 32 bytes. + fn from_bytes(bytes: &[u8]) -> Result { + >::from_bytes(bytes) + .map_err(|_| KEMError::DecodingError("An X25519 public key must be 32 bytes")) + } +} diff --git a/crypto/ed/tests/x25519_dhkem_tests.rs b/crypto/ed/tests/x25519_dhkem_tests.rs new file mode 100644 index 000000000..caf8f2ab8 --- /dev/null +++ b/crypto/ed/tests/x25519_dhkem_tests.rs @@ -0,0 +1,285 @@ +//! X25519 as a KEM: DHKEM(X25519, HKDF-SHA256) of RFC 9180 Section 4.1, against the RFC 9180 +//! Appendix A.1 vectors and the core-test-framework KEM suites. + +use bouncycastle_core::errors::{KEMError, RNGError}; +use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::kem::{TestFrameworkKEM, TestFrameworkKEMKeys}; +use bouncycastle_ed::x25519::{ + DHKEM_SS_LEN, POINT_LEN, SCALAR_LEN, X25519, X25519PrivateKey, X25519PublicKey, +}; +use bouncycastle_hex as hex; +use bouncycastle_hpke::dhkem::DHKEM; + +type X25519DHKEM = DHKEM; + +/// The DHKEM values of one RFC 9180 Appendix A.1 vector set. +struct Vector { + ikm_e: &'static str, + pk_em: &'static str, + sk_em: &'static str, + ikm_r: &'static str, + pk_rm: &'static str, + sk_rm: &'static str, + enc: &'static str, + shared_secret: &'static str, +} + +/// RFC 9180 Appendix A.1.1 (Base mode) and A.1.2 (PSK mode). The PSK only enters the HPKE key +/// schedule, so for the KEM these are two independent sets. +const VECTORS: [Vector; 2] = [ + Vector { + ikm_e: "7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234", + pk_em: "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431", + sk_em: "52c4a758a802cd8b936eceea314432798d5baf2d7e9235dc084ab1b9cfa2f736", + ikm_r: "6db9df30aa07dd42ee5e8181afdb977e538f5e1fec8a06223f33f7013e525037", + pk_rm: "3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d", + sk_rm: "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8", + enc: "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431", + shared_secret: "fe0e18c9f024ce43799ae393c7e8fe8fce9d218875e8227b0187c04e7d2ea1fc", + }, + Vector { + ikm_e: "78628c354e46f3e169bd231be7b2ff1c77aa302460a26dbfa15515684c00130b", + pk_em: "0ad0950d9fb9588e59690b74f1237ecdf1d775cd60be2eca57af5a4b0471c91b", + sk_em: "463426a9ffb42bb17dbe6044b9abd1d4e4d95f9041cef0e99d7824eef2b6f588", + ikm_r: "d4a09d09f575fef425905d2ab396c1449141463f698f8efdb7accfaff8995098", + pk_rm: "9fed7e8c17387560e92cc6462a68049657246a09bfa8ade7aefe589672016366", + sk_rm: "c5eb01eb457fe6c6f57577c5413b931550a162c71a03ac8d196babbd4e5ce0fd", + enc: "0ad0950d9fb9588e59690b74f1237ecdf1d775cd60be2eca57af5a4b0471c91b", + shared_secret: "727699f009ffe3c076315019c69648366b69171439bd7dd0807743bde76986cd", + }, +]; + +fn decode_32(s: &str) -> [u8; 32] { + hex::decode(s).unwrap().try_into().unwrap() +} + +fn seed(s: &str) -> KeyMaterial256 { + KeyMaterial256::from_bytes_as_type(&decode_32(s), KeyType::Seed).unwrap() +} + +/// Clamps as `decodeScalar25519` does (RFC 7748 Section 5). +fn clamp(mut k: [u8; 32]) -> [u8; 32] { + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; + k +} + +/// `X25519::keygen` with its error mapped, in the shape the KEM test framework takes. +fn keygen() -> Result<(X25519PublicKey, X25519PrivateKey), KEMError> { + X25519::keygen().map_err(|_| KEMError::KeyGenError("X25519 keygen failed")) +} + +/// `Encap` with the vector's ephemeral key: the RNG emits skEm, which keygen uses as the private +/// key unchanged. +#[test] +fn rfc9180_encaps() { + for (i, v) in VECTORS.iter().enumerate() { + let pk_r = X25519PublicKey::from_bytes(&decode_32(v.pk_rm)).unwrap(); + let mut rng = FixedSeedRNG::new(decode_32(v.sk_em)); + let (ss, enc) = X25519::encaps_rng(&pk_r, &mut rng).unwrap(); + assert_eq!(enc, decode_32(v.enc), "vector {i}: enc"); + assert_eq!(enc, decode_32(v.pk_em), "vector {i}: enc = pkEm"); + assert_eq!(ss.ref_to_bytes(), decode_32(v.shared_secret), "vector {i}: shared_secret"); + assert_eq!(ss.key_type(), KeyType::CryptographicRandom, "vector {i}"); + assert_eq!(ss.security_strength(), SecurityStrength::_128bit, "vector {i}"); + } +} + +#[test] +fn rfc9180_decaps() { + for (i, v) in VECTORS.iter().enumerate() { + let sk_r = X25519PrivateKey::from_bytes(&decode_32(v.sk_rm)).unwrap(); + assert_eq!(sk_r.pk().as_bytes(), &decode_32(v.pk_rm), "vector {i}: pk(skRm)"); + let ss = X25519::decaps(&sk_r, &decode_32(v.enc)).unwrap(); + assert_eq!(ss.ref_to_bytes(), decode_32(v.shared_secret), "vector {i}: shared_secret"); + assert_eq!(ss.key_type(), KeyType::CryptographicRandom, "vector {i}"); + assert_eq!(ss.security_strength(), SecurityStrength::_128bit, "vector {i}"); + } +} + +/// `DeriveKeyPair` (RFC 9180 Section 7.1.3) maps ikmE and ikmR to the vectors' key pairs. +/// +/// The vectors list skEm and skRm unclamped, while RFC 9180 Section 7.1.2 requires a serialized +/// private key to be clamped, so the encodings are compared against the clamped vector values. +#[test] +fn rfc9180_derive_key_pair() { + for (i, v) in VECTORS.iter().enumerate() { + for (ikm, pkm, skm) in [(v.ikm_e, v.pk_em, v.sk_em), (v.ikm_r, v.pk_rm, v.sk_rm)] { + let (pk, sk) = X25519DHKEM::derive_key_pair(&seed(ikm)).unwrap(); + assert_eq!(pk.encode(), decode_32(pkm), "vector {i}: pk from ikm {ikm}"); + assert_eq!(sk.encode(), clamp(decode_32(skm)), "vector {i}: sk from ikm {ikm}"); + } + } +} + +#[test] +fn derive_key_pair_rejects_bad_ikm() { + let ikm = VECTORS[0].ikm_e; + + let mac_key = KeyMaterial256::from_bytes_as_type(&decode_32(ikm), KeyType::MACKey).unwrap(); + match X25519DHKEM::derive_key_pair(&mac_key) { + Err(KEMError::KeyGenError(_)) => { /* good */ } + _ => panic!("expected KeyGenError for the wrong key type"), + } + + let short = KeyMaterial256::from_bytes_as_type(&decode_32(ikm)[..31], KeyType::Seed).unwrap(); + match X25519DHKEM::derive_key_pair(&short) { + Err(KEMError::KeyGenError(_)) => { /* good */ } + _ => panic!("expected KeyGenError for a 31-byte ikm"), + } + + let mut weak = seed(ikm); + weak.set_security_strength(SecurityStrength::_112bit).unwrap(); + match X25519DHKEM::derive_key_pair(&weak) { + Err(KEMError::KeyGenError(_)) => { /* good */ } + _ => panic!("expected KeyGenError for a 112-bit ikm"), + } + + // Exactly the algorithm's strength is enough. + let mut just_right = seed(ikm); + just_right.set_security_strength(SecurityStrength::_128bit).unwrap(); + X25519DHKEM::derive_key_pair(&just_right).unwrap(); + + // A longer ikm is allowed. + let long = KeyMaterial256::from_bytes_as_type(&[0x5A; 32], KeyType::Seed).unwrap(); + X25519DHKEM::derive_key_pair(&long).unwrap(); +} + +/// `encaps_rng` rejects an RNG below 128 bits and accepts one at exactly 128 bits. +#[test] +fn encaps_rng_security_strength() { + let (pk, _) = X25519::keygen().unwrap(); + + let mut weak = FixedSeedRNG::new([0x5A; 32]); + weak.set_security_strength(SecurityStrength::_112bit); + match X25519::encaps_rng(&pk, &mut weak) { + Err(KEMError::RNGError(_)) => { /* good */ } + _ => panic!("expected RNGError for a 112-bit RNG"), + } + + let mut just_right = FixedSeedRNG::new([0x5A; 32]); + just_right.set_security_strength(SecurityStrength::_128bit); + X25519::encaps_rng(&pk, &mut just_right).unwrap(); +} + +/// An RNG at full strength whose every request fails. +struct FailingRNG; + +impl RNG for FailingRNG { + fn add_seed_keymaterial(&mut self, _: &dyn KeyMaterialTrait) -> Result<(), RNGError> { + Ok(()) + } + fn next_int(&mut self) -> Result { + Err(RNGError::GenericError("failing RNG")) + } + fn next_bytes(&mut self, _: usize) -> Result, RNGError> { + Err(RNGError::GenericError("failing RNG")) + } + fn next_bytes_out(&mut self, _: &mut [u8]) -> Result { + Err(RNGError::GenericError("failing RNG")) + } + fn fill_keymaterial_out(&mut self, _: &mut dyn KeyMaterialTrait) -> Result { + Err(RNGError::GenericError("failing RNG")) + } + fn security_strength(&self) -> SecurityStrength { + SecurityStrength::_256bit + } +} + +/// An RNG failure while generating the ephemeral key pair is passed through as the RNG's error. +#[test] +fn encaps_rng_passes_through_rng_failure() { + let (pk, _) = X25519::keygen().unwrap(); + match X25519::encaps_rng(&pk, &mut FailingRNG) { + Err(KEMError::RNGError(RNGError::GenericError(_))) => { /* good */ } + other => panic!("expected the RNG's error, got {other:?}"), + } +} + +#[test] +fn kem_framework() { + // DHKEM is randomized. A modified ciphertext is another public key, so decaps succeeds with a + // different shared secret, which is the behaviour the framework checks for an implicitly + // rejecting KEM. Full bit-flipping is off: flipping bit 255 of enc gives the same shared + // secret, since X25519 ignores that bit (RFC 7748 Section 5). + TestFrameworkKEM::new(false, true).test_kem::< + X25519PublicKey, + X25519PrivateKey, + X25519, + POINT_LEN, + SCALAR_LEN, + POINT_LEN, + DHKEM_SS_LEN, + >(keygen, false); +} + +#[test] +fn kem_keys_framework() { + TestFrameworkKEMKeys::new() + .test_keys::(keygen); +} + +/// Low-order public keys give an all-zero X25519 output, which RFC 9180 Section 7.1.4 requires +/// both sides to reject. +#[test] +fn rejects_low_order_public_key() { + let (_, sk) = X25519::keygen().unwrap(); + let mut zero_bit_255 = [0_u8; 32]; + zero_bit_255[31] = 0x80; + let p = decode_32("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"); + + for u in [[0_u8; 32], p, zero_bit_255] { + match X25519::encaps(&X25519PublicKey::from_bytes(&u).unwrap()) { + Err(KEMError::InvalidPublicKey(_)) => { /* good */ } + other => panic!("encaps: expected InvalidPublicKey for u = {u:02x?}, got {other:?}"), + } + match X25519::decaps(&sk, &u) { + Err(KEMError::DecapsulationFailed) => { /* good */ } + other => panic!("decaps: expected DecapsulationFailed for u = {u:02x?}, got {other:?}"), + } + } +} + +/// RFC 9180 Section 7.1.2: SerializePrivateKey clamps its output and DeserializePrivateKey clamps +/// its input, and keys differing only in the clamped bits are the same key. +#[test] +fn private_key_encoding_is_clamped() { + let raw = decode_32(VECTORS[0].sk_rm); + assert_ne!(raw, clamp(raw), "the vector key must be unclamped for this test to mean anything"); + + let sk = X25519PrivateKey::from_bytes(&raw).unwrap(); + assert_eq!(sk.encode(), clamp(raw)); + let mut out = [0xFF_u8; 32]; + assert_eq!(sk.encode_out(&mut out), 32); + assert_eq!(out, clamp(raw)); + + // A key from keygen_from_seed keeps the unclamped bytes, but is equal to the clamped one. + let (_, unclamped) = X25519::keygen_from_seed(&seed(VECTORS[0].sk_rm)).unwrap(); + assert!(unclamped == sk); + assert_eq!(unclamped.encode(), clamp(raw)); + assert!(unclamped.clone() == sk); + + let (_, other) = X25519::keygen().unwrap(); + assert!(other != sk); +} + +#[test] +fn public_key_encoding() { + let bytes = decode_32(VECTORS[0].pk_rm); + let pk = >::from_bytes(&bytes).unwrap(); + assert_eq!(pk.encode(), bytes); + let mut out = [0xFF_u8; 32]; + assert_eq!(pk.encode_out(&mut out), 32); + assert_eq!(out, bytes); + assert_eq!( + pk.to_string(), + format!("X25519PublicKey {{ {} }}", VECTORS[0].pk_rm), + "Display shows the bytes in hex" + ); +} diff --git a/crypto/ed/tests/x25519_key_tests.rs b/crypto/ed/tests/x25519_key_tests.rs new file mode 100644 index 000000000..d8393e648 --- /dev/null +++ b/crypto/ed/tests/x25519_key_tests.rs @@ -0,0 +1,220 @@ +use bouncycastle_core::errors::{KeyAgreementError, RNGError}; +use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{KeyAgreementPrivateKey, KeyAgreementPublicKey}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_ed::x25519::{POINT_LEN, SCALAR_LEN, X25519, X25519PrivateKey, X25519PublicKey}; +use bouncycastle_hex as hex; + +/// RFC 7748 Section 6.1: Alice's private key a and public key X25519(a, 9). +const ALICE_SK: &str = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"; +const ALICE_PK: &str = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"; + +fn seed(hex_str: &str, key_type: KeyType) -> KeyMaterial256 { + KeyMaterial256::from_bytes_as_type(&hex::decode(hex_str).unwrap(), key_type).unwrap() +} + +#[test] +fn core_framework_tests() { + use bouncycastle_core_test_framework::key_agreement::TestFrameworkKeyAgreementKeys; + + let tf = TestFrameworkKeyAgreementKeys::new(); + + tf.test_keys::(X25519::keygen); +} + +#[test] +fn keygen_from_seed() { + for key_type in [KeyType::Seed, KeyType::CryptographicRandom] { + let (pk, sk) = X25519::keygen_from_seed(&seed(ALICE_SK, key_type)).unwrap(); + assert_eq!(pk.as_bytes().as_slice(), hex::decode(ALICE_PK).unwrap()); + X25519::keypair_consistency_check(&pk, &sk).unwrap(); + } +} + +#[test] +fn keygen_from_seed_rejects_bad_seeds() { + // Wrong key type. + match X25519::keygen_from_seed(&seed(ALICE_SK, KeyType::MACKey)) { + Err(KeyAgreementError::KeyGenError(_)) => { /* good */ } + _ => panic!("expected KeyGenError"), + } + + // All zeros: KeyMaterial marks it KeyType::Zeroized. + match X25519::keygen_from_seed(&seed(&"00".repeat(32), KeyType::Seed)) { + Err(KeyAgreementError::KeyGenError(_)) => { /* good */ } + _ => panic!("expected KeyGenError"), + } + + // Undersized. + let short = seed(&ALICE_SK[..62], KeyType::Seed); + assert_eq!(short.key_len(), 31); + match X25519::keygen_from_seed(&short) { + Err(KeyAgreementError::KeyGenError(_)) => { /* good */ } + _ => panic!("expected KeyGenError"), + } + + // Security strength below the algorithm's, and exactly at it. + let mut weak = seed(ALICE_SK, KeyType::Seed); + weak.set_security_strength(SecurityStrength::_112bit).unwrap(); + match X25519::keygen_from_seed(&weak) { + Err(KeyAgreementError::KeyGenError(_)) => { /* good */ } + _ => panic!("expected KeyGenError"), + } + let mut just_right = seed(ALICE_SK, KeyType::Seed); + just_right.set_security_strength(SecurityStrength::_128bit).unwrap(); + X25519::keygen_from_seed(&just_right).unwrap(); +} + +#[test] +fn keygen_from_rng() { + // The RNG output is used as the seed, so this must match keygen_from_seed. + let seed_bytes: [u8; 32] = hex::decode(ALICE_SK).unwrap().try_into().unwrap(); + let mut rng = FixedSeedRNG::new(seed_bytes); + let (pk, sk) = X25519::keygen_from_rng(&mut rng).unwrap(); + assert_eq!(pk.as_bytes().as_slice(), hex::decode(ALICE_PK).unwrap()); + X25519::keypair_consistency_check(&pk, &sk).unwrap(); + + // RNG too weak. + let mut rng = FixedSeedRNG::new(seed_bytes); + rng.set_security_strength(SecurityStrength::_112bit); + match X25519::keygen_from_rng(&mut rng) { + Err(KeyAgreementError::RNGError(RNGError::SecurityStrengthInsufficientForAlgorithm)) => { + /* good */ + } + _ => panic!("expected RNGError::SecurityStrengthInsufficientForAlgorithm"), + } + + // RNG just right. + let mut rng = FixedSeedRNG::new(seed_bytes); + rng.set_security_strength(SecurityStrength::_128bit); + X25519::keygen_from_rng(&mut rng).unwrap(); +} + +#[test] +fn keygen() { + let (pk1, sk1) = X25519::keygen().unwrap(); + let (pk2, sk2) = X25519::keygen().unwrap(); + assert_ne!(pk1, pk2); + X25519::keypair_consistency_check(&pk1, &sk1).unwrap(); + X25519::keypair_consistency_check(&pk2, &sk2).unwrap(); +} + +#[test] +fn keypair_consistency_check() { + let (pk1, sk1) = X25519::keygen().unwrap(); + let (pk2, sk2) = X25519::keygen().unwrap(); + X25519::keypair_consistency_check(&pk1, &sk1).unwrap(); + + match X25519::keypair_consistency_check(&pk1, &sk2) { + Err(KeyAgreementError::ConsistencyCheckFailed(_)) => { /* good */ } + _ => panic!("expected ConsistencyCheckFailed"), + } + match X25519::keypair_consistency_check(&pk2, &sk1) { + Err(KeyAgreementError::ConsistencyCheckFailed(_)) => { /* good */ } + _ => panic!("expected ConsistencyCheckFailed"), + } +} + +#[test] +fn private_key_pk() { + let (_, sk) = X25519::keygen_from_seed(&seed(ALICE_SK, KeyType::Seed)).unwrap(); + assert_eq!(sk.pk().as_bytes().as_slice(), hex::decode(ALICE_PK).unwrap()); + + let (pk, sk) = X25519::keygen().unwrap(); + assert_eq!(sk.pk(), pk); +} + +#[test] +fn private_key_consistency_check() { + let (pk1, sk1) = X25519::keygen().unwrap(); + let (pk2, sk2) = X25519::keygen().unwrap(); + sk1.consistency_check(&pk1).unwrap(); + sk2.consistency_check(&pk2).unwrap(); + + match sk1.consistency_check(&pk2) { + Err(KeyAgreementError::ConsistencyCheckFailed(_)) => { /* good */ } + _ => panic!("expected ConsistencyCheckFailed"), + } + match sk2.consistency_check(&pk1) { + Err(KeyAgreementError::ConsistencyCheckFailed(_)) => { /* good */ } + _ => panic!("expected ConsistencyCheckFailed"), + } +} + +#[test] +fn public_key_to_bytes() { + let (pk, _) = X25519::keygen_from_seed(&seed(ALICE_SK, KeyType::Seed)).unwrap(); + let expected = hex::decode(ALICE_PK).unwrap(); + + let as_ref: &[u8] = pk.as_ref(); + assert_eq!(as_ref, expected); +} + +fn decode_32(s: &str) -> [u8; 32] { + hex::decode(s).unwrap().try_into().unwrap() +} + +/// Clamps as `decodeScalar25519` does (RFC 7748 Section 5). +fn clamp(mut k: [u8; 32]) -> [u8; 32] { + k[0] &= 248; + k[31] &= 127; + k[31] |= 64; + k +} + +#[test] +fn public_key_encoding() { + let bytes = decode_32(ALICE_PK); + let pk = X25519PublicKey::from_bytes(&bytes).unwrap(); + assert_eq!(KeyAgreementPublicKey::encode(&pk), bytes); + + let mut out = [0xFF_u8; 32]; + assert_eq!(KeyAgreementPublicKey::encode_out(&pk, &mut out), 32); + assert_eq!(out, bytes); + + // Decoding round-trips, and accepts any 32 bytes, including bit 255 set (RFC 7748 Section 5). + let decoded = >::from_bytes(&bytes).unwrap(); + assert_eq!(decoded, pk); + let top_bit = [0xFF_u8; 32]; + let decoded = >::from_bytes(&top_bit).unwrap(); + assert_eq!(KeyAgreementPublicKey::encode(&decoded), top_bit); + + for len in [0, 31, 33] { + match >::from_bytes(&vec![1_u8; len]) { + Err(KeyAgreementError::DecodingError(_)) => { /* good */ } + _ => panic!("expected DecodingError for {len} bytes"), + } + } +} + +#[test] +fn private_key_encoding() { + // The RFC 7748 Section 6.1 key is unclamped, so this exercises the clamping. + let raw = decode_32(ALICE_SK); + assert_ne!(raw, clamp(raw)); + + let sk = >::from_bytes(&raw).unwrap(); + assert_eq!(KeyAgreementPrivateKey::encode(&sk), clamp(raw)); + assert_eq!(sk.pk().as_bytes(), &decode_32(ALICE_PK)); + + let mut out = [0xFF_u8; 32]; + assert_eq!(KeyAgreementPrivateKey::encode_out(&sk, &mut out), 32); + assert_eq!(out, clamp(raw)); + + // A key built from the unclamped seed is the same key, with the same encoding, and it + // round-trips through its encoding. + let (_, from_seed) = X25519::keygen_from_seed(&seed(ALICE_SK, KeyType::Seed)).unwrap(); + assert!(from_seed == sk); + assert_eq!(KeyAgreementPrivateKey::encode(&from_seed), clamp(raw)); + let encoded = KeyAgreementPrivateKey::encode(&from_seed); + let decoded = >::from_bytes(&encoded).unwrap(); + assert!(decoded == from_seed); + + for len in [0, 31, 33] { + match >::from_bytes(&vec![1_u8; len]) { + Err(KeyAgreementError::DecodingError(_)) => { /* good */ } + _ => panic!("expected DecodingError for {len} bytes"), + } + } +} diff --git a/crypto/ed/tests/x25519_tests.rs b/crypto/ed/tests/x25519_tests.rs new file mode 100644 index 000000000..cb4aa0be9 --- /dev/null +++ b/crypto/ed/tests/x25519_tests.rs @@ -0,0 +1,174 @@ +use bouncycastle_core::errors::KeyAgreementError; +use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +use bouncycastle_core::traits::{KeyAgreement, KeyAgreementPublicKey}; +use bouncycastle_ed::x25519::{POINT_LEN, SCALAR_LEN, X25519, X25519PrivateKey, X25519PublicKey}; +use bouncycastle_hex as hex; + +#[test] +fn core_framework_tests() { + use bouncycastle_core_test_framework::key_agreement::TestFrameworkKeyAgreement; + + let tf = TestFrameworkKeyAgreement::new(); + + tf.test_key_agreement::( + X25519::keygen, + ); +} + +#[test] +fn iterated() { + rfc7748_check_iterated(1000); +} + +/// Ignored because it's quite slow, can be run with +/// cargo test -p bouncycastle-ed -- --ignored +/// or +/// cargo test -p bouncycastle-ed -- --include-ignored +#[ignore] +#[test] +fn iterated_full() { + rfc7748_check_iterated(1000000); +} + +/// The first X25519 single-call test vector of RFC 7748 Section 5.2. +#[test] +fn vector_1() { + check_vector( + "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4", + "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c", + "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552", + "Vector #1", + ); +} + +/// The second X25519 single-call test vector of RFC 7748 Section 5.2. +#[test] +fn vector_2() { + check_vector( + "4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d", + "e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493", + "95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957", + "Vector #2", + ); +} + +/// RFC 7748 Section 6.1: Alice's and Bob's public keys, X25519(a, 9) and X25519(b, 9), and the +/// shared secret K that each derives from their own private key and the other's public key. +#[test] +fn rfc7748_section_6_1() { + let alice_sk = sk_from_bytes(&decode_32( + "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", + )); + let bob_sk = sk_from_bytes(&decode_32( + "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", + )); + + let alice_pk = alice_sk.pk(); + let bob_pk = bob_sk.pk(); + check_value( + alice_pk.as_bytes(), + "Section 6.1 Alice's public key", + "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a", + ); + check_value( + bob_pk.as_bytes(), + "Section 6.1 Bob's public key", + "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f", + ); + + let k = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"; + let alice_k = X25519::key_agreement(&bob_pk, &alice_sk).unwrap(); + let bob_k = X25519::key_agreement(&alice_pk, &bob_sk).unwrap(); + check_value(alice_k.as_ref(), "Section 6.1 K (Alice)", k); + check_value(bob_k.as_ref(), "Section 6.1 K (Bob)", k); +} + +/// [`X25519::key_agreement`] rejects a public key that gives an all-zero shared secret, however +/// that key is encoded: u = 0, u = p (non-canonical, reduces to 0), and u = 0 with the ignored +/// bit 255 set. +#[test] +fn key_agreement_rejects_all_zero_shared_secret() { + let zero = [0_u8; POINT_LEN]; + let p = decode_32("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"); + let mut zero_bit_255 = zero; + zero_bit_255[31] = 0x80; + + for u in [zero, p, zero_bit_255] { + let sk = sk_from_bytes(&decode_32( + "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", + )); + match X25519::key_agreement(&X25519PublicKey::from_bytes(&u).unwrap(), &sk) { + Err(KeyAgreementError::InvalidPublicKey(_)) => { /* good */ } + Err(e) => panic!("expected InvalidPublicKey for u = {u:02x?}, got {e:?}"), + Ok(_) => panic!("expected InvalidPublicKey for u = {u:02x?}, got Ok"), + } + } +} + +fn decode_32(s: &str) -> [u8; 32] { + hex::decode(s).unwrap().try_into().unwrap() +} + +/// A private key from its 32 bytes, via [`X25519::keygen_from_seed`]. +fn sk_from_bytes(bytes: &[u8]) -> X25519PrivateKey { + let seed = KeyMaterial256::from_bytes_as_type(bytes, KeyType::Seed).unwrap(); + X25519::keygen_from_seed(&seed).unwrap().1 +} + +/// The iterated test of RFC 7748 Section 5.2: starting from k = u = 9, each iteration sets k to +/// `X25519(k, u)` and u to the old k, and k is checked after 1, 1,000 and 1,000,000 iterations. +/// +/// Feeding each output back in as the next scalar and u-coordinate exercises the ladder, field +/// arithmetic and encodings on `count` effectively random inputs against a single known answer, +/// and one wrong step corrupts every value after it. The single-call vectors cover two inputs. +fn rfc7748_check_iterated(count: usize) { + assert_eq!(POINT_LEN, SCALAR_LEN); + + let mut k = [0_u8; POINT_LEN]; + k[0] = 9; + let mut u = [0_u8; POINT_LEN]; + u[0] = 9; + let mut iterations = 0; + while iterations < count { + let r = + X25519::key_agreement(&X25519PublicKey::from_bytes(&u).unwrap(), &sk_from_bytes(&k)) + .unwrap(); + + u = k; + k.copy_from_slice(r.as_ref()); + + iterations += 1; + + match iterations { + 1 => check_value( + &k, + "Iterated @1", + "422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079", + ), + 1000 => check_value( + &k, + "Iterated @1000", + "684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51", + ), + 1000000 => check_value( + &k, + "Iterated @1000000", + "7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424", + ), + _ => {} + } + } +} + +fn check_value(n: &[u8], text: &str, se: &str) { + let e = hex::decode(se).unwrap(); + assert_eq!(e.as_slice(), n, "{}", text); +} + +fn check_vector(sk: &str, su: &str, se: &str, text: &str) { + let k = sk_from_bytes(&decode_32(sk)); + let u = X25519PublicKey::from_bytes(&decode_32(su)).unwrap(); + + let r = X25519::key_agreement(&u, &k).unwrap(); + check_value(r.as_ref(), text, se); +} diff --git a/crypto/ed/tests/x25519_wycheproof.rs b/crypto/ed/tests/x25519_wycheproof.rs new file mode 100644 index 000000000..e033c671d --- /dev/null +++ b/crypto/ed/tests/x25519_wycheproof.rs @@ -0,0 +1,73 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/x25519_test.json`. +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` relative to the root of this git project. If it is +//! absent the tests print a warning and pass, matching the convention used by the other vector +//! suites. +//! +//! Every case is either `valid` or `acceptable`, and every case has an expected shared secret, so +//! each one is checked for exact output. The `acceptable` cases are low-order, twist and +//! non-canonical public keys, which RFC 7748 Section 5 requires to be processed rather than +//! rejected. The exception is those whose shared secret is all zeros (flag `ZeroSharedSecret`), +//! which `key_agreement` rejects with `KeyAgreementError::InvalidPublicKey`. + +use bouncycastle_core::errors::KeyAgreementError; +use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +use bouncycastle_core::traits::{KeyAgreement, KeyAgreementPublicKey}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; +use bouncycastle_ed::x25519::{POINT_LEN, SCALAR_LEN, X25519, X25519PrivateKey, X25519PublicKey}; + +#[test] +fn wycheproof_x25519() { + let filename = "x25519_test.json"; + let Some(doc) = wycheproof_json(filename) else { return }; + + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some("XDH"), "{filename}"); + + let (mut valid, mut acceptable, mut zero_shared) = (0usize, 0usize, 0usize); + for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { + assert_eq!(group.get("curve").and_then(Value::as_str), Some("curve25519"), "{filename}"); + + for test in group.get("tests").and_then(Value::as_array).expect("tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let ctx = format!("{filename} tcId {tc_id}"); + match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => valid += 1, + "acceptable" => acceptable += 1, + other => panic!("{ctx}: unexpected result {other}"), + } + + let k: [u8; SCALAR_LEN] = + hex_field(test, "private", tc_id).try_into().expect("a 32-byte private key"); + let u: [u8; POINT_LEN] = + hex_field(test, "public", tc_id).try_into().expect("a 32-byte public key"); + let shared = hex_field(test, "shared", tc_id); + + let (k, u) = (sk_from_bytes(&k), X25519PublicKey::from_bytes(&u).unwrap()); + match X25519::key_agreement(&u, &k) { + Ok(r) => assert_eq!(r.as_ref(), shared.as_slice(), "{ctx}: shared secret"), + Err(KeyAgreementError::InvalidPublicKey(_)) => { + assert!(shared.iter().all(|&b| b == 0), "{ctx}: rejected a non-zero secret"); + zero_shared += 1; + } + Err(e) => panic!("{ctx}: unexpected error {e:?}"), + } + } + } + + println!( + "Wycheproof X25519: {} cases ({valid} valid, {acceptable} acceptable; \ + {zero_shared} with an all-zero shared secret)", + valid + acceptable + ); + assert!( + valid > 0 && acceptable > 0 && zero_shared > 0, + "{filename}: expected every kind of case" + ); +} + +/// A private key from its 32 bytes, via [`X25519::keygen_from_seed`]. +fn sk_from_bytes(bytes: &[u8]) -> X25519PrivateKey { + let seed = KeyMaterial256::from_bytes_as_type(bytes, KeyType::Seed).unwrap(); + X25519::keygen_from_seed(&seed).unwrap().1 +} diff --git a/crypto/hpke/Cargo.toml b/crypto/hpke/Cargo.toml new file mode 100644 index 000000000..c54b2ea89 --- /dev/null +++ b/crypto/hpke/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bouncycastle-hpke" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-hmac.workspace = true +bouncycastle-utils.workspace = true diff --git a/crypto/hpke/src/dhkem.rs b/crypto/hpke/src/dhkem.rs new file mode 100644 index 000000000..566339dfd --- /dev/null +++ b/crypto/hpke/src/dhkem.rs @@ -0,0 +1,303 @@ +//! The DH-Based KEM (DHKEM) of RFC 9180 Section 4.1, generic over the Diffie-Hellman group. +//! +//! A group's crate implements [`DHKEMParams`] for its algorithm struct and then calls +//! [`DHKEM::encap_rng`] and [`DHKEM::decap`] from its +//! [`KEMEncapsulator`](bouncycastle_core::traits::KEMEncapsulator) and +//! [`KEMDecapsulator`](bouncycastle_core::traits::KEMDecapsulator) impls. + +use bouncycastle_core::errors::{KEMError, KeyAgreementError, RNGError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, MAC, RNG}; +use bouncycastle_hmac::HMAC; +use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; + +/// The version label that RFC 9180 Section 4 puts at the front of every labeled KDF input. +const HPKE_VERSION_LABEL: &[u8] = b"HPKE-v1"; + +// The labels of RFC 9180 Sections 4.1 and 7.1.3. +const LABEL_EAE_PRK: &[u8] = b"eae_prk"; +const LABEL_SHARED_SECRET: &[u8] = b"shared_secret"; +const LABEL_DKP_PRK: &[u8] = b"dkp_prk"; +const LABEL_SK: &[u8] = b"sk"; + +/// The largest Nh among the KDFs of RFC 9180 Section 7.2 (HKDF-SHA512). The PRK and HKDF-Expand +/// block buffers are sized to it. +const MAX_NH: usize = 64; + +/// The Diffie-Hellman group of a DHKEM, and the KDF it is paired with: DHKEM(Group, KDF) in RFC 9180 +/// Section 4.1. +/// +/// Implemented by a group's algorithm struct. The const parameters are the group's sizes from RFC +/// 9180 Section 7.1: `N_PK` is Npk (which equals Nenc), `N_SK` is Nsk and `N_DH` is Ndh. +pub trait DHKEMParams: Algorithm { + /// The group's public key. + type PublicKey; + /// The group's private key. + type PrivateKey; + /// The hash of the DHKEM's KDF, which is HKDF over this hash. + type KDFHash: Hash + HashAlgParams + Default; + /// The `kem_id` of RFC 9180 Section 7.1, Table 2. + const KEM_ID: u16; + + /// `GenerateKeyPair()` (RFC 9180 Section 4), drawing its randomness from `rng`. + fn generate_key_pair_rng( + rng: &mut dyn RNG, + ) -> Result<(Self::PublicKey, Self::PrivateKey), KEMError>; + + /// The private key with the byte string `sk` as its value, as `DeriveKeyPair` (RFC 9180 + /// Section 7.1.3) requires. + fn private_key_from_bytes(sk: &[u8; N_SK]) -> Self::PrivateKey; + + /// `pk(skX)` (RFC 9180 Section 4): the public key of `sk`. + fn pk(sk: &Self::PrivateKey) -> Self::PublicKey; + + /// `DH(skX, pkY)` (RFC 9180 Section 4.1), including the validation of its output that RFC 9180 + /// Section 7.1.4 requires for the group. A validation failure is an `Err`. + fn dh( + sk: &Self::PrivateKey, + pk: &Self::PublicKey, + ) -> Result, KeyAgreementError>; + + /// `SerializePublicKey(pkX)` (RFC 9180 Section 7.1.1). + fn serialize_public_key(pk: &Self::PublicKey) -> [u8; N_PK]; + + /// `DeserializePublicKey(pkXm)` (RFC 9180 Section 7.1.1). + fn deserialize_public_key(pkm: &[u8; N_PK]) -> Result; +} + +/// The DHKEM of RFC 9180 Section 4.1 over the group `G`, without the optional `AuthEncap` and +/// `AuthDecap`. +/// +/// The const parameters are `G`'s sizes (see [`DHKEMParams`]) plus `N_SECRET`, the Nsecret of RFC +/// 9180 Section 7.1, which must equal the output length of [`DHKEMParams::KDFHash`] (Section 4.1: +/// "the size Nsecret of the KEM shared secret is equal to the output length of the hash function +/// underlying the KDF"). +pub struct DHKEM +{ + _group: PhantomData, +} + +impl< + G: DHKEMParams, + const N_PK: usize, + const N_SK: usize, + const N_DH: usize, + const N_SECRET: usize, +> DHKEM +{ + /// `suite_id = concat("KEM", I2OSP(kem_id, 2))` (RFC 9180 Section 4.1). + const SUITE_ID: [u8; 5] = { + let kem_id = G::KEM_ID.to_be_bytes(); + [b'K', b'E', b'M', kem_id[0], kem_id[1]] + }; + + /// Checked when a DHKEM is instantiated, so a wrong set of const parameters fails to compile. + const PARAMS_CHECK: () = { + let nh = ::OUTPUT_LEN; + assert!(N_SECRET == nh, "Nsecret must equal the output length of the KDF's hash"); + assert!(nh <= MAX_NH, "the KDF's hash output is longer than MAX_NH"); + // HKDF-Expand's L, which here is Nsecret or Nsk, must be at most 255*HashLen (RFC 5869 + // Section 2.3), and fit the two bytes of I2OSP(L, 2) in LabeledExpand. + assert!(N_SK <= 255 * nh && N_SK <= 0xFFFF, "Nsk is too long for HKDF-Expand"); + }; + + /// `Encap(pkR)` (RFC 9180 Section 4.1), drawing the ephemeral key pair from `rng`. + /// + /// Returns the shared secret and `enc`, as a [`KeyType::CryptographicRandom`] key at the + /// group's security strength. + /// + /// # Errors + /// + /// * [`KEMError::RNGError`] if `rng`'s security strength is below the group's. + /// * [`KEMError::InvalidPublicKey`] if `pk_r` fails the group's validation (RFC 9180 Section + /// 7.1.4: "the sender MUST validate the recipient's public key pkR"). + /// * Any error from [`DHKEMParams::generate_key_pair_rng`]. + pub fn encap_rng( + pk_r: &G::PublicKey, + rng: &mut dyn RNG, + ) -> Result<(KeyMaterial, [u8; N_PK]), KEMError> { + let () = Self::PARAMS_CHECK; + + if rng.security_strength() < G::MAX_SECURITY_STRENGTH { + return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; + } + + // skE, pkE = GenerateKeyPair() + let (pk_e, sk_e) = G::generate_key_pair_rng(rng)?; + // dh = DH(skE, pkR) + let dh = G::dh(&sk_e, pk_r).map_err(|_| { + KEMError::InvalidPublicKey("The recipient's public key failed validation") + })?; + // enc = SerializePublicKey(pkE) + let enc = G::serialize_public_key(&pk_e); + + // pkRm = SerializePublicKey(pkR) + let pk_rm = G::serialize_public_key(pk_r); + // kem_context = concat(enc, pkRm) + // shared_secret = ExtractAndExpand(dh, kem_context) + // Deviation from the RFC: kem_context is passed as its two parts rather than concatenated, + // since it is only ever fed to HMAC, which absorbs the parts in order. + let shared_secret = Self::extract_and_expand(&*dh, &[&enc, &pk_rm]); + // return shared_secret, enc + Ok((shared_secret, enc)) + } + + /// `Decap(enc, skR)` (RFC 9180 Section 4.1). + /// + /// Returns the shared secret, as a [`KeyType::CryptographicRandom`] key at the group's security + /// strength. + /// + /// # Errors + /// + /// [`KEMError::DecapsulationFailed`] if `enc` does not deserialize, or fails the group's + /// validation (RFC 9180 Section 7.1.4: "the recipient MUST validate the ephemeral public key + /// pkE"). + pub fn decap( + enc: &[u8; N_PK], + sk_r: &G::PrivateKey, + ) -> Result, KEMError> { + let () = Self::PARAMS_CHECK; + + // pkE = DeserializePublicKey(enc) + let pk_e = G::deserialize_public_key(enc).map_err(|_| KEMError::DecapsulationFailed)?; + // dh = DH(skR, pkE) + let dh = G::dh(sk_r, &pk_e).map_err(|_| KEMError::DecapsulationFailed)?; + + // pkRm = SerializePublicKey(pk(skR)) + let pk_rm = G::serialize_public_key(&G::pk(sk_r)); + // kem_context = concat(enc, pkRm) + // shared_secret = ExtractAndExpand(dh, kem_context) + // Deviation from the RFC: kem_context is passed in two parts; see encap_rng. + let shared_secret = Self::extract_and_expand(&*dh, &[enc, &pk_rm]); + // return shared_secret + Ok(shared_secret) + } + + /// `DeriveKeyPair(ikm)` (RFC 9180 Section 7.1.3), for groups whose private key is the byte string + /// itself, such as X25519 and X448. + /// + /// Section 7.1.3: "the ikm parameter given to DeriveKeyPair() SHOULD have length at least Nsk, + /// and SHOULD have at least Nsk bytes of entropy". This requires `ikm` to be at least Nsk bytes, + /// of [`KeyType::Seed`] or [`KeyType::CryptographicRandom`], and at least the group's security + /// strength. + /// + /// # Errors + /// + /// [`KEMError::KeyGenError`] if `ikm` does not meet those requirements. + pub fn derive_key_pair( + ikm: &impl KeyMaterialTrait, + ) -> Result<(G::PublicKey, G::PrivateKey), KEMError> { + let () = Self::PARAMS_CHECK; + + if !(ikm.key_type() == KeyType::Seed || ikm.key_type() == KeyType::CryptographicRandom) + || ikm.key_len() < N_SK + { + return Err(KEMError::KeyGenError( + "ikm must be at least Nsk bytes and KeyType::Seed or KeyType::CryptographicRandom.", + )); + } + if ikm.security_strength() < G::MAX_SECURITY_STRENGTH { + return Err(KEMError::KeyGenError( + "ikm SecurityStrength must match algorithm security strength", + )); + } + + // dkp_prk = LabeledExtract("", "dkp_prk", ikm) + let dkp_prk = Self::labeled_extract(LABEL_DKP_PRK, ikm.ref_to_bytes()); + // sk = LabeledExpand(dkp_prk, "sk", "", Nsk) + let mut sk = Secret::<[u8; N_SK]>::new(); + Self::labeled_expand(&dkp_prk, LABEL_SK, &[], &mut *sk); + let sk = G::private_key_from_bytes(&sk); + // return (sk, pk(sk)) + Ok((G::pk(&sk), sk)) + } + + /// `ExtractAndExpand(dh, kem_context)` (RFC 9180 Section 4.1), with `kem_context` given as the + /// parts it is the concatenation of. + /// + /// The output is labelled as a full-entropy key at the group's strength: it is HKDF output, and + /// so pseudorandom, unlike the DH output it is derived from. + // #[inline(never)]: inlined into encap_rng, its two HMAC states were allocated for the whole + // encapsulation, on top of the key generation that sets the peak. + #[inline(never)] + fn extract_and_expand(dh: &[u8], kem_context: &[&[u8]]) -> KeyMaterial { + // eae_prk = LabeledExtract("", "eae_prk", dh) + let eae_prk = Self::labeled_extract(LABEL_EAE_PRK, dh); + // shared_secret = LabeledExpand(eae_prk, "shared_secret", kem_context, Nsecret) + let mut shared_secret = Secret::<[u8; N_SECRET]>::new(); + Self::labeled_expand(&eae_prk, LABEL_SHARED_SECRET, kem_context, &mut *shared_secret); + + // Cannot fail: N_SECRET bytes fit a KeyMaterial, and an all-zero output gives + // Ok(Zeroized). + let mut key = + KeyMaterial::from_bytes_as_type(&*shared_secret, KeyType::CryptographicRandom).unwrap(); + if key.key_type() == KeyType::CryptographicRandom { + // Cannot fail: N_SECRET >= 32 bytes is enough for any strength, and the inferred + // strength (from_bytes_as_type sets it from the length) is lowered, never raised. + key.set_security_strength(G::MAX_SECURITY_STRENGTH).unwrap(); + } + key + } + + /// `LabeledExtract("", label, ikm)` (RFC 9180 Section 4), where `Extract` is HKDF-Extract + /// (RFC 5869 Section 2.2) over [`DHKEMParams::KDFHash`]. Every call in a DHKEM has an empty salt. + /// + /// Returns the PRK as a MAC key, ready for [`Self::labeled_expand`]. + fn labeled_extract(label: &[u8], ikm: &[u8]) -> KeyMaterial { + // PRK = HMAC-Hash(salt, IKM) + // An empty salt is an empty HMAC key. RFC 5869 Section 2.2 says an absent salt "is set to a + // string of HashLen zeros", which is the same HMAC key, as HMAC zero-pads its key. + // Cannot fail: an empty KeyMaterial is Zeroized, which HMAC accepts as a weak key. + let mut hmac = HMAC::::new_allow_weak_key(&KeyMaterial::<0>::new()).unwrap(); + // labeled_ikm = concat("HPKE-v1", suite_id, label, ikm) + // Deviation from the RFC: the parts of labeled_ikm are absorbed by HMAC in order rather than + // concatenated first. + hmac.do_update(HPKE_VERSION_LABEL); + hmac.do_update(&Self::SUITE_ID); + hmac.do_update(label); + hmac.do_update(ikm); + + let mut prk = Secret::<[u8; MAX_NH]>::new(); + // Cannot fail: the buffer is at least the hash output length (PARAMS_CHECK). + let nh = hmac.do_final_out(&mut *prk).unwrap(); + // Cannot fail: nh <= MAX_NH bytes fit a KeyMaterial. + KeyMaterial::from_bytes_as_type(&prk[..nh], KeyType::MACKey).unwrap() + } + + /// `LabeledExpand(prk, label, info, L)` (RFC 9180 Section 4), where `Expand` is HKDF-Expand (RFC + /// 5869 Section 2.3) over [`DHKEMParams::KDFHash`], `L` is `out.len()`, and `info` is given as + /// the parts it is the concatenation of. + fn labeled_expand(prk: &KeyMaterial, label: &[u8], info: &[&[u8]], out: &mut [u8]) { + let nh = ::OUTPUT_LEN; + // L is Nsecret or Nsk, which PARAMS_CHECK bounds by 255*HashLen and by 0xFFFF. + debug_assert!(out.len() <= 255 * nh && out.len() <= 0xFFFF); + let l = (out.len() as u16).to_be_bytes(); + + // T(0) = empty string (zero length) + let mut t = Secret::<[u8; MAX_NH]>::new(); + let mut t_len = 0; + // N = ceil(L/HashLen); T = T(1) | T(2) | ... | T(N); OKM = first L octets of T + // Each chunk of `out` is one T(i), the last one truncated. + for (i, okm_chunk) in out.chunks_mut(nh).enumerate() { + // T(i) = HMAC-Hash(PRK, T(i-1) | info | i), with info = labeled_info + // Cannot fail: the PRK is a MAC key of the hash's full output length. + let mut hmac = HMAC::::new(prk).unwrap(); + hmac.do_update(&t[..t_len]); + // labeled_info = concat(I2OSP(L, 2), "HPKE-v1", suite_id, label, info) + // Deviation from the RFC: absorbed in parts, as in labeled_extract. + hmac.do_update(&l); + hmac.do_update(HPKE_VERSION_LABEL); + hmac.do_update(&Self::SUITE_ID); + hmac.do_update(label); + for part in info { + hmac.do_update(part); + } + // i <= N <= 255, since L <= 255*HashLen. + hmac.do_update(&[(i + 1) as u8]); + // Cannot fail: the buffer is at least the hash output length (PARAMS_CHECK). + t_len = hmac.do_final_out(&mut *t).unwrap(); + okm_chunk.copy_from_slice(&t[..okm_chunk.len()]); + } + } +} diff --git a/crypto/hpke/src/lib.rs b/crypto/hpke/src/lib.rs new file mode 100644 index 000000000..95a263872 --- /dev/null +++ b/crypto/hpke/src/lib.rs @@ -0,0 +1,37 @@ +//! Building blocks of Hybrid Public Key Encryption (HPKE), as specified in RFC 9180. +//! +//! This is a utility crate and is not intended to be used directly. It provides +//! [`dhkem::DHKEM`] -- the DH-Based KEM of RFC 9180 Section 4.1, generic over any Diffie-Hellman +//! group that implements [`dhkem::DHKEMParams`], the extension point through which a group +//! declares its keys, its DH operation and the KDF that the DHKEM pairs it with. Each group's crate +//! implements that trait and exposes the result through the +//! [`KEMEncapsulator`](bouncycastle_core::traits::KEMEncapsulator) and +//! [`KEMDecapsulator`](bouncycastle_core::traits::KEMDecapsulator) traits on its own algorithm +//! struct +//! +//! For example: +//! +//! | KEM | kem_id | Exposed as | +//! |------------------------------|--------|--------------------------------| +//! | DHKEM(X25519, HKDF-SHA256) | 0x0020 | `bouncycastle_ed::x25519::X25519` | +//! +//! The KDF is HKDF over the hash named by [`dhkem::DHKEMParams::KDFHash`], computed on +//! [`HMAC`](bouncycastle_hmac::HMAC) as RFC 5869 defines it, so any hash that +//! [`HMAC`](bouncycastle_hmac::HMAC) supports can be used. +//! +//! The rest of HPKE (the key schedule, AEAD encryption and secret export of RFC 9180 Section 5) is +//! not yet implemented. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! * [`dhkem::DHKEMParams::dh`] must perform the group's validation of the DH output (RFC 9180 +//! Section 7.1.4), such as rejecting the all-zero X25519 output. [`dhkem::DHKEM`] relies on it to +//! reject unusable public keys. +//! * [`dhkem::DHKEM::derive_key_pair`] is deterministic: the key pair is only as secret as the +//! input keying material. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +pub mod dhkem; diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index fb55fb094..fa6e5548a 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -30,3 +30,7 @@ path = "src/bench_ccm_mem_usage.rs" [[bin]] name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" + +[[bin]] +name = "bench_x25519_mem_usage" +path = "src/bench_x25519_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_x25519_mem_usage.rs b/mem_usage_benches/src/bench_x25519_mem_usage.rs new file mode 100644 index 000000000..69fe6a016 --- /dev/null +++ b/mem_usage_benches/src/bench_x25519_mem_usage.rs @@ -0,0 +1,169 @@ +//! The purpose of this binary is to perform a single run of the primitive under test so that +//! its peak memory usage can be measured with: +//! +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_x25519_mem_usage > /dev/null +//! +//! ms_print massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! Note: print!() forces the compiler not to optimize away the actual code. The name of the bench +//! goes to stderr so that the junk on stdout can be piped to /dev/null. +//! +//! Main is at the bottom, controls which bench is actually run. +//! +//! The keys and the ciphertext are the published test vectors (RFC 7748 Section 6.1 and RFC 9180 +//! Appendix A.1.1), hard-coded so that no keygen runs in a bench that does not measure it. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::core::key_material::{KeyMaterial256, KeyType}; +use bouncycastle::core::traits::{ + KEMDecapsulator, KEMEncapsulator, KeyAgreement, KeyAgreementPrivateKey, KeyAgreementPublicKey, +}; +use bouncycastle::ed::x25519::{X25519, X25519PrivateKey, X25519PublicKey}; + +/// Runs the operation under test in its own stack frame, with the key already loaded by the +/// caller. +/// +/// `#[inline(never)]` matters here too: the bench functions inline the whole operation, so +/// without this boundary the operation's frame would be allocated on entry to the bench and the +/// key-load helper's frame would stack on top of it, reporting their sum instead of their max. +/// The closure returns nothing, so no result is copied back across the boundary. +#[inline(never)] +fn measure(f: impl FnOnce()) { + f() +} + +/// This exists so I can use /usr/bin/time to measure the base memory footprint of the harness. +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// This prints the in-memory size of the public and private key structs. +fn print_struct_sizes() { + println!("X25519PublicKey: {}", size_of::()); + println!("X25519PrivateKey: {}", size_of::()); +} + +/// Loads RFC 7748 Section 6.1's Alice private key. `#[inline(never)]` so its byte array and decode +/// temporaries are popped before the operation runs. +#[inline(never)] +fn load_alice_sk() -> X25519PrivateKey { + X25519PrivateKey::from_bytes(&[ + 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, + ]) + .unwrap() +} + +/// Loads RFC 7748 Section 6.1's Bob public key. +#[inline(never)] +fn load_bob_pk() -> X25519PublicKey { + X25519PublicKey::from_bytes(&[ + 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, + ]) + .unwrap() +} + +/// Loads RFC 9180 Appendix A.1.1's skRm. +#[inline(never)] +fn load_sk_rm() -> X25519PrivateKey { + X25519PrivateKey::from_bytes(&[ + 0x46, 0x12, 0xc5, 0x50, 0x26, 0x3f, 0xc8, 0xad, 0x58, 0x37, 0x5d, 0xf3, 0xf5, 0x57, 0xaa, + 0xc5, 0x31, 0xd2, 0x68, 0x50, 0x90, 0x3e, 0x55, 0xa9, 0xf2, 0x3f, 0x21, 0xd8, 0x53, 0x4e, + 0x8a, 0xc8, + ]) + .unwrap() +} + +/// Loads RFC 9180 Appendix A.1.1's pkRm. +#[inline(never)] +fn load_pk_rm() -> X25519PublicKey { + X25519PublicKey::from_bytes(&[ + 0x39, 0x48, 0xcf, 0xe0, 0xad, 0x1d, 0xdb, 0x69, 0x5d, 0x78, 0x0e, 0x59, 0x07, 0x71, 0x95, + 0xda, 0x6c, 0x56, 0x50, 0x6b, 0x02, 0x73, 0x29, 0x79, 0x4a, 0xb0, 0x2b, 0xca, 0x80, 0x81, + 0x5c, 0x4d, + ]) + .unwrap() +} + +/// Key generation from a seed (RFC 7748 Section 6.1's Alice private key): one fixed-base scalar +/// multiplication. The seed is the input, so it is built in the bench body. +fn bench_x25519_keygen() { + eprintln!("X25519/KeyGen"); + + let seed = KeyMaterial256::from_bytes_as_type( + &[ + 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, + ], + KeyType::Seed, + ) + .unwrap(); + + measure(|| { + let (pk, _sk) = X25519::keygen_from_seed(&seed).unwrap(); + print!("{:x?}", pk.encode()); + }); +} + +/// Key agreement: one variable-base scalar multiplication and the all-zero check. +fn bench_x25519_key_agreement() { + eprintln!("X25519/KeyAgreement"); + + let sk = load_alice_sk(); + let pk = load_bob_pk(); + measure(|| { + let k = X25519::key_agreement(&pk, &sk).unwrap(); + print!("{:x?}", k.as_ref()); + }); +} + +/// DHKEM(X25519, HKDF-SHA256) encapsulation, including the OS-seeded DRBG that generates the +/// ephemeral key, since that is part of what an encapsulation costs. +fn bench_x25519_encaps() { + eprintln!("X25519/Encaps"); + + let pk = load_pk_rm(); + measure(|| { + let (ss, ct) = X25519::encaps(&pk).unwrap(); + print!("{:x?}", ss); + print!("{:x?}", ct); + }); +} + +/// DHKEM(X25519, HKDF-SHA256) decapsulation of RFC 9180 Appendix A.1.1's enc. +fn bench_x25519_decaps() { + eprintln!("X25519/Decaps"); + + let ct: [u8; 32] = [ + 0x37, 0xfd, 0xa3, 0x56, 0x7b, 0xdb, 0xd6, 0x28, 0xe8, 0x86, 0x68, 0xc3, 0xc8, 0xd7, 0xe9, + 0x7d, 0x1d, 0x12, 0x53, 0xb6, 0xd4, 0xea, 0x6d, 0x44, 0xc1, 0x50, 0xf7, 0x41, 0xf1, 0xbf, + 0x44, 0x31, + ]; + + let sk = load_sk_rm(); + measure(|| { + let ss = X25519::decaps(&sk, &ct).unwrap(); + print!("{:x?}", ss); + }); +} + +fn main() { + // print_struct_sizes() + // bench_do_nothing() + bench_x25519_keygen() + // bench_x25519_key_agreement() + // bench_x25519_encaps() + // bench_x25519_decaps() +} diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index 8c3f55926..677db1c96 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -4,3 +4,4 @@ mod bench_ccm_mem_usage; mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha3_mem_usage; +mod bench_x25519_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index ae250f332..5d5fc328a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,12 @@ pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; pub use bouncycastle_cipher as cipher; pub use bouncycastle_core as core; +pub use bouncycastle_ed as ed; pub use bouncycastle_factory as factory; pub use bouncycastle_hex as hex; pub use bouncycastle_hkdf as hkdf; pub use bouncycastle_hmac as hmac; +pub use bouncycastle_hpke as hpke; pub use bouncycastle_mldsa as mldsa; pub use bouncycastle_mldsa_lowmemory as mldsa_lowmemory; pub use bouncycastle_mlkem as mlkem;