Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4e5d288
Some incremental progress towards 25519
ounsworth Aug 20, 2026
71183d8
updated cargo.toml
ounsworth Aug 24, 2026
b455d0c
progress towards 25519
ounsworth Aug 25, 2026
ba80ce5
progress on 25519
ounsworth Aug 26, 2026
a0aeddf
progress on 25519
ounsworth Aug 26, 2026
cf2677b
progress on 25519
ounsworth Aug 26, 2026
d2cf4f1
Adding tests and a todo list for finishing this feature.
Oct 29, 2026
c467db0
Progress on x25519
Oct 30, 2026
f182bb1
Progress on x25519
Oct 30, 2026
07b6629
Progress on x25519
Oct 30, 2026
6b1df21
Progress on x25519
Oct 30, 2026
d806822
Progress on x25519
Oct 30, 2026
acfbad7
cargo mutants pass for x25519
Oct 30, 2026
6df5b79
Implemented core::traits::KeyAgreement for X25519. And started writin…
Oct 30, 2026
a467ea3
Impl'd KEMEncapsulator / KEMDecapsulator for x25519 via the new crypt…
Oct 30, 2026
313ca10
Added key encoding / decoding routines for x25519.
Oct 30, 2026
87f0ea1
Added core-test-framework/key_agreement.rs
Oct 30, 2026
5e54dde
Added a cli for x25519
Oct 30, 2026
ab7997c
Re-ran mutants. Nothing new found.
Oct 30, 2026
85045e8
Added perf and memory benches.
Oct 30, 2026
a338dbf
Split the ladder into two functions, which lowers x25519 stack usage …
Oct 31, 2026
f0f45f7
Docs and style tweaks.
Oct 31, 2026
270fac8
DONE X25519!!
Oct 31, 2026
11a04ec
doc tweak
Oct 31, 2026
78ab66f
Suggestions from Nicola Tuveri: have a separate type for the DerivedK…
Oct 31, 2026
a2744b3
Removed the X25519PublicKey -> X25519DerivedKey promotion from the pu…
Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`, 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.

Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions QUALITY_AND_STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<T>` 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;
Expand Down
26 changes: 26 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String>,

#[arg(long)]
/// The public key file (in hex or binary) for agree and encaps
pkfile: Option<String>,

#[arg(long)]
/// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from.
ctfile: Option<String>,

#[arg(short)]
/// Output in hex format.
x: bool,
},
}

fn main() {
Expand Down Expand Up @@ -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")
}
Expand Down
160 changes: 160 additions & 0 deletions cli/src/x25519_cmd.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pkfile: &Option<String>,
ctfile: &Option<String>,
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<String>) -> Vec<u8> {
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);
}
}
}
Loading
Loading