From f1f0e43d584e0f15c92e211501b6403bc48ffd6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Lindstr=C3=B8m?= Date: Mon, 24 Aug 2026 13:05:40 +0200 Subject: [PATCH 1/2] Make copy_key optional in fastcrypto-pq fastcrypto-pq depended on fastcrypto with features = ["copy_key"] and implemented KeyPair::copy without the cfg gate the other schemes use. Because Cargo unifies features per build, that turned copy_key on for the whole dependency graph, making Ed25519KeyPair::copy and friends available in crates that deliberately built without it. Forward the feature instead and gate MLDSA65KeyPair::copy like ed25519 does. The two tests that only used copy() to reach the private key now build it from the keypair bytes, so they keep running without the feature. --- fastcrypto-pq/Cargo.toml | 8 ++++++-- fastcrypto-pq/README.md | 2 +- fastcrypto-pq/src/mldsa65/mod.rs | 2 +- fastcrypto-pq/src/tests/mldsa65_tests.rs | 5 +++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/fastcrypto-pq/Cargo.toml b/fastcrypto-pq/Cargo.toml index b5900fb09b..4042a24ef6 100644 --- a/fastcrypto-pq/Cargo.toml +++ b/fastcrypto-pq/Cargo.toml @@ -10,8 +10,7 @@ repository = "https://github.com/MystenLabs/fastcrypto" publish = false [dependencies] -# copy_key is not optional here -fastcrypto = { path = "../fastcrypto", features = ["copy_key"] } +fastcrypto = { path = "../fastcrypto" } fastcrypto-derive = { path = "../fastcrypto-derive", version = "0.1.4" } # The C backend (mldsa-native, pinned as a submodule inside the wrapper) mysten-mldsa-native-rs = { git = "https://github.com/MystenLabs/mysten-mldsa-native-rs.git", rev = "ea1a9794c7ad0c68b9410121964b7439540826bc", version = "0.1.0" } @@ -23,6 +22,11 @@ bincode.workspace = true zeroize.workspace = true [features] +# Forwards to fastcrypto's copy_key, which gates KeyPair::copy for every scheme. Optional +# here so that depending on this crate does not turn private key copying on for the rest of +# the dependency graph through feature unification. +copy_key = ["fastcrypto/copy_key"] + # Forwards to the wrapper's verified assembly backends: NEON on aarch64, AVX2 on x86_64 # behind a runtime CPU probe. Identical signatures either way, roughly 2.5x faster. native = ["mysten-mldsa-native-rs/native"] diff --git a/fastcrypto-pq/README.md b/fastcrypto-pq/README.md index 4b36392bf7..3e9ce58b14 100644 --- a/fastcrypto-pq/README.md +++ b/fastcrypto-pq/README.md @@ -55,7 +55,7 @@ smart-contract vaults rather than the native transaction path. ```bash # everything, both schemes -cargo test -p fastcrypto-pq --features native +cargo test -p fastcrypto-pq --all-features # only the SLH-DSA building blocks cargo test -p fastcrypto-pq sphincs diff --git a/fastcrypto-pq/src/mldsa65/mod.rs b/fastcrypto-pq/src/mldsa65/mod.rs index f3f269ead4..e388f9f4b6 100644 --- a/fastcrypto-pq/src/mldsa65/mod.rs +++ b/fastcrypto-pq/src/mldsa65/mod.rs @@ -177,7 +177,7 @@ impl KeyPair for MLDSA65KeyPair { self.private } - // Not cfg-gated on copy_key; this crate always enables fastcrypto/copy_key + #[cfg(feature = "copy_key")] fn copy(&self) -> Self { MLDSA65KeyPair { public: self.public.clone(), diff --git a/fastcrypto-pq/src/tests/mldsa65_tests.rs b/fastcrypto-pq/src/tests/mldsa65_tests.rs index 35310d43c0..8ecfd01a24 100644 --- a/fastcrypto-pq/src/tests/mldsa65_tests.rs +++ b/fastcrypto-pq/src/tests/mldsa65_tests.rs @@ -180,7 +180,7 @@ fn serialize_deserialize() { verify_serialization(&signature, Some(signature.as_ref())); // The secret side serializes as the 32-byte seed - let private = keypair.copy().private(); + let private = MLDSA65PrivateKey::from_bytes(keypair.as_ref()).unwrap(); verify_serialization(&private, Some(private.as_ref())); let kp_bytes = bincode::serialize(&keypair).unwrap(); assert_eq!(kp_bytes, keypair.as_ref()); @@ -241,6 +241,7 @@ fn keypair_from_str_roundtrip() { } #[test] +#[cfg(feature = "copy_key")] fn copy_key_pair_preserves_identity() { let keypair = keys().pop().unwrap(); let copied = keypair.copy(); @@ -290,7 +291,7 @@ fn debug_output_is_redacted_for_secrets() { let keypair = keys().pop().unwrap(); // Secret material must never appear in formatted output. - let private = keypair.copy().private(); + let private = MLDSA65PrivateKey::from_bytes(keypair.as_ref()).unwrap(); assert!(!format!("{private:?}").contains(&Base64::encode(keypair.as_ref()))); // Public types print their base64 form, which assertion failures rely on. From 71f524a25a10ae176d160b35d1c259f05ed2c514 Mon Sep 17 00:00:00 2001 From: mahdi-mysten Date: Thu, 3 Sep 2026 12:16:05 +0200 Subject: [PATCH 2/2] [chore] Drop the forced copy_key feature from fastcrypto-cli this is bc failed with E0046 before this push --- fastcrypto-cli/Cargo.toml | 2 +- fastcrypto-cli/src/sigs_cli.rs | 40 +++++++++++++++++----------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/fastcrypto-cli/Cargo.toml b/fastcrypto-cli/Cargo.toml index e67d1a4a13..23e7bd395b 100644 --- a/fastcrypto-cli/Cargo.toml +++ b/fastcrypto-cli/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/MystenLabs/fastcrypto" [dependencies] clap = { version = "4.1.8", features = ["derive"] } -fastcrypto = { path = "../fastcrypto", features = ["copy_key"] } +fastcrypto = { path = "../fastcrypto" } fastcrypto-vdf = { path = "../fastcrypto-vdf", features = ["experimental"] } hex = "0.4.3" rand.workspace = true diff --git a/fastcrypto-cli/src/sigs_cli.rs b/fastcrypto-cli/src/sigs_cli.rs index 370c7ebe48..95e539585a 100644 --- a/fastcrypto-cli/src/sigs_cli.rs +++ b/fastcrypto-cli/src/sigs_cli.rs @@ -126,38 +126,38 @@ fn execute(cmd: Command) -> Result<(), FastCryptoError> { let (sk, pk) = match SignatureScheme::from_str(&arg.scheme) { Ok(SignatureScheme::Ed25519) => { let kp = Ed25519KeyPair::generate(rng); - ( - Hex::encode(kp.copy().private().as_ref()), - Hex::encode(kp.public().as_ref()), - ) + { + let pk = Hex::encode(kp.public().as_ref()); + (Hex::encode(kp.private().as_ref()), pk) + } } Ok(SignatureScheme::Secp256k1) | Ok(SignatureScheme::Secp256k1Recoverable) => { let kp = Secp256k1KeyPair::generate(rng); - ( - Hex::encode(kp.copy().private().as_ref()), - Hex::encode(kp.public().as_ref()), - ) + { + let pk = Hex::encode(kp.public().as_ref()); + (Hex::encode(kp.private().as_ref()), pk) + } } Ok(SignatureScheme::Secp256r1) | Ok(SignatureScheme::Secp256r1Recoverable) => { let kp = Secp256r1KeyPair::generate(rng); - ( - Hex::encode(kp.copy().private().as_ref()), - Hex::encode(kp.public().as_ref()), - ) + { + let pk = Hex::encode(kp.public().as_ref()); + (Hex::encode(kp.private().as_ref()), pk) + } } Ok(SignatureScheme::BLS12381MinSig) => { let kp = fastcrypto::bls12381::min_sig::BLS12381KeyPair::generate(rng); - ( - Hex::encode(kp.copy().private().as_ref()), - Hex::encode(kp.public().as_ref()), - ) + { + let pk = Hex::encode(kp.public().as_ref()); + (Hex::encode(kp.private().as_ref()), pk) + } } Ok(SignatureScheme::BLS12381MinPk) => { let kp = fastcrypto::bls12381::min_pk::BLS12381KeyPair::generate(rng); - ( - Hex::encode(kp.copy().private().as_ref()), - Hex::encode(kp.public().as_ref()), - ) + { + let pk = Hex::encode(kp.public().as_ref()); + (Hex::encode(kp.private().as_ref()), pk) + } } Err(_) => return Err(FastCryptoError::InvalidInput), };