Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
d118e89
core, core-test-framework: two-party key agreement on the key objects
dghgit Nov 7, 2026
cef9554
ec: P-256 key agreement example on the agreement roles
dghgit Nov 7, 2026
fd555a0
core, core-test-framework, ec: AgreementKey::agree takes the peer key…
dghgit Nov 7, 2026
568cc4a
ec: the P-256 key agreement example takes its HKDF parameters as boun…
dghgit Nov 7, 2026
9345b9b
core-test-framework: the key agreement suite accepts a derived key's …
dghgit Nov 7, 2026
433eca4
eccdh: ECC CDH (SP 800-56A Rev. 3 Section 5.7.1.2) over P-256, P-384,…
dghgit Nov 7, 2026
8b37a09
cli: eccdh-p256, -p384, -p521, -secp256k1, -bp256r1, -bp384r1 and -bp…
dghgit Nov 7, 2026
b7560d8
mem_usage_benches: ECC CDH stack harness; eccdh: the stack table
dghgit Nov 7, 2026
f868d4c
eccdh, ec: the P-256 key agreement example moves onto the crate's own…
dghgit Nov 7, 2026
2d4aa0f
x963kdf: the ANSI X9.63 KDF (SEC 1 v2 Section 3.6.1) over SHA-224, SH…
dghgit Nov 7, 2026
0554ed4
factory: X963KDF-SHA256, -SHA384 and -SHA512 in KDFFactory
dghgit Nov 7, 2026
47803c0
cli: x963kdf-sha256, x963kdf-sha384 and x963kdf-sha512
dghgit Nov 7, 2026
56a795d
eccdh: SEC 1 Section 6.1's ECDH scheme on every curve, Z through the …
dghgit Nov 7, 2026
b7b5358
x963kdf: the test pins which check refuses a length just below step 2…
dghgit Nov 7, 2026
713bb70
eccdh: the kdf module's docs link KdfParams by its import
dghgit Nov 7, 2026
05faf7c
x963kdf: a key with no strength credits nothing, and a zero-length de…
dghgit Nov 7, 2026
6a4f246
x963kdf: the crate docs' Z states its strength
dghgit Nov 7, 2026
a1b1209
x963kdf: a derivation from no keys is refused
dghgit Nov 8, 2026
8491143
eccdh: the KDF roles take the derived key's length from KdfParams
dghgit Nov 8, 2026
3870f52
core: the agreement worker and role write the shared secret into the …
dghgit Nov 8, 2026
b13462e
core-test-framework: the key-agreement suite drives agree_out over pr…
dghgit Nov 8, 2026
dc81356
eccdh: the plain and KDF workers implement agree_out, and the pairing…
dghgit Nov 8, 2026
a9c4e81
release notes: the agreement _out form
dghgit Nov 8, 2026
2a2690d
eccdh: the pairing docs say the length is agreed with the info and do…
dghgit Nov 8, 2026
e3e9239
eccdh, factory, x963kdf: the adaptations the merges of feature/kem-re…
dghgit Nov 8, 2026
ea8b62e
Merge branch 'release/0.1.4alpha' into feature/eccdh
dghgit Nov 8, 2026
475a58c
concatkdf: the NIST concatenation KDF (SP 800-56C Rev. 2 Section 4.1,…
dghgit Nov 9, 2026
64264b3
factory: ConcatKDF-SHA256, -SHA384, -SHA512 and -SHA3-256, -SHA3-384,…
dghgit Nov 9, 2026
603330c
cli: concatkdf-sha256, -sha384, -sha512, -sha3-256, -sha3-384 and -sh…
dghgit Nov 9, 2026
d671502
eccdh: SP 800-56A Rev. 3 Section 5.8's one-step derivation on every c…
dghgit Nov 9, 2026
6c4ddd4
Merge branch 'release/0.1.4alpha' into feature/eccdh
dghgit Nov 10, 2026
9aa487f
ec: the extra-random-bits reduction holds the DRBG output, its reduct…
dghgit Nov 10, 2026
111839b
ec: the coordinate range check is pinned on a point that is on the cu…
dghgit Nov 10, 2026
5335bf8
core-test-framework: agree_out's zeroized-on-error contract is tested…
dghgit Nov 10, 2026
37108b9
eccdh: the loaded private key and the agreement's intermediates stay …
dghgit Nov 10, 2026
3017075
eccdh: the Wycheproof runner pins how many vectors agreed and require…
dghgit Nov 10, 2026
315784a
factory: the three X9.63 KDF arms are checked against CAVP known answers
dghgit Nov 10, 2026
f11eb90
cli: the eccdh help sends Z to a KDF through --ikm-file
dghgit Nov 10, 2026
baa6a31
ec: SM2's coordinate range check is pinned the same way, and its comm…
dghgit Nov 10, 2026
19f3c09
core: KeyAgreementError carries the KDF's error, and its variants are…
dghgit Nov 10, 2026
c54f5a7
core-test-framework: the key-agreement suites implement Default
dghgit Nov 10, 2026
f60e59a
x963kdf: the empty-key edge is documented and pinned, the bound is he…
dghgit Nov 10, 2026
e3d5f96
concatkdf: the empty-key edge is pinned, and the bound tests fit a 32…
dghgit Nov 10, 2026
45422de
eccdh: one spelling of the pairing names, the KDF's error passed thro…
dghgit Nov 10, 2026
49b453d
ec: the scalars reduce from borrowed limbs into their Secret, and the…
dghgit Nov 10, 2026
91cdd8e
eccdh: the private key is built from the borrowed Secret limbs, Z is …
dghgit Nov 10, 2026
85f525a
concatkdf: the step 4 wording, the multi-key doc and the post-loop ze…
dghgit Nov 10, 2026
742f6a9
x963kdf: the step 2 bound test matches the bound's own message
dghgit Nov 10, 2026
10064f9
cli: a known answer for each concatkdf-* command, and the eccdh help …
dghgit Nov 10, 2026
265882c
ec: the extra_bits comment on what is left speaks for its own frame
dghgit Nov 10, 2026
0024f61
eccdh: the crate docs name the private key's missing _out form and th…
dghgit Nov 10, 2026
29114c8
eccdh: z_from_product leaves the buffer zeroized on its two late erro…
dghgit Nov 10, 2026
55a3696
Merge release/0.1.4alpha c999768f (PR #180: BLAKE2b-512, BLAKE2s-256 …
dghgit Nov 11, 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
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ bouncycastle-blake3 = { path = "./crypto/blake3" }
bouncycastle-chacha20 = { path = "./crypto/chacha20" }
bouncycastle-chacha20-poly1305-aead = { path = "./crypto/chacha20-poly1305-aead" }
bouncycastle-cipher = { path = "./crypto/cipher" }
bouncycastle-concatkdf = { path = "./crypto/concatkdf" }
bouncycastle-core = { path = "crypto/core" }
bouncycastle-cipher-test-framework = { path = "./crypto/cipher-test-framework" }
bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" }
bouncycastle-ec = { path = "./crypto/ec" }
bouncycastle-eccdh = { path = "./crypto/eccdh" }
bouncycastle-ecdsa = { path = "./crypto/ecdsa" }
bouncycastle-factory = { path = "./crypto/factory" }
bouncycastle-hex = { path = "./crypto/hex" }
Expand All @@ -44,6 +46,7 @@ bouncycastle-sm2 = { path = "./crypto/sm2" }
bouncycastle-sm3 = { path = "./crypto/sm3" }
bouncycastle-sm4 = { path = "./crypto/sm4" }
bouncycastle-utils = { path = "./crypto/utils" }
bouncycastle-x963kdf = { path = "./crypto/x963kdf" }


# *** External Dependencies ***
Expand Down Expand Up @@ -80,7 +83,9 @@ bouncycastle-blake3.workspace = true
bouncycastle-chacha20.workspace = true
bouncycastle-chacha20-poly1305-aead.workspace = true
bouncycastle-cipher.workspace = true
bouncycastle-concatkdf.workspace = true
bouncycastle-core.workspace = true
bouncycastle-eccdh.workspace = true
bouncycastle-ecdsa.workspace = true
bouncycastle-factory.workspace = true
bouncycastle-hex.workspace = true
Expand All @@ -97,3 +102,4 @@ bouncycastle-sha3.workspace = true
bouncycastle-sm2.workspace = true
bouncycastle-sm3.workspace = true
bouncycastle-sm4.workspace = true
bouncycastle-x963kdf.workspace = true
21 changes: 21 additions & 0 deletions alpha_0.1.4_release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,25 @@
standard describes: SP 800-56C Rev. 2 Section 4.1's one-step KDF puts a counter before `Z || FixedInfo` and
admits the SHA-3 hashes and KMAC as its function, and SHAKE not at all. HKDF over SHA-3 remains as
`HKDF<SHA3_256>` and its siblings, written out; there is no alias or factory entry for it.
* Two-party key agreement gets the same shape ahead of the ECC CDH crate: the `AgreementKey` role in
`core::agreement_key`, `AgreementKey<Params, SS_LEN>` with `()` the raw primitive's parameters and a KDF's
`KdfParams` a KDF-bearing variant's, hands out a
`KeyAgreement` worker whose `agree_out(&peer_pk, &mut z)` writes `Z` into the caller's buffer as
`KeyType::SharedSecret`, zeroized first, with `agree(&peer_pk)` provided over it on the worker and the key; `KeyAgreementError` and
the encodable `KeyAgreementPrivateKey` / `KeyAgreementPublicKey` traits go with it.
* `bouncycastle-eccdh`: ECC CDH (SP 800-56A Rev. 3 Section 5.7.1.2) over P-256, P-384, P-521, secp256k1,
brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1, with its own key types per curve (full public-key
validation at decode, key generation by extra random bits), the markers `ECCDHP256` and friends on the
`AgreementKey` role over `()`, and `ECCDHP256_HKDF_SHA256` over `KdfParams<HKDF_SHA256, LEN>` on the P-256 keys;
tested against the CAVP ECC CDH primitive vectors, RFC 7027 Appendix A and the Wycheproof ECDH sets.
* `bouncycastle-x963kdf`: the ANSI X9.63 KDF of SEC 1 v2 Section 3.6.1 over SHA-224, SHA-256, SHA-384 and
SHA-512, on the `KDF` trait and in `KDFFactory` (`X963KDF-SHA256` and so on) and as the `x963kdf-sha*` CLI
subcommands, checked against the CAVP ANS X9.63-2001 component vectors; `KdfParams<X963KDF_SHA256, LEN>` and its
siblings, the KDF instance, `SharedInfo` and `keydatalen`, are the parameters of the ECC CDH keys' second agreement role, SEC 1 Section 6.1's ECDH scheme
(`ECCDHP256_X963KDF_SHA256`, `ECCDHP384_X963KDF_SHA384`, `ECCDHP521_X963KDF_SHA512` and the rest).
* `bouncycastle-concatkdf`: the NIST concatenation KDF, SP 800-56C Rev. 2 Section 4.1's one-step KDF with
`H(x) = hash(x)`, over SHA-2 and SHA-3 (`ConcatKDF_SHA256` and so on) on the `KDF` trait and in `KDFFactory`
(`ConcatKDF-SHA256` and so on) and as the `concatkdf-sha*` CLI subcommands, checked against the ACVP KDA OneStep
SP800-56Cr2 vectors; `KdfParams<ConcatKDF_SHA256, LEN>` and its siblings are a third KDF role on the ECC CDH keys,
SP 800-56A Rev. 3 Section 5.8's one-step derivation (`ECCDHP256_ConcatKDF_SHA256` and the rest).

76 changes: 76 additions & 0 deletions cli/src/concatkdf_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! The `concatkdf-*` subcommands: the NIST concatenation KDF of SP 800-56C Rev. 2 Section 4.1
//! (Option 1, `H(x) = hash(x)`), keying material of `--len` bytes from a shared secret (`--ikm`)
//! and the optional `FixedInfo` (`--additional-input`), as the `x963kdf-*` subcommands take
//! theirs.

use crate::helpers::{decode_hex_arg, load_secret, read_input_file, write_bytes_or_hex};
use bouncycastle::concatkdf::{
ConcatKDF_SHA3_256, ConcatKDF_SHA3_384, ConcatKDF_SHA3_512, ConcatKDF_SHA256, ConcatKDF_SHA384,
ConcatKDF_SHA512,
};
use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait};
use std::process::exit;

/// The most keying material one invocation produces: the `hkdf-*` and `x963kdf-*` commands'
/// bound, well inside Section 4.1 step 2's.
const MAX_OUT_LEN: usize = 255 * 64;

#[allow(non_camel_case_types)]
pub(crate) enum ConcatKDFVariant {
SHA256,
SHA384,
SHA512,
SHA3_256,
SHA3_384,
SHA3_512,
}

pub(crate) fn concatkdf_cmd(
variant: ConcatKDFVariant,
ikm: &Option<String>,
ikm_file: &Option<String>,
additional_input: &Option<String>,
additional_input_file: &Option<String>,
len: usize,
output_hex: bool,
) {
// The shared secret Z, from hex on the command line or from a file; the file wins.
let z = load_secret(ikm, ikm_file, "ikm");
// FixedInfo may be empty.
let fixed_info = match (additional_input_file, additional_input) {
(Some(file), _) => read_input_file(file),
(None, Some(hex_str)) => decode_hex_arg(hex_str, "additional-input"),
(None, None) => Vec::new(),
};
if len > MAX_OUT_LEN {
eprintln!("Error: --len must be at most {MAX_OUT_LEN}.");
exit(-1);
}

let mut out_key = KeyMaterial::<MAX_OUT_LEN>::new();
let result = match variant {
ConcatKDFVariant::SHA256 => {
ConcatKDF_SHA256::derive_out(&z, &fixed_info, len, &mut out_key)
}
ConcatKDFVariant::SHA384 => {
ConcatKDF_SHA384::derive_out(&z, &fixed_info, len, &mut out_key)
}
ConcatKDFVariant::SHA512 => {
ConcatKDF_SHA512::derive_out(&z, &fixed_info, len, &mut out_key)
}
ConcatKDFVariant::SHA3_256 => {
ConcatKDF_SHA3_256::derive_out(&z, &fixed_info, len, &mut out_key)
}
ConcatKDFVariant::SHA3_384 => {
ConcatKDF_SHA3_384::derive_out(&z, &fixed_info, len, &mut out_key)
}
ConcatKDFVariant::SHA3_512 => {
ConcatKDF_SHA3_512::derive_out(&z, &fixed_info, len, &mut out_key)
}
};
if let Err(e) = result {
eprintln!("Error: key derivation failed: {e:?}");
exit(-1);
}
write_bytes_or_hex(out_key.ref_to_bytes(), output_hex);
}
246 changes: 246 additions & 0 deletions cli/src/eccdh_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
//! Generic CLI wiring for every curve in `bouncycastle-eccdh`: one instantiation of
//! [`eccdh_cmd`] per curve, as `ecdsa_cmd.rs` does for ECDSA, since every curve implements the
//! identical `KeyAgreementPrivateKey` / `KeyAgreementPublicKey` / `DerivePublicKey` /
//! `AgreementKey<()>` shape. `agree` writes the raw shared secret `Z` of SP 800-56A Rev. 3
//! Section 5.7.1.2, which is keying material for a KDF (`hkdf-sha256` and friends take it from
//! a file through `--ikm-file`; the inline `--ikm` would leave it in the command history), not a
//! key.

use crate::helpers::{
println_stdout, read_input_file, read_secret_file_or_stdin, write_bytes_or_hex,
};
use bouncycastle::core::agreement_key::AgreementKey;
use bouncycastle::core::errors::KeyAgreementError;
use bouncycastle::core::key_material::KeyMaterialTrait;
use bouncycastle::core::traits::{KeyAgreementPrivateKey, KeyAgreementPublicKey, RNG};
use bouncycastle::eccdh::keys_common::DerivePublicKey;
use bouncycastle::eccdh::{
eccdh_bp256r1, eccdh_bp384r1, eccdh_bp512r1, eccdh_p256, eccdh_p256k1, eccdh_p384, eccdh_p521,
};
use bouncycastle::eccdh::{
keys_bp256r1, keys_bp384r1, keys_bp512r1, keys_p256, keys_p256k1, keys_p384, keys_p521,
};
use bouncycastle::rng::HashDRBG_SHA512;
use clap::ValueEnum;
use std::process::exit;

#[derive(ValueEnum, Clone, Debug)]
pub(crate) enum ECCDHAction {
/// Generate and output a new private key.
Keygen,
/// Derive and output the public key matching a private key read from stdin (raw bytes, or
/// hex with --hex-input).
PkFromSk,
/// Accepts a private key (stdin) and a public key (--pkfile), and checks that they match.
CheckConsistency,
/// Compute the shared secret Z from the own private key (stdin or --skfile) and the peer's
/// public key (--pkfile), and output it. Z is for a KDF, not a key.
Agree,
}

#[allow(clippy::type_complexity)] // the keygen function pointer is the one per-curve input
pub(crate) fn eccdh_cmd<
PK: KeyAgreementPublicKey<PK_LEN>,
SK: KeyAgreementPrivateKey<SK_LEN>
+ DerivePublicKey<PK, PK_LEN>
+ AgreementKey<(), SS_LEN, PublicKey = PK>,
const SK_LEN: usize,
const PK_LEN: usize,
const SS_LEN: usize,
>(
action: &ECCDHAction,
keygen: fn(&mut dyn RNG) -> Result<(PK, SK), KeyAgreementError>,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
alg_name: &str,
) {
match action {
ECCDHAction::Keygen => {
let (_pk, sk) = keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap_or_else(|e| {
eprintln!("Error: key generation failed: {e:?}");
exit(-1);
});
write_bytes_or_hex(&sk.encode(), output_hex);
}
ECCDHAction::PkFromSk => {
let sk =
parse_sk::<SK, SK_LEN>(read_secret_file_or_stdin(skfile).ref_to_bytes(), alg_name);
write_bytes_or_hex(&sk.derive_pk().encode(), output_hex);
}
ECCDHAction::CheckConsistency => {
let sk =
parse_sk::<SK, SK_LEN>(read_secret_file_or_stdin(skfile).ref_to_bytes(), alg_name);
let pk = parse_pk::<PK, PK_LEN>(&require_file(pkfile, "pkfile"), alg_name);
// SP 800-56A Rev. 3 Section 5.6.2.1.4 method (b): recompute dG and compare.
if sk.derive_pk() == pk {
println_stdout("SUCCESS: pk and sk match.");
} else {
eprintln!("FAILURE: pk and sk do not match.");
exit(-1);
}
}
ECCDHAction::Agree => {
let sk =
parse_sk::<SK, SK_LEN>(read_secret_file_or_stdin(skfile).ref_to_bytes(), alg_name);
let peer_pk = parse_pk::<PK, PK_LEN>(&require_file(pkfile, "pkfile"), alg_name);
let z = sk.agree(&peer_pk, ()).unwrap_or_else(|e| {
eprintln!("Error: key agreement failed: {e:?}");
exit(-1);
});
write_bytes_or_hex(z.ref_to_bytes(), output_hex);
}
}
}

/// `--pkfile`, which cannot fall back to stdin (that carries the private key).
fn require_file(file: &Option<String>, flag_name: &str) -> Vec<u8> {
match file {
Some(f) => read_input_file(f),
None => {
eprintln!("Error: no {flag_name} provided.");
exit(-1);
}
}
}

/// The key from its encoding; the bytes are what the file held, or its hex decoded under
/// `--hex-input`, with nothing guessed from their shape.
fn parse_sk<SK: KeyAgreementPrivateKey<SK_LEN>, const SK_LEN: usize>(
bytes: &[u8],
alg_name: &str,
) -> SK {
match SK::from_bytes(bytes) {
Ok(sk) => sk,
Err(_) => {
eprintln!("Error: couldn't parse the input as a valid {alg_name} private key.");
exit(-1);
}
}
}

/// See [`parse_sk`]. A public key that parses has passed full validation.
fn parse_pk<PK: KeyAgreementPublicKey<PK_LEN>, const PK_LEN: usize>(
bytes: &[u8],
alg_name: &str,
) -> PK {
match PK::from_bytes(bytes) {
Ok(pk) => pk,
Err(_) => {
eprintln!("Error: couldn't parse the input as a valid {alg_name} public key.");
exit(-1);
}
}
}

pub(crate) fn eccdh_p256_cmd(
action: &ECCDHAction,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
) {
eccdh_cmd::<_, _, { keys_p256::SK_LEN }, { keys_p256::PK_LEN }, { eccdh_p256::SS_LEN }>(
action,
keys_p256::keygen,
skfile,
pkfile,
output_hex,
"ECC CDH P-256",
)
}

pub(crate) fn eccdh_p384_cmd(
action: &ECCDHAction,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
) {
eccdh_cmd::<_, _, { keys_p384::SK_LEN }, { keys_p384::PK_LEN }, { eccdh_p384::SS_LEN }>(
action,
keys_p384::keygen,
skfile,
pkfile,
output_hex,
"ECC CDH P-384",
)
}

pub(crate) fn eccdh_p521_cmd(
action: &ECCDHAction,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
) {
eccdh_cmd::<_, _, { keys_p521::SK_LEN }, { keys_p521::PK_LEN }, { eccdh_p521::SS_LEN }>(
action,
keys_p521::keygen,
skfile,
pkfile,
output_hex,
"ECC CDH P-521",
)
}

pub(crate) fn eccdh_secp256k1_cmd(
action: &ECCDHAction,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
) {
eccdh_cmd::<_, _, { keys_p256k1::SK_LEN }, { keys_p256k1::PK_LEN }, { eccdh_p256k1::SS_LEN }>(
action,
keys_p256k1::keygen,
skfile,
pkfile,
output_hex,
"ECC CDH secp256k1",
)
}

pub(crate) fn eccdh_bp256r1_cmd(
action: &ECCDHAction,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
) {
eccdh_cmd::<_, _, { keys_bp256r1::SK_LEN }, { keys_bp256r1::PK_LEN }, { eccdh_bp256r1::SS_LEN }>(
action,
keys_bp256r1::keygen,
skfile,
pkfile,
output_hex,
"ECC CDH brainpoolP256r1",
)
}

pub(crate) fn eccdh_bp384r1_cmd(
action: &ECCDHAction,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
) {
eccdh_cmd::<_, _, { keys_bp384r1::SK_LEN }, { keys_bp384r1::PK_LEN }, { eccdh_bp384r1::SS_LEN }>(
action,
keys_bp384r1::keygen,
skfile,
pkfile,
output_hex,
"ECC CDH brainpoolP384r1",
)
}

pub(crate) fn eccdh_bp512r1_cmd(
action: &ECCDHAction,
skfile: &Option<String>,
pkfile: &Option<String>,
output_hex: bool,
) {
eccdh_cmd::<_, _, { keys_bp512r1::SK_LEN }, { keys_bp512r1::PK_LEN }, { eccdh_bp512r1::SS_LEN }>(
action,
keys_bp512r1::keygen,
skfile,
pkfile,
output_hex,
"ECC CDH brainpoolP512r1",
)
}
Loading
Loading