From 6ec83cf95efe6cd6a8d8abe6d7826f7d78407f07 Mon Sep 17 00:00:00 2001 From: Mike Lodder Date: Thu, 15 Oct 2026 17:06:06 -0600 Subject: [PATCH] feat: update to only use traits with no-default-features Signed-off-by: Mike Lodder --- CHANGELOG.md | 52 ++ Cargo.toml | 288 +++++-- README.md | 86 +- benches/multikey_bench.rs | 3 +- src/attrid.rs | 48 +- src/cipher.rs | 2 +- src/error.rs | 2 + src/kdf.rs | 7 +- src/keysplit.rs | 698 +++++++++------- src/lib.rs | 106 ++- src/mk.rs | 1194 +++++++++++++++------------- src/nonce.rs | 29 +- src/serde/de.rs | 60 +- src/serde/mod.rs | 99 ++- src/serde/ser.rs | 10 +- src/types.rs | 1 + src/views.rs | 119 ++- src/views/aead.rs | 126 ++- src/views/bcrypt.rs | 5 +- src/views/bls12381.rs | 37 +- src/views/bls12381_g1_fndsa512.rs | 10 +- src/views/bls12381_g1_mayo1.rs | 12 +- src/views/bls12381_g1_mayo2.rs | 12 +- src/views/bls12381_g1_mldsa65.rs | 10 +- src/views/builder.rs | 207 ++--- src/views/chacha20.rs | 12 +- src/views/classic_mceliece.rs | 116 ++- src/views/dispatch.rs | 381 +++++++-- src/views/dkg_threshold.rs | 2 +- src/views/ed25519.rs | 18 +- src/views/ed25519_fndsa512.rs | 10 +- src/views/ed25519_mayo2.rs | 14 +- src/views/ed25519_mldsa65.rs | 10 +- src/views/fn_dsa.rs | 17 +- src/views/frodokem.rs | 14 +- src/views/lamport.rs | 9 +- src/views/lamport_merkle.rs | 11 +- src/views/mayo.rs | 23 +- src/views/ml_dsa.rs | 19 +- src/views/ml_kem.rs | 15 +- src/views/nist_p.rs | 59 +- src/views/rsa.rs | 32 +- src/views/secp256k1.rs | 31 +- src/views/slh_dsa.rs | 16 +- src/views/sntrup.rs | 15 +- src/views/threshold_marker.rs | 17 +- src/views/threshold_meta.rs | 128 ++- src/views/x25519.rs | 12 +- src/views/x25519_frodokem640.rs | 16 +- src/views/x25519_mceliece348864.rs | 69 +- src/views/x25519_mlkem768.rs | 12 +- src/views/x25519_sntrup761.rs | 12 +- src/views/xeddsa.rs | 9 +- src/views/xmss.rs | 15 +- tests/custom_codec_tests.rs | 5 +- tests/edge_case_tests.rs | 22 +- tests/merkle_lamport_tests.rs | 1 - tests/proptest_tests.rs | 26 +- tests/security_tests.rs | 25 +- 59 files changed, 2666 insertions(+), 1750 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b00a2..6776127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,58 @@ All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-10-13 + +### Added + +- One feature per algorithm or curve: `bcrypt`, `bls12-381`, `chacha20`, `curve25519` (Ed25519, X25519, and XEdDSA), `fn-dsa`, `frodokem`, `lamport`, `mayo`, `ml-dsa`, `ml-kem`, `p256`, `p384`, `p521`, `rsa`, `secp256k1`, `slh-dsa`, `sntrup`, and `xmss`. Each one pulls in only the crates its key types use. All of them are in the default feature set, so a default build has the same key types as 2.x. +- The `threshold` feature (default). It gates the `keysplit` module and the threshold marker attributes and their re-exports, and pulls in `elliptic-curve`, `serde` (derives only), `serde_cbor_2`, and `vsss-rs`. +- The `aead` feature, a dependency bundle (AES-GCM, ChaCha20-Poly1305, HKDF) that the sealing and ECDH features turn on themselves. Enabling it alone adds no key types. +- Hybrid key types have no feature of their own. A hybrid compiles when every feature it combines is on, e.g. Ed25519-ML-DSA-65 needs `curve25519` and `ml-dsa`. +- With `default-features = false` the crate depends on no algorithm crate. It provides the `Multikey` type, its attributes, the view traits, and `ViewBuilder`, so callers can supply their own implementations through `ViewBuilder::with_local_codec`. +- `impl From<&Multikey> for Vec` and `impl From<&Nonce> for Vec` encode without cloning. The owned `From` impls delegate to them, and binary serde serialization uses them instead of cloning the value. +- `FromStr` for `AttrId`. The existing `TryFrom<&str>` impl now delegates to it, so `"key-data".parse::()` works and the old calls are unchanged. This clears the `clippy::try_from_instead_of_from_str` error that failed `cargo clippy -- -D warnings`. + +### Changed + +- **Breaking:** a build with `default-features = false` loses every built-in key view, key generation, key splitting, and SSH conversion. In 2.x that setting only dropped `serde`, `lamport`, and `xmss`. Add the algorithm features you use. +- **Breaking:** creating, converting, or viewing a key whose algorithm feature is off returns an `UnsupportedCodec` error. SSH conversion returns `UnsupportedAlgorithm`, and the seal and open views return `SealError::NotEncryptionKey`. +- **Breaking:** `KdfError::Bcrypt` exists only with `bcrypt`, and `ThresholdError::Bls` only with `bls12-381`. Both enums are `#[non_exhaustive]`, so wildcard matches are unaffected. +- **Breaking:** `encrypt_threshold_meta`, `decrypt_threshold_meta`, and `generate_meta_key` are re-exported only with `chacha20`. Without it, `stamp_disclosure_attrs` and `read_threshold_params` return `ThresholdError::MetaEncryption` for the `Partial` and `FullConfidential` modes, and the confidential threshold is never written in plaintext. +- **Breaking:** the re-exported `ThresholdDisclosure::FullConfidentialial` is now `ThresholdDisclosure::FullConfidential`, following multi-sig 2.0. The encoded value (2) is unchanged, so existing shares decode as before; `Display` now prints `full-confidential`. +- The Classic McEliece deprecation notes (README, crate docs, the McEliece views, `MCELIECE_KEY_CODECS`) now give the reason: the 2026 structural key-recovery attacks on binary Goppa codes (Hemmert–Wiemers and Hemmert at BSI, Ghoshal–Ishai–Jain–Sun, Weis, Saarinen) and BSI's 29 September 2026 advice against Classic McEliece in new developments. +- `legacy_chacha20_fallback` now turns on `chacha20`. +- `wasm` now turns on `getrandom/wasm_js` and the optional `getrandom_02` dependency itself. +- The MAYO views import `signature` directly instead of through the `ml_dsa` re-export, so `mayo` builds without `ml-dsa`. +- The crate-private `SALT_LENGTH` moved from the bcrypt view into `kdf`, so `chacha20` builds without `bcrypt`. +- The benchmark declares `required-features = ["curve25519"]`. +- Threshold marker and key-split CBOR encoding moved from `ciborium` to `serde_cbor_2`. +- The XMSS views use `pq-xmss` instead of `xmss`. +- Removed redundant copies on the encode and decode paths. Attribute bytes are written straight into the output instead of being cloned into a `Varbytes` first (about 40 view encoders and `Multikey` itself). Decoded attributes are moved instead of cloned. The `Builder` setters no longer copy each attribute twice, and `ct_eq` no longer deep-clones both keys. In paired benchmarks against 2.3.0 on an idle machine, `multikey_to_bytes` (which then cloned the key before encoding) is about 60% faster and decoding about 5% faster; key generation is unchanged. The benchmark now encodes by reference. +- The `Multikey`, `Nonce`, and sealed-message encoders write each varint (sigil, codec, lengths, attribute IDs) straight into the output buffer instead of allocating a `Vec` per field, so an encode makes one allocation. Paired runs on an idle machine: `multikey_to_bytes` 46–47 ns → 21 ns; decoding unchanged. +- The public-key paths (`verify` in 20 views and `to_ssh_public_key` in 9) no longer clone the secret key; they share one `public_form` helper that borrows a public key and derives one from a secret key. Only the comment string is copied. +- `ct_eq` and binary serde serialization of `Multikey` zeroize the encoded bytes they build, and `From` moves the BLS shares instead of cloning each one. +- `Builder::with_key_bytes` no longer leaves an unzeroized copy of the secret key bytes behind, and decoding moves attribute buffers into `Zeroizing` instead of copying them. +- Binary serde deserialization of `Multikey` and `Nonce` decodes straight from the input bytes instead of copying them into an intermediate buffer, and zeroizes an owned input buffer after decoding. Human-readable deserialization moves attribute bytes into `Zeroizing` instead of cloning them, so secret attributes no longer leave an unzeroized copy behind. +- Re-enabled `clippy::redundant_clone`, and fixed the pre-existing needless-borrow and `map_or` clippy errors. +- Moved the crate-level lint settings from `lib.rs` into the `[lints]` table in `Cargo.toml`, so they now apply to tests and benches as well as the library. Fixed the one trivial cast in `tests/security_tests.rs` that this exposed. +- Fixed a redundant intra-doc link and two broken private-item links, so `cargo doc` builds with `RUSTDOCFLAGS="-D warnings"` with and without `--document-private-items`. + +### Removed + +- **Breaking:** the `Views` trait and `impl Views for Multikey`, deprecated since 2.1.0. Use `ViewBuilder` instead: `mk.sign_view()` becomes `ViewBuilder::new(&mk).sign().build()`, `mk.cipher_view(&key)` becomes `ViewBuilder::new(&mk).cipher(&key).build()`, and likewise for the other view kinds. + +### Dependencies + +- `aes-gcm`, `bcrypt-pbkdf`, `blake3`, `blsful`, `chacha20`, `chacha20poly1305`, `curve25519-dalek`, `ed25519-dalek`, `elliptic-curve`, `fn-dsa`, `frodo-kem-rs`, `getrandom`, `hkdf`, `k256`, `ml-dsa`, `ml-kem`, `p256`, `p384`, `p521`, `pkcs1`, `poly1305`, `pq-mayo`, `rand_core` 0.6, `sad-rsa`, `sec1`, `sha2`, `signature`, `slh-dsa`, `sntrup`, `typenum`, `vsss-rs`, and `x25519-dalek` are now optional. +- `ssh-key` is a required dependency with only `alloc`. Each algorithm feature turns on the `ssh-key` feature it needs; `p256`, `p384`, and `p521` also turn on `ssh-key/rand_core`, which `EcdsaKeypair::random` needs and which multi-sig 1.x had been enabling indirectly. +- Requirement `multi-hash` raised 2.0 → 3.0 with `default-features = false`. multi-key never hashes with a fixed codec (fingerprints use the caller's codec), so it enables no hash family; enable the `multi-hash` features for the codecs you fingerprint with. `serde` turns on `multi-hash/serde`. +- Requirement `multi-sig` raised 1.6 → 2.0 with `default-features = false`. The `bls12-381`, `chacha20`, `lamport`, `serde`, and `xmss` features turn on the matching multi-sig features, so multi-sig no longer pulls in BLS, AEAD, or Lamport crates that the enabled multi-key features do not need. +- Replaced `ciborium` with `serde_cbor_2` and `xmss` with `pq-xmss`. +- Replaced `mceliece348864` with `pq-mceliece` (348864 parameter set only) for the deprecated Classic McEliece views. Keys still derive from the stored 32-byte seed the same way, so existing McEliece keys and ciphertexts stay compatible in both directions. The `deprecated` feature no longer pulls in `rand_chacha` or `rand_core` 0.6. +- Added `serde_cbor_2` as a dev-dependency. +- `zeroize` now enables its `derive` feature. 2.x used `#[derive(Zeroize, ZeroizeOnDrop)]` but only compiled because another dependency turned that feature on. + ## [2.3.0] - 2026-10-09 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 2d34e55..203196d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,126 +1,272 @@ [package] -name = "multi-key" -version = "2.3.0" -edition = "2024" -rust-version = "1.99" -authors = ["Dave Grantham "] +authors = ["Dave Grantham ", "Michael Lodder "] +categories = ["cryptography", "encoding"] description = "Multikey self-describing cryptographic key data" -repository = "https://github.com/cryptidtech/multi-key.git" -readme = "README.md" -license = "Apache-2.0" +documentation = "https://docs.rs/multi-key" +edition = "2024" +homepage = "https://crates.io/crates/multi-key" keywords = ["multiformats", "multikey", "crypto", "post-quantum"] -categories = ["cryptography", "encoding"] +license = "Apache-2.0" +name = "multi-key" +readme = "README.md" +repository = "https://github.com/cryptidtech/multi-key.git" +rust-version = "1.99" +version = "3.0.0" [features] -default = ["serde", "lamport", "xmss"] -wasm = ["getrandom/wasm_js"] +# Each algorithm or curve is its own feature, so callers compile in only the +# key types they use. With `default-features = false` the crate exposes only +# the Multikey data type, its attributes, and the view traits; callers supply +# their own implementations through `ViewBuilder::with_local_codec`. Hybrid +# key types compile when every feature they combine is enabled, e.g. +# Ed25519-ML-DSA-65 needs both `curve25519` and `ml-dsa`. +# +# Shared AEAD layer (AES-GCM, ChaCha20-Poly1305, HKDF) used by every sealing +# (KEM/ECDH) key type. Enabled by the features that need it. +aead = ["dep:aes-gcm", "dep:chacha20poly1305", "dep:hkdf", "dep:sha2"] +# bcrypt-pbkdf key derivation. +bcrypt = ["dep:bcrypt-pbkdf"] +# BLS12-381 G1/G2 signing keys and their threshold shares. +bls12-381 = ["dep:blsful", "dep:elliptic-curve", "dep:vsss-rs", "multi-sig/bls12-381"] +# ChaCha20-Poly1305 symmetric cipher keys. +chacha20 = ["dep:chacha20", "dep:chacha20poly1305", "dep:poly1305", "multi-sig/chacha20"] +# Ed25519 signing, X25519 sealing, and XEdDSA. +curve25519 = [ + "aead", + "dep:curve25519-dalek", + "dep:ed25519-dalek", + "dep:getrandom", + "dep:sha2", + "dep:x25519-dalek", + "ssh-key/ed25519", +] +default = [ + "bcrypt", + "bls12-381", + "chacha20", + "curve25519", + "fn-dsa", + "frodokem", + "lamport", + "mayo", + "ml-dsa", + "ml-kem", + "p256", + "p384", + "p521", + "rsa", + "secp256k1", + "serde", + "slh-dsa", + "sntrup", + "threshold", + "xmss", +] +# Deprecated Classic McEliece KEM support. 2026 structural key-recovery attacks +# on binary Goppa codes led Germany's BSI to advise against it in new +# developments; see the README's Deprecated Algorithms section. +# Excluded from the default build; enable to keep the McEliece key views and +# their tests compiling. +deprecated = ["aead", "chacha20/rng", "dep:blake3", "dep:chacha20", "dep:pq-mceliece"] +# FN-DSA (Falcon) signing keys. +fn-dsa = ["dep:fn-dsa", "dep:rand_core_06"] +# FrodoKEM sealing keys. +frodokem = ["aead", "dep:blake3", "dep:frodo-kem-rs", "dep:getrandom"] +# Lamport and merkle-tree Lamport one-time signing keys. +lamport = [ + "dep:blake2", + "dep:blake3", + "dep:lamport_signature_plus", + "dep:sha2", + "dep:sha3", + "dep:shake", + "multi-sig/lamport", +] # Legacy bare-ChaCha20 fallback for keys encrypted before AEAD was added. # When disabled (default), AEAD decryption failure is a hard error — never # silently downgrade to unauthenticated ChaCha20. Enable only to migrate # pre-AEAD keystores; a warning is emitted on every fallback. -legacy_chacha20_fallback = [] -lamport = ["dep:lamport_signature_plus", "dep:sha3", "dep:blake2", "dep:shake"] -xmss = ["dep:xmss"] -# Deprecated Classic McEliece KEM support (key recovery attacks now solve the -# TII McEliece challenges; see https://github.com/mjosaarinen/tii-solved). -# Excluded from the default build; enable to keep the McEliece key views and -# their tests compiling. -deprecated = ["dep:mceliece348864"] +legacy_chacha20_fallback = ["chacha20"] +# MAYO signing keys. +mayo = ["dep:pq-mayo", "dep:signature"] +# ML-DSA (FIPS 204) signing keys. +ml-dsa = ["dep:ml-dsa"] +# ML-KEM (FIPS 203) sealing keys. +ml-kem = ["aead", "dep:ml-kem"] +# NIST P-256 ECDSA signing keys. +p256 = [ + "aead", + "dep:elliptic-curve", + "dep:p256", + "dep:sec1", + "dep:typenum", + "ssh-key/p256", + "ssh-key/rand_core", +] +# NIST P-384 ECDSA signing keys. +p384 = [ + "aead", + "dep:elliptic-curve", + "dep:p384", + "dep:sec1", + "dep:typenum", + "ssh-key/p384", + "ssh-key/rand_core", +] +# NIST P-521 ECDSA signing keys. +p521 = [ + "aead", + "dep:elliptic-curve", + "dep:p521", + "dep:sec1", + "dep:typenum", + "ssh-key/p521", + "ssh-key/rand_core", +] +# RSA signing keys. +rsa = ["aead", "dep:getrandom", "dep:pkcs1", "dep:sad-rsa", "dep:sha2"] +# secp256k1 ECDSA signing and ECDH sealing keys. +secp256k1 = ["aead", "dep:elliptic-curve", "dep:k256"] +serde = ["dep:serde", "multi-hash/serde", "multi-sig/serde"] +# SLH-DSA (FIPS 205) signing keys. +slh-dsa = ["dep:getrandom", "dep:slh-dsa"] # Tests that take over 60 seconds in debug builds (SLH-DSA, Classic McEliece, # XMSS). Excluded from the default `cargo test` run; enable with # `cargo test --features slow-tests` to run the full suite. slow-tests = [] +# Streamlined NTRU Prime sealing keys. +sntrup = ["aead", "dep:blake3", "dep:sntrup"] +# Key splitting into verifiable shares and the threshold marker attributes. +threshold = ["dep:elliptic-curve", "dep:serde", "dep:serde_cbor_2", "dep:vsss-rs"] +wasm = ["dep:getrandom_02", "getrandom/wasm_js"] +# XMSS stateful signing keys. +xmss = ["dep:pq-xmss", "multi-sig/xmss"] [dependencies] -aes-gcm = "0.11" -bcrypt-pbkdf = "0.11" -blake3 = { version = "1.8", features = ["traits-preview", "zeroize"] } -ciborium = "0.2" -curve25519-dalek = { version = "5.0.0", features = ["group"] } +aes-gcm = { version = "0.11", optional = true } +bcrypt-pbkdf = { version = "0.11", optional = true } +blake2 = { version = "0.11.0", optional = true } +blake3 = { version = "1.8", features = ["traits-preview", "zeroize"], optional = true } # blsful configured per-target below (blst for native, rust for wasm) -frodo-kem-rs = { version = "0.9", default-features = false, features = ["frodo640aes", "frodo976aes", "frodo1344aes", "frodo640shake", "frodo976shake", "frodo1344shake"] } -chacha20 = "0.10" -chacha20poly1305 = "0.11" -ed25519-dalek = { version = "3.0", features = ["pkcs8", "rand_core", "alloc"] } -elliptic-curve = "0.14" -fn-dsa = "0.4" -getrandom = { version = "0.4", features = ["sys_rng"] } +chacha20 = { version = "0.10", optional = true } +chacha20poly1305 = { version = "0.11", optional = true } +curve25519-dalek = { version = "5.0.0", features = ["group"], optional = true } +ed25519-dalek = { version = "3.0", features = ["pkcs8", "rand_core", "alloc"], optional = true } +elliptic-curve = { version = "0.14", optional = true } +fn-dsa = { version = "0.4", optional = true } +frodo-kem-rs = { version = "0.9", default-features = false, features = ["frodo640aes", "frodo976aes", "frodo1344aes", "frodo640shake", "frodo976shake", "frodo1344shake"], optional = true } +getrandom = { version = "0.4", features = ["sys_rng"], optional = true } hex = "0.4" -hkdf = "0.13" -k256 = { version = "0.14", features = ["ecdh"] } -mceliece348864 = { version = "1.0", optional = true } -ml-dsa = "0.1.1" -ml-kem = "0.3" +hkdf = { version = "0.13", optional = true } +k256 = { version = "0.14", features = ["ecdh"], optional = true } +lamport_signature_plus = { version = "0.5.0", optional = true } +ml-dsa = { version = "0.1.1", optional = true } +ml-kem = { version = "0.3", optional = true } multi-base = { version = "1.0", default-features = false } multi-codec = "1.6" -multi-hash = "2.0" -multi-sig = "1.6" +multi-hash = { version = "3.0", default-features = false } +multi-sig = { version = "2.0", default-features = false } multi-trait = { version = "1.1", default-features = false } multi-util = "1.2" -p256 = { version = "0.14", features = ["ecdsa", "ecdh"] } -p384 = { version = "0.14", features = ["ecdsa", "ecdh"] } -p521 = { version = "0.14", features = ["ecdsa", "ecdh"] } +p256 = { version = "0.14", features = ["ecdsa", "ecdh"], optional = true } +p384 = { version = "0.14", features = ["ecdsa", "ecdh"], optional = true } +p521 = { version = "0.14", features = ["ecdsa", "ecdh"], optional = true } # Compatibility pin. sad-rsa 0.2.3 (the newest release of its line) cannot # compile against pkcs1 0.8.0-rc.5, which changed `RsaPrivateKey` and # `RsaPublicKey` from lifetime parameters to generic parameters. sad-rsa # permits `pkcs1 ^0.8.0-rc.4`, so a fresh resolve picks rc.5 and fails. Pin # the exact rc.4 dependency to force it; remove this entry once sad-rsa # publishes a rc.5-compatible release. -pkcs1 = "=0.8.0-rc.4" -poly1305 = "0.9" -pq-mayo = "0.6.0" +pkcs1 = { version = "=0.8.0-rc.4", optional = true } +poly1305 = { version = "0.9", optional = true } +pq-mayo = { version = "0.6.0", optional = true } +pq-mceliece = { version = "0.3", default-features = false, features = ["decapsulate", "encapsulate", "keygen", "mceliece348864"], optional = true } +pq-xmss = { version = "0.2.0", optional = true } rand = "0.10" -rand_chacha = "0.3" rand_core = "0.10" -rand_core_06 = { package = "rand_core", version = "0.6", features = ["getrandom"] } -sad-rsa = { version = "0.2.3", features = ["sha2"] } -sec1 = "0.8" +rand_core_06 = { package = "rand_core", version = "0.6", features = ["getrandom"], optional = true } +sad-rsa = { version = "0.2.3", features = ["sha2"], optional = true } +sec1 = { version = "0.8", optional = true } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } -sha2 = "0.11" -signature = "3" -slh-dsa = "0.2.0-rc.5" -sntrup = "0.4.0" +serde_cbor_2 = { version = "0.13.0", optional = true } +sha2 = { version = "0.11", optional = true } +sha3 = { version = "0.12", optional = true } +shake = { version = "0.1", optional = true } +signature = { version = "3", optional = true } +slh-dsa = { version = "0.2.0-rc.5", optional = true } +sntrup = { version = "0.4.0", optional = true } ssh-encoding = "0.3" +ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["alloc"] } subtle = "2" thiserror = { version = "2.0" } -typenum = "1.20" +typenum = { version = "1.20", optional = true } unsigned-varint = { version = "0.8", features = ["std"] } -vsss-rs = "6.0" -x25519-dalek = { version = "3.0", features = ["static_secrets", "getrandom"] } -zeroize = "1" -# Lamport support (feature-gated, default-enabled) -lamport_signature_plus = { version = "0.5.0", optional = true } -sha3 = { version = "0.12", optional = true } -blake2 = { version = "0.11.0", optional = true } -shake = { version = "0.1", optional = true } -# XMSS support (feature-gated, default-enabled) -xmss = { version = "0.1.0-pre.0", optional = true } +vsss-rs = { version = "6", optional = true } +x25519-dalek = { version = "3.0", features = ["static_secrets", "getrandom"], optional = true } +zeroize = { version = "1", features = ["derive"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -blsful = { version = "4.1.0", default-features = false, features = ["rust"] } -getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] } -ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["alloc", "ecdsa", "ed25519", "p256", "p384", "p521"] } +blsful = { version = "4.1.0", default-features = false, features = ["rust"], optional = true } +getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"], optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -blsful = { version = "4.1.0", default-features = false, features = ["blst"] } -ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["alloc", "ecdsa", "ed25519", "p256", "p384", "p521"] } +blsful = { version = "4.1.0", default-features = false, features = ["blst"], optional = true } [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } +# the hash families the tests fingerprint keys with +multi-hash = { version = "3.0", default-features = false, features = ["blake2", "sha3"] } proptest = "1.11" +serde_cbor_2 = "0.13.0" serde_json = "1.0" serde_test = "1.0" [[bench]] -name = "multikey_bench" harness = false +name = "multikey_bench" path = "benches/multikey_bench.rs" +required-features = ["curve25519"] +# Pedantic, nursery, and cargo lints are on. The allows suppress stylistic +# lints that would require large-scale churn for minimal security benefit. [lints.clippy] -pedantic = { level = "warn", priority = -1 } -nursery = { level = "warn", priority = -1 } +branches_sharing_code = "allow" cargo = { level = "warn", priority = -1 } +cast_possible_truncation = "allow" +doc_markdown = "allow" +elidable_lifetime_names = "allow" +enum_glob_use = "allow" +explicit_iter_loop = "allow" +if_not_else = "allow" +items_after_statements = "allow" +large_stack_frames = "allow" +match_same_arms = "allow" +missing_const_for_fn = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" multiple_crate_versions = { level = "allow", priority = 1 } +must_use_candidate = "allow" +needless_pass_by_value = "allow" +nursery = { level = "warn", priority = -1 } +option_if_let_else = "allow" +or_fun_call = "allow" +pedantic = { level = "warn", priority = -1 } +redundant_pub_crate = "allow" +return_self_not_must_use = "allow" +semicolon_if_nothing_returned = "allow" +similar_names = "allow" +single_char_pattern = "allow" +single_match_else = "allow" +too_long_first_doc_paragraph = "allow" +too_many_lines = "allow" +uninlined_format_args = "allow" +use_self = "allow" +useless_let_if_seq = "allow" [lints.rust] +missing_docs = "warn" +trivial_casts = "deny" +trivial_numeric_casts = "deny" unsafe_code = "deny" +unused_import_braces = "deny" +unused_qualifications = "deny" diff --git a/README.md b/README.md index d37c202..703109e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Multi-Key -A Rust implementation of the [multiformats][MULTIFORMATS] [multikey specification][MULTIKEY] and [nonce specification][NONCE]. The published crate is **`multi-key`**. Depend on it as `multi-key = "2"` in `Cargo.toml`. Import it as `multi_key` in Rust, for example `use multi_key::Builder;`. +A Rust implementation of the [multiformats][MULTIFORMATS] [multikey specification][MULTIKEY] and [nonce specification][NONCE]. The published crate is **`multi-key`**. Depend on it as `multi-key = "3"` in `Cargo.toml`. Import it as `multi_key` in Rust, for example `use multi_key::Builder;`. ## Current Status @@ -23,6 +23,8 @@ This implementation of the multikey specification supports an extensive set of p See the [Supported Key Formats](#supported-key-formats) section below for the exhaustive list of codecs. +Each algorithm and curve sits behind its own [Cargo feature](#cargo-features), all on by default except the deprecated Classic McEliece (`deprecated`). With `default-features = false` the crate pulls in no algorithm crate and provides only the `Multikey` type, its attributes, the view traits, and `ViewBuilder`. + This implementation supports encrypting and decrypting keys at rest with ChaCha20-Poly1305 AEAD. The keys are derived via the bcrypt PBKDF from a preimage. A legacy bare-ChaCha20 fallback is honored on decrypt so older keystores continue to work. Re-encrypting upgrades them to the authenticated AEAD format. KEM-based message encryption uses `SealView` and `OpenView` with a choice of four AEAD codecs (ChaCha20-Poly1305, XChaCha20-Poly1305, AES-GCM-128, AES-GCM-256). HKDF-SHA512 derives the AEAD key from the KEM shared secret. @@ -76,7 +78,18 @@ identifiers come from the [multicodec][MULTICODEC] registry and are surfaced as ### Deprecated Algorithms -Classic McEliece (the `Mceliece348864Pub`/`Priv` codecs) and the X25519-McEliece-348864 hybrid are **deprecated**. Key-recovery attacks now solve the TII McEliece challenges; see [tii-solved](https://github.com/mjosaarinen/tii-solved) for the recovered keys. The McEliece views compile only with the `deprecated` cargo feature and are excluded from the default build. Stored McEliece multikeys still decode in default builds, but `Builder::new_from_random_bytes` fails for McEliece codecs with `UnsupportedCodec`. +Classic McEliece (the `Mceliece348864Pub`/`Priv` codecs) and the X25519-McEliece-348864 hybrid are **deprecated**. Do not use them in new designs; use ML-KEM (`ml-kem`), FrodoKEM (`frodokem`), or the X25519-ML-KEM-768 and X25519-FrodoKEM-640 hybrids instead. + +**Why.** Classic McEliece rests on the assumption that, given only the public key (a random basis of a secret binary Goppa code), no one can correct errors efficiently. Until recently the best key-recovery attack was brute force with support splitting, at over 2^3000 even for the smallest parameter set. During 2026 a series of structural attacks changed that: + +- Hemmert and Wiemers of Germany's Federal Office for Information Security (BSI) gave a higher-order-vanishing distinguisher for Goppa codes (CRYPTO 2026), and Hemmert turned it into a key-recovery attack ([ePrint 2026/1339](https://eprint.iacr.org/2026/1339)). +- Ghoshal, Ishai, Jain, and Sun gave a distinguisher with claimed quasi-polynomial complexity, estimated at 2^120 to 2^124 operations for the parameter sets BSI recommends, and a heuristic key recovery at 2^140 to 2^148 ([ePrint 2026/1630](https://eprint.iacr.org/2026/1630)). +- Weis claims an improved key recovery at 2^99 to 2^102 operations for those sets ([ePrint 2026/1984](https://eprint.iacr.org/2026/1984)). +- Saarinen recovered the keys of previously unsolved TII McEliece challenges, including TII-252 and TII-254 ([ePrint 2026/1778](https://eprint.iacr.org/2026/1778), [ePrint 2026/1986](https://eprint.iacr.org/2026/1986); recovered keys in [tii-solved](https://github.com/mjosaarinen/tii-solved)). + +On 29 September 2026 BSI advised that Classic McEliece should no longer be used in new developments or when planning new cryptographic applications, and pointed to FrodoKEM, ML-KEM, and (once standardized) HQC instead ([BSI note](https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Crypto/Notes_Classic_McEliece.pdf)). BSI also notes that most of these results are preprints still under review, that none yet gives a practical attack on the parameter sets it recommends, and that a hybrid with a classical scheme still keeps at least the classical scheme's security. It expects to revise its guidance in early 2027. The 348864 set this crate implements is the smallest Classic McEliece parameter set. + +The McEliece views compile only with the `deprecated` cargo feature and are excluded from the default build. Stored McEliece multikeys still decode in default builds, but `Builder::new_from_random_bytes` fails for McEliece codecs with `UnsupportedCodec`. ### KEMs / Key Agreement @@ -187,11 +200,6 @@ let kdf = ViewBuilder::new(&mk).kdf(&kdf_key).build()?; let derived = kdf.derive_key(b"passphrase")?; ``` -The original `multi_key::Views` extension trait is deprecated since 2.1.0 -in favor of `ViewBuilder`. It stays as a source-compatible delegating shim -with the same `*_view()` methods, marked as deprecated at compile time. -Note that `multi_sig::Views` is a different, unrelated trait. - ### Custom Protocol Keys A custom protocol key (sigil-0) carries the `Codec::Identity` codec with the `AlgorithmName` attribute (code 27, the protocol name) and the `KeyType` attribute (code 28, one raw byte) from [provenance-specifications][PROVENANCE]. The `KeyType` byte convention is absent or 0 for public keys and 1 for secret keys. Custom keys have no built-in views, so `ViewBuilder` dispatches them to caller-supplied local-codec factories registered with `with_local_codec`. Different custom protocols share the `Codec::Identity` codec, so a factory identifies the key's protocol through the `AlgorithmName` attribute (not through the codec): @@ -320,7 +328,7 @@ By default, threshold `t` and share count `n` are stored as **plaintext** attrib |---|---|---|---|---| | `Full` (default, 0) | plaintext attribute | plaintext attribute | everyone | everyone | | `Partial` (1) | encrypted (AEAD) | plaintext attribute | key-holder only | everyone (auditable) | -| `FullConfidentialial` (2) | encrypted (AEAD) | encrypted (AEAD) | key-holder only | key-holder only | +| `FullConfidential` (2) | encrypted (AEAD) | encrypted (AEAD) | key-holder only | key-holder only | The encrypted values are sealed with **ChaCha20-Poly1305 AEAD**. They are stored as a CBOR-encoded `ThresholdMetadata` blob in `AttrId::EncryptedThresholdMeta`. The cipher parameters (codec and nonce) are recorded in `AttrId::ThresholdMetaCipher` so the blob is self-describing for decryption. A separate **meta key** (a 32-byte symmetric `Multikey` with `Codec::Chacha20Poly1305`) is required to encrypt and decrypt the metadata. @@ -330,11 +338,11 @@ The encrypted values are sealed with **ChaCha20-Poly1305 AEAD**. They are stored - **`Partial`** — Use when the total number of participants `n` should be auditable (for example, for governance transparency) but the threshold `t` should be hidden from share holders and observers. Hiding `t` means an adversary who compromises some shares does not know how many more they need to reconstruct the key. The `meta_key` is required to read `t`. The value `n` is freely readable. -- **`FullConfidentialial`** — Use when both `t` and `n` must be kept secret. An observer who sees a share cannot determine the group size or how many shares are needed. This is the strongest confidentiality mode. The `meta_key` is required to read both `t` and `n`. +- **`FullConfidential`** — Use when both `t` and `n` must be kept secret. An observer who sees a share cannot determine the group size or how many shares are needed. This is the strongest confidentiality mode. The `meta_key` is required to read both `t` and `n`. ### Trade-offs -| Consideration | Full | Partial | FullConfidentialial | +| Consideration | Full | Partial | FullConfidential | |---|---|---|---| | Backward compatible | yes | yes (attribute defaults to Full if absent) | yes | | Observer learns `t` | yes | no | no | @@ -344,7 +352,7 @@ The encrypted values are sealed with **ChaCha20-Poly1305 AEAD**. They are stored | Risk if `meta_key` lost | n/a | `t` irrecoverable | `t` and `n` irrecoverable | | Performance overhead | none | negligible (AEAD on ~10 bytes) | negligible | -**Key management risk.** Losing the `meta_key` makes `t` (Partial) or both `t` and `n` (FullConfidentialial) irrecoverable. This prevents key combination. The `meta_key` should be stored or backed up with the existing at-rest encryption mechanisms. You can always convert back to `Full` mode (with the `meta_key`) before you lose it. +**Key management risk.** Losing the `meta_key` makes `t` (Partial) or both `t` and `n` (FullConfidential) irrecoverable. This prevents key combination. The `meta_key` should be stored or backed up with the existing at-rest encryption mechanisms. You can always convert back to `Full` mode (with the `meta_key`) before you lose it. **DKG note.** DKG threshold values (`t` and `n`) are inherently known to all participants because they are agreed during the DKG ceremony. The confidentiality modes do not apply to DKG shares. A `to_disclosure()` call on a DKG share returns an error. Future work could add "hidden threshold DKG" where participants do not know `t`. That requires protocol-level changes (FROST-style), not just encoding changes. @@ -362,10 +370,10 @@ let meta_mk = Builder::new(Codec::Chacha20Poly1305) .with_key_bytes(&meta_key.as_slice()) .try_build()?; -// BLS Shamir split with FullConfidentialial disclosure +// BLS Shamir split with FullConfidential disclosure let shares = ViewBuilder::new(&mk).threshold().build()? .split_with_disclosure(3, 5, - ThresholdDisclosure::FullConfidentialial, Some(&meta_mk))?; + ThresholdDisclosure::FullConfidential, Some(&meta_mk))?; ``` **2. Builder construction:** @@ -382,7 +390,7 @@ let share = Builder::new(Codec::Bls12381G2PrivShare) ```rust let encrypted = ViewBuilder::new(&share).disclosure().build()? - .to_disclosure(ThresholdDisclosure::FullConfidentialial, Some(&meta_mk), None)?; + .to_disclosure(ThresholdDisclosure::FullConfidential, Some(&meta_mk), None)?; ``` ### Reading Threshold Parameters from Encrypted Shares @@ -420,11 +428,11 @@ The `to_disclosure()` method converts between any pair of modes. It reads the cu let partial = ViewBuilder::new(&full).disclosure().build()? .to_disclosure(ThresholdDisclosure::Partial, Some(&meta_mk), None)?; -// Partial → FullConfidentialial +// Partial → FullConfidential let confidential = ViewBuilder::new(&partial).disclosure().build()? - .to_disclosure(ThresholdDisclosure::FullConfidentialial, Some(&meta_mk), Some(&meta_mk))?; + .to_disclosure(ThresholdDisclosure::FullConfidential, Some(&meta_mk), Some(&meta_mk))?; -// FullConfidentialial → Full +// FullConfidential → Full let full_again = ViewBuilder::new(&confidential).disclosure().build()? .to_disclosure(ThresholdDisclosure::Full, None, Some(&meta_mk))?; ``` @@ -450,10 +458,52 @@ Individual KEM views may restrict the allowed AEAD codec. For example, X25519-ML ## Cargo Features +Each algorithm and curve is its own feature, so you compile in only the key types you use. With `default-features = false` the crate provides only the `Multikey` data type, its attributes, the view traits, and the `ViewBuilder`; supply your own view implementations with `ViewBuilder::with_local_codec`. A hybrid key type compiles when every feature it combines is enabled. For example, Ed25519-ML-DSA-65 needs both `curve25519` and `ml-dsa`, and X25519-ML-KEM-768 needs both `curve25519` and `ml-kem`. + | Feature | Default | Description | |---|---|---| +| `aead` | yes | Shared AEAD layer (AES-GCM, ChaCha20-Poly1305, HKDF) for sealing key types; enabled by the features that need it | +| `bcrypt` | yes | bcrypt-pbkdf key derivation | +| `bls12-381` | yes | BLS12-381 G1/G2 keys and threshold shares | +| `chacha20` | yes | ChaCha20-Poly1305 symmetric cipher keys, and the encrypted threshold disclosure modes (`encrypt_threshold_meta`, `decrypt_threshold_meta`, `generate_meta_key`) | +| `curve25519` | yes | Ed25519, X25519, and XEdDSA (`curve25519-dalek`, `ed25519-dalek`, `x25519-dalek`) | +| `deprecated` | no | Classic McEliece (see Deprecations) | +| `fn-dsa` | yes | FN-DSA (Falcon) | +| `frodokem` | yes | FrodoKEM | +| `lamport` | yes | Lamport and merkle-tree Lamport one-time signatures | +| `legacy_chacha20_fallback` | no | Bare-ChaCha20 fallback for keys encrypted before AEAD was added | +| `mayo` | yes | MAYO | +| `ml-dsa` | yes | ML-DSA (FIPS 204) | +| `ml-kem` | yes | ML-KEM (FIPS 203) | +| `p256` | yes | NIST P-256 | +| `p384` | yes | NIST P-384 | +| `p521` | yes | NIST P-521 | +| `rsa` | yes | RSA | +| `secp256k1` | yes | secp256k1 | | `serde` | yes | Serde serialization for `Multikey`, `KeyShare`, `SharePayload`, `ThresholdParticipant` | -| `wasm` | no | WebAssembly support via `getrandom/wasm_js`; switches `blsful` to the `rust` backend and `ssh-key` to `ecdsa`/`ed25519`/`p256`/`p384`/`p521` features on `wasm32` | +| `slh-dsa` | yes | SLH-DSA (FIPS 205) | +| `slow-tests` | no | Runs the tests that take over 60 seconds in debug builds (SLH-DSA, Classic McEliece, XMSS) | +| `sntrup` | yes | Streamlined NTRU Prime | +| `threshold` | yes | Key splitting (`keysplit`) and the threshold marker attributes (`elliptic-curve`, `vsss-rs`, CBOR via `serde_cbor_2`) | +| `wasm` | no | WebAssembly randomness via `getrandom/wasm_js`; on `wasm32` targets `blsful` always uses its `rust` backend | +| `xmss` | yes | XMSS | + +## Upgrading from 2.x + +3.0 replaces the always-on crypto dependencies with one feature per algorithm or curve. The default build has every feature except `deprecated`, `legacy_chacha20_fallback`, `slow-tests`, and `wasm`, so `multi-key = "3"` has the same key types as 2.x. + +- If you used `default-features = false` only to drop serde, add back the algorithms you use, e.g. `features = ["curve25519", "ml-dsa"]`, plus `threshold`, `lamport`, or `xmss` if you used those. A hybrid needs every feature it combines. +- Creating, converting, or viewing a key whose feature is off returns an `UnsupportedCodec` error (`UnsupportedAlgorithm` for SSH conversion, `SealError::NotEncryptionKey` for the seal and open views). +- `KdfError::Bcrypt` exists only with `bcrypt`, and `ThresholdError::Bls` only with `bls12-381`. Both enums are `#[non_exhaustive]`, so wildcard matches still compile. +- Key splitting (`keysplit`) and the threshold marker need the `threshold` feature. +- The `Partial` and `FullConfidential` disclosure modes and `encrypt_threshold_meta`, `decrypt_threshold_meta`, and `generate_meta_key` need the `chacha20` feature. +- `ThresholdDisclosure::FullConfidentialial` is now `FullConfidential` (multi-sig 2.0). The wire value is unchanged. +- Encode by reference with `Vec::::from(&mk)` to avoid cloning the key, including its secret bytes. The owned `From` still works. +- `FingerprintView` hashes through multi-hash 3.0, which compiles in only the hash families you enable. Add the families you fingerprint with to your own `multi-hash` dependency, e.g. `multi-hash = { version = "3.0", features = ["sha3"] }`; a disabled family returns `UnsupportedHash`. +- `AttrId` parses from its name with `FromStr`, e.g. `"key-data".parse::()`. +- The deprecated `Views` trait is gone. Replace `mk.sign_view()` with `ViewBuilder::new(&mk).sign().build()`, and likewise for the other view kinds; `cipher` and `kdf` take the second key, e.g. `.cipher(&key)`. + +See [CHANGELOG.md](CHANGELOG.md) for the full list. ## Security diff --git a/benches/multikey_bench.rs b/benches/multikey_bench.rs index 70f3b42..c8d0e5d 100644 --- a/benches/multikey_bench.rs +++ b/benches/multikey_bench.rs @@ -1,6 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 //! Performance benchmarks for multi-key -#![allow(clippy::semicolon_if_nothing_returned)] use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; use multi_codec::Codec; @@ -40,7 +39,7 @@ fn bench_encoding(c: &mut Criterion) { c.bench_function("multikey_to_bytes", |b| { b.iter(|| { - let _bytes: Vec = black_box(mk.clone()).into(); + let _bytes = Vec::::from(black_box(&mk)); }) }); } diff --git a/src/attrid.rs b/src/attrid.rs index e3d21fb..5de02cc 100644 --- a/src/attrid.rs +++ b/src/attrid.rs @@ -69,11 +69,11 @@ pub enum AttrId { /// ‖ limit), produced by the controller's signing key. Lets a verifier reject /// a tampered marker before trusting it (defeats TSIG-1 marker forgery). ThresholdMarkerSig, - /// Threshold disclosure mode (varuint u8): 0=Full, 1=Partial, 2=FullConfidentialial. + /// Threshold disclosure mode (varuint u8): 0=Full, 1=Partial, 2=FullConfidential. /// Absent means Full (legacy backward-compatible). ThresholdDisclosure, /// AEAD-encrypted `ThresholdMetadata` CBOR blob. Present in Partial and - /// FullConfidentialial modes. + /// FullConfidential modes. EncryptedThresholdMeta, /// CBOR-encoded `ThresholdMetaCipher` (cipher codec + nonce) for decrypting /// `EncryptedThresholdMeta`. @@ -202,10 +202,10 @@ impl<'a> TryDecodeFrom<'a> for AttrId { } } -impl TryFrom<&str> for AttrId { - type Error = Error; +impl std::str::FromStr for AttrId { + type Err = Error; - fn try_from(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_str() { "key-is-encrypted" => Ok(AttrId::KeyIsEncrypted), "key-data" => Ok(AttrId::KeyData), @@ -242,8 +242,46 @@ impl TryFrom<&str> for AttrId { } } +impl TryFrom<&str> for AttrId { + type Error = Error; + + fn try_from(s: &str) -> Result { + s.parse() + } +} + impl fmt::Display for AttrId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.as_str()) } } + +#[cfg(test)] +mod tests { + use super::*; + + /// every attribute name parses back to its id through `FromStr` and the + /// delegating `TryFrom<&str>`, case-insensitively + #[test] + fn test_from_str_round_trip() { + let ids: Vec = (0..=u8::MAX) + .filter_map(|code| AttrId::try_from(code).ok()) + .collect(); + assert!(!ids.is_empty()); + for id in ids { + assert!(id.as_str().parse::().unwrap() == id, "{id}"); + assert!(AttrId::try_from(id.as_str()).unwrap() == id, "{id}"); + let upper = id.as_str().to_ascii_uppercase(); + assert!(upper.parse::().unwrap() == id, "{id}"); + } + } + + /// an unknown attribute name is rejected + #[test] + fn test_from_str_unknown() { + assert!(matches!( + "no-such-attribute".parse::(), + Err(Error::Attributes(AttributesError::InvalidAttributeName(_))) + )); + } +} diff --git a/src/cipher.rs b/src/cipher.rs index 24b2936..71cedbd 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -79,7 +79,7 @@ impl Builder { } } -#[cfg(test)] +#[cfg(all(test, feature = "bcrypt", feature = "chacha20", feature = "curve25519"))] mod tests { use super::*; use crate::{ViewBuilder, kdf, mk}; diff --git a/src/error.rs b/src/error.rs index 5372192..95aaf87 100644 --- a/src/error.rs +++ b/src/error.rs @@ -254,6 +254,7 @@ pub enum CipherError { #[non_exhaustive] pub enum KdfError { /// Bcrypt PBKDF error + #[cfg(feature = "bcrypt")] #[error(transparent)] Bcrypt(#[from] bcrypt_pbkdf::Error), /// Error with the KDF codec @@ -355,6 +356,7 @@ pub enum SignError { #[non_exhaustive] pub enum ThresholdError { /// Bls error + #[cfg(feature = "bls12-381")] #[error(transparent)] Bls(#[from] blsful::BlsError), /// Invalid threshold and limit diff --git a/src/kdf.rs b/src/kdf.rs index 361f5b3..0049062 100644 --- a/src/kdf.rs +++ b/src/kdf.rs @@ -5,6 +5,11 @@ use multi_util::Varuint; use rand_core::CryptoRng; use zeroize::Zeroizing; +/// KDF salt length in bytes, shared by the bcrypt view and the cipher views +/// that carry KDF attributes. +#[cfg(any(feature = "bcrypt", feature = "chacha20"))] +pub(crate) const SALT_LENGTH: usize = 32; + /// Multikey builder constructs kdf multikeys. #[derive(Clone, Debug, Default)] pub struct Builder { @@ -87,7 +92,7 @@ impl Builder { } } -#[cfg(test)] +#[cfg(all(test, feature = "bcrypt", feature = "chacha20"))] mod tests { use super::*; use crate::{ViewBuilder, cipher}; diff --git a/src/keysplit.rs b/src/keysplit.rs index ec0882f..bd6b35c 100644 --- a/src/keysplit.rs +++ b/src/keysplit.rs @@ -21,37 +21,25 @@ use crate::mk::Attributes; use crate::{AttrId, Builder, Error, Multikey}; -use blsful::inner_types::{G1Projective, G2Projective, Scalar as BlsScalar}; -use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar as DalekScalar}; -use elliptic_curve::ff::PrimeField; -use elliptic_curve::group::GroupEncoding; use multi_codec::Codec; use multi_util::CodecInfo; use rand_core::CryptoRng; use serde::{Deserialize, Serialize}; -use vsss_rs::{ - DefaultShare, FeldmanVerifierSet, Gf256, IdentifierPrimeField, ReadableShareSet, - ShareVerifierGroup, ValueGroup, feldman, -}; +use vsss_rs::Gf256; use zeroize::Zeroizing; -type Ds = DefaultShare, IdentifierPrimeField>; - fn err(e: E) -> Error { Error::KeySplit(e.to_string()) } -/// Serialize a value to CBOR bytes using `ciborium` (replaces the -/// unmaintained `serde_cbor` dependency). +/// Serialize a value to CBOR bytes using `serde_cbor_2`. fn cbor_to_vec(value: &T) -> Result, Error> { - let mut buf = Vec::new(); - ciborium::into_writer(value, &mut buf).map_err(err)?; - Ok(buf) + serde_cbor_2::to_vec(value).map_err(err) } -/// Deserialize a value from CBOR bytes using `ciborium`. +/// Deserialize a value from CBOR bytes using `serde_cbor_2`. fn cbor_from_slice(bytes: &[u8]) -> Result { - ciborium::from_reader(bytes).map_err(err) + serde_cbor_2::from_slice(bytes).map_err(err) } /// Sharing scheme tag stored in each share payload. @@ -85,167 +73,203 @@ struct SharePayload { dual: Option, } -// ---- scalar helpers --------------------------------------------------------- - -/// Decode a canonical scalar repr into a field element. -fn scalar(bytes: &[u8]) -> Result { - let mut repr = F::Repr::default(); - let r = repr.as_mut(); - if bytes.len() != r.len() { - return Err(err("wrong scalar length")); - } - r.copy_from_slice(bytes); - Option::::from(F::from_repr(repr)).ok_or_else(|| err("non-canonical scalar")) -} - -/// BLS scalars are big-endian in multikey but little-endian in `bls12_381_plus`. -fn bls_from_be(be: &[u8]) -> Result { - let mut le = be.to_vec(); - le.reverse(); - scalar::(&le) -} - -fn rev(mut v: Vec) -> Vec { - v.reverse(); - v -} - -fn wrapped_from_seed(secret: &[u8]) -> Result { - let seed: [u8; 32] = secret - .try_into() - .map_err(|_| err("ed25519/x25519 seed must be 32 bytes"))?; - Ok(DalekScalar::from_bytes_mod_order(seed)) -} - // ---- per-curve Feldman (monomorphized to avoid ShareVerifier ambiguity) ----- /// `(shares as (id, value) pairs, shared verifier commitment set)`. type EccShares = (Vec<(Vec, Vec)>, Vec>); -macro_rules! feldman_curve { - ($split:ident, $combine:ident, $verify:ident, $f:ty, $g:ty) => { - fn $split( - secret: $f, - threshold: usize, - limit: usize, - rng: impl CryptoRng, - ) -> Result { - let (shares, verifiers) = feldman::split_secret::, ShareVerifierGroup<$g>>( - threshold, - limit, - &IdentifierPrimeField(secret), - None, - rng, - ) - .map_err(err)?; - let vbytes: Vec> = verifiers - .iter() - .map(|v| AsRef::<[u8]>::as_ref(&<$g as GroupEncoding>::to_bytes(&v.0)).to_vec()) - .collect(); - let pairs = shares - .iter() - .map(|s| { - let id = <$f as PrimeField>::to_repr(&s.identifier.0); - let val = <$f as PrimeField>::to_repr(&s.value.0); - ( - AsRef::<[u8]>::as_ref(&id).to_vec(), - AsRef::<[u8]>::as_ref(&val).to_vec(), - ) - }) - .collect(); - Ok((pairs, vbytes)) +/// Feldman VSS over the elliptic curves: scalar decoding plus one +/// split/combine/verify triple per enabled curve feature. +#[cfg(any( + feature = "bls12-381", + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" +))] +mod curves { + use super::{EccShares, Error, err}; + #[cfg(feature = "bls12-381")] + use blsful::inner_types::{G1Projective, G2Projective, Scalar as BlsScalar}; + #[cfg(feature = "curve25519")] + use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar as DalekScalar}; + use elliptic_curve::{ff::PrimeField, group::GroupEncoding}; + use rand_core::CryptoRng; + use vsss_rs::{ + DefaultShare, FeldmanVerifierSet, IdentifierPrimeField, ReadableShareSet, + ShareVerifierGroup, ValueGroup, feldman, + }; + + type Ds = DefaultShare, IdentifierPrimeField>; + + /// Decode a canonical scalar repr into a field element. + pub(super) fn scalar(bytes: &[u8]) -> Result { + let mut repr = F::Repr::default(); + let r = repr.as_mut(); + if bytes.len() != r.len() { + return Err(err("wrong scalar length")); } + r.copy_from_slice(bytes); + Option::::from(F::from_repr(repr)).ok_or_else(|| err("non-canonical scalar")) + } + + /// BLS scalars are big-endian in multikey but little-endian in `bls12_381_plus`. + #[cfg(feature = "bls12-381")] + pub(super) fn bls_scalar_from_be(be: &[u8]) -> Result { + let mut le = be.to_vec(); + le.reverse(); + scalar::(&le) + } + + /// The inverse of [`bls_scalar_from_be`]: little-endian scalar bytes to + /// the big-endian form multikey stores. + #[cfg(feature = "bls12-381")] + pub(super) fn bls_to_be(mut le: Vec) -> Vec { + le.reverse(); + le + } + + #[cfg(feature = "curve25519")] + pub(super) fn wrapped_from_seed(secret: &[u8]) -> Result { + let seed: [u8; 32] = secret + .try_into() + .map_err(|_| err("ed25519/x25519 seed must be 32 bytes"))?; + Ok(DalekScalar::from_bytes_mod_order(seed)) + } + + macro_rules! feldman_curve { + ($split:ident, $combine:ident, $verify:ident, $f:ty, $g:ty) => { + pub(super) fn $split( + secret: $f, + threshold: usize, + limit: usize, + rng: impl CryptoRng, + ) -> Result { + let (shares, verifiers) = feldman::split_secret::, ShareVerifierGroup<$g>>( + threshold, + limit, + &IdentifierPrimeField(secret), + None, + rng, + ) + .map_err(err)?; + let vbytes: Vec> = verifiers + .iter() + .map(|v| AsRef::<[u8]>::as_ref(&<$g as GroupEncoding>::to_bytes(&v.0)).to_vec()) + .collect(); + let pairs = shares + .iter() + .map(|s| { + let id = <$f as PrimeField>::to_repr(&s.identifier.0); + let val = <$f as PrimeField>::to_repr(&s.value.0); + ( + AsRef::<[u8]>::as_ref(&id).to_vec(), + AsRef::<[u8]>::as_ref(&val).to_vec(), + ) + }) + .collect(); + Ok((pairs, vbytes)) + } - fn $combine(pairs: &[(Vec, Vec)]) -> Result, Error> { - let ds: Vec> = pairs - .iter() - .map(|(i, v)| { - Ok::<_, Error>(DefaultShare { - identifier: IdentifierPrimeField(scalar::<$f>(i)?), - value: IdentifierPrimeField(scalar::<$f>(v)?), + pub(super) fn $combine(pairs: &[(Vec, Vec)]) -> Result, Error> { + let ds: Vec> = pairs + .iter() + .map(|(i, v)| { + Ok::<_, Error>(DefaultShare { + identifier: IdentifierPrimeField(scalar::<$f>(i)?), + value: IdentifierPrimeField(scalar::<$f>(v)?), + }) }) - }) - .collect::>()?; - let secret: IdentifierPrimeField<$f> = ds.combine().map_err(err)?; - let repr = <$f as PrimeField>::to_repr(&secret.0); - Ok(AsRef::<[u8]>::as_ref(&repr).to_vec()) - } + .collect::>()?; + let secret: IdentifierPrimeField<$f> = ds.combine().map_err(err)?; + let repr = <$f as PrimeField>::to_repr(&secret.0); + Ok(AsRef::<[u8]>::as_ref(&repr).to_vec()) + } - fn $verify(id: &[u8], val: &[u8], vb: &[Vec]) -> Result<(), Error> { - let share: Ds<$f> = DefaultShare { - identifier: IdentifierPrimeField(scalar::<$f>(id)?), - value: IdentifierPrimeField(scalar::<$f>(val)?), - }; - let verifiers: Vec> = vb - .iter() - .map(|b| { - let mut repr = <$g as GroupEncoding>::Repr::default(); - if AsRef::<[u8]>::as_ref(&repr).len() != b.len() { - return Err(err("bad verifier length")); - } - AsMut::<[u8]>::as_mut(&mut repr).copy_from_slice(b); - Option::<$g>::from(<$g as GroupEncoding>::from_bytes(&repr)) - .map(ValueGroup) - .ok_or_else(|| err("bad verifier")) - }) - .collect::>()?; - verifiers - .verify_share(&share) - .map_err(|_| err("feldman verification failed")) - } - }; + pub(super) fn $verify(id: &[u8], val: &[u8], vb: &[Vec]) -> Result<(), Error> { + let share: Ds<$f> = DefaultShare { + identifier: IdentifierPrimeField(scalar::<$f>(id)?), + value: IdentifierPrimeField(scalar::<$f>(val)?), + }; + let verifiers: Vec> = vb + .iter() + .map(|b| { + let mut repr = <$g as GroupEncoding>::Repr::default(); + if AsRef::<[u8]>::as_ref(&repr).len() != b.len() { + return Err(err("bad verifier length")); + } + AsMut::<[u8]>::as_mut(&mut repr).copy_from_slice(b); + Option::<$g>::from(<$g as GroupEncoding>::from_bytes(&repr)) + .map(ValueGroup) + .ok_or_else(|| err("bad verifier")) + }) + .collect::>()?; + verifiers + .verify_share(&share) + .map_err(|_| err("feldman verification failed")) + } + }; + } + + #[cfg(feature = "secp256k1")] + feldman_curve!( + split_k256, + combine_k256, + verify_k256, + k256::Scalar, + k256::ProjectivePoint + ); + #[cfg(feature = "p256")] + feldman_curve!( + split_p256, + combine_p256, + verify_p256, + p256::Scalar, + p256::ProjectivePoint + ); + #[cfg(feature = "p384")] + feldman_curve!( + split_p384, + combine_p384, + verify_p384, + p384::Scalar, + p384::ProjectivePoint + ); + #[cfg(feature = "p521")] + feldman_curve!( + split_p521, + combine_p521, + verify_p521, + p521::Scalar, + p521::ProjectivePoint + ); + #[cfg(feature = "curve25519")] + feldman_curve!( + split_25519, + combine_25519, + verify_25519, + DalekScalar, + RistrettoPoint + ); + #[cfg(feature = "bls12-381")] + feldman_curve!( + split_blsg1, + combine_blsg1, + verify_blsg1, + BlsScalar, + G1Projective + ); + #[cfg(feature = "bls12-381")] + feldman_curve!( + split_blsg2, + combine_blsg2, + verify_blsg2, + BlsScalar, + G2Projective + ); } -feldman_curve!( - split_k256, - combine_k256, - verify_k256, - k256::Scalar, - k256::ProjectivePoint -); -feldman_curve!( - split_p256, - combine_p256, - verify_p256, - p256::Scalar, - p256::ProjectivePoint -); -feldman_curve!( - split_p384, - combine_p384, - verify_p384, - p384::Scalar, - p384::ProjectivePoint -); -feldman_curve!( - split_p521, - combine_p521, - verify_p521, - p521::Scalar, - p521::ProjectivePoint -); -feldman_curve!( - split_25519, - combine_25519, - verify_25519, - DalekScalar, - RistrettoPoint -); -feldman_curve!( - split_blsg1, - combine_blsg1, - verify_blsg1, - BlsScalar, - G1Projective -); -feldman_curve!( - split_blsg2, - combine_blsg2, - verify_blsg2, - BlsScalar, - G2Projective -); - fn is_feldman_codec(codec: Codec) -> bool { matches!( codec, @@ -267,42 +291,81 @@ fn ecc_split( n: usize, rng: impl CryptoRng, ) -> Result { - match codec { - Codec::Secp256K1Priv => split_k256(scalar::(secret)?, t, n, rng), - Codec::P256Priv => split_p256(scalar::(secret)?, t, n, rng), - Codec::P384Priv => split_p384(scalar::(secret)?, t, n, rng), - Codec::P521Priv => split_p521(scalar::(secret)?, t, n, rng), - Codec::Ed25519Priv | Codec::X25519Priv => { - split_25519(wrapped_from_seed(secret)?, t, n, rng) + // the tuple carries the parameters into the arms, so a build whose features + // gate every arm out still uses them + match (codec, secret, t, n, rng) { + #[cfg(feature = "secp256k1")] + (Codec::Secp256K1Priv, secret, t, n, rng) => { + curves::split_k256(curves::scalar::(secret)?, t, n, rng) + } + #[cfg(feature = "p256")] + (Codec::P256Priv, secret, t, n, rng) => { + curves::split_p256(curves::scalar::(secret)?, t, n, rng) + } + #[cfg(feature = "p384")] + (Codec::P384Priv, secret, t, n, rng) => { + curves::split_p384(curves::scalar::(secret)?, t, n, rng) + } + #[cfg(feature = "p521")] + (Codec::P521Priv, secret, t, n, rng) => { + curves::split_p521(curves::scalar::(secret)?, t, n, rng) + } + #[cfg(feature = "curve25519")] + (Codec::Ed25519Priv | Codec::X25519Priv, secret, t, n, rng) => { + curves::split_25519(curves::wrapped_from_seed(secret)?, t, n, rng) + } + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G1Priv, secret, t, n, rng) => { + curves::split_blsg1(curves::bls_scalar_from_be(secret)?, t, n, rng) + } + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G2Priv, secret, t, n, rng) => { + curves::split_blsg2(curves::bls_scalar_from_be(secret)?, t, n, rng) } - Codec::Bls12381G1Priv => split_blsg1(bls_from_be::(secret)?, t, n, rng), - Codec::Bls12381G2Priv => split_blsg2(bls_from_be::(secret)?, t, n, rng), _ => Err(err("unsupported curve codec")), } } fn ecc_combine(codec: Codec, pairs: &[(Vec, Vec)]) -> Result, Error> { - match codec { - Codec::Secp256K1Priv => combine_k256(pairs), - Codec::P256Priv => combine_p256(pairs), - Codec::P384Priv => combine_p384(pairs), - Codec::P521Priv => combine_p521(pairs), - Codec::Ed25519Priv | Codec::X25519Priv => combine_25519(pairs), - Codec::Bls12381G1Priv => combine_blsg1(pairs).map(rev), - Codec::Bls12381G2Priv => combine_blsg2(pairs).map(rev), + // the tuple carries the parameters into the arms, so a build whose features + // gate every arm out still uses them + match (codec, pairs) { + #[cfg(feature = "secp256k1")] + (Codec::Secp256K1Priv, pairs) => curves::combine_k256(pairs), + #[cfg(feature = "p256")] + (Codec::P256Priv, pairs) => curves::combine_p256(pairs), + #[cfg(feature = "p384")] + (Codec::P384Priv, pairs) => curves::combine_p384(pairs), + #[cfg(feature = "p521")] + (Codec::P521Priv, pairs) => curves::combine_p521(pairs), + #[cfg(feature = "curve25519")] + (Codec::Ed25519Priv | Codec::X25519Priv, pairs) => curves::combine_25519(pairs), + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G1Priv, pairs) => curves::combine_blsg1(pairs).map(curves::bls_to_be), + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G2Priv, pairs) => curves::combine_blsg2(pairs).map(curves::bls_to_be), _ => Err(err("unsupported curve codec")), } } fn ecc_verify(codec: Codec, id: &[u8], val: &[u8], vb: &[Vec]) -> Result<(), Error> { - match codec { - Codec::Secp256K1Priv => verify_k256(id, val, vb), - Codec::P256Priv => verify_p256(id, val, vb), - Codec::P384Priv => verify_p384(id, val, vb), - Codec::P521Priv => verify_p521(id, val, vb), - Codec::Ed25519Priv | Codec::X25519Priv => verify_25519(id, val, vb), - Codec::Bls12381G1Priv => verify_blsg1(id, val, vb), - Codec::Bls12381G2Priv => verify_blsg2(id, val, vb), + // the tuple carries the parameters into the arms, so a build whose features + // gate every arm out still uses them + match (codec, id, val, vb) { + #[cfg(feature = "secp256k1")] + (Codec::Secp256K1Priv, id, val, vb) => curves::verify_k256(id, val, vb), + #[cfg(feature = "p256")] + (Codec::P256Priv, id, val, vb) => curves::verify_p256(id, val, vb), + #[cfg(feature = "p384")] + (Codec::P384Priv, id, val, vb) => curves::verify_p384(id, val, vb), + #[cfg(feature = "p521")] + (Codec::P521Priv, id, val, vb) => curves::verify_p521(id, val, vb), + #[cfg(feature = "curve25519")] + (Codec::Ed25519Priv | Codec::X25519Priv, id, val, vb) => curves::verify_25519(id, val, vb), + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G1Priv, id, val, vb) => curves::verify_blsg1(id, val, vb), + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G2Priv, id, val, vb) => curves::verify_blsg2(id, val, vb), _ => Err(err("unsupported curve codec")), } } @@ -523,11 +586,25 @@ pub fn combine(shares: &[Multikey]) -> Result { .try_build() } -#[cfg(test)] +#[cfg(all( + test, + any( + feature = "curve25519", + feature = "deprecated", + feature = "fn-dsa", + feature = "frodokem", + feature = "mayo", + feature = "ml-dsa", + feature = "ml-kem", + feature = "p256", + feature = "rsa", + feature = "sntrup", + all(feature = "slh-dsa", feature = "slow-tests") + ) +))] mod tests { use super::*; use crate::ViewBuilder; - use crate::mk; fn gen_key(codec: Codec) -> Multikey { Builder::new_from_random_bytes(codec, &mut rand::rng()) @@ -546,114 +623,154 @@ mod tests { .to_vec() } - /// Split → verify every share → combine a non-contiguous subset → assert the - /// reconstructed key is byte-identical to the original. Run for both 2-of-3 - /// and 3-of-5 with disjoint/non-contiguous subsets. - fn assert_roundtrip(codec: Codec) { - let mk = gen_key(codec); - let original = secret(&mk); - - // 2-of-3 - let shares = - split(&mk, 2, 3, rand::rng()).unwrap_or_else(|e| panic!("{codec:?} split 2/3: {e}")); - assert_eq!(shares.len(), 3, "{codec:?} share count"); - for s in &shares { - assert_eq!(s.codec(), Codec::KeySplitShare, "{codec:?} share codec"); - verify_share(s).unwrap_or_else(|e| panic!("{codec:?} verify_share: {e}")); + /// split → verify → combine round trips, one test per key family + #[cfg(any( + feature = "curve25519", + feature = "deprecated", + feature = "fn-dsa", + feature = "frodokem", + feature = "mayo", + feature = "ml-dsa", + feature = "ml-kem", + feature = "rsa", + feature = "sntrup", + all(feature = "slh-dsa", feature = "slow-tests") + ))] + mod roundtrip { + use super::*; + + /// Split → verify every share → combine a non-contiguous subset → assert the + /// reconstructed key is byte-identical to the original. Run for both 2-of-3 + /// and 3-of-5 with disjoint/non-contiguous subsets. + fn assert_roundtrip(codec: Codec) { + let mk = gen_key(codec); + let original = secret(&mk); + + // 2-of-3 + let shares = split(&mk, 2, 3, rand::rng()) + .unwrap_or_else(|e| panic!("{codec:?} split 2/3: {e}")); + assert_eq!(shares.len(), 3, "{codec:?} share count"); + for s in &shares { + assert_eq!(s.codec(), Codec::KeySplitShare, "{codec:?} share codec"); + verify_share(s).unwrap_or_else(|e| panic!("{codec:?} verify_share: {e}")); + } + // combine the non-contiguous subset {0, 2} + let got = combine(&[shares[0].clone(), shares[2].clone()]) + .unwrap_or_else(|e| panic!("{codec:?} combine 2/3: {e}")); + assert_eq!(secret(&got), original, "{codec:?} 2-of-3 exact roundtrip"); + assert_eq!(got.codec(), codec, "{codec:?} reconstructed codec"); + + // 3-of-5 with the non-contiguous subset {1, 3, 4} + let shares5 = split(&mk, 3, 5, rand::rng()) + .unwrap_or_else(|e| panic!("{codec:?} split 3/5: {e}")); + assert_eq!(shares5.len(), 5, "{codec:?} share count 5"); + let got5 = combine(&[shares5[1].clone(), shares5[3].clone(), shares5[4].clone()]) + .unwrap_or_else(|e| panic!("{codec:?} combine 3/5: {e}")); + assert_eq!(secret(&got5), original, "{codec:?} 3-of-5 exact roundtrip"); } - // combine the non-contiguous subset {0, 2} - let got = combine(&[shares[0].clone(), shares[2].clone()]) - .unwrap_or_else(|e| panic!("{codec:?} combine 2/3: {e}")); - assert_eq!(secret(&got), original, "{codec:?} 2-of-3 exact roundtrip"); - assert_eq!(got.codec(), codec, "{codec:?} reconstructed codec"); - - // 3-of-5 with the non-contiguous subset {1, 3, 4} - let shares5 = - split(&mk, 3, 5, rand::rng()).unwrap_or_else(|e| panic!("{codec:?} split 3/5: {e}")); - assert_eq!(shares5.len(), 5, "{codec:?} share count 5"); - let got5 = combine(&[shares5[1].clone(), shares5[3].clone(), shares5[4].clone()]) - .unwrap_or_else(|e| panic!("{codec:?} combine 3/5: {e}")); - assert_eq!(secret(&got5), original, "{codec:?} 3-of-5 exact roundtrip"); - } - fn assert_all(codecs: &[Codec]) { - for &c in codecs { - assert_roundtrip(c); + fn assert_all(codecs: &[Codec]) { + for &c in codecs { + assert_roundtrip(c); + } } - } - // ── Elliptic-curve + Ed25519 (KEY_CODECS = ed25519, secp256k1, bls-g1, - // bls-g2, p256, p384, p521). Ed25519 restores exactly via the dual. ── - #[test] - fn roundtrip_key_codecs() { - assert_all(&mk::KEY_CODECS); - } + // ── Elliptic-curve + Ed25519 (KEY_CODECS = ed25519, secp256k1, bls-g1, + // bls-g2, p256, p384, p521). Ed25519 restores exactly via the dual. ── + #[cfg(all( + feature = "bls12-381", + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" + ))] + #[test] + fn roundtrip_key_codecs() { + assert_all(&crate::mk::KEY_CODECS); + } - #[test] - fn roundtrip_x25519() { - assert_all(&mk::X25519_KEY_CODECS); - } + #[cfg(feature = "curve25519")] + #[test] + fn roundtrip_x25519() { + assert_all(&crate::mk::X25519_KEY_CODECS); + } - // ── RSA ── - #[test] - fn roundtrip_rsa() { - assert_all(&mk::RSA_KEY_CODECS); - } + // ── RSA ── + #[cfg(feature = "rsa")] + #[test] + fn roundtrip_rsa() { + assert_all(&crate::mk::RSA_KEY_CODECS); + } - // ── Post-quantum signatures ── - #[test] - fn roundtrip_ml_dsa() { - assert_all(&mk::ML_DSA_KEY_CODECS); - } + // ── Post-quantum signatures ── + #[cfg(feature = "ml-dsa")] + #[test] + fn roundtrip_ml_dsa() { + assert_all(&crate::mk::ML_DSA_KEY_CODECS); + } - #[test] - fn roundtrip_fn_dsa() { - assert_all(&mk::FN_DSA_KEY_CODECS); - } + #[cfg(feature = "fn-dsa")] + #[test] + fn roundtrip_fn_dsa() { + assert_all(&crate::mk::FN_DSA_KEY_CODECS); + } - #[test] - fn roundtrip_mayo() { - assert_all(&mk::MAYO_KEY_CODECS); - } + #[cfg(feature = "mayo")] + #[test] + fn roundtrip_mayo() { + assert_all(&crate::mk::MAYO_KEY_CODECS); + } - #[cfg(feature = "slow-tests")] - #[test] - fn roundtrip_slh_dsa() { - assert_all(&mk::SLH_DSA_KEY_CODECS); - } + #[cfg(all(feature = "slh-dsa", feature = "slow-tests"))] + #[test] + fn roundtrip_slh_dsa() { + assert_all(&crate::mk::SLH_DSA_KEY_CODECS); + } - // ── Post-quantum KEMs ── - #[test] - fn roundtrip_ml_kem() { - assert_all(&mk::ML_KEM_KEY_CODECS); - } + // ── Post-quantum KEMs ── + #[cfg(feature = "ml-kem")] + #[test] + fn roundtrip_ml_kem() { + assert_all(&crate::mk::ML_KEM_KEY_CODECS); + } - #[test] - fn roundtrip_sntrup() { - assert_all(&mk::SNTRUP_KEY_CODECS); - } + #[cfg(feature = "sntrup")] + #[test] + fn roundtrip_sntrup() { + assert_all(&crate::mk::SNTRUP_KEY_CODECS); + } - #[cfg(feature = "deprecated")] - #[allow(deprecated)] - #[test] - fn roundtrip_mceliece() { - assert_all(&mk::MCELIECE_KEY_CODECS); - } + #[cfg(feature = "deprecated")] + #[allow(deprecated)] + #[test] + fn roundtrip_mceliece() { + assert_all(&crate::mk::MCELIECE_KEY_CODECS); + } - #[test] - fn roundtrip_frodokem() { - assert_all(&mk::FRODOKEM_KEY_CODECS); - } + #[cfg(feature = "frodokem")] + #[test] + fn roundtrip_frodokem() { + assert_all(&crate::mk::FRODOKEM_KEY_CODECS); + } - // ── Hybrid (classical + post-quantum) keys → gf256 byte path ── - #[test] - fn roundtrip_hybrids() { - assert_all(&mk::X25519_SNTRUP761_KEY_CODECS); - assert_all(&mk::X25519_MLKEM768_KEY_CODECS); - assert_all(&mk::ED25519_MAYO2_KEY_CODECS); + // ── Hybrid (classical + post-quantum) keys → gf256 byte path ── + #[cfg(all( + feature = "curve25519", + feature = "mayo", + feature = "ml-kem", + feature = "sntrup" + ))] + #[test] + fn roundtrip_hybrids() { + assert_all(&crate::mk::X25519_SNTRUP761_KEY_CODECS); + assert_all(&crate::mk::X25519_MLKEM768_KEY_CODECS); + assert_all(&crate::mk::ED25519_MAYO2_KEY_CODECS); + } } // ── Scheme/property checks ── + #[cfg(feature = "curve25519")] #[test] fn ed25519_x25519_carry_verifiable_dual() { for &codec in &[Codec::Ed25519Priv, Codec::X25519Priv] { @@ -667,6 +784,13 @@ mod tests { } } + #[cfg(all( + feature = "bls12-381", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" + ))] #[test] fn feldman_curves_emit_commitments() { for &codec in &[ @@ -685,6 +809,7 @@ mod tests { } // ── Serialization: a KeySplitShare is a Multikey; CBOR + JSON round-trip ── + #[cfg(all(feature = "p256", feature = "serde"))] #[test] fn share_survives_cbor_and_json() { let mk = gen_key(Codec::P256Priv); @@ -715,6 +840,7 @@ mod tests { } // ── Negative tests ── + #[cfg(feature = "p256")] #[test] fn tampered_feldman_share_fails_verify() { let mk = gen_key(Codec::P256Priv); @@ -728,6 +854,7 @@ mod tests { ); } + #[cfg(feature = "curve25519")] #[test] fn tampered_dual_fails_verify() { let mk = gen_key(Codec::Ed25519Priv); @@ -738,6 +865,7 @@ mod tests { assert!(verify_share(&bad).is_err(), "tampered dual must fail"); } + #[cfg(feature = "p256")] #[test] fn below_threshold_does_not_recover() { let mk = gen_key(Codec::P256Priv); @@ -748,6 +876,7 @@ mod tests { } } + #[cfg(all(feature = "p256", feature = "secp256k1"))] #[test] fn mixed_and_empty_and_public_rejected() { let a = split(&gen_key(Codec::P256Priv), 2, 3, rand::rng()).unwrap(); @@ -766,6 +895,7 @@ mod tests { assert!(split(&pk, 2, 3, rand::rng()).is_err(), "public key"); } + #[cfg(feature = "p256")] #[test] fn invalid_params_rejected() { let mk = gen_key(Codec::P256Priv); diff --git a/src/lib.rs b/src/lib.rs index 828708a..270a95a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ //! ### Generating a Key Pair //! //! ```rust +//! # #[cfg(feature = "curve25519")] { //! use multi_key::Builder; //! use multi_codec::Codec; //! @@ -30,11 +31,13 @@ //! .unwrap() //! .try_build() //! .unwrap(); +//! # } //! ``` //! //! ### Encoding and Decoding //! //! ```rust +//! # #[cfg(feature = "curve25519")] { //! use multi_key::{Builder, Multikey}; //! use multi_codec::Codec; //! @@ -45,11 +48,12 @@ //! .unwrap(); //! //! // Encode to bytes -//! let bytes: Vec = mk1.clone().into(); +//! let bytes = Vec::::from(&mk1); //! //! // Decode from bytes //! let mk2 = Multikey::try_from(bytes.as_ref()).unwrap(); //! assert_eq!(mk1, mk2); +//! # } //! ``` //! //! ### Creating Views @@ -64,6 +68,7 @@ //! `ViewBuilder::new(&mk).cipher(&cipher_key).build()`. //! //! ```rust +//! # #[cfg(feature = "curve25519")] { //! use multi_key::{Builder, ViewBuilder}; //! use multi_codec::Codec; //! @@ -78,10 +83,21 @@ //! //! let verifier = ViewBuilder::new(&mk).verify().build().unwrap(); //! verifier.verify(&sig, Some(b"message")).unwrap(); +//! # } //! ``` //! //! ## Features //! +//! Each algorithm and curve is its own feature: `bcrypt`, `bls12-381`, +//! `chacha20`, `curve25519` (Ed25519, X25519, XEdDSA), `fn-dsa`, `frodokem`, +//! `lamport`, `mayo`, `ml-dsa`, `ml-kem`, `p256`, `p384`, `p521`, `rsa`, +//! `secp256k1`, `slh-dsa`, `sntrup`, and `xmss`, all enabled by default. +//! `threshold` (default) adds key splitting and the threshold marker +//! attributes. A hybrid key type compiles when every feature it combines is +//! enabled, e.g. Ed25519-ML-DSA-65 needs `curve25519` and `ml-dsa`. With +//! `default-features = false` the crate provides only the [`Multikey`] data +//! type, its attributes, the view traits, and the [`ViewBuilder`]. +//! //! - **`serde`** (default): Enables serde serialization //! - **`wasm`**: Enables WebAssembly support with getrandom/js //! - **`legacy_chacha20_fallback`** (default off): Enables the legacy bare-ChaCha20 @@ -95,19 +111,30 @@ //! ## Deprecations //! //! Classic McEliece (the `Mceliece348864Pub`/`Priv` codecs and the -//! `X25519-McEliece-348864` hybrid) is deprecated. Key-recovery attacks now -//! solve the TII McEliece challenges; see -//! for the recovered keys. The -//! McEliece views compile only with the `deprecated` feature. Stored +//! `X25519-McEliece-348864` hybrid) is deprecated; use ML-KEM, FrodoKEM, or +//! their X25519 hybrids instead. During 2026 structural attacks on binary +//! Goppa codes cut the estimated key-recovery cost from over 2^3000 to as low +//! as a claimed 2^99 to 2^102 operations: +//! +//! - Hemmert and Wiemers (BSI), higher-order-vanishing distinguisher +//! (CRYPTO 2026), and Hemmert's key recovery, +//! +//! - Ghoshal, Ishai, Jain, and Sun, quasi-polynomial distinguisher, +//! +//! - Weis, improved key recovery, +//! - Saarinen, recovered TII McEliece challenge keys, +//! , , +//! +//! +//! On 29 September 2026 Germany's BSI advised against Classic McEliece in new +//! developments: . +//! Most of these results are still preprints, and BSI reports no practical +//! attack yet on the parameter sets it recommends. +//! +//! The McEliece views compile only with the `deprecated` feature. Stored //! McEliece multikeys still decode in default builds, but key generation //! fails with `UnsupportedCodec`. //! -//! The `Views` extension trait (and its impl for `Multikey`) is deprecated -//! since 2.1.0. It stays as a source-compatible delegating shim that calls -//! the same dispatch core [`ViewBuilder`] uses; construct views with -//! [`ViewBuilder`] instead. Note that `multi_sig::Views` is a different, -//! unrelated trait. -//! //! ## Security //! //! - Private keys are automatically zeroized on drop @@ -118,50 +145,6 @@ //! //! See [`KEY_CODECS`] for the complete list of supported key algorithms. -#![warn(missing_docs)] -#![deny( - unsafe_code, - trivial_casts, - trivial_numeric_casts, - unused_import_braces, - unused_qualifications -)] -// Pedantic/nursery lints are enabled at the workspace level via -// `[lints.clippy]` in Cargo.toml. The following allows suppress stylistic -// lints that would require large-scale churn for minimal security benefit. -#![allow( - clippy::doc_markdown, - clippy::elidable_lifetime_names, - clippy::use_self, - clippy::missing_errors_doc, - clippy::missing_panics_doc, - clippy::must_use_candidate, - clippy::return_self_not_must_use, - clippy::semicolon_if_nothing_returned, - clippy::or_fun_call, - clippy::missing_const_for_fn, - clippy::multiple_crate_versions, - clippy::too_many_lines, - clippy::option_if_let_else, - clippy::single_match_else, - clippy::uninlined_format_args, - clippy::cast_possible_truncation, - clippy::redundant_pub_crate, - clippy::redundant_clone, - clippy::items_after_statements, - clippy::if_not_else, - clippy::explicit_iter_loop, - clippy::enum_glob_use, - clippy::branches_sharing_code, - clippy::too_long_first_doc_paragraph, - clippy::useless_let_if_seq, - clippy::large_stack_frames, - clippy::match_same_arms, - clippy::similar_names, - clippy::single_char_pattern, - clippy::needless_pass_by_value -)] - /// Errors produced by this library pub mod error; pub use error::Error; @@ -178,21 +161,23 @@ pub mod kdf; /// Key views pub mod views; -#[allow(deprecated)] -pub use views::Views; pub use views::builder::{ AttrKind, CipherAttrKind, CipherKind, ConvKind, DataKind, DisclosureKind, FingerprintKind, KdfAttrKind, KdfKind, MerkleStateKind, OpenKind, SealKind, SignKind, ThresholdAttrKind, ThresholdKeyKind, ThresholdKind, Unselected, VerifyKind, ViewBuilder, ViewKind, }; +#[cfg(feature = "threshold")] pub use views::threshold_marker::{ self, MarkerView, ThresholdParticipant, ThresholdScheme, group_public_key, participants, set_group_public_key, set_participants, threshold_kind, threshold_params, }; pub use views::threshold_meta::{ self, DisclosureView, ThresholdDisclosure, ThresholdMetaCipher, ThresholdMetadata, - decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, read_threshold_params, - stamp_disclosure_attrs, + read_threshold_params, stamp_disclosure_attrs, +}; +#[cfg(feature = "chacha20")] +pub use views::threshold_meta::{ + decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, }; pub use views::{ AttrView, CipherAttrView, CipherView, ConvView, DataView, FingerprintView, KdfAttrView, @@ -201,6 +186,7 @@ pub use views::{ }; /// Key splitting / recombination (verifiable threshold shares) +#[cfg(feature = "threshold")] pub mod keysplit; /// Multikey type and functions @@ -231,6 +217,7 @@ pub mod serde; /// Commonly used items /// /// ``` +/// # #[cfg(feature = "curve25519")] { /// use multi_key::prelude::*; /// /// let mut rng = rand::rng(); @@ -238,6 +225,7 @@ pub mod serde; /// .unwrap() /// .try_build() /// .unwrap(); +/// # } /// ``` pub mod prelude { pub use super::*; diff --git a/src/mk.rs b/src/mk.rs index bfcb352..e733b5f 100644 --- a/src/mk.rs +++ b/src/mk.rs @@ -1,39 +1,59 @@ // SPDX-License-Identifier: Apache-2.0 -#[allow(deprecated)] -use crate::views::Views; +#[cfg(feature = "lamport")] +use crate::error::ThresholdError; +#[cfg(feature = "bls12-381")] +use crate::views::bls12381; +#[cfg(feature = "fn-dsa")] +use crate::views::fn_dsa; +#[cfg(feature = "frodokem")] +use crate::views::frodokem; #[cfg(feature = "lamport")] use crate::views::lamport; #[cfg(feature = "lamport")] use crate::views::lamport_merkle; +#[cfg(feature = "ml-dsa")] +use crate::views::ml_dsa; +#[cfg(feature = "rsa")] +use crate::views::rsa; +#[cfg(feature = "secp256k1")] +use crate::views::secp256k1; +#[cfg(feature = "slh-dsa")] +use crate::views::slh_dsa; +#[cfg(all(feature = "curve25519", feature = "frodokem"))] +use crate::views::x25519_frodokem640; #[cfg(feature = "xmss")] use crate::views::xmss; -use crate::{ - AttrId, AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, KdfView, MerkleStateView, OpenView, SealView, SignView, ThresholdAttrView, - ThresholdDisclosureView, ThresholdKeyView, ThresholdView, VerifyView, - error::{ConversionsError, ThresholdError}, - views::{ - bls12381, fn_dsa, frodokem, ml_dsa, rsa, secp256k1, slh_dsa, threshold_meta, - x25519_frodokem640, - }, -}; +use crate::{AttrId, Error, error::ConversionsError, views::threshold_meta}; +#[cfg(feature = "fn-dsa")] use ::fn_dsa::{ FN_DSA_LOGN_512, FN_DSA_LOGN_1024, KeyPairGenerator, KeyPairGeneratorStandard, sign_key_size, vrfy_key_size, }; +#[cfg(feature = "secp256k1")] use elliptic_curve::Generate; +#[cfg(feature = "p521")] use elliptic_curve::sec1::ToSec1Point; use multi_base::Base; use multi_codec::Codec; -use multi_trait::{EncodeInto, Null, TryDecodeFrom}; +use multi_trait::{EncodeInto, EncodeIntoBuffer, Null, TryDecodeFrom}; use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, Varuint}; use rand_core::CryptoRng; -use ssh_key::{ - EcdsaCurve, PrivateKey, PublicKey, - private::{EcdsaKeypair, KeypairData}, - public::{EcdsaPublicKey, KeyData}, -}; +#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] +use ssh_key::{EcdsaCurve, private::EcdsaKeypair, public::EcdsaPublicKey}; +use ssh_key::{PrivateKey, PublicKey}; +#[cfg(any( + feature = "bls12-381", + feature = "curve25519", + feature = "fn-dsa", + feature = "ml-dsa", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1", + feature = "slh-dsa" +))] +use ssh_key::{private::KeypairData, public::KeyData}; use std::{collections::BTreeMap, fmt}; use subtle::ConstantTimeEq; use zeroize::Zeroizing; @@ -96,13 +116,14 @@ pub const SNTRUP_KEY_CODECS: [Codec; 5] = [ /// the list of Classic McEliece key codecs supported for key generation /// -/// This constant is deprecated. Key-recovery attacks now solve the TII -/// McEliece challenges; see . -/// The McEliece views compile only with the `deprecated` feature. +/// This constant is deprecated: 2026 structural key-recovery attacks on +/// binary Goppa codes led Germany's BSI to advise against Classic McEliece in +/// new developments. See the crate-level Deprecations section. The McEliece +/// views compile only with the `deprecated` feature. #[cfg(feature = "deprecated")] #[deprecated( since = "2.0.0", - note = "Classic McEliece key recovery attacks: see https://github.com/mjosaarinen/tii-solved" + note = "Classic McEliece is weakened by 2026 key-recovery attacks and BSI advises against new use; use ML-KEM or FrodoKEM" )] #[allow(deprecated)] pub const MCELIECE_KEY_CODECS: [Codec; 1] = [Codec::Mceliece348864Priv]; @@ -282,32 +303,50 @@ impl EncodingInfo for Multikey { impl ConstantTimeEq for Multikey { fn ct_eq(&self, other: &Self) -> subtle::Choice { // Compare the canonical wire encoding byte-for-byte in constant time. - let a: Vec = self.clone().into(); - let b: Vec = other.clone().into(); + // the encodings carry secret key bytes, so wipe them when done + let a = Zeroizing::new(Vec::::from(self)); + let b = Zeroizing::new(Vec::::from(other)); a.ct_eq(&b) } } impl From for Vec { fn from(mk: Multikey) -> Self { - let mut v = Vec::default(); + Self::from(&mk) + } +} + +/// Encode a borrowed multikey without cloning its comment or attributes +impl From<&Multikey> for Vec { + fn from(mk: &Multikey) -> Self { + // the payloads plus a generous allowance for the varint prefixes + let payload: usize = mk.attributes.values().map(|a| a.len() + 12).sum(); + let mut v = Self::with_capacity(32 + mk.comment.len() + payload); + // varints are written straight into `v`, with no per-field allocation // add in the sigil - v.append(&mut SIGIL.into()); + u64::from(SIGIL).encode_into_buffer(&mut v); // add in the key codec - v.append(&mut mk.codec.into()); + u64::from(mk.codec).encode_into_buffer(&mut v); // add in the comment - v.append(&mut Varbytes::new(mk.comment.as_bytes().to_vec()).into()); + encode_varbytes_into(&mut v, mk.comment.as_bytes()); // add in the number of codec-specific attributes - v.append(&mut Varuint(mk.attributes.len()).into()); + mk.attributes.len().encode_into_buffer(&mut v); // add in the codec-specific attributes - mk.attributes.iter().for_each(|(id, attr)| { - v.append(&mut (*id).into()); - v.append(&mut Varbytes::new(attr.to_vec()).into()); - }); + for (id, attr) in &mk.attributes { + u8::from(*id).encode_into_buffer(&mut v); + encode_varbytes_into(&mut v, attr); + } v } } +/// Append `data` in the `Varbytes` wire form (varint length, then the +/// bytes) without first copying it into a `Varbytes` +pub(crate) fn encode_varbytes_into(out: &mut Vec, data: &[u8]) { + data.len().encode_into_buffer(out); + out.extend_from_slice(data); +} + impl<'a> TryFrom<&'a [u8]> for Multikey { type Error = Error; @@ -353,7 +392,7 @@ impl<'a> TryDecodeFrom<'a> for Multikey { // Per-attribute size is already capped by Varbytes' // MAX_DECODED_SIZE (16 MiB). The total decoded-size cap // below provides a second layer of protection. - if attributes.insert(id, (*attr).clone().into()).is_some() { + if attributes.insert(id, attr.to_inner().into()).is_some() { return Err(Error::DuplicateAttribute(id.code())); } // Enforce total decoded size cap @@ -405,94 +444,6 @@ impl fmt::Debug for Multikey { } } -#[allow(deprecated)] -impl Views for Multikey { - /// Provide a read-only view of the basic attributes in the viewed Multikey - fn attr_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_attr_view(self) - } - - /// Provide a read-only view of the cipher attributes in the viewed Multikey - fn cipher_attr_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_cipher_attr_view(self) - } - - /// Provide a read-only view to key data in the viewed Multikey - fn data_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_data_view(self) - } - - /// Provide a read-only view of the kdf attributes in the viewed Multikey - fn kdf_attr_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_kdf_attr_view(self) - } - - /// Provide a read-only view of the threshold attributes in the viewed Multikey - fn threshold_attr_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_threshold_attr_view(self) - } - - /// Provide a read-only view of higher-level threshold key metadata in the viewed Multikey - fn threshold_key_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_threshold_key_view(self) - } - - /// Provide an interface to do encryption/decryption of the viewed Multikey - fn cipher_view<'a>(&'a self, cipher: &'a Multikey) -> Result, Error> { - crate::views::dispatch::dispatch_cipher_view(self, cipher) - } - - /// Provide an interface to do key conversions from the viewe Multikey - fn conv_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_conv_view(self) - } - - /// Provide an interface to do key conversions from the viewe Multikey - fn fingerprint_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_fingerprint_view(self) - } - - /// Provide an interface to do kdf operations from the viewed Multikey - fn kdf_view<'a>(&'a self, kdf: &'a Multikey) -> Result, Error> { - crate::views::dispatch::dispatch_kdf_view(self, kdf) - } - - /// Provide an interface to seal (encrypt) plaintext - fn seal_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_seal_view(self) - } - - /// Provide an interface to open (decrypt) sealed data - fn open_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_open_view(self) - } - - /// Provide an interface to sign a message and return a Multisig - fn sign_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_sign_view(self) - } - - /// Provide an interface to do threshold operations on the Multikey - fn threshold_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_threshold_view(self) - } - - /// Provide an interface to verify a Multisig and optional message - fn verify_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_verify_view(self) - } - - /// Provide an interface for threshold disclosure mode operations - fn disclosure_view<'a>(&'a self) -> Result, Error> { - Ok(crate::views::dispatch::dispatch_disclosure_view(self)) - } - - /// Provide an interface for merkle-tree state introspection - fn merkle_state_view<'a>(&'a self) -> Result, Error> { - crate::views::dispatch::dispatch_merkle_state_view(self) - } -} - /// Multikey builder constructs private keys only. If you need a public key you /// must first generate a priate key and then get the public key from that. #[derive(Clone, Default)] @@ -515,30 +466,42 @@ impl Builder { /// new builder from random bytes source pub fn new_from_random_bytes(codec: Codec, rng: &mut impl CryptoRng) -> Result { - let key_bytes = match codec { - Codec::Ed25519Priv => ed25519_dalek::SigningKey::generate(rng).to_bytes().to_vec(), - Codec::P256Priv => EcdsaKeypair::random(rng, EcdsaCurve::NistP256) + // the tuple carries the parameters into the arms, so a build whose features + // gate every arm out still uses them + let key_bytes: Vec = match (codec, rng) { + #[cfg(feature = "curve25519")] + (Codec::Ed25519Priv, rng) => { + ed25519_dalek::SigningKey::generate(rng).to_bytes().to_vec() + } + #[cfg(feature = "p256")] + (Codec::P256Priv, rng) => EcdsaKeypair::random(rng, EcdsaCurve::NistP256) .map_err(|e| ConversionsError::Ssh(e.into()))? .private_key_bytes() .to_vec(), - Codec::P384Priv => EcdsaKeypair::random(rng, EcdsaCurve::NistP384) + #[cfg(feature = "p384")] + (Codec::P384Priv, rng) => EcdsaKeypair::random(rng, EcdsaCurve::NistP384) .map_err(|e| ConversionsError::Ssh(e.into()))? .private_key_bytes() .to_vec(), - Codec::P521Priv => EcdsaKeypair::random(rng, EcdsaCurve::NistP521) + #[cfg(feature = "p521")] + (Codec::P521Priv, rng) => EcdsaKeypair::random(rng, EcdsaCurve::NistP521) .map_err(|e| ConversionsError::Ssh(e.into()))? .private_key_bytes() .to_vec(), - Codec::Secp256K1Priv => k256::SecretKey::generate_from_rng(rng).to_bytes().to_vec(), - Codec::Bls12381G1Priv => blsful::Bls12381G1::new_secret_key() - .to_be_bytes() - .as_slice() - .to_vec(), - Codec::Bls12381G2Priv => blsful::Bls12381G2::new_secret_key() - .to_be_bytes() - .as_slice() - .to_vec(), - Codec::FnDsa512Priv => { + #[cfg(feature = "secp256k1")] + (Codec::Secp256K1Priv, rng) => { + k256::SecretKey::generate_from_rng(rng).to_bytes().to_vec() + } + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G1Priv, _) => { + blsful::Bls12381G1::new_secret_key().to_be_bytes().to_vec() + } + #[cfg(feature = "bls12-381")] + (Codec::Bls12381G2Priv, _) => { + blsful::Bls12381G2::new_secret_key().to_be_bytes().to_vec() + } + #[cfg(feature = "fn-dsa")] + (Codec::FnDsa512Priv, _) => { let mut kg = KeyPairGeneratorStandard::default(); let mut sign_key = vec![0u8; sign_key_size(FN_DSA_LOGN_512)]; let mut vrfy_key = vec![0u8; vrfy_key_size(FN_DSA_LOGN_512)]; @@ -550,7 +513,8 @@ impl Builder { ); sign_key } - Codec::FnDsa1024Priv => { + #[cfg(feature = "fn-dsa")] + (Codec::FnDsa1024Priv, _) => { let mut kg = KeyPairGeneratorStandard::default(); let mut sign_key = vec![0u8; sign_key_size(FN_DSA_LOGN_1024)]; let mut vrfy_key = vec![0u8; vrfy_key_size(FN_DSA_LOGN_1024)]; @@ -562,96 +526,116 @@ impl Builder { ); sign_key } - Codec::MlDsa65Priv | Codec::MlDsa87Priv => { + #[cfg(feature = "ml-dsa")] + (Codec::MlDsa65Priv | Codec::MlDsa87Priv, rng) => { let mut seed = [0u8; 32]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Mayo1Priv | Codec::Mayo2Priv => { + #[cfg(feature = "mayo")] + (Codec::Mayo1Priv | Codec::Mayo2Priv, rng) => { let mut seed = [0u8; 24]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Mayo3Priv => { + #[cfg(feature = "mayo")] + (Codec::Mayo3Priv, rng) => { let mut seed = [0u8; 32]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Mayo5Priv => { + #[cfg(feature = "mayo")] + (Codec::Mayo5Priv, rng) => { let mut seed = [0u8; 40]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Mlkem768Priv | Codec::Mlkem1024Priv => { + #[cfg(feature = "ml-kem")] + (Codec::Mlkem768Priv | Codec::Mlkem1024Priv, rng) => { // 64-byte seed: d (32) || z (32) let mut seed = [0u8; 64]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Sntrup761Priv - | Codec::Sntrup857Priv - | Codec::Sntrup953Priv - | Codec::Sntrup1013Priv - | Codec::Sntrup1277Priv => { + #[cfg(feature = "sntrup")] + ( + Codec::Sntrup761Priv + | Codec::Sntrup857Priv + | Codec::Sntrup953Priv + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Priv, + rng, + ) => { let mut seed = [0u8; 32]; rng.fill_bytes(&mut seed); seed.to_vec() } #[cfg(feature = "deprecated")] #[allow(deprecated)] - Codec::Mceliece348864Priv => { + (Codec::Mceliece348864Priv, rng) => { let mut seed = [0u8; 32]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::FrodoKem640AesPriv - | Codec::FrodoKem976AesPriv - | Codec::FrodoKem1344AesPriv - | Codec::FrodoKem640ShakePriv - | Codec::FrodoKem976ShakePriv - | Codec::FrodoKem1344ShakePriv => frodokem::generate_private_key(codec)?.to_vec(), - Codec::X25519Priv => { + #[cfg(feature = "frodokem")] + ( + Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePriv, + _, + ) => frodokem::generate_private_key(codec)?.to_vec(), + #[cfg(feature = "curve25519")] + (Codec::X25519Priv, rng) => { let mut secret = [0u8; 32]; rng.fill_bytes(&mut secret); secret.to_vec() } - Codec::X25519Sntrup761Priv => { + #[cfg(all(feature = "curve25519", feature = "sntrup"))] + (Codec::X25519Sntrup761Priv, rng) => { // x25519_seed (32) || sntrup761_seed (32) = 64 bytes let mut seed = [0u8; 64]; rng.fill_bytes(&mut seed); seed.to_vec() } - #[cfg(feature = "deprecated")] + #[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] - Codec::X25519Mceliece348864Priv => { + (Codec::X25519Mceliece348864Priv, rng) => { // x25519_seed (32) || mceliece_seed (32) = 64 bytes let mut seed = [0u8; 64]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePriv => { + #[cfg(all(feature = "curve25519", feature = "frodokem"))] + (Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePriv, _) => { // x25519_seed (32) || frodokem full secret key x25519_frodokem640::generate_private_key(codec) } - Codec::X25519Mlkem768Priv => { + #[cfg(all(feature = "curve25519", feature = "ml-kem"))] + (Codec::X25519Mlkem768Priv, rng) => { // x25519_seed (32) || ml_kem_d (32) || ml_kem_z (32) = 96 bytes let mut seed = [0u8; 96]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Ed25519Mayo2Priv => { + #[cfg(all(feature = "curve25519", feature = "mayo"))] + (Codec::Ed25519Mayo2Priv, rng) => { // ed25519_seed (32) || mayo2_seed (24) = 56 bytes let mut seed = [0u8; 56]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Ed25519Mldsa65Priv => { + #[cfg(all(feature = "curve25519", feature = "ml-dsa"))] + (Codec::Ed25519Mldsa65Priv, rng) => { // ed25519_seed (32) || ml_dsa_seed (32) = 64 bytes let mut seed = [0u8; 64]; rng.fill_bytes(&mut seed); seed.to_vec() } - Codec::Bls12381G1Mldsa65Priv => { + #[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] + (Codec::Bls12381G1Mldsa65Priv, rng) => { // bls_g1_secret (32) || ml_dsa_65_seed (32) = 64 bytes let mut out = Vec::with_capacity(64); out.extend_from_slice(&blsful::Bls12381G1::new_secret_key().to_be_bytes()); @@ -660,7 +644,8 @@ impl Builder { out.extend_from_slice(&mldsa_seed); out } - Codec::Bls12381G1Fndsa512Priv => { + #[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] + (Codec::Bls12381G1Fndsa512Priv, _) => { // bls_g1_secret (32) || fn_dsa_512_signing_key let mut out = Vec::new(); out.extend_from_slice(&blsful::Bls12381G1::new_secret_key().to_be_bytes()); @@ -676,7 +661,8 @@ impl Builder { out.extend_from_slice(&sign_key); out } - Codec::Bls12381G1Mayo1Priv | Codec::Bls12381G1Mayo2Priv => { + #[cfg(all(feature = "bls12-381", feature = "mayo"))] + (Codec::Bls12381G1Mayo1Priv | Codec::Bls12381G1Mayo2Priv, rng) => { // bls_g1_secret (32) || mayo_seed (24) = 56 bytes let mut out = Vec::with_capacity(56); out.extend_from_slice(&blsful::Bls12381G1::new_secret_key().to_be_bytes()); @@ -685,7 +671,8 @@ impl Builder { out.extend_from_slice(&mayo_seed); out } - Codec::Ed25519Fndsa512Priv => { + #[cfg(all(feature = "curve25519", feature = "fn-dsa"))] + (Codec::Ed25519Fndsa512Priv, rng) => { // ed25519_seed (32) || fn_dsa_512_signing_key let mut ed_seed = [0u8; 32]; rng.fill_bytes(&mut ed_seed); @@ -703,7 +690,8 @@ impl Builder { out.extend_from_slice(&sign_key); out } - Codec::Rsa2048Priv => { + #[cfg(feature = "rsa")] + (Codec::Rsa2048Priv, _) => { // Probe the OS RNG up front so an entropy failure surfaces as an // error instead of a panic inside UnwrapErr during keygen. rsa::OsRng::check_entropy() @@ -716,7 +704,8 @@ impl Builder { .as_bytes() .to_vec() } - Codec::Rsa3072Priv => { + #[cfg(feature = "rsa")] + (Codec::Rsa3072Priv, _) => { rsa::OsRng::check_entropy() .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; let key = @@ -727,7 +716,8 @@ impl Builder { .as_bytes() .to_vec() } - Codec::Rsa4096Priv => { + #[cfg(feature = "rsa")] + (Codec::Rsa4096Priv, _) => { rsa::OsRng::check_entropy() .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; let key = @@ -738,49 +728,68 @@ impl Builder { .as_bytes() .to_vec() } - Codec::SlhDsaSha2128FPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_128f>(), - Codec::SlhDsaSha2128SPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_128s>(), - Codec::SlhDsaSha2192FPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_192f>(), - Codec::SlhDsaSha2192SPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_192s>(), - Codec::SlhDsaSha2256FPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_256f>(), - Codec::SlhDsaSha2256SPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_256s>(), - Codec::SlhDsaShake128FPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake128f>(), - Codec::SlhDsaShake128SPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake128s>(), - Codec::SlhDsaShake192FPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake192f>(), - Codec::SlhDsaShake192SPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake192s>(), - Codec::SlhDsaShake256FPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake256f>(), - Codec::SlhDsaShake256SPriv => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake256s>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaSha2128FPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_128f>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaSha2128SPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_128s>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaSha2192FPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_192f>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaSha2192SPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_192s>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaSha2256FPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_256f>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaSha2256SPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Sha2_256s>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaShake128FPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake128f>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaShake128SPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake128s>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaShake192FPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake192f>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaShake192SPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake192s>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaShake256FPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake256f>(), + #[cfg(feature = "slh-dsa")] + (Codec::SlhDsaShake256SPriv, _) => slh_dsa::gen_slh_dsa_key::<::slh_dsa::Shake256s>(), #[cfg(feature = "lamport")] - Codec::LamportSha3256Priv - | Codec::LamportSha3384Priv - | Codec::LamportSha3512Priv - | Codec::LamportSha2256Priv - | Codec::LamportSha2384Priv - | Codec::LamportSha2512Priv - | Codec::LamportBlake2B512Priv - | Codec::LamportBlake2S256Priv - | Codec::LamportBlake3256Priv - | Codec::LamportShake128Priv - | Codec::LamportShake256Priv => lamport::generate_private_key(codec)?.to_vec(), + ( + Codec::LamportSha3256Priv + | Codec::LamportSha3384Priv + | Codec::LamportSha3512Priv + | Codec::LamportSha2256Priv + | Codec::LamportSha2384Priv + | Codec::LamportSha2512Priv + | Codec::LamportBlake2B512Priv + | Codec::LamportBlake2S256Priv + | Codec::LamportBlake3256Priv + | Codec::LamportShake128Priv + | Codec::LamportShake256Priv, + _, + ) => lamport::generate_private_key(codec)?.to_vec(), #[cfg(feature = "lamport")] - Codec::LamportMerkleSha3256Priv - | Codec::LamportMerkleSha3384Priv - | Codec::LamportMerkleSha3512Priv - | Codec::LamportMerkleSha2256Priv - | Codec::LamportMerkleSha2384Priv - | Codec::LamportMerkleSha2512Priv - | Codec::LamportMerkleBlake2B512Priv - | Codec::LamportMerkleBlake2S256Priv - | Codec::LamportMerkleBlake3256Priv - | Codec::LamportMerkleShake128Priv - | Codec::LamportMerkleShake256Priv => { - lamport_merkle::generate_private_key_with_depth(codec, 1)?.to_vec() - } + ( + Codec::LamportMerkleSha3256Priv + | Codec::LamportMerkleSha3384Priv + | Codec::LamportMerkleSha3512Priv + | Codec::LamportMerkleSha2256Priv + | Codec::LamportMerkleSha2384Priv + | Codec::LamportMerkleSha2512Priv + | Codec::LamportMerkleBlake2B512Priv + | Codec::LamportMerkleBlake2S256Priv + | Codec::LamportMerkleBlake3256Priv + | Codec::LamportMerkleShake128Priv + | Codec::LamportMerkleShake256Priv, + _, + ) => lamport_merkle::generate_private_key_with_depth(codec, 1)?.to_vec(), #[cfg(feature = "xmss")] - Codec::XmssSha210256Priv | Codec::XmssSha216256Priv | Codec::XmssSha220256Priv => { + (Codec::XmssSha210256Priv | Codec::XmssSha216256Priv | Codec::XmssSha220256Priv, _) => { xmss::generate_private_key(codec)?.to_vec() } - _ => return Err(ConversionsError::UnsupportedCodec(codec).into()), + // `Err(..)?` rather than `return Err(..)`: when no key feature is + // enabled this is the only arm, and a diverging `return` would + // make everything after the match unreachable + _ => Err(ConversionsError::UnsupportedCodec(codec))?, }; let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); @@ -860,10 +869,12 @@ impl Builder { pub fn new_from_ssh_public_key(sshkey: &PublicKey) -> Result { use ssh_key::Algorithm::*; match sshkey.algorithm() { + #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] Ecdsa { curve } => { use EcdsaCurve::*; // SSH stores uncompressed SEC1 points; we store compressed internally let (key_bytes, codec) = match curve { + #[cfg(feature = "p256")] NistP256 => { if let KeyData::Ecdsa(EcdsaPublicKey::NistP256(point)) = sshkey.key_data() { let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(point.as_bytes()) @@ -878,6 +889,7 @@ impl Builder { .into()); } } + #[cfg(feature = "p384")] NistP384 => { if let KeyData::Ecdsa(EcdsaPublicKey::NistP384(point)) = sshkey.key_data() { let vk = p384::ecdsa::VerifyingKey::from_sec1_bytes(point.as_bytes()) @@ -892,6 +904,7 @@ impl Builder { .into()); } } + #[cfg(feature = "p521")] NistP521 => { if let KeyData::Ecdsa(EcdsaPublicKey::NistP521(point)) = sshkey.key_data() { let vk = p521::ecdsa::VerifyingKey::from_sec1_bytes(point.as_bytes()) @@ -907,6 +920,13 @@ impl Builder { .into()); } } + #[cfg(not(all(feature = "p256", feature = "p384", feature = "p521")))] + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()); + } }; let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); @@ -918,6 +938,7 @@ impl Builder { }) } Other(name) => match name.as_str() { + #[cfg(feature = "secp256k1")] secp256k1::ALGORITHM_NAME => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -937,6 +958,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G1 => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -956,6 +978,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G1_SHARE => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -982,6 +1005,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G2 => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1001,6 +1025,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G2_SHARE => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1028,6 +1053,7 @@ impl Builder { }) } // ML-DSA public keys + #[cfg(feature = "ml-dsa")] ml_dsa::ALGORITHM_NAME_65 => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1047,6 +1073,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "ml-dsa")] ml_dsa::ALGORITHM_NAME_87 => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1067,6 +1094,7 @@ impl Builder { }) } // FN-DSA public keys + #[cfg(feature = "fn-dsa")] fn_dsa::ALGORITHM_NAME_512 => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1086,6 +1114,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "fn-dsa")] fn_dsa::ALGORITHM_NAME_1024 => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1106,6 +1135,7 @@ impl Builder { }) } // SLH-DSA public keys + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_128F => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1125,6 +1155,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_128S => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1144,6 +1175,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_192F => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1163,6 +1195,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_192S => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1182,6 +1215,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_256F => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1201,6 +1235,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_256S => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1220,6 +1255,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_128F => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1239,6 +1275,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_128S => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1258,6 +1295,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_192F => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1277,6 +1315,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_192S => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1296,6 +1335,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_256F => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1315,6 +1355,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_256S => { let key_bytes = match sshkey.key_data() { KeyData::Other(pk) => pk.key.as_ref().to_vec(), @@ -1336,6 +1377,7 @@ impl Builder { } s => Err(ConversionsError::UnsupportedAlgorithm(s.to_string()).into()), }, + #[cfg(feature = "curve25519")] Ed25519 => { let key_bytes = match sshkey.key_data() { KeyData::Ed25519(e) => e.0.to_vec(), @@ -1363,9 +1405,11 @@ impl Builder { pub fn new_from_ssh_private_key(sshkey: &PrivateKey) -> Result { use ssh_key::Algorithm::*; match sshkey.algorithm() { + #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] Ecdsa { curve } => { use EcdsaCurve::*; let (key_bytes, codec) = match curve { + #[cfg(feature = "p256")] NistP256 => { if let KeypairData::Ecdsa(EcdsaKeypair::NistP256 { private, .. }) = sshkey.key_data() @@ -1378,6 +1422,7 @@ impl Builder { .into()); } } + #[cfg(feature = "p384")] NistP384 => { if let KeypairData::Ecdsa(EcdsaKeypair::NistP384 { private, .. }) = sshkey.key_data() @@ -1390,6 +1435,7 @@ impl Builder { .into()); } } + #[cfg(feature = "p521")] NistP521 => { if let KeypairData::Ecdsa(EcdsaKeypair::NistP521 { private, .. }) = sshkey.key_data() @@ -1402,6 +1448,13 @@ impl Builder { .into()); } } + #[cfg(not(all(feature = "p256", feature = "p384", feature = "p521")))] + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()); + } }; let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); @@ -1413,6 +1466,7 @@ impl Builder { }) } Other(name) => match name.as_str() { + #[cfg(feature = "secp256k1")] secp256k1::ALGORITHM_NAME => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1432,6 +1486,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G1 => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1451,6 +1506,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G1_SHARE => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1477,6 +1533,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G2 => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1496,6 +1553,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "bls12-381")] bls12381::ALGORITHM_NAME_G2_SHARE => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1523,6 +1581,7 @@ impl Builder { }) } // ML-DSA private keys + #[cfg(feature = "ml-dsa")] ml_dsa::ALGORITHM_NAME_65 => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1542,6 +1601,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "ml-dsa")] ml_dsa::ALGORITHM_NAME_87 => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1562,6 +1622,7 @@ impl Builder { }) } // FN-DSA private keys + #[cfg(feature = "fn-dsa")] fn_dsa::ALGORITHM_NAME_512 => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1581,6 +1642,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "fn-dsa")] fn_dsa::ALGORITHM_NAME_1024 => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1601,6 +1663,7 @@ impl Builder { }) } // SLH-DSA private keys + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_128F => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1620,6 +1683,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_128S => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1639,6 +1703,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_192F => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1658,6 +1723,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_192S => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1677,6 +1743,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_256F => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1696,6 +1763,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHA2_256S => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1715,6 +1783,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_128F => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1734,6 +1803,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_128S => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1753,6 +1823,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_192F => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1772,6 +1843,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_192S => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1791,6 +1863,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_256F => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1810,6 +1883,7 @@ impl Builder { ..Default::default() }) } + #[cfg(feature = "slh-dsa")] slh_dsa::ALGORITHM_NAME_SHAKE_256S => { let key_bytes = match sshkey.key_data() { KeypairData::Other(kp) => kp.private.as_ref().to_vec(), @@ -1831,6 +1905,7 @@ impl Builder { } s => Err(ConversionsError::UnsupportedAlgorithm(s.to_string()).into()), }, + #[cfg(feature = "curve25519")] Ed25519 => { let key_bytes = match sshkey.key_data() { KeypairData::Ed25519(e) => e.private.to_bytes().to_vec(), @@ -1858,8 +1933,11 @@ impl Builder { /// /// Currently only supports [Codec::Ed25519Priv] seeds. pub fn new_from_seed(codec: Codec, seed: &[u8]) -> Result { - match codec { - Codec::Ed25519Priv => { + // the tuple carries the parameters into the arms, so a build whose features + // gate every arm out still uses them + match (codec, seed) { + #[cfg(feature = "curve25519")] + (Codec::Ed25519Priv, seed) => { let keypair = ssh_key::private::Ed25519Keypair::from_seed(seed.try_into().map_err(|_| { ConversionsError::UnsupportedAlgorithm( @@ -1870,7 +1948,7 @@ impl Builder { .map_err(|e| ConversionsError::Ssh(e.into()))?; Self::new_from_ssh_private_key(&private_key) } - _ => Err(ConversionsError::UnsupportedCodec(codec).into()), + (codec, _) => Err(ConversionsError::UnsupportedCodec(codec).into()), } } @@ -1886,16 +1964,16 @@ impl Builder { self } - fn with_attribute(mut self, attr: AttrId, data: &Vec) -> Self { + fn with_attribute(mut self, attr: AttrId, data: Vec) -> Self { let mut attributes = self.attributes.unwrap_or_default(); - attributes.insert(attr, data.to_owned().into()); + attributes.insert(attr, data.into()); self.attributes = Some(attributes); self } /// add in the key bytes directly pub fn with_key_bytes(self, bytes: &(impl AsRef<[u8]> + ?Sized)) -> Self { - self.with_attribute(AttrId::KeyData, &bytes.as_ref().to_vec()) + self.with_attribute(AttrId::KeyData, bytes.as_ref().to_vec()) } /// Set the algorithm name attribute of a custom protocol key. @@ -1910,7 +1988,7 @@ impl Builder { /// The name is stored as given and the empty string is accepted: /// validation of custom-key semantics is future spec work. pub fn with_algorithm_name(self, name: &str) -> Self { - self.with_attribute(AttrId::AlgorithmName, &name.as_bytes().to_vec()) + self.with_attribute(AttrId::AlgorithmName, name.as_bytes().to_vec()) } /// Set the key type attribute of a custom protocol key. @@ -1922,32 +2000,32 @@ impl Builder { /// The byte is stored unvalidated: validation of custom-key semantics is /// future spec work. pub fn with_key_type(self, key_type: u8) -> Self { - self.with_attribute(AttrId::KeyType, &vec![key_type]) + self.with_attribute(AttrId::KeyType, vec![key_type]) } /// add in the merkle-tree depth value (one raw byte, 1..=3) pub fn with_depth(self, depth: u8) -> Self { - self.with_attribute(AttrId::Depth, &vec![depth]) + self.with_attribute(AttrId::Depth, vec![depth]) } /// add in the threshold value pub fn with_threshold(self, threshold: usize) -> Self { - self.with_attribute(AttrId::Threshold, &Varuint(threshold).into()) + self.with_attribute(AttrId::Threshold, Varuint(threshold).into()) } /// add in the limit value pub fn with_limit(self, limit: usize) -> Self { - self.with_attribute(AttrId::Limit, &Varuint(limit).into()) + self.with_attribute(AttrId::Limit, Varuint(limit).into()) } /// add in the share identifier value pub fn with_identifier(self, identifier: &impl AsRef<[u8]>) -> Self { - self.with_attribute(AttrId::ShareIdentifier, &identifier.as_ref().to_vec()) + self.with_attribute(AttrId::ShareIdentifier, identifier.as_ref().to_vec()) } /// add in the threshold data pub fn with_threshold_data(self, tdata: &impl AsRef<[u8]>) -> Self { - self.with_attribute(AttrId::ThresholdData, &tdata.as_ref().to_vec()) + self.with_attribute(AttrId::ThresholdData, tdata.as_ref().to_vec()) } /// Set the disclosure mode for a threshold share being built. @@ -1955,9 +2033,9 @@ impl Builder { /// In `ThresholdDisclosure::Full` mode, t and n are stored as plaintext /// attributes (same as `with_threshold()`/`with_limit()`). /// In `ThresholdDisclosure::Partial` mode, t is encrypted and n is plaintext. - /// In `ThresholdDisclosure::FullConfidentialial` mode, both t and n are encrypted. + /// In `ThresholdDisclosure::FullConfidential` mode, both t and n are encrypted. /// - /// `meta_key` is required for Partial/FullConfidentialial modes. + /// `meta_key` is required for Partial/FullConfidential modes. pub fn with_disclosure( self, mode: threshold_meta::ThresholdDisclosure, @@ -2059,289 +2137,48 @@ impl Builder { #[cfg(test)] mod tests { use super::*; - use crate::views::{bcrypt, chacha20}; - use crate::{ViewBuilder, cipher, kdf}; - use multi_sig::EncodedMultisig; - use ssh_key::private::Ed25519Keypair; - - #[test] - fn test_random() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let mk = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_comment("test key") - .try_build() - .unwrap(); - let (vpk, epk) = { - let conv = ViewBuilder::new(&mk).conv().build().unwrap(); - let pk = conv.to_public_key().unwrap(); - (Into::>::into(pk.clone()), EncodedMultikey::from(pk)) - }; - println!("encoded pubkey: {}: {}", codec, epk); - println!("encoded pubkey v: {}: {}", codec, hex::encode(vpk)); - println!( - "encoded privkey: {}: {}", - codec, - EncodedMultikey::from(mk.clone()) - ); - println!( - "encoded privkey v: {}: {}", - codec, - hex::encode(Into::>::into(mk.clone())) - ); - let _v: Vec = mk.into(); - } - } - - #[test] - fn test_encoded_random() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let mk = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_base_encoding(Base::Base32Lower) - .with_comment("test key") - .try_build_encoded() - .unwrap(); - let s = mk.to_string(); - //println!("encoded privkey: {}: {}", codec, s); - assert_eq!(mk, EncodedMultikey::try_from(s.as_str()).unwrap()); - } - } - - #[test] - fn test_random_public_ssh_key_roundtrip() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let mk = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_comment("test key") - .try_build() - .unwrap(); - let conv = ViewBuilder::new(&mk).conv().build().unwrap(); - let pk = conv.to_public_key().unwrap(); - let ssh_key = conv.to_ssh_public_key().unwrap(); - let mk2 = Builder::new_from_ssh_public_key(&ssh_key) - .unwrap() - .try_build() - .unwrap(); - assert_eq!(pk, mk2); - } - } - - #[test] - fn test_random_private_ssh_key_roundtrip() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let mk = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_comment("test key") - .try_build() - .unwrap(); - let conv = ViewBuilder::new(&mk).conv().build().unwrap(); - let ssh_key = conv.to_ssh_private_key().unwrap(); - let mk2 = Builder::new_from_ssh_private_key(&ssh_key) - .unwrap() - .try_build() - .unwrap(); - assert_eq!(mk, mk2); - } - } - - #[test] - fn test_ssh_key_roundtrip() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let sk1 = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_comment("test key") - .try_build() - .unwrap(); - let cv = ViewBuilder::new(&sk1).conv().build().unwrap(); - let public_key = cv.to_ssh_public_key().unwrap(); - let private_key = cv.to_ssh_private_key().unwrap(); - - let pk1 = cv.to_public_key().unwrap(); - let cv = ViewBuilder::new(&pk1).conv().build().unwrap(); - assert_eq!(public_key, cv.to_ssh_public_key().unwrap()); - - let sk2 = Builder::new_from_ssh_private_key(&private_key) - .unwrap() - .try_build() - .unwrap(); - assert_eq!(sk1, sk2); - let pk2 = Builder::new_from_ssh_public_key(&public_key) - .unwrap() - .try_build() - .unwrap(); - assert_eq!(pk1, pk2); - } - } - - #[test] - fn test_encryption_roundtrip() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let mk1 = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_comment("test key") - .try_build() - .unwrap(); - - let attr = ViewBuilder::new(&mk1).attr().build().unwrap(); - assert!(!attr.is_encrypted()); - assert!(!attr.is_public_key()); - assert!(attr.is_secret_key()); - let kd = ViewBuilder::new(&mk1).data().build().unwrap(); - assert!(kd.key_bytes().is_ok()); - assert!(kd.secret_bytes().is_ok()); - - let mk2 = { - let kdfmk = kdf::Builder::new(Codec::BcryptPbkdf) - .with_random_salt(bcrypt::SALT_LENGTH, &mut rng) - .with_rounds(10) - .try_build() - .unwrap(); - let ciphermk = cipher::Builder::new(Codec::Chacha20Poly1305) - .with_random_nonce(chacha20::nonce_length(), &mut rng) - .try_build() - .unwrap(); - // get the kdf view on the cipher multikey so we can generate a - // new cipher multikey with the same parameters and the generated key - let kdf = ViewBuilder::new(&ciphermk).kdf(&kdfmk).build().unwrap(); - // derive a key from the passphrase and add it to the cipher multikey - let ciphermk = kdf - .derive_key(b"for great justice, move every zig!") - .unwrap(); - // get the cipher view on the unencrypted ed25519 secret key so - // that we can create a new ed25519 secret key with an encrypted - // key and the kdf and cipher attributes and data - let cipher = ViewBuilder::new(&mk1).cipher(&ciphermk).build().unwrap(); - // encrypt the multikey using the cipher - - cipher.encrypt().unwrap() - }; - - let attr = ViewBuilder::new(&mk2).attr().build().unwrap(); - assert!(attr.is_encrypted()); - assert!(!attr.is_public_key()); - assert!(attr.is_secret_key()); - let kd = ViewBuilder::new(&mk2).data().build().unwrap(); - assert!(kd.key_bytes().is_ok()); - assert!(kd.secret_bytes().is_err()); // encrypted key - - let mk3 = { - let kdfmk = kdf::Builder::default() - .try_from_multikey(&mk2) - .unwrap() - .try_build() - .unwrap(); - let ciphermk = cipher::Builder::default() - .try_from_multikey(&mk2) - .unwrap() - .try_build() - .unwrap(); - // get the kdf view - let kdf = ViewBuilder::new(&ciphermk).kdf(&kdfmk).build().unwrap(); - // derive a key from the passphrase and add it to the cipher multikey - let ciphermk = kdf - .derive_key(b"for great justice, move every zig!") - .unwrap(); - // get the cipher view - let cipher = ViewBuilder::new(&mk2).cipher(&ciphermk).build().unwrap(); - // decrypt the multikey using the cipher - - cipher.decrypt().unwrap() - }; - - let attr = ViewBuilder::new(&mk3).attr().build().unwrap(); - assert!(!attr.is_encrypted()); - assert!(!attr.is_public_key()); - assert!(attr.is_secret_key()); - let kd = ViewBuilder::new(&mk3).data().build().unwrap(); - assert!(kd.key_bytes().is_ok()); - assert!(kd.secret_bytes().is_ok()); - - // ensure the round trip worked - assert_eq!(mk1, mk3); - } - } - - #[test] - fn test_signing_detached_roundtrip() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let mk = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_comment("test key") - .try_build() - .unwrap(); - - let attr = ViewBuilder::new(&mk).attr().build().unwrap(); - assert!(!attr.is_encrypted()); - assert!(!attr.is_public_key()); - assert!(attr.is_secret_key()); - let kd = ViewBuilder::new(&mk).data().build().unwrap(); - assert!(kd.key_bytes().is_ok()); - assert!(kd.secret_bytes().is_ok()); - let conv = ViewBuilder::new(&mk).conv().build().unwrap(); - let pk = EncodedMultikey::new(Base::Base16Lower, conv.to_public_key().unwrap()); - println!("{} pubkey: {}", codec, pk); - - let msg = b"for great justice, move every zig!"; - - let signmk = ViewBuilder::new(&mk).sign().build().unwrap(); - let signature = if codec == Codec::Bls12381G1Priv || codec == Codec::Bls12381G2Priv { - signmk.sign(msg.as_slice(), false, Some(2_u8)).unwrap() - } else { - signmk.sign(msg.as_slice(), false, None).unwrap() - }; - let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); - println!("signaure: {}", sig); - - let verifymk = ViewBuilder::new(&mk).verify().build().unwrap(); - assert!(verifymk.verify(&signature, Some(msg.as_slice())).is_ok()); - } - } + /// the allocation-free encoder produces exactly the bytes of the + /// field-by-field `Vec` encoding, including multi-byte varint lengths #[test] - fn test_signing_merged_roundtrip() { - for codec in KEY_CODECS { - let mut rng = rand::rng(); - let mk = Builder::new_from_random_bytes(codec, &mut rng) - .unwrap() - .with_comment("test key") - .try_build() - .unwrap(); - - let attr = ViewBuilder::new(&mk).attr().build().unwrap(); - assert!(!attr.is_encrypted()); - assert!(!attr.is_public_key()); - assert!(attr.is_secret_key()); - let kd = ViewBuilder::new(&mk).data().build().unwrap(); - assert!(kd.key_bytes().is_ok()); - assert!(kd.secret_bytes().is_ok()); - - let msg = - hex::decode("8bb78be51ac7cc98f44e38947ff8a128764ec039b89687a790dfa8444ba97682") - .unwrap(); - - let signmk = ViewBuilder::new(&mk).sign().build().unwrap(); - let signature = if codec == Codec::Bls12381G1Priv || codec == Codec::Bls12381G2Priv { - signmk.sign(&msg, true, Some(2_u8)).unwrap() - } else { - signmk.sign(&msg, true, None).unwrap() - }; - - // make sure the message is stored correctly in the signature - assert_eq!(signature.message, msg); - - let verifymk = ViewBuilder::new(&mk).verify().build().unwrap(); - assert!(verifymk.verify(&signature, None).is_ok()); + fn test_encode_matches_reference_layout() { + let comment = "c".repeat(200); + let mk = Builder::new(Codec::Identity) + .with_comment(&comment) + .with_key_bytes(&[7u8; 300]) + .with_algorithm_name("reference-layout") + .try_build() + .unwrap(); + let mut want: Vec = SIGIL.into(); + want.extend(Vec::::from(mk.codec)); + want.extend(Vec::::from(Varbytes::new(comment.into_bytes()))); + want.extend(Vec::::from(Varuint(mk.attributes.len()))); + for (id, attr) in &mk.attributes { + want.extend(Vec::::from(*id)); + want.extend(Vec::::from(Varbytes::new(attr.to_vec()))); } - } + assert_eq!(Vec::::from(&mk), want); + assert_eq!(Multikey::try_from(want.as_slice()).unwrap(), mk); + } + #[cfg(any( + feature = "bls12-381", + feature = "curve25519", + feature = "fn-dsa", + feature = "ml-dsa", + all(feature = "slh-dsa", feature = "slow-tests") + ))] + use crate::ViewBuilder; + #[cfg(any( + feature = "bls12-381", + feature = "fn-dsa", + feature = "ml-dsa", + all(feature = "slh-dsa", feature = "slow-tests") + ))] + use multi_sig::EncodedMultisig; + #[cfg(feature = "curve25519")] + use ssh_key::private::Ed25519Keypair; + #[cfg(feature = "fn-dsa")] #[test] fn test_fn_dsa_random() { for codec in FN_DSA_KEY_CODECS { @@ -2354,24 +2191,20 @@ mod tests { let (vpk, epk) = { let conv = ViewBuilder::new(&mk).conv().build().unwrap(); let pk = conv.to_public_key().unwrap(); - (Into::>::into(pk.clone()), EncodedMultikey::from(pk)) + (Vec::::from(&pk), EncodedMultikey::from(pk)) }; println!("encoded pubkey: {}: {}", codec, epk); println!("encoded pubkey v: {}: {}", codec, hex::encode(vpk)); - println!( - "encoded privkey: {}: {}", - codec, - EncodedMultikey::from(mk.clone()) - ); println!( "encoded privkey v: {}: {}", codec, - hex::encode(Into::>::into(mk.clone())) + hex::encode(Vec::::from(&mk)) ); - let _v: Vec = mk.into(); + println!("encoded privkey: {}: {}", codec, EncodedMultikey::from(mk)); } } + #[cfg(feature = "fn-dsa")] #[test] fn test_fn_dsa_encoded_random() { for codec in FN_DSA_KEY_CODECS { @@ -2387,6 +2220,7 @@ mod tests { } } + #[cfg(feature = "fn-dsa")] #[test] fn test_fn_dsa_signing_detached_roundtrip() { for codec in FN_DSA_KEY_CODECS { @@ -2411,7 +2245,7 @@ mod tests { let msg = b"for great justice, move every zig!"; let signmk = ViewBuilder::new(&mk).sign().build().unwrap(); - let signature = signmk.sign(msg.as_slice(), false, None).unwrap(); + let signature = signmk.sign(msg, false, None).unwrap(); let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); println!("signature: {}", sig); @@ -2420,6 +2254,7 @@ mod tests { } } + #[cfg(feature = "fn-dsa")] #[test] fn test_fn_dsa_signing_merged_roundtrip() { for codec in FN_DSA_KEY_CODECS { @@ -2453,6 +2288,7 @@ mod tests { } } + #[cfg(feature = "ml-dsa")] #[test] fn test_ml_dsa_random() { for codec in ML_DSA_KEY_CODECS { @@ -2465,24 +2301,20 @@ mod tests { let (vpk, epk) = { let conv = ViewBuilder::new(&mk).conv().build().unwrap(); let pk = conv.to_public_key().unwrap(); - (Into::>::into(pk.clone()), EncodedMultikey::from(pk)) + (Vec::::from(&pk), EncodedMultikey::from(pk)) }; println!("encoded pubkey: {}: {}", codec, epk); println!("encoded pubkey v: {}: {}", codec, hex::encode(vpk)); - println!( - "encoded privkey: {}: {}", - codec, - EncodedMultikey::from(mk.clone()) - ); println!( "encoded privkey v: {}: {}", codec, - hex::encode(Into::>::into(mk.clone())) + hex::encode(Vec::::from(&mk)) ); - let _v: Vec = mk.into(); + println!("encoded privkey: {}: {}", codec, EncodedMultikey::from(mk)); } } + #[cfg(feature = "ml-dsa")] #[test] fn test_ml_dsa_encoded_random() { for codec in ML_DSA_KEY_CODECS { @@ -2498,6 +2330,7 @@ mod tests { } } + #[cfg(feature = "ml-dsa")] #[test] fn test_ml_dsa_signing_detached_roundtrip() { // ML-DSA signing allocates large key/signature buffers on the stack; @@ -2528,7 +2361,7 @@ mod tests { let msg = b"for great justice, move every zig!"; let signmk = ViewBuilder::new(&mk).sign().build().unwrap(); - let signature = signmk.sign(msg.as_slice(), false, None).unwrap(); + let signature = signmk.sign(msg, false, None).unwrap(); let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); println!("signature: {}", sig); @@ -2541,6 +2374,7 @@ mod tests { .unwrap(); } + #[cfg(feature = "ml-dsa")] #[test] fn test_ml_dsa_signing_merged_roundtrip() { // ML-DSA signing allocates large key/signature buffers on the stack; @@ -2585,7 +2419,7 @@ mod tests { .unwrap(); } - #[cfg(feature = "slow-tests")] + #[cfg(all(feature = "slh-dsa", feature = "slow-tests"))] #[test] fn test_slh_dsa_random() { for codec in SLH_DSA_KEY_CODECS { @@ -2598,25 +2432,20 @@ mod tests { let (vpk, epk) = { let conv = ViewBuilder::new(&mk).conv().build().unwrap(); let pk = conv.to_public_key().unwrap(); - (Into::>::into(pk.clone()), EncodedMultikey::from(pk)) + (Vec::::from(&pk), EncodedMultikey::from(pk)) }; println!("encoded pubkey: {}: {}", codec, epk); println!("encoded pubkey v: {}: {}", codec, hex::encode(vpk)); - println!( - "encoded privkey: {}: {}", - codec, - EncodedMultikey::from(mk.clone()) - ); println!( "encoded privkey v: {}: {}", codec, - hex::encode(Into::>::into(mk.clone())) + hex::encode(Vec::::from(&mk)) ); - let _v: Vec = mk.into(); + println!("encoded privkey: {}: {}", codec, EncodedMultikey::from(mk)); } } - #[cfg(feature = "slow-tests")] + #[cfg(all(feature = "slh-dsa", feature = "slow-tests"))] #[test] fn test_slh_dsa_encoded_random() { for codec in SLH_DSA_KEY_CODECS { @@ -2632,7 +2461,7 @@ mod tests { } } - #[cfg(feature = "slow-tests")] + #[cfg(all(feature = "slh-dsa", feature = "slow-tests"))] #[test] fn test_slh_dsa_signing_detached_roundtrip() { // SLH-DSA signing uses large stack buffers internally (Merkle trees, WOTS+). @@ -2662,7 +2491,7 @@ mod tests { let msg = b"for great justice, move every zig!"; let signmk = ViewBuilder::new(&mk).sign().build().unwrap(); - let signature = signmk.sign(msg.as_slice(), false, None).unwrap(); + let signature = signmk.sign(msg, false, None).unwrap(); let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); println!("signature: {}", sig); @@ -2675,7 +2504,7 @@ mod tests { .unwrap(); } - #[cfg(feature = "slow-tests")] + #[cfg(all(feature = "slh-dsa", feature = "slow-tests"))] #[test] fn test_slh_dsa_signing_merged_roundtrip() { // SLH-DSA signing uses large stack buffers internally (Merkle trees, WOTS+). @@ -2719,6 +2548,7 @@ mod tests { .unwrap(); } + #[cfg(feature = "bls12-381")] #[test] fn test_bls_key_combine() { let mut rng = rand::rng(); @@ -2740,7 +2570,7 @@ mod tests { .unwrap(); let signmk = ViewBuilder::new(&shares[0]).sign().build().unwrap(); - let signature = signmk.sign(msg.as_slice(), false, Some(2_u8)).unwrap(); + let signature = signmk.sign(&msg, false, Some(2_u8)).unwrap(); let ms: EncodedMultisig = BaseEncoded::new(Base::Base32Z, signature); let s = ms.to_string(); println!("Bls Sig Share: {}", s); @@ -2763,6 +2593,7 @@ mod tests { assert_eq!(mk1, mk3); } + #[cfg(feature = "bls12-381")] #[test] fn test_bls_share_ssh_key_roundtrip() { let mut rng = rand::rng(); @@ -2795,6 +2626,7 @@ mod tests { assert_eq!(pk1, pk2); } + #[cfg(feature = "curve25519")] #[test] fn test_from_ssh_pubkey() { let mut rng = rand::rng(); @@ -2818,6 +2650,7 @@ mod tests { assert!(kd.secret_bytes().is_err()); // public key } + #[cfg(feature = "curve25519")] #[test] fn test_from_ssh_privkey() { let mut rng = rand::rng(); @@ -2840,6 +2673,7 @@ mod tests { assert!(kd.secret_bytes().is_ok()); } + #[cfg(feature = "curve25519")] #[test] fn test_pub_from_string() { let s = "fba24ed010874657374206b6579010120f9ddcd5118319cc69e6985ef3f4ee3b6c591d46255e1ae5569c8662111b7d3c2".to_string(); @@ -2856,6 +2690,7 @@ mod tests { assert!(kd.secret_bytes().is_err()); // public key } + #[cfg(feature = "curve25519")] #[test] fn test_priv_from_string() { let s = "fba2480260874657374206b657901012064e58adf88f85cbec6a0448a0803f9d28cf9231a7141be413f83cf6aa883cd04".to_string(); @@ -2872,6 +2707,7 @@ mod tests { assert!(kd.secret_bytes().is_ok()); } + #[cfg(feature = "curve25519")] #[test] fn test_pub_from_vec() { let b = hex::decode("ba24ed010874657374206b6579010120f9ddcd5118319cc69e6985ef3f4ee3b6c591d46255e1ae5569c8662111b7d3c2").unwrap(); @@ -2887,6 +2723,7 @@ mod tests { assert!(kd.secret_bytes().is_err()); // public key } + #[cfg(feature = "curve25519")] #[test] fn test_priv_from_vec() { let b = hex::decode("ba2480260874657374206b657901012064e58adf88f85cbec6a0448a0803f9d28cf9231a7141be413f83cf6aa883cd04").unwrap(); @@ -2911,6 +2748,7 @@ mod tests { assert!(mk2.is_null()); } + #[cfg(feature = "curve25519")] #[test] fn test_from_seed() { let seed = hex::decode("f9ddcd5118319cc69e6985ef3f4ee3b6c591d46255e1ae5569c8662111b7d3c2") @@ -2931,6 +2769,7 @@ mod tests { assert!(kd.secret_bytes().is_ok()); } + #[cfg(feature = "curve25519")] #[test] fn test_too_many_attributes_rejected() { use multi_trait::EncodeInto; @@ -2955,6 +2794,7 @@ mod tests { } } + #[cfg(feature = "curve25519")] #[test] fn test_valid_roundtrip_with_caps() { // Sanity: a well-formed multikey still round-trips with the caps. @@ -2962,8 +2802,292 @@ mod tests { .with_key_bytes(&[0u8; 32]) .try_build() .unwrap(); - let v: Vec = mk.clone().into(); + let v = Vec::::from(&mk); let mk2 = Multikey::try_from(v.as_slice()).unwrap(); assert_eq!(mk, mk2); } + + /// tests that walk every key codec in [`KEY_CODECS`] + #[cfg(all( + feature = "bls12-381", + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" + ))] + mod key_codecs { + use super::*; + #[cfg(all(feature = "bcrypt", feature = "chacha20"))] + use crate::{cipher, kdf}; + + #[cfg(all(feature = "bcrypt", feature = "chacha20"))] + #[test] + fn test_encryption_roundtrip() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let mk1 = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test key") + .try_build() + .unwrap(); + + let attr = ViewBuilder::new(&mk1).attr().build().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = ViewBuilder::new(&mk1).data().build().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + let mk2 = { + let kdfmk = kdf::Builder::new(Codec::BcryptPbkdf) + .with_random_salt(kdf::SALT_LENGTH, &mut rng) + .with_rounds(10) + .try_build() + .unwrap(); + let ciphermk = cipher::Builder::new(Codec::Chacha20Poly1305) + .with_random_nonce(::chacha20::Nonce::default().len(), &mut rng) + .try_build() + .unwrap(); + // get the kdf view on the cipher multikey so we can generate a + // new cipher multikey with the same parameters and the generated key + let kdf = ViewBuilder::new(&ciphermk).kdf(&kdfmk).build().unwrap(); + // derive a key from the passphrase and add it to the cipher multikey + let ciphermk = kdf + .derive_key(b"for great justice, move every zig!") + .unwrap(); + // get the cipher view on the unencrypted ed25519 secret key so + // that we can create a new ed25519 secret key with an encrypted + // key and the kdf and cipher attributes and data + let cipher = ViewBuilder::new(&mk1).cipher(&ciphermk).build().unwrap(); + // encrypt the multikey using the cipher + + cipher.encrypt().unwrap() + }; + + let attr = ViewBuilder::new(&mk2).attr().build().unwrap(); + assert!(attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = ViewBuilder::new(&mk2).data().build().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_err()); // encrypted key + + let mk3 = { + let kdfmk = kdf::Builder::default() + .try_from_multikey(&mk2) + .unwrap() + .try_build() + .unwrap(); + let ciphermk = cipher::Builder::default() + .try_from_multikey(&mk2) + .unwrap() + .try_build() + .unwrap(); + // get the kdf view + let kdf = ViewBuilder::new(&ciphermk).kdf(&kdfmk).build().unwrap(); + // derive a key from the passphrase and add it to the cipher multikey + let ciphermk = kdf + .derive_key(b"for great justice, move every zig!") + .unwrap(); + // get the cipher view + let cipher = ViewBuilder::new(&mk2).cipher(&ciphermk).build().unwrap(); + // decrypt the multikey using the cipher + + cipher.decrypt().unwrap() + }; + + let attr = ViewBuilder::new(&mk3).attr().build().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = ViewBuilder::new(&mk3).data().build().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + // ensure the round trip worked + assert_eq!(mk1, mk3); + } + } + + #[test] + fn test_random() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test key") + .try_build() + .unwrap(); + let (vpk, epk) = { + let conv = ViewBuilder::new(&mk).conv().build().unwrap(); + let pk = conv.to_public_key().unwrap(); + (Vec::::from(&pk), EncodedMultikey::from(pk)) + }; + println!("encoded pubkey: {}: {}", codec, epk); + println!("encoded pubkey v: {}: {}", codec, hex::encode(vpk)); + println!( + "encoded privkey v: {}: {}", + codec, + hex::encode(Vec::::from(&mk)) + ); + println!("encoded privkey: {}: {}", codec, EncodedMultikey::from(mk)); + } + } + #[test] + fn test_encoded_random() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_base_encoding(Base::Base32Lower) + .with_comment("test key") + .try_build_encoded() + .unwrap(); + let s = mk.to_string(); + assert_eq!(mk, EncodedMultikey::try_from(s.as_str()).unwrap()); + } + } + #[test] + fn test_random_public_ssh_key_roundtrip() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test key") + .try_build() + .unwrap(); + let conv = ViewBuilder::new(&mk).conv().build().unwrap(); + let pk = conv.to_public_key().unwrap(); + let ssh_key = conv.to_ssh_public_key().unwrap(); + let mk2 = Builder::new_from_ssh_public_key(&ssh_key) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(pk, mk2); + } + } + #[test] + fn test_random_private_ssh_key_roundtrip() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test key") + .try_build() + .unwrap(); + let conv = ViewBuilder::new(&mk).conv().build().unwrap(); + let ssh_key = conv.to_ssh_private_key().unwrap(); + let mk2 = Builder::new_from_ssh_private_key(&ssh_key) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(mk, mk2); + } + } + #[test] + fn test_ssh_key_roundtrip() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test key") + .try_build() + .unwrap(); + let cv = ViewBuilder::new(&sk1).conv().build().unwrap(); + let public_key = cv.to_ssh_public_key().unwrap(); + let private_key = cv.to_ssh_private_key().unwrap(); + + let pk1 = cv.to_public_key().unwrap(); + let cv = ViewBuilder::new(&pk1).conv().build().unwrap(); + assert_eq!(public_key, cv.to_ssh_public_key().unwrap()); + + let sk2 = Builder::new_from_ssh_private_key(&private_key) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(sk1, sk2); + let pk2 = Builder::new_from_ssh_public_key(&public_key) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(pk1, pk2); + } + } + #[test] + fn test_signing_detached_roundtrip() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test key") + .try_build() + .unwrap(); + + let attr = ViewBuilder::new(&mk).attr().build().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = ViewBuilder::new(&mk).data().build().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + let conv = ViewBuilder::new(&mk).conv().build().unwrap(); + let pk = EncodedMultikey::new(Base::Base16Lower, conv.to_public_key().unwrap()); + println!("{} pubkey: {}", codec, pk); + + let msg = b"for great justice, move every zig!"; + + let signmk = ViewBuilder::new(&mk).sign().build().unwrap(); + let signature = if codec == Codec::Bls12381G1Priv || codec == Codec::Bls12381G2Priv + { + signmk.sign(msg, false, Some(2_u8)).unwrap() + } else { + signmk.sign(msg, false, None).unwrap() + }; + let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); + println!("signature: {}", sig); + + let verifymk = ViewBuilder::new(&mk).verify().build().unwrap(); + assert!(verifymk.verify(&signature, Some(msg.as_slice())).is_ok()); + } + } + #[test] + fn test_signing_merged_roundtrip() { + for codec in KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test key") + .try_build() + .unwrap(); + + let attr = ViewBuilder::new(&mk).attr().build().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = ViewBuilder::new(&mk).data().build().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + let msg = + hex::decode("8bb78be51ac7cc98f44e38947ff8a128764ec039b89687a790dfa8444ba97682") + .unwrap(); + + let signmk = ViewBuilder::new(&mk).sign().build().unwrap(); + let signature = if codec == Codec::Bls12381G1Priv || codec == Codec::Bls12381G2Priv + { + signmk.sign(&msg, true, Some(2_u8)).unwrap() + } else { + signmk.sign(&msg, true, None).unwrap() + }; + + // make sure the message is stored correctly in the signature + assert_eq!(signature.message, msg); + + let verifymk = ViewBuilder::new(&mk).verify().build().unwrap(); + assert!(verifymk.verify(&signature, None).is_ok()); + } + } + } } diff --git a/src/nonce.rs b/src/nonce.rs index 655d402..d84acee 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -1,9 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 +use crate::mk::encode_varbytes_into; use crate::{Error, error::NonceError}; use core::fmt; use multi_base::Base; use multi_codec::Codec; -use multi_trait::{Null, TryDecodeFrom}; +use multi_trait::{EncodeIntoBuffer, Null, TryDecodeFrom}; use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes}; use rand_core::CryptoRng; @@ -62,11 +63,18 @@ impl AsRef<[u8]> for Nonce { impl From for Vec { fn from(val: Nonce) -> Self { - let mut v = Vec::default(); + Self::from(&val) + } +} + +/// Encode a borrowed nonce without cloning it +impl From<&Nonce> for Vec { + fn from(val: &Nonce) -> Self { + let mut v = Self::with_capacity(16 + val.nonce.len()); // add the sigil - v.append(&mut SIGIL.into()); + u64::from(SIGIL).encode_into_buffer(&mut v); // add the nonce bytes - v.append(&mut Varbytes::new(val.nonce.clone()).into()); + encode_varbytes_into(&mut v, &val.nonce); v } } @@ -168,6 +176,18 @@ impl Builder { #[cfg(test)] mod tests { use super::*; + + /// the allocation-free encoder matches the field-by-field encoding + #[test] + fn test_encode_matches_reference_layout() { + let n = Nonce { + nonce: vec![9u8; 200], + }; + let mut want: Vec = SIGIL.into(); + want.extend(Vec::::from(Varbytes::new(n.nonce.clone()))); + assert_eq!(Vec::::from(&n), want); + } + #[cfg(feature = "curve25519")] use crate::{ViewBuilder, mk}; #[test] @@ -203,6 +223,7 @@ mod tests { assert_eq!(n, EncodedNonce::try_from(s.as_str()).unwrap()); } + #[cfg(feature = "curve25519")] #[test] fn test_nonce_multisig_roundtrip() { let mut rng = rand::rng(); diff --git a/src/serde/de.rs b/src/serde/de.rs index feb1fee..dc1a332 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -4,7 +4,7 @@ use crate::{ mk::{self, Attributes}, nonce, }; -use core::fmt; +use core::{fmt, marker::PhantomData}; use multi_codec::Codec; use multi_util::EncodedVarbytes; use serde::{ @@ -13,28 +13,31 @@ use serde::{ }; use zeroize::Zeroizing; -/// Visitor that accepts borrowed bytes, owned bytes, and byte buffers. -/// Used by the non-human-readable `Deserialize` paths so that deserializers -/// which cannot lend borrowed slices (e.g. `ciborium`) still work. -struct ByteBufVisitor; +/// Visitor for the non-human-readable `Deserialize` paths. It decodes `T` +/// straight from the input bytes, so it works with deserializers that lend +/// borrowed slices and with reader-based ones that hand over an owned buffer. +/// An owned buffer is zeroized after decoding because a `Multikey` can carry +/// secret key bytes. +struct ByteBufVisitor(PhantomData); -impl<'de> Visitor<'de> for ByteBufVisitor { - type Value = Vec; +impl Visitor<'_> for ByteBufVisitor +where + T: for<'a> TryFrom<&'a [u8], Error = crate::Error>, +{ + type Value = T; fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("byte buffer") } - fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { - Ok(v.to_vec()) - } - + // serde's default `visit_borrowed_bytes` forwards here fn visit_bytes(self, v: &[u8]) -> Result { - Ok(v.to_vec()) + T::try_from(v).map_err(E::custom) } fn visit_byte_buf(self, v: Vec) -> Result { - Ok(v) + let v = Zeroizing::new(v); + self.visit_bytes(&v) } } @@ -85,8 +88,7 @@ impl<'de> Deserialize<'de> for Nonce { if deserializer.is_human_readable() { deserializer.deserialize_struct(nonce::SIGIL.as_str(), FIELDS, NonceVisitor) } else { - let b = deserializer.deserialize_byte_buf(ByteBufVisitor)?; - Ok(Self::try_from(b.as_slice()).map_err(D::Error::custom)?) + deserializer.deserialize_byte_buf(ByteBufVisitor(PhantomData)) } } } @@ -113,25 +115,13 @@ impl<'de> Deserialize<'de> for AttrId { AttrId::try_from(c).map_err(E::custom) } + // serde's default `visit_borrowed_str` and `visit_string` + // forward here fn visit_str(self, s: &str) -> Result where E: Error, { - AttrId::try_from(s).map_err(E::custom) - } - - fn visit_borrowed_str(self, s: &'de str) -> Result - where - E: Error, - { - AttrId::try_from(s).map_err(E::custom) - } - - fn visit_string(self, s: String) -> Result - where - E: Error, - { - AttrId::try_from(s.as_str()).map_err(E::custom) + s.parse().map_err(E::custom) } } @@ -193,10 +183,9 @@ impl<'de> Deserialize<'de> for Multikey { } let attr: Vec<(AttrId, EncodedVarbytes)> = map.next_value()?; let mut a = Attributes::new(); - attr.iter() - .try_for_each(|(id, attr)| -> Result<(), V::Error> { - let i = *id; - let v: Zeroizing> = (***attr).clone().into(); + attr.into_iter() + .try_for_each(|(i, attr)| -> Result<(), V::Error> { + let v: Zeroizing> = attr.to_inner().to_inner().into(); if a.insert(i, v).is_some() { return Err(Error::duplicate_field( "duplicate attribute id", @@ -223,8 +212,7 @@ impl<'de> Deserialize<'de> for Multikey { if deserializer.is_human_readable() { deserializer.deserialize_struct(mk::SIGIL.as_str(), FIELDS, MultikeyVisitor) } else { - let b = deserializer.deserialize_byte_buf(ByteBufVisitor)?; - Ok(Self::try_from(b.as_slice()).map_err(D::Error::custom)?) + deserializer.deserialize_byte_buf(ByteBufVisitor(PhantomData)) } } } diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 2cee2a2..f49f67e 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -5,28 +5,46 @@ mod ser; #[cfg(test)] mod tests { - use crate::{Builder, EncodedMultikey, Multikey, ViewBuilder, cipher, kdf, nonce}; + #[cfg(feature = "curve25519")] + use crate::Builder; + #[cfg(any( + feature = "curve25519", + all(feature = "bcrypt", feature = "bls12-381", feature = "chacha20") + ))] + use crate::ViewBuilder; + use crate::{EncodedMultikey, Multikey, nonce}; + #[cfg(all( + feature = "bcrypt", + feature = "chacha20", + any(feature = "bls12-381", feature = "curve25519") + ))] + use crate::{cipher, kdf}; + #[cfg(feature = "curve25519")] use multi_base::Base; + #[cfg(any( + feature = "curve25519", + all(feature = "bcrypt", feature = "bls12-381", feature = "chacha20") + ))] use multi_codec::Codec; + #[cfg(feature = "curve25519")] use multi_hash::EncodedMultihash; use multi_trait::Null; + #[cfg(feature = "curve25519")] use multi_util::BaseEncoded; + #[cfg(feature = "curve25519")] use serde::{Deserialize, Serialize}; - use serde_test::{Configure, Token, assert_tokens}; + use serde_test::{Configure, Token, assert_de_tokens, assert_de_tokens_error, assert_tokens}; - /// Serialize a value to CBOR bytes using `ciborium`. - fn cbor_to_vec(value: &T) -> Vec { - let mut buf = Vec::new(); - ciborium::into_writer(value, &mut buf).expect("CBOR serialize"); - buf - } + #[cfg(feature = "curve25519")] use std::collections::BTreeMap; + #[cfg(feature = "curve25519")] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] struct Wrapper { pub map: BTreeMap, } + #[cfg(feature = "curve25519")] #[test] fn test_serde_macros() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -53,14 +71,15 @@ mod tests { w1.map.insert(skh, sk); w1.map.insert(pkh, pk); - let b = cbor_to_vec(&w1); - let w2 = ciborium::from_reader(b.as_slice()).unwrap(); + let b = serde_cbor_2::to_vec(&w1).unwrap(); + let w2 = serde_cbor_2::from_slice(&b).unwrap(); assert_eq!(w1, w2); let s = serde_json::to_string(&w1).unwrap(); let w3 = serde_json::from_str(&s).unwrap(); assert_eq!(w1, w3); } + #[cfg(feature = "curve25519")] #[test] fn test_serde_compact() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -99,6 +118,7 @@ mod tests { ); } + #[cfg(feature = "curve25519")] #[test] fn test_serde_encoded_string() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -118,6 +138,7 @@ mod tests { ); } + #[cfg(feature = "curve25519")] #[test] fn test_serde_readable() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -157,6 +178,7 @@ mod tests { ); } + #[cfg(all(feature = "bcrypt", feature = "chacha20", feature = "curve25519"))] #[test] fn test_serde_encrypted_secret_key_compact() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -228,6 +250,7 @@ mod tests { assert_eq!(mk2, mk3); } + #[cfg(all(feature = "bcrypt", feature = "chacha20", feature = "curve25519"))] #[test] fn test_serde_encrypted_secret_key_readable() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -287,6 +310,7 @@ mod tests { assert_eq!(mk2, mk3); } + #[cfg(all(feature = "bcrypt", feature = "chacha20", feature = "curve25519"))] #[test] fn test_serde_encrypted_secret_key_json() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -341,6 +365,7 @@ mod tests { assert_eq!(mk2, mk3); } + #[cfg(all(feature = "bcrypt", feature = "bls12-381", feature = "chacha20"))] #[test] fn test_serde_encrypted_bls_secret_key_share_json() { /* @@ -404,6 +429,7 @@ mod tests { assert_eq!(mk2, mk3); } + #[cfg(all(feature = "bcrypt", feature = "chacha20", feature = "curve25519"))] #[test] fn test_chacha20_aead_roundtrip() { // A secret key encrypted with ChaCha20Poly1305 must decrypt back to the @@ -481,6 +507,7 @@ mod tests { ); } + #[cfg(feature = "curve25519")] #[test] fn test_encoded_public_key() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") @@ -617,4 +644,56 @@ mod tests { let n: nonce::EncodedNonce = nonce::Nonce::null().into(); assert_tokens(&n.readable(), &[Token::Str("fbb2400")]); } + + /// the compact form decodes from borrowed, transient, and owned bytes + #[test] + fn test_compact_decode_all_byte_forms() { + const MK: &[u8] = &[0xba, 0x24, 0, 0, 0]; + const NONCE: &[u8] = &[0xbb, 0x24, 0]; + assert_eq!(Vec::::from(&Multikey::null()), MK); + assert_eq!(Vec::::from(&nonce::Nonce::null()), NONCE); + let mk = Multikey::null().compact(); + assert_de_tokens(&mk, &[Token::BorrowedBytes(MK)]); + assert_de_tokens(&mk, &[Token::Bytes(MK)]); + assert_de_tokens(&mk, &[Token::ByteBuf(MK)]); + let n = nonce::Nonce::null().compact(); + assert_de_tokens(&n, &[Token::BorrowedBytes(NONCE)]); + assert_de_tokens(&n, &[Token::Bytes(NONCE)]); + assert_de_tokens(&n, &[Token::ByteBuf(NONCE)]); + } + + /// malformed compact bytes surface the crate error message + #[test] + fn test_compact_decode_error() { + let err = Multikey::try_from([0u8].as_slice()) + .unwrap_err() + .to_string(); + assert_de_tokens_error::>(&[Token::Bytes(&[0])], &err); + } + + /// `AttrId` deserializes from every string form (serde forwards borrowed + /// and owned strings to `visit_str`) and from its `u8` code + #[test] + fn test_attrid_deserialize_all_forms() { + use crate::AttrId; + use serde::{ + Deserialize, + de::{ + IntoDeserializer, + value::{BorrowedStrDeserializer, Error, StrDeserializer, StringDeserializer}, + }, + }; + let from_str = AttrId::deserialize(StrDeserializer::::new("key-data")); + assert!(from_str.ok() == Some(AttrId::KeyData)); + let from_borrowed = AttrId::deserialize(BorrowedStrDeserializer::::new("key-data")); + assert!(from_borrowed.ok() == Some(AttrId::KeyData)); + let from_string = + AttrId::deserialize(StringDeserializer::::new("key-data".to_string())); + assert!(from_string.ok() == Some(AttrId::KeyData)); + let code = u8::from(AttrId::KeyData); + let from_u8 = AttrId::deserialize(IntoDeserializer::::into_deserializer(code)); + assert!(from_u8.ok() == Some(AttrId::KeyData)); + let bogus = AttrId::deserialize(StrDeserializer::::new("bogus")); + assert!(bogus.is_err()); + } } diff --git a/src/serde/ser.rs b/src/serde/ser.rs index c1e2940..778fdf4 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -2,6 +2,7 @@ use crate::{AttrId, Multikey, Nonce, mk, nonce}; use multi_util::{EncodedVarbytes, EncodingInfo, Varbytes}; use serde::ser::{self, SerializeStruct}; +use zeroize::Zeroizing; /// Serialize instance of [`crate::Nonce`] impl ser::Serialize for Nonce { @@ -17,8 +18,8 @@ impl ser::Serialize for Nonce { )?; ss.end() } else { - let v: Vec = self.clone().into(); - serializer.serialize_bytes(v.as_slice()) + let v: Vec = self.into(); + serializer.serialize_bytes(&v) } } } @@ -61,8 +62,9 @@ impl ser::Serialize for Multikey { ss.serialize_field("attributes", &attributes)?; ss.end() } else { - let v: Vec = self.clone().into(); - serializer.serialize_bytes(v.as_slice()) + // the encoding carries secret key bytes, so wipe it when done + let v = Zeroizing::new(Vec::::from(self)); + serializer.serialize_bytes(&v) } } } diff --git a/src/types.rs b/src/types.rs index 8d2f64c..5b85eac 100644 --- a/src/types.rs +++ b/src/types.rs @@ -202,6 +202,7 @@ mod tests { assert!(!debug.contains("1")); } + #[cfg(feature = "curve25519")] #[test] fn test_key_scheme() { let scheme = KeyScheme::new(Codec::Ed25519Pub); diff --git a/src/views.rs b/src/views.rs index c411cdc..e59488a 100644 --- a/src/views.rs +++ b/src/views.rs @@ -2,7 +2,7 @@ //! View traits and their per-algorithm implementations. //! //! A "view" is an abstract, algorithm-specific interface over the attributes -//! and key material of a [`Multikey`](crate::Multikey). The traits here +//! and key material of a [`Multikey`]. The traits here //! cover general, cipher, kdf, and threshold attribute inspection //! ([`AttrView`], [`CipherAttrView`], [`KdfAttrView`], [`ThresholdAttrView`], //! [`ThresholdKeyView`]), key data and conversion ([`DataView`], @@ -11,12 +11,10 @@ //! [`ThresholdView`]), and introspection ([`ThresholdDisclosureView`], //! [`MerkleStateView`]). //! -//! Construct views with -//! [`ViewBuilder`](crate::views::builder::ViewBuilder): a fluent -//! kind selector plus optional local-codec factories for custom protocol -//! keys. The deprecated [`Views`] trait delegates to the same internal -//! dispatch core the builder uses. The marker and threshold metadata -//! helpers are also public: [`threshold_marker`] and [`threshold_meta`]. +//! Construct views with [`ViewBuilder`](crate::views::builder::ViewBuilder): +//! a fluent kind selector plus optional local-codec factories for custom +//! protocol keys. The marker and threshold metadata helpers are also public: +//! `threshold_marker` (with the `threshold` feature) and [`threshold_meta`]. use crate::{Error, Multikey}; use multi_codec::Codec; use multi_hash::Multihash; @@ -24,54 +22,87 @@ use multi_sig::Multisig; use zeroize::Zeroizing; // algorithms implement different sets of view +#[cfg(feature = "bcrypt")] pub(crate) mod bcrypt; +#[cfg(feature = "bls12-381")] pub(crate) mod bls12381; +#[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] pub(crate) mod bls12381_g1_fndsa512; +#[cfg(all(feature = "bls12-381", feature = "mayo"))] pub(crate) mod bls12381_g1_mayo1; +#[cfg(all(feature = "bls12-381", feature = "mayo"))] pub(crate) mod bls12381_g1_mayo2; +#[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] pub(crate) mod bls12381_g1_mldsa65; +#[cfg(all( + feature = "bls12-381", + any(feature = "fn-dsa", feature = "mayo", feature = "ml-dsa") +))] pub(crate) mod bls12381_hybrid; +#[cfg(feature = "chacha20")] pub(crate) mod chacha20; #[cfg(feature = "deprecated")] #[allow(deprecated)] pub(crate) mod classic_mceliece; pub(crate) mod dkg_threshold; +#[cfg(feature = "curve25519")] pub(crate) mod ed25519; +#[cfg(all(feature = "curve25519", feature = "fn-dsa"))] pub(crate) mod ed25519_fndsa512; +#[cfg(all(feature = "curve25519", feature = "mayo"))] pub(crate) mod ed25519_mayo2; +#[cfg(all(feature = "curve25519", feature = "ml-dsa"))] pub(crate) mod ed25519_mldsa65; +#[cfg(feature = "fn-dsa")] pub(crate) mod fn_dsa; +#[cfg(feature = "frodokem")] pub(crate) mod frodokem; +#[cfg(feature = "frodokem")] pub(crate) mod frodokem_helper; #[cfg(feature = "lamport")] pub(crate) mod lamport; #[cfg(feature = "lamport")] pub(crate) mod lamport_merkle; +#[cfg(feature = "mayo")] pub(crate) mod mayo; +#[cfg(feature = "ml-dsa")] pub(crate) mod ml_dsa; +#[cfg(feature = "ml-kem")] pub(crate) mod ml_kem; +#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] pub(crate) mod nist_p; +#[cfg(feature = "rsa")] pub(crate) mod rsa; +#[cfg(feature = "secp256k1")] pub(crate) mod secp256k1; +#[cfg(feature = "slh-dsa")] pub(crate) mod slh_dsa; +#[cfg(feature = "sntrup")] pub(crate) mod sntrup; /// Split (Shamir) threshold marker attributes and scheme classifier. +#[cfg(feature = "threshold")] pub mod threshold_marker; /// Threshold disclosure modes and encrypted metadata helpers. pub mod threshold_meta; /// XEdDSA view: an Ed25519-form signature over an X25519 key (POP-only). +#[cfg(feature = "curve25519")] pub(crate) mod xeddsa; pub use threshold_meta::{ DisclosureView, ThresholdDisclosure, ThresholdDisclosureView, ThresholdMetaCipher, - ThresholdMetadata, decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key, - read_threshold_params, stamp_disclosure_attrs, + ThresholdMetadata, read_threshold_params, stamp_disclosure_attrs, }; +#[cfg(feature = "chacha20")] +pub use threshold_meta::{decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key}; +#[cfg(feature = "curve25519")] pub(crate) mod x25519; +#[cfg(all(feature = "curve25519", feature = "frodokem"))] pub(crate) mod x25519_frodokem640; -#[cfg(feature = "deprecated")] +#[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] pub(crate) mod x25519_mceliece348864; +#[cfg(all(feature = "curve25519", feature = "ml-kem"))] pub(crate) mod x25519_mlkem768; +#[cfg(all(feature = "curve25519", feature = "sntrup"))] pub(crate) mod x25519_sntrup761; #[cfg(feature = "xmss")] pub(crate) mod xmss; @@ -83,8 +114,21 @@ pub(crate) mod dispatch; // optional local-codec factories, and a `.build()` terminal pub mod builder; -// shared AEAD helper used by ml_kem, sntrup, x25519, and hybrid KEM views -// (plus the deprecated classic_mceliece views) +// shared AEAD seal/open helpers for the KEM and ECDH views. Gated on the +// features that call them rather than on `aead`, which alone would leave +// them unused. +#[cfg(any( + feature = "curve25519", + feature = "deprecated", + feature = "frodokem", + feature = "ml-kem", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "rsa", + feature = "secp256k1", + feature = "sntrup" +))] pub(crate) mod aead; /// @@ -322,54 +366,3 @@ pub trait MerkleStateView { /// Number of unused leaves (secret keys only). fn remaining_signatures(&self) -> Result; } - -/// trait for getting the other views -/// -/// Deprecated: construct views with [`ViewBuilder`](builder::ViewBuilder) -/// instead. The builder selects a view kind fluently, dispatches through -/// the same internal dispatch core this trait delegates to, and adds -/// local-codec factories for custom protocol keys. This trait and its impl -/// for `Multikey` remain as a source-compatible delegating shim. -/// -/// Note: `multi_sig::Views` is a different, unrelated trait over -/// [`Multisig`]; this deprecation does not affect it. -#[deprecated( - since = "2.1.0", - note = "use ViewBuilder instead; this shim trait and its impl for Multikey will be removed in the next major release. multi_sig::Views is a different, unrelated trait" -)] -pub trait Views { - /// Provide a read-only view of the basic attributes in the viewed Multikey - fn attr_view<'a>(&'a self) -> Result, Error>; - /// Provide a read-only view of the cipher attributes in the viewed Multikey - fn cipher_attr_view<'a>(&'a self) -> Result, Error>; - /// Provide a read-only view to key data in the viewed Multikey - fn data_view<'a>(&'a self) -> Result, Error>; - /// Provide a read-only view of the kdf attributes in the viewed Multikey - fn kdf_attr_view<'a>(&'a self) -> Result, Error>; - /// Provide a read-only view of the threshold attributes in the viewed Multikey - fn threshold_attr_view<'a>(&'a self) -> Result, Error>; - /// Provide a read-only view of higher-level threshold key metadata in the viewed Multikey - fn threshold_key_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface to do encryption/decryption of the viewed Multikey - fn cipher_view<'a>(&'a self, cipher: &'a Multikey) -> Result, Error>; - /// Provide an interface to do key conversions from the viewe Multikey - fn conv_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface to do key conversions from the viewe Multikey - fn fingerprint_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface to do kdf operations from the viewed Multikey - fn kdf_view<'a>(&'a self, kdf: &'a Multikey) -> Result, Error>; - /// Provide an interface to seal (encrypt) plaintext - fn seal_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface to open (decrypt) sealed data - fn open_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface to sign a message and return a Multisig - fn sign_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface to threshold operations on the Mutlikey - fn threshold_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface to verify a Multisig and optional message - fn verify_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface for threshold disclosure mode operations - fn disclosure_view<'a>(&'a self) -> Result, Error>; - /// Provide an interface for merkle-tree state introspection - fn merkle_state_view<'a>(&'a self) -> Result, Error>; -} diff --git a/src/views/aead.rs b/src/views/aead.rs index dc02cf3..b39fdc9 100644 --- a/src/views/aead.rs +++ b/src/views/aead.rs @@ -1,24 +1,34 @@ // SPDX-License-Identifier: Apache-2.0 -//! Shared AEAD helper functions for ML-KEM and X25519 seal/open operations. +//! Shared AEAD helper functions for the KEM and ECDH seal/open operations. -use crate::error::SealError; +use crate::{error::SealError, mk::encode_varbytes_into}; use aes_gcm::{ Aes128Gcm, Aes256Gcm, aead::{Aead, KeyInit, Payload}, }; use chacha20poly1305::{ChaCha20Poly1305, XChaCha20Poly1305}; -use hkdf::Hkdf; use multi_codec::Codec; -use sha2::Sha512; +use multi_trait::EncodeIntoBuffer; use zeroize::Zeroizing; /// Derive an AEAD key from a shared secret using HKDF-SHA512. +#[cfg(any( + feature = "curve25519", + feature = "deprecated", + feature = "frodokem", + feature = "ml-kem", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1", + feature = "sntrup" +))] pub(crate) fn derive_aead_key( shared_secret: &[u8], info: &[u8], key_len: usize, ) -> Result>, SealError> { - let hk = Hkdf::::new(None, shared_secret); + let hk = hkdf::Hkdf::::new(None, shared_secret); let mut okm = Zeroizing::new(vec![0u8; key_len]); hk.expand(info, &mut okm) .map_err(|e| SealError::KeyDerivationFailed(e.to_string()))?; @@ -174,6 +184,74 @@ pub(crate) fn aead_open( Ok(Zeroizing::new(pt)) } +/// Encode the AEAD part of a sealed message: +/// `[aead_codec Codec][nonce Varbytes][ct+tag Varbytes]`. ECDH views carry the +/// ephemeral public key outside this blob. +#[cfg(any( + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" +))] +pub(crate) fn encode_sealed(aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { + let mut out = Vec::new(); + encode_sealed_into(&mut out, aead_codec, nonce, ct_tag); + out +} + +/// Encode a KEM-sealed message: +/// `[kem_ct Varbytes][aead_codec Codec][nonce Varbytes][ct+tag Varbytes]`. +#[cfg(any( + feature = "deprecated", + feature = "frodokem", + feature = "ml-kem", + feature = "rsa", + feature = "sntrup" +))] +pub(crate) fn encode_kem_sealed( + kem_ct: &[u8], + aead_codec: Codec, + nonce: &[u8], + ct_tag: &[u8], +) -> Vec { + let mut out = Vec::new(); + encode_varbytes_into(&mut out, kem_ct); + encode_sealed_into(&mut out, aead_codec, nonce, ct_tag); + out +} + +/// Encode a hybrid X25519 + KEM sealed message: +/// `[ephemeral_x25519_pub Varbytes][kem_ct Varbytes][nonce Varbytes][ct+tag Varbytes]`. +/// The AEAD codec is fixed by the hybrid codec, so it is not encoded. +#[cfg(all( + feature = "curve25519", + any( + feature = "deprecated", + feature = "frodokem", + feature = "ml-kem", + feature = "sntrup" + ) +))] +pub(crate) fn encode_hybrid_sealed( + ephemeral_pub: &[u8], + kem_ct: &[u8], + nonce: &[u8], + ct_tag: &[u8], +) -> Vec { + let mut out = Vec::new(); + for field in [ephemeral_pub, kem_ct, nonce, ct_tag] { + encode_varbytes_into(&mut out, field); + } + out +} + +fn encode_sealed_into(out: &mut Vec, aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) { + u64::from(aead_codec).encode_into_buffer(out); + encode_varbytes_into(out, nonce); + encode_varbytes_into(out, ct_tag); +} + #[cfg(test)] mod tests { use super::*; @@ -223,6 +301,17 @@ mod tests { assert!(aead_open(Codec::Chacha20Poly1305, &key, &nonce, &ct, b"").is_err()); } + #[cfg(any( + feature = "curve25519", + feature = "deprecated", + feature = "frodokem", + feature = "ml-kem", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1", + feature = "sntrup" + ))] #[test] fn test_hkdf_derive() { let secret = [0xabu8; 32]; @@ -232,4 +321,31 @@ mod tests { let key2 = derive_aead_key(&secret, b"test", 32).unwrap(); assert_eq!(key.as_slice(), key2.as_slice()); } + + /// the sealed-message encoders produce the documented wire layout + #[cfg(all( + feature = "curve25519", + any( + feature = "deprecated", + feature = "frodokem", + feature = "ml-kem", + feature = "sntrup" + ) + ))] + #[test] + fn test_sealed_wire_layout() { + let mut tail: Vec = Codec::Chacha20Poly1305.into(); + tail.extend_from_slice(&[1, 4, 2, 5, 6]); + assert_eq!(encode_sealed(Codec::Chacha20Poly1305, &[4], &[5, 6]), tail); + let mut kem = vec![2, 8, 9]; + kem.extend_from_slice(&tail); + assert_eq!( + encode_kem_sealed(&[8, 9], Codec::Chacha20Poly1305, &[4], &[5, 6]), + kem + ); + assert_eq!( + encode_hybrid_sealed(&[1], &[2, 3], &[4], &[5, 6]), + vec![1, 1, 2, 2, 3, 1, 4, 2, 5, 6] + ); + } } diff --git a/src/views/bcrypt.rs b/src/views/bcrypt.rs index 2304026..80d5f2e 100644 --- a/src/views/bcrypt.rs +++ b/src/views/bcrypt.rs @@ -5,8 +5,7 @@ use multi_codec::Codec; use multi_util::Varuint; use zeroize::Zeroizing; -/// constants for Bcrypt -pub const SALT_LENGTH: usize = 32; +use crate::kdf::SALT_LENGTH; pub(crate) struct View<'a> { mk: &'a Multikey, @@ -64,7 +63,7 @@ impl<'a> KdfView for View<'a> { res.attributes.insert(AttrId::KeyData, key.clone()); let _ = res.attributes.remove(&AttrId::KeyIsEncrypted); res.attributes.insert(AttrId::KdfCodec, kdf_codec.into()); - res.attributes.insert(AttrId::KdfSalt, salt.clone()); + res.attributes.insert(AttrId::KdfSalt, salt); res.attributes.insert(AttrId::KdfRounds, rounds.into()); Ok(res) } diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index cf4dbbc..7809496 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 +use crate::mk::encode_varbytes_into; use crate::views::dispatch::{ - dispatch_attr_view, dispatch_conv_view, dispatch_data_view, dispatch_disclosure_view, - dispatch_fingerprint_view, dispatch_threshold_attr_view, + dispatch_attr_view, dispatch_data_view, dispatch_disclosure_view, dispatch_fingerprint_view, + dispatch_threshold_attr_view, public_form, }; use crate::{ AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, @@ -91,7 +92,7 @@ impl From for Vec { // add in the limit v.append(&mut Varuint(val.2).into()); // add in the key share data - v.append(&mut Varbytes::new(val.3.clone()).into()); + encode_varbytes_into(&mut v, &val.3); v } } @@ -139,9 +140,9 @@ impl From for Vec { // add in the number of key shares v.append(&mut Varuint(val.0.len()).into()); // add in the key shares - val.0.iter().for_each(|(_, share)| { - v.append(&mut share.clone().into()); - }); + for share in val.0.into_values() { + v.append(&mut share.into()); + } v } } @@ -485,10 +486,7 @@ impl<'a> ConvView for View<'a> { /// try to convert a Multikey to an ssh_key::PublicKey fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -553,7 +551,7 @@ impl<'a> ConvView for View<'a> { ), key: opaque_key_bytes, }), - pk.comment, + pk.comment.clone(), )) } @@ -1188,14 +1186,7 @@ impl<'a> VerifyView for View<'a> { /// error rather than being misverified on the wrong curve. This avoids the /// downgrade heuristic described in the security audit (M1). fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; // get the secret key bytes let key_bytes = { @@ -1469,10 +1460,10 @@ impl<'a> SealView for View<'a> { let pub_key = PublicKey::::try_from(key_bytes.as_slice()) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; let ct = pub_key - .encrypt_time_lock(SignatureSchemes::ProofOfPossession, plaintext, &id) + .encrypt_time_lock(SignatureSchemes::ProofOfPossession, plaintext, id) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; let mut out = Vec::new(); - out.append(&mut Varbytes::new(id.to_vec()).into()); + encode_varbytes_into(&mut out, &id); let ct_bytes: Vec = Vec::try_from(&ct) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; out.extend_from_slice(&ct_bytes); @@ -1482,10 +1473,10 @@ impl<'a> SealView for View<'a> { let pub_key = PublicKey::::try_from(key_bytes.as_slice()) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; let ct = pub_key - .encrypt_time_lock(SignatureSchemes::ProofOfPossession, plaintext, &id) + .encrypt_time_lock(SignatureSchemes::ProofOfPossession, plaintext, id) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; let mut out = Vec::new(); - out.append(&mut Varbytes::new(id.to_vec()).into()); + encode_varbytes_into(&mut out, &id); let ct_bytes: Vec = Vec::try_from(&ct) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; out.extend_from_slice(&ct_bytes); diff --git a/src/views/bls12381_g1_fndsa512.rs b/src/views/bls12381_g1_fndsa512.rs index 09c6065..9b48929 100644 --- a/src/views/bls12381_g1_fndsa512.rs +++ b/src/views/bls12381_g1_fndsa512.rs @@ -7,7 +7,7 @@ //! Public encoding is classical-first: bls_g1_pub (96) || fn_dsa_verifying_key. use super::bls12381_hybrid as bls; -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -195,13 +195,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/bls12381_g1_mayo1.rs b/src/views/bls12381_g1_mayo1.rs index c3da4c8..baf2deb 100644 --- a/src/views/bls12381_g1_mayo1.rs +++ b/src/views/bls12381_g1_mayo1.rs @@ -7,17 +7,17 @@ //! Public encoding is classical-first: bls_g1_pub (96) || mayo1_pub. use super::bls12381_hybrid as bls; -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, error::{AttributesError, ConversionsError, SignError, VerifyError}, }; -use ml_dsa::signature::{Signer, Verifier}; use multi_codec::Codec; use multi_hash::{Multihash, mh}; use multi_sig::{Multisig, ViewBuilder, ms}; use pq_mayo::{KeyPair, Mayo1, Signature, VerifyingKey}; +use signature::{Signer, Verifier}; use zeroize::Zeroizing; const MAYO1_SEED_LEN: usize = 24; @@ -183,13 +183,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/bls12381_g1_mayo2.rs b/src/views/bls12381_g1_mayo2.rs index c4f86f6..5cf7c7a 100644 --- a/src/views/bls12381_g1_mayo2.rs +++ b/src/views/bls12381_g1_mayo2.rs @@ -7,17 +7,17 @@ //! Public encoding is classical-first: bls_g1_pub (96) || mayo2_pub. use super::bls12381_hybrid as bls; -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, error::{AttributesError, ConversionsError, SignError, VerifyError}, }; -use ml_dsa::signature::{Signer, Verifier}; use multi_codec::Codec; use multi_hash::{Multihash, mh}; use multi_sig::{Multisig, ViewBuilder, ms}; use pq_mayo::{KeyPair, Mayo2, Signature, VerifyingKey}; +use signature::{Signer, Verifier}; use zeroize::Zeroizing; const MAYO2_SEED_LEN: usize = 24; @@ -183,13 +183,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/bls12381_g1_mldsa65.rs b/src/views/bls12381_g1_mldsa65.rs index fee2c1f..6e388e2 100644 --- a/src/views/bls12381_g1_mldsa65.rs +++ b/src/views/bls12381_g1_mldsa65.rs @@ -6,7 +6,7 @@ //! Public encoding is classical-first: bls_g1_pub (96) || mldsa65_pub. use super::bls12381_hybrid as bls; -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -195,13 +195,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/builder.rs b/src/views/builder.rs index 4256111..6fa49bb 100644 --- a/src/views/builder.rs +++ b/src/views/builder.rs @@ -3,14 +3,13 @@ //! Builder-pattern view creation over Multikeys. //! //! A [`ViewBuilder`] selects one view kind, optionally registers local codec -//! factories for that kind, and builds the view. It is the builder-pattern -//! alternative to the many `*_view()` methods of the deprecated -//! [`crate::Views`] trait: it dispatches through the same dispatch core and -//! covers standard codecs and custom protocol keys alike. +//! factories for that kind, and builds the view. It covers standard codecs +//! and custom protocol keys alike. //! //! The three steps for a standard key: //! //! ``` +//! # #[cfg(feature = "curve25519")] { //! use multi_codec::Codec; //! use multi_key::{Builder, ViewBuilder}; //! @@ -24,6 +23,7 @@ //! let sig = signer.sign(b"message", false, None).unwrap(); //! let verifier = ViewBuilder::new(&mk).verify().build().unwrap(); //! verifier.verify(&sig, Some(b"message")).unwrap(); +//! # } //! ``` //! //! A custom protocol key (codec [`multi_codec::Codec::Identity`] carrying the @@ -48,7 +48,7 @@ //! .attributes //! .insert(AttrId::KeyType, Zeroizing::new(vec![1])); //! // decode the key from wire bytes, the way a custom key usually arrives -//! let bytes: Vec = custom.clone().into(); +//! let bytes = Vec::::from(&custom); //! let decoded = Multikey::try_from(bytes.as_ref()).unwrap(); //! //! struct CustomSign { @@ -108,9 +108,7 @@ //! # Second key lifetime //! //! The `cipher` and `kdf` kinds store their second key by reference for the -//! builder's lifetime (`'mk`). This is slightly narrower than -//! [`crate::Views::cipher_view`]/[`crate::Views::kdf_view`], which let the -//! caller choose the overlap of the two borrows at the call site. +//! builder's lifetime (`'mk`). use crate::{ AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView, KdfAttrView, @@ -411,11 +409,11 @@ type MerkleStateFactories = BTreeMap; /// /// # Dispatch order /// -/// 1. The built-in views dispatch first, exactly as the `Views` trait -/// methods do: on the Multikey's codec, on the second key's codec for the -/// `cipher` and `kdf` kinds, and on the attribute-derived codec (the -/// `CipherCodec`/`KdfCodec` attribute value, falling back to the key's -/// codec) for the `cipher_attr` and `kdf_attr` kinds. +/// 1. The built-in views dispatch first: on the Multikey's codec, on the +/// second key's codec for the `cipher` and `kdf` kinds, and on the +/// attribute-derived codec (the `CipherCodec`/`KdfCodec` attribute value, +/// falling back to the key's codec) for the `cipher_attr` and `kdf_attr` +/// kinds. /// 2. A local factory registered for the codec that has no built-in view /// applies on fallthrough only. The factory lookup key is the codec the /// built-in dispatch actually failed on: the codec carried by the @@ -443,9 +441,8 @@ type MerkleStateFactories = BTreeMap; /// `Send + Sync + 'static`, so a factory cannot capture the builder's borrows; /// it works from owned state (a clone, an `Arc`, static data). /// -/// Note that the returned view boxes carry no `Send`/`Sync` supertrait, same -/// as the `Views` trait today. The builder itself is `Send + Sync` with and -/// without factories. +/// Note that the returned view boxes carry no `Send`/`Sync` supertrait. The +/// builder itself is `Send + Sync` with and without factories. pub struct ViewBuilder<'mk, K: ViewKind = Unselected> { /// the Multikey being viewed mk: &'mk Multikey, @@ -1229,7 +1226,18 @@ impl<'mk> ViewBuilder<'mk, MerkleStateKind> { #[cfg(test)] mod tests { use super::*; - use crate::{AttrId, Builder, KEY_CODECS, ThresholdDisclosure}; + #[cfg(all( + feature = "bls12-381", + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" + ))] + use crate::KEY_CODECS; + #[cfg(feature = "curve25519")] + use crate::ThresholdDisclosure; + use crate::{AttrId, Builder}; use multi_sig::Multisig; use std::sync::{Arc, Mutex}; use zeroize::Zeroizing; @@ -1244,6 +1252,14 @@ mod tests { rec.lock().unwrap().push(entry.to_string()); } + /// the key's `AlgorithmName` attribute, or "" when absent or not UTF-8 + fn algorithm_name(mk: &Multikey) -> &str { + mk.attributes + .get(&AttrId::AlgorithmName) + .and_then(|v| std::str::from_utf8(v).ok()) + .unwrap_or_default() + } + /// A custom protocol key: codec `Identity` plus the `AlgorithmName` and /// `KeyType` attributes, decoded from wire bytes the way such keys arrive. fn custom_key(algorithm_name: &str) -> Multikey { @@ -1261,6 +1277,7 @@ mod tests { Multikey::try_from(bytes.as_ref()).unwrap() } + #[cfg(feature = "curve25519")] fn ed25519_key() -> Multikey { let mut rng = rand::rng(); Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) @@ -1269,6 +1286,7 @@ mod tests { .unwrap() } + #[cfg(all(feature = "chacha20", feature = "curve25519"))] fn chacha_key() -> Multikey { let mut mk = Builder::new(Codec::Chacha20Poly1305) .with_key_bytes("chacha-key-material-32-bytes-x") @@ -1322,7 +1340,9 @@ mod tests { } } + #[cfg(feature = "curve25519")] struct NoKdfAttr; + #[cfg(feature = "curve25519")] impl KdfAttrView for NoKdfAttr { fn kdf_codec(&self) -> Result { Err(Error::UnsupportedAlgorithm("NoKdfAttr.kdf_codec".into())) @@ -1335,7 +1355,9 @@ mod tests { } } + #[cfg(feature = "curve25519")] struct NoDisclosure; + #[cfg(feature = "curve25519")] impl ThresholdDisclosureView for NoDisclosure { fn disclosure_mode(&self) -> Result { Err(Error::UnsupportedAlgorithm( @@ -1359,14 +1381,18 @@ mod tests { } } + #[cfg(all(feature = "chacha20", feature = "curve25519"))] struct NoSeal; + #[cfg(all(feature = "chacha20", feature = "curve25519"))] impl SealView for NoSeal { fn seal(&self, _: &[u8], _: Codec, _: &[u8]) -> Result<(Vec, Option), Error> { Err(Error::UnsupportedAlgorithm("NoSeal.seal".into())) } } + #[cfg(all(feature = "chacha20", feature = "curve25519"))] struct NoOpen; + #[cfg(all(feature = "chacha20", feature = "curve25519"))] impl OpenView for NoOpen { fn open( &self, @@ -1378,8 +1404,16 @@ mod tests { } } + #[cfg(all( + feature = "bls12-381", + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" + ))] #[test] - fn test_builder_matches_shim() { + fn test_builder_matches_dispatch() { for codec in KEY_CODECS { let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(codec, &mut rng) @@ -1387,80 +1421,87 @@ mod tests { .try_build() .unwrap(); - // the basic-attributes view is identical to the shim (which - // delegates to the dispatch core) - let shim = dispatch_attr_view(&mk).unwrap(); + // the basic-attributes view is identical to the dispatch core's + let direct = dispatch_attr_view(&mk).unwrap(); let built = ViewBuilder::new(&mk).attr().build().unwrap(); - assert_eq!(shim.is_encrypted(), built.is_encrypted()); - assert_eq!(shim.is_public_key(), built.is_public_key()); - assert_eq!(shim.is_secret_key(), built.is_secret_key()); - assert_eq!(shim.is_secret_key_share(), built.is_secret_key_share()); + assert_eq!(direct.is_encrypted(), built.is_encrypted()); + assert_eq!(direct.is_public_key(), built.is_public_key()); + assert_eq!(direct.is_secret_key(), built.is_secret_key()); + assert_eq!(direct.is_secret_key_share(), built.is_secret_key_share()); - // the key-data view is identical to the shim - let shim = dispatch_data_view(&mk).unwrap(); + // the key-data view is identical to the dispatch core's + let direct = dispatch_data_view(&mk).unwrap(); let built = ViewBuilder::new(&mk).data().build().unwrap(); - assert_eq!(shim.key_bytes().unwrap(), built.key_bytes().unwrap()); - assert_eq!(shim.secret_bytes().unwrap(), built.secret_bytes().unwrap()); + assert_eq!(direct.key_bytes().unwrap(), built.key_bytes().unwrap()); + assert_eq!( + direct.secret_bytes().unwrap(), + built.secret_bytes().unwrap() + ); } } + #[cfg(all(feature = "chacha20", feature = "curve25519"))] #[test] - fn test_cipher_attr_matches_shim() { + fn test_cipher_attr_matches_dispatch() { let mk = chacha_key(); // built-in path: both construct the chacha20 view - let shim = dispatch_cipher_attr_view(&mk).unwrap(); + let direct = dispatch_cipher_attr_view(&mk).unwrap(); let built = ViewBuilder::new(&mk).cipher_attr().build().unwrap(); - assert_eq!(shim.cipher_codec().unwrap(), built.cipher_codec().unwrap()); assert_eq!( - shim.nonce_bytes().unwrap().len(), + direct.cipher_codec().unwrap(), + built.cipher_codec().unwrap() + ); + assert_eq!( + direct.nonce_bytes().unwrap().len(), built.nonce_bytes().unwrap().len() ); - assert_eq!(shim.key_length().unwrap(), built.key_length().unwrap()); + assert_eq!(direct.key_length().unwrap(), built.key_length().unwrap()); - // a signing codec has no cipher-attributes view: same error as the shim + // a signing codec has no cipher-attributes view: same error as dispatch let mk = ed25519_key(); - let shim = dispatch_cipher_attr_view(&mk).err().unwrap(); + let direct = dispatch_cipher_attr_view(&mk).err().unwrap(); let built = ViewBuilder::new(&mk).cipher_attr().build().err().unwrap(); - assert_eq!(shim.to_string(), built.to_string()); + assert_eq!(direct.to_string(), built.to_string()); - // a signing codec has no kdf-attributes view: same error as the shim - let shim = dispatch_kdf_attr_view(&mk).err().unwrap(); + // a signing codec has no kdf-attributes view: same error as dispatch + let direct = dispatch_kdf_attr_view(&mk).err().unwrap(); let built = ViewBuilder::new(&mk).kdf_attr().build().err().unwrap(); - assert_eq!(shim.to_string(), built.to_string()); + assert_eq!(direct.to_string(), built.to_string()); } + #[cfg(feature = "curve25519")] #[test] - fn test_unsupported_matches_shim_error() { + fn test_unsupported_matches_dispatch_error() { // a DKG share codec has no built-in sign view let mk = Builder::new(Codec::Ed25519ThreshPrivShare) .with_key_bytes("share") .try_build() .unwrap(); - let shim = dispatch_sign_view(&mk).err().unwrap(); + let direct = dispatch_sign_view(&mk).err().unwrap(); let built = ViewBuilder::new(&mk).sign().build().err().unwrap(); - assert_eq!(shim.to_string(), built.to_string()); + assert_eq!(direct.to_string(), built.to_string()); assert!(matches!( built, Error::Conversions(ConversionsError::UnsupportedCodec(_)) )); // the same key also has no threshold view - let shim = dispatch_threshold_view(&mk).err().unwrap(); + let direct = dispatch_threshold_view(&mk).err().unwrap(); let built = ViewBuilder::new(&mk).threshold().build().err().unwrap(); - assert_eq!(shim.to_string(), built.to_string()); + assert_eq!(direct.to_string(), built.to_string()); // a signing codec is not a seal/open key: exact variant preserved let mk = ed25519_key(); - let shim = dispatch_seal_view(&mk).err().unwrap(); + let direct = dispatch_seal_view(&mk).err().unwrap(); let built = ViewBuilder::new(&mk).seal().build().err().unwrap(); - assert_eq!(shim.to_string(), built.to_string()); + assert_eq!(direct.to_string(), built.to_string()); assert!(matches!(built, Error::Seal(SealError::NotEncryptionKey))); // a signing codec has no merkle state either - let shim = dispatch_merkle_state_view(&mk).err().unwrap(); + let direct = dispatch_merkle_state_view(&mk).err().unwrap(); let built = ViewBuilder::new(&mk).merkle_state().build().err().unwrap(); - assert_eq!(shim.to_string(), built.to_string()); + assert_eq!(direct.to_string(), built.to_string()); } #[test] @@ -1471,12 +1512,7 @@ mod tests { let view = ViewBuilder::new(&mk) .sign() .with_local_codec(Codec::Identity, move |key: &Multikey| { - let name = key - .attributes - .get(&AttrId::AlgorithmName) - .map(|v| v.as_slice().to_vec()) - .unwrap_or_default(); - record(&factory_rec, &String::from_utf8(name).unwrap_or_default()); + record(&factory_rec, algorithm_name(key)); Ok(Box::new(NoSign)) }) .build() @@ -1499,23 +1535,9 @@ mod tests { let view = ViewBuilder::new(&mk) .cipher(&ck) .with_local_codec(Codec::Identity, move |mk: &Multikey, cipher: &Multikey| { - let viewed = mk - .attributes - .get(&AttrId::AlgorithmName) - .map(|v| v.as_slice().to_vec()) - .unwrap_or_default(); - let attached = cipher - .attributes - .get(&AttrId::AlgorithmName) - .map(|v| v.as_slice().to_vec()) - .unwrap_or_default(); record( &factory_rec, - &format!( - "{}|{}", - String::from_utf8(viewed).unwrap_or_default(), - String::from_utf8(attached).unwrap_or_default() - ), + &format!("{}|{}", algorithm_name(mk), algorithm_name(cipher)), ); Ok(Box::new(NoCipher)) }) @@ -1542,23 +1564,9 @@ mod tests { let view = ViewBuilder::new(&mk) .kdf(&kk) .with_local_codec(Codec::Identity, move |mk: &Multikey, kdf: &Multikey| { - let viewed = mk - .attributes - .get(&AttrId::AlgorithmName) - .map(|v| v.as_slice().to_vec()) - .unwrap_or_default(); - let attached = kdf - .attributes - .get(&AttrId::AlgorithmName) - .map(|v| v.as_slice().to_vec()) - .unwrap_or_default(); record( &factory_rec, - &format!( - "{}|{}", - String::from_utf8(viewed).unwrap_or_default(), - String::from_utf8(attached).unwrap_or_default() - ), + &format!("{}|{}", algorithm_name(mk), algorithm_name(kdf)), ); Ok(Box::new(NoKdf)) }) @@ -1576,6 +1584,7 @@ mod tests { ); } + #[cfg(feature = "curve25519")] #[test] fn test_builtin_wins() { let mk = ed25519_key(); @@ -1593,8 +1602,8 @@ mod tests { // a factory registered for a supported standard codec is never called assert!(rec.lock().unwrap().is_empty()); - // the built-in view works: sign with the builder, verify with the - // shim's dispatch path + // the built-in view works: sign with the builder, verify through + // the dispatch core directly let sig = signer.sign(b"hello", false, None).unwrap(); dispatch_verify_view(&mk) .unwrap() @@ -1651,6 +1660,7 @@ mod tests { assert_eq!(err.to_string(), "Unsupported key algorithm: NoSign"); } + #[cfg(feature = "curve25519")] #[test] fn test_cipher_attr_factory_lookup_key() { // the key's CipherCodec attribute names Ed25519Pub: that derived codec @@ -1691,17 +1701,18 @@ mod tests { }) .build(); assert!(rec.lock().unwrap().is_empty()); - // the fallthrough error matches the shim and names the derived codec + // the fallthrough error matches dispatch and names the derived codec assert!(matches!( &built, Err(Error::Cipher(CipherError::UnsupportedCodec( Codec::Ed25519Pub ))) )); - let shim = dispatch_cipher_attr_view(&mk).err().unwrap(); - assert_eq!(shim.to_string(), built.err().unwrap().to_string()); + let direct = dispatch_cipher_attr_view(&mk).err().unwrap(); + assert_eq!(direct.to_string(), built.err().unwrap().to_string()); } + #[cfg(feature = "curve25519")] #[test] fn test_kdf_attr_factory_lookup_key() { // the key's KdfCodec attribute names Ed25519Priv: that derived codec @@ -1713,15 +1724,15 @@ mod tests { Zeroizing::new(Vec::from(Codec::Ed25519Priv)), ); - // no factory: the fallthrough error matches the shim and names the + // no factory: the fallthrough error matches dispatch and names the // derived codec - let shim = dispatch_kdf_attr_view(&mk).err().unwrap(); + let direct = dispatch_kdf_attr_view(&mk).err().unwrap(); let built = ViewBuilder::new(&mk).kdf_attr().build().err().unwrap(); assert!(matches!( built, Error::Kdf(KdfError::UnsupportedCodec(Codec::Ed25519Priv)) )); - assert_eq!(shim.to_string(), built.to_string()); + assert_eq!(direct.to_string(), built.to_string()); // a factory registered for the derived codec applies let rec = recorder(); @@ -1781,6 +1792,7 @@ mod tests { ); } + #[cfg(feature = "curve25519")] #[test] fn test_disclosure_factory_never_consulted() { let mk = ed25519_key(); @@ -1802,6 +1814,7 @@ mod tests { assert_eq!(mode, ThresholdDisclosure::Full); } + #[cfg(feature = "curve25519")] #[test] fn test_send_sync() { fn assert_send() {} @@ -1839,6 +1852,7 @@ mod tests { }); } + #[cfg(all(feature = "chacha20", feature = "curve25519"))] #[test] fn test_seal_open_factory_keyed_on_key_codec() { // a signing codec is not an encryption key: seal and open fall @@ -1888,6 +1902,7 @@ mod tests { )); } + #[cfg(feature = "curve25519")] #[test] fn test_cipher_kdf_factory_lookup_key_is_second_key_codec() { // the cipher and kdf kinds dispatch on the SECOND key's codec, so the @@ -1907,7 +1922,7 @@ mod tests { assert!(matches!(built, Err(Error::UnsupportedAlgorithm(s)) if s == "second-key factory")); // a factory keyed on the VIEWED key's codec is never consulted; the - // fallthrough error matches the shim and names the second key's codec + // fallthrough error matches dispatch and names the second key's codec let rec = recorder(); let factory_rec = rec.clone(); let built = ViewBuilder::new(&mk) @@ -1924,8 +1939,8 @@ mod tests { Codec::Ed25519Priv ))) )); - let shim = dispatch_cipher_view(&mk, &ck).err().unwrap(); - assert_eq!(shim.to_string(), built.err().unwrap().to_string()); + let direct = dispatch_cipher_view(&mk, &ck).err().unwrap(); + assert_eq!(direct.to_string(), built.err().unwrap().to_string()); // the kdf kind keys on the kdf key's codec the same way let rec = recorder(); diff --git a/src/views/chacha20.rs b/src/views/chacha20.rs index 085ca55..8043956 100644 --- a/src/views/chacha20.rs +++ b/src/views/chacha20.rs @@ -18,16 +18,10 @@ use multi_trait::TryDecodeFrom; use multi_util::Varuint; use zeroize::Zeroizing; -use super::bcrypt::SALT_LENGTH; +use crate::kdf::SALT_LENGTH; pub const KEY_SIZE: usize = poly1305::KEY_SIZE; -/// Return the length of the [Nonce] -#[cfg(test)] -pub(crate) fn nonce_length() -> usize { - Nonce::default().len() -} - pub(crate) struct View<'a> { mk: &'a Multikey, cipher: Option<&'a Multikey>, @@ -319,9 +313,9 @@ impl<'a> CipherView for View<'a> { .insert(AttrId::CipherCodec, cipher_codec.into()); res.attributes .insert(AttrId::CipherKeyLen, key_length.into()); - res.attributes.insert(AttrId::CipherNonce, nonce.clone()); + res.attributes.insert(AttrId::CipherNonce, nonce); res.attributes.insert(AttrId::KdfCodec, kdf_codec.into()); - res.attributes.insert(AttrId::KdfSalt, salt.clone()); + res.attributes.insert(AttrId::KdfSalt, salt); res.attributes.insert(AttrId::KdfRounds, rounds.into()); Ok(res) } diff --git a/src/views/classic_mceliece.rs b/src/views/classic_mceliece.rs index 74a42d1..078bbca 100644 --- a/src/views/classic_mceliece.rs +++ b/src/views/classic_mceliece.rs @@ -3,15 +3,18 @@ //! //! # Deprecation //! -//! This view is deprecated. Key-recovery attacks now solve the TII McEliece -//! challenges; see for the -//! recovered keys. It compiles only with the `deprecated` feature. +//! This view is deprecated: 2026 structural key-recovery attacks on binary +//! Goppa codes led Germany's BSI to advise against Classic McEliece in new +//! developments. See the crate-level Deprecations section for the papers and +//! the BSI note. It compiles only with the `deprecated` feature. //! //! Note: mceliece460896 codec entries exist in the codec table for future -//! interoperability, but only mceliece348864 is currently supported for key -//! generation and operations. The upstream `classic-mceliece-rust` crate uses -//! mutually exclusive feature flags per variant, preventing both from being -//! compiled in a single dependency graph. +//! interoperability, but only mceliece348864 is wired up for key generation +//! and operations. +//! +//! The secret key is a 32-byte seed. The Classic McEliece key seed is the +//! first 32 bytes of a ChaCha20 stream keyed by it, which reproduces the keys +//! the earlier `mceliece348864` crate generated from the same seed. use crate::views::dispatch::dispatch_data_view; use crate::{ @@ -24,12 +27,28 @@ use multi_codec::Codec; use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; -use rand_chacha::ChaCha20Rng; -use rand_core_06::SeedableRng; +use pq_mceliece::{Algorithm, DecapsulationKey, EncapsulationKey}; use zeroize::Zeroizing; const MCELIECE_SEED_LENGTH: usize = 32; +/// The Classic McEliece parameter set these views use. +pub(crate) const ALGORITHM: Algorithm = Algorithm::McEliece348864; + +/// Rebuild the keypair from a stored 32-byte seed: the key seed is the first +/// 32 bytes of the ChaCha20 stream the stored seed keys. +pub(crate) fn keypair_from_seed( + seed: [u8; MCELIECE_SEED_LENGTH], +) -> Result<(EncapsulationKey, DecapsulationKey), Error> { + use chacha20::ChaCha20Rng; + use rand_core::{Rng, SeedableRng}; + let mut key_seed = Zeroizing::new([0u8; MCELIECE_SEED_LENGTH]); + ChaCha20Rng::from_seed(seed).fill_bytes(key_seed.as_mut()); + ALGORITHM + .generate_keypair_from_seed(key_seed.as_ref()) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()).into()) +} + /// Decoded sealed message: (kem_ct, aead_codec, nonce, ciphertext+tag) type SealedParts = (Vec, Codec, Vec, Vec); @@ -108,13 +127,11 @@ impl<'a> ConvView for View<'a> { .try_into() .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; - let mut rng = ChaCha20Rng::from_seed(seed); - let (pk, _sk) = mceliece348864::keypair_boxed(&mut rng); - let pub_bytes: Vec = pk.as_ref().to_vec(); + let (ek, _dk) = keypair_from_seed(seed)?; Builder::new(Codec::Mceliece348864Pub) .with_comment(&self.mk.comment) - .with_key_bytes(&pub_bytes) + .with_key_bytes(ek.value()) .try_build() } @@ -148,16 +165,6 @@ impl<'a> FingerprintView for View<'a> { } } -/// Encode a sealed message: [kem_ct Varbytes][aead_codec Codec][nonce Varbytes][ct+tag Varbytes] -fn encode_sealed(kem_ct: &[u8], aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(kem_ct.to_vec()).into()); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - /// Decode a sealed message: [kem_ct Varbytes][aead_codec Codec][nonce Varbytes][ct+tag Varbytes] fn decode_sealed(data: &[u8]) -> Result { let (kem_ct, ptr) = Varbytes::try_decode_from(data) @@ -192,15 +199,14 @@ impl<'a> SealView for View<'a> { let pub_bytes = self.key_bytes()?; - let pk_array: Box<[u8; mceliece348864::CRYPTO_PUBLICKEYBYTES]> = pub_bytes - .to_vec() - .into_boxed_slice() - .try_into() - .map_err(|_| SealError::EncapsulationFailed("invalid public key size".into()))?; - let pk = mceliece348864::PublicKey::from(pk_array); - let (ct, ss) = mceliece348864::encapsulate_boxed(&pk, &mut rand_core_06::OsRng); - let kem_ct: &[u8] = ct.as_ref(); - let shared_secret: &[u8] = ss.as_ref(); + let ek = ALGORITHM + .encapsulation_key_from_bytes(&pub_bytes) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let (ct, ss) = ALGORITHM + .encapsulate(&ek, rand::rng()) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let kem_ct = ct.value(); + let shared_secret = ss.value(); // Derive AEAD key from shared secret via HKDF let key_len = aead::key_size(aead_codec)?; @@ -209,7 +215,10 @@ impl<'a> SealView for View<'a> { // Encrypt plaintext let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; - Ok((encode_sealed(kem_ct, aead_codec, &nonce, &ct_tag), None)) + Ok(( + aead::encode_kem_sealed(kem_ct, aead_codec, &nonce, &ct_tag), + None, + )) } } @@ -243,15 +252,14 @@ impl<'a> OpenView for View<'a> { .try_into() .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; - let mut rng = ChaCha20Rng::from_seed(seed); - let (_pk, sk) = mceliece348864::keypair_boxed(&mut rng); - - let ct_array: [u8; mceliece348864::CRYPTO_CIPHERTEXTBYTES] = kem_ct - .try_into() - .map_err(|_| SealError::DecapsulationFailed("invalid ciphertext size".into()))?; - let ct = mceliece348864::Ciphertext::from(ct_array); - let ss = mceliece348864::decapsulate_boxed(&ct, &sk); - let shared_secret: &[u8] = ss.as_ref(); + let (_ek, dk) = keypair_from_seed(seed)?; + let ct = ALGORITHM + .ciphertext_from_bytes(&kem_ct) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + let ss = ALGORITHM + .decapsulate(&dk, &ct) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + let shared_secret = ss.value(); // Derive AEAD key from shared secret via HKDF let key_len = aead::key_size(aead_codec)?; @@ -270,6 +278,30 @@ mod tests { use crate::ViewBuilder; use crate::mk::MCELIECE_KEY_CODECS; + /// keys rebuilt from a stored seed match the ones the earlier + /// `mceliece348864` crate derived from it (golden bytes from that crate) + #[test] + fn test_seed_derivation_matches_previous_crate() { + for (seed, head, tail) in [ + ( + [7u8; 32], + "51065e02c66c81226bc150f59ae125df", + "cc84d13ac4a8ca24f7076a0e4f6b352b", + ), + ( + [0x5au8; 32], + "d717a6a0c43bdbb9588eb545255da943", + "292a7ca6b221011d5543933a060d4d4e", + ), + ] { + let (ek, _dk) = keypair_from_seed(seed).unwrap(); + let pk = ek.value(); + assert_eq!(pk.len(), 261_120); + assert_eq!(hex::encode(&pk[..16]), head); + assert_eq!(hex::encode(&pk[pk.len() - 16..]), tail); + } + } + #[test] fn test_mceliece_key_gen_roundtrip() { for codec in MCELIECE_KEY_CODECS { diff --git a/src/views/dispatch.rs b/src/views/dispatch.rs index 074a1c5..b2082d8 100644 --- a/src/views/dispatch.rs +++ b/src/views/dispatch.rs @@ -2,32 +2,111 @@ //! The shared Multikey view dispatch core. //! -//! One constructor function per view kind dispatches on the Multikey codec -//! exactly as the original `impl Views for Multikey` bodies did, so the -//! deprecated `Views` shim and the `ViewBuilder` share one implementation. +//! One constructor function per view kind dispatches on the Multikey codec; +//! the `ViewBuilder` builds every built-in view through it. -#[cfg(feature = "xmss")] -use crate::views::xmss; +#[cfg(feature = "bcrypt")] +use crate::views::bcrypt; +#[cfg(feature = "bls12-381")] +use crate::views::bls12381; +#[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] +use crate::views::bls12381_g1_fndsa512; +#[cfg(all(feature = "bls12-381", feature = "mayo"))] +use crate::views::bls12381_g1_mayo1; +#[cfg(all(feature = "bls12-381", feature = "mayo"))] +use crate::views::bls12381_g1_mayo2; +#[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] +use crate::views::bls12381_g1_mldsa65; +#[cfg(feature = "chacha20")] +use crate::views::chacha20; #[cfg(feature = "deprecated")] -use crate::views::{classic_mceliece, x25519_mceliece348864}; +use crate::views::classic_mceliece; +#[cfg(feature = "curve25519")] +use crate::views::ed25519; +#[cfg(all(feature = "curve25519", feature = "fn-dsa"))] +use crate::views::ed25519_fndsa512; +#[cfg(all(feature = "curve25519", feature = "mayo"))] +use crate::views::ed25519_mayo2; +#[cfg(all(feature = "curve25519", feature = "ml-dsa"))] +use crate::views::ed25519_mldsa65; +#[cfg(feature = "fn-dsa")] +use crate::views::fn_dsa; +#[cfg(feature = "frodokem")] +use crate::views::frodokem; +#[cfg(feature = "lamport")] +use crate::views::lamport; #[cfg(feature = "lamport")] -use crate::views::{lamport, lamport_merkle}; +use crate::views::lamport_merkle; +#[cfg(feature = "mayo")] +use crate::views::mayo; +#[cfg(feature = "ml-dsa")] +use crate::views::ml_dsa; +#[cfg(feature = "ml-kem")] +use crate::views::ml_kem; +#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] +use crate::views::nist_p; +#[cfg(feature = "rsa")] +use crate::views::rsa; +#[cfg(feature = "secp256k1")] +use crate::views::secp256k1; +#[cfg(feature = "slh-dsa")] +use crate::views::slh_dsa; +#[cfg(feature = "sntrup")] +use crate::views::sntrup; +#[cfg(feature = "curve25519")] +use crate::views::x25519; +#[cfg(all(feature = "curve25519", feature = "frodokem"))] +use crate::views::x25519_frodokem640; +#[cfg(all(feature = "curve25519", feature = "deprecated"))] +use crate::views::x25519_mceliece348864; +#[cfg(all(feature = "curve25519", feature = "ml-kem"))] +use crate::views::x25519_mlkem768; +#[cfg(all(feature = "curve25519", feature = "sntrup"))] +use crate::views::x25519_sntrup761; +#[cfg(feature = "curve25519")] +use crate::views::xeddsa; +#[cfg(feature = "xmss")] +use crate::views::xmss; use crate::{ AttrId, AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView, KdfAttrView, KdfView, MerkleStateView, Multikey, OpenView, SealView, SignView, ThresholdAttrView, ThresholdDisclosureView, ThresholdKeyView, ThresholdView, VerifyView, error::{AttributesError, CipherError, ConversionsError, KdfError, SealError}, - views::{ - bcrypt, bls12381, bls12381_g1_fndsa512, bls12381_g1_mayo1, bls12381_g1_mayo2, - bls12381_g1_mldsa65, chacha20, ed25519, ed25519_fndsa512, ed25519_mayo2, ed25519_mldsa65, - fn_dsa, frodokem, mayo, ml_dsa, ml_kem, nist_p, rsa, secp256k1, slh_dsa, sntrup, - threshold_meta, x25519, x25519_frodokem640, x25519_mlkem768, x25519_sntrup761, xeddsa, - }, + views::threshold_meta, }; use multi_codec::Codec; + +/// The public form of `mk`: the key itself when it is already public, +/// otherwise the public key derived through its conversion view. Verify and +/// SSH-export paths use it so they never clone a secret key. +#[cfg(any( + feature = "bls12-381", + feature = "curve25519", + feature = "fn-dsa", + feature = "lamport", + feature = "mayo", + feature = "ml-dsa", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "rsa", + feature = "secp256k1", + feature = "slh-dsa", + feature = "xmss" +))] +pub(crate) fn public_form(mk: &Multikey) -> Result, Error> { + use std::borrow::Cow; + if dispatch_attr_view(mk)?.is_secret_key() { + Ok(Cow::Owned(dispatch_conv_view(mk)?.to_public_key()?)) + } else { + Ok(Cow::Borrowed(mk)) + } +} + /// Builds the basic-attributes view for the viewed Multikey. pub(crate) fn dispatch_attr_view<'a>(mk: &'a Multikey) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1PrivShare | Codec::Bls12381G1Priv | Codec::Bls12381G1Pub @@ -36,9 +115,13 @@ pub(crate) fn dispatch_attr_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), + #[cfg(feature = "chacha20")] Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(mk)?)), + #[cfg(feature = "slh-dsa")] Codec::SlhDsaSha2128FPub | Codec::SlhDsaSha2128SPub | Codec::SlhDsaSha2192FPub @@ -63,9 +146,11 @@ pub(crate) fn dispatch_attr_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(slh_dsa::View::try_from(mk)?)), + #[cfg(feature = "ml-dsa")] Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(mk)?)) } + #[cfg(feature = "mayo")] Codec::Mayo1Pub | Codec::Mayo1Priv | Codec::Mayo2Pub @@ -74,12 +159,15 @@ pub(crate) fn dispatch_attr_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(mayo::View::try_from(mk)?)), + #[cfg(feature = "fn-dsa")] Codec::FnDsa512Pub | Codec::FnDsa512Priv | Codec::FnDsa1024Pub | Codec::FnDsa1024Priv => { Ok(Box::new(fn_dsa::View::try_from(mk)?)) } + #[cfg(feature = "ml-kem")] Codec::Mlkem768Pub | Codec::Mlkem768Priv | Codec::Mlkem1024Pub | Codec::Mlkem1024Priv => { Ok(Box::new(ml_kem::View::try_from(mk)?)) } + #[cfg(feature = "sntrup")] Codec::Sntrup761Pub | Codec::Sntrup761Priv | Codec::Sntrup857Pub @@ -95,6 +183,7 @@ pub(crate) fn dispatch_attr_view<'a>(mk: &'a Multikey) -> Result { Ok(Box::new(classic_mceliece::View::try_from(mk)?)) } + #[cfg(feature = "frodokem")] Codec::FrodoKem640AesPub | Codec::FrodoKem640AesPriv | Codec::FrodoKem976AesPub @@ -107,51 +196,63 @@ pub(crate) fn dispatch_attr_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(frodokem::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(mk)?)), + #[cfg(all(feature = "curve25519", feature = "sntrup"))] Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { Ok(Box::new(x25519_sntrup761::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "frodokem"))] Codec::X25519Frodokem640AesPub | Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePub | Codec::X25519Frodokem640ShakePriv => { Ok(Box::new(x25519_frodokem640::View::try_from(mk)?)) } - #[cfg(feature = "deprecated")] + #[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { Ok(Box::new(x25519_mceliece348864::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-kem"))] Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { Ok(Box::new(x25519_mlkem768::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "mayo"))] Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { Ok(Box::new(ed25519_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-dsa"))] Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { Ok(Box::new(ed25519_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { Ok(Box::new(bls12381_g1_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { Ok(Box::new(bls12381_g1_fndsa512::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { Ok(Box::new(bls12381_g1_mayo1::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { Ok(Box::new(bls12381_g1_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "fn-dsa"))] Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { Ok(Box::new(ed25519_fndsa512::View::try_from(mk)?)) } - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub @@ -248,6 +349,7 @@ pub(crate) fn dispatch_cipher_attr_view<'a>( mk.codec }; match codec { + #[cfg(feature = "chacha20")] Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(mk)?)), _ => Err(CipherError::UnsupportedCodec(codec).into()), } @@ -255,6 +357,7 @@ pub(crate) fn dispatch_cipher_attr_view<'a>( /// Builds the key-data view for the viewed Multikey. pub(crate) fn dispatch_data_view<'a>(mk: &'a Multikey) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1PrivShare | Codec::Bls12381G1Priv | Codec::Bls12381G1Pub @@ -263,9 +366,13 @@ pub(crate) fn dispatch_data_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), + #[cfg(feature = "chacha20")] Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(mk)?)), + #[cfg(feature = "slh-dsa")] Codec::SlhDsaSha2128FPub | Codec::SlhDsaSha2128SPub | Codec::SlhDsaSha2192FPub @@ -290,9 +397,11 @@ pub(crate) fn dispatch_data_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(slh_dsa::View::try_from(mk)?)), + #[cfg(feature = "ml-dsa")] Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(mk)?)) } + #[cfg(feature = "mayo")] Codec::Mayo1Pub | Codec::Mayo1Priv | Codec::Mayo2Pub @@ -301,12 +410,15 @@ pub(crate) fn dispatch_data_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(mayo::View::try_from(mk)?)), + #[cfg(feature = "fn-dsa")] Codec::FnDsa512Pub | Codec::FnDsa512Priv | Codec::FnDsa1024Pub | Codec::FnDsa1024Priv => { Ok(Box::new(fn_dsa::View::try_from(mk)?)) } + #[cfg(feature = "ml-kem")] Codec::Mlkem768Pub | Codec::Mlkem768Priv | Codec::Mlkem1024Pub | Codec::Mlkem1024Priv => { Ok(Box::new(ml_kem::View::try_from(mk)?)) } + #[cfg(feature = "sntrup")] Codec::Sntrup761Pub | Codec::Sntrup761Priv | Codec::Sntrup857Pub @@ -322,6 +434,7 @@ pub(crate) fn dispatch_data_view<'a>(mk: &'a Multikey) -> Result { Ok(Box::new(classic_mceliece::View::try_from(mk)?)) } + #[cfg(feature = "frodokem")] Codec::FrodoKem640AesPub | Codec::FrodoKem640AesPriv | Codec::FrodoKem976AesPub @@ -334,51 +447,63 @@ pub(crate) fn dispatch_data_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(frodokem::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(mk)?)), + #[cfg(all(feature = "curve25519", feature = "sntrup"))] Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { Ok(Box::new(x25519_sntrup761::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "frodokem"))] Codec::X25519Frodokem640AesPub | Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePub | Codec::X25519Frodokem640ShakePriv => { Ok(Box::new(x25519_frodokem640::View::try_from(mk)?)) } - #[cfg(feature = "deprecated")] + #[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { Ok(Box::new(x25519_mceliece348864::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-kem"))] Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { Ok(Box::new(x25519_mlkem768::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "mayo"))] Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { Ok(Box::new(ed25519_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-dsa"))] Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { Ok(Box::new(ed25519_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { Ok(Box::new(bls12381_g1_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { Ok(Box::new(bls12381_g1_fndsa512::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { Ok(Box::new(bls12381_g1_mayo1::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { Ok(Box::new(bls12381_g1_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "fn-dsa"))] Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { Ok(Box::new(ed25519_fndsa512::View::try_from(mk)?)) } - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub @@ -475,6 +600,7 @@ pub(crate) fn dispatch_kdf_attr_view<'a>( mk.codec }; match codec { + #[cfg(feature = "bcrypt")] Codec::BcryptPbkdf => Ok(Box::new(bcrypt::View::try_from(mk)?)), _ => Err(KdfError::UnsupportedCodec(codec).into()), } @@ -484,6 +610,7 @@ pub(crate) fn dispatch_threshold_attr_view<'a>( mk: &'a Multikey, ) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1PrivShare | Codec::Bls12381G1Priv | Codec::Bls12381G1Pub @@ -524,14 +651,18 @@ pub(crate) fn dispatch_cipher_view<'a>( mk: &'a Multikey, cipher: &'a Multikey, ) -> Result, Error> { - match cipher.codec { - Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::new(mk, cipher))), - _ => Err(CipherError::UnsupportedCodec(cipher.codec).into()), + // the tuple carries the parameters into the arms, so a build whose features + // gate every arm out still uses them + match (cipher.codec, mk) { + #[cfg(feature = "chacha20")] + (Codec::Chacha20Poly1305, mk) => Ok(Box::new(chacha20::View::new(mk, cipher))), + (codec, _) => Err(CipherError::UnsupportedCodec(codec).into()), } } /// Builds the key-conversion view for the viewed Multikey. pub(crate) fn dispatch_conv_view<'a>(mk: &'a Multikey) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1PrivShare | Codec::Bls12381G1Priv | Codec::Bls12381G1Pub @@ -540,8 +671,11 @@ pub(crate) fn dispatch_conv_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), + #[cfg(feature = "slh-dsa")] Codec::SlhDsaSha2128FPub | Codec::SlhDsaSha2128SPub | Codec::SlhDsaSha2192FPub @@ -566,9 +700,11 @@ pub(crate) fn dispatch_conv_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(slh_dsa::View::try_from(mk)?)), + #[cfg(feature = "ml-dsa")] Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(mk)?)) } + #[cfg(feature = "mayo")] Codec::Mayo1Pub | Codec::Mayo1Priv | Codec::Mayo2Pub @@ -577,12 +713,15 @@ pub(crate) fn dispatch_conv_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(mayo::View::try_from(mk)?)), + #[cfg(feature = "fn-dsa")] Codec::FnDsa512Pub | Codec::FnDsa512Priv | Codec::FnDsa1024Pub | Codec::FnDsa1024Priv => { Ok(Box::new(fn_dsa::View::try_from(mk)?)) } + #[cfg(feature = "ml-kem")] Codec::Mlkem768Pub | Codec::Mlkem768Priv | Codec::Mlkem1024Pub | Codec::Mlkem1024Priv => { Ok(Box::new(ml_kem::View::try_from(mk)?)) } + #[cfg(feature = "sntrup")] Codec::Sntrup761Pub | Codec::Sntrup761Priv | Codec::Sntrup857Pub @@ -598,6 +737,7 @@ pub(crate) fn dispatch_conv_view<'a>(mk: &'a Multikey) -> Result { Ok(Box::new(classic_mceliece::View::try_from(mk)?)) } + #[cfg(feature = "frodokem")] Codec::FrodoKem640AesPub | Codec::FrodoKem640AesPriv | Codec::FrodoKem976AesPub @@ -610,51 +750,63 @@ pub(crate) fn dispatch_conv_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(frodokem::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(mk)?)), + #[cfg(all(feature = "curve25519", feature = "sntrup"))] Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { Ok(Box::new(x25519_sntrup761::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "frodokem"))] Codec::X25519Frodokem640AesPub | Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePub | Codec::X25519Frodokem640ShakePriv => { Ok(Box::new(x25519_frodokem640::View::try_from(mk)?)) } - #[cfg(feature = "deprecated")] + #[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { Ok(Box::new(x25519_mceliece348864::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-kem"))] Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { Ok(Box::new(x25519_mlkem768::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "mayo"))] Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { Ok(Box::new(ed25519_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-dsa"))] Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { Ok(Box::new(ed25519_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { Ok(Box::new(bls12381_g1_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { Ok(Box::new(bls12381_g1_fndsa512::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { Ok(Box::new(bls12381_g1_mayo1::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { Ok(Box::new(bls12381_g1_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "fn-dsa"))] Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { Ok(Box::new(ed25519_fndsa512::View::try_from(mk)?)) } - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub @@ -746,6 +898,7 @@ pub(crate) fn dispatch_fingerprint_view<'a>( mk: &'a Multikey, ) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1PrivShare | Codec::Bls12381G1Priv | Codec::Bls12381G1Pub @@ -754,9 +907,13 @@ pub(crate) fn dispatch_fingerprint_view<'a>( | Codec::Bls12381G2Priv | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), + #[cfg(feature = "chacha20")] Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(mk)?)), + #[cfg(feature = "slh-dsa")] Codec::SlhDsaSha2128FPub | Codec::SlhDsaSha2128SPub | Codec::SlhDsaSha2192FPub @@ -781,9 +938,11 @@ pub(crate) fn dispatch_fingerprint_view<'a>( | Codec::SlhDsaShake192SPriv | Codec::SlhDsaShake256FPriv | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(mk)?)), + #[cfg(feature = "ml-dsa")] Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(mk)?)) } + #[cfg(feature = "mayo")] Codec::Mayo1Pub | Codec::Mayo1Priv | Codec::Mayo2Pub @@ -792,12 +951,15 @@ pub(crate) fn dispatch_fingerprint_view<'a>( | Codec::Mayo3Priv | Codec::Mayo5Pub | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(mk)?)), + #[cfg(feature = "fn-dsa")] Codec::FnDsa512Pub | Codec::FnDsa512Priv | Codec::FnDsa1024Pub | Codec::FnDsa1024Priv => { Ok(Box::new(fn_dsa::View::try_from(mk)?)) } + #[cfg(feature = "ml-kem")] Codec::Mlkem768Pub | Codec::Mlkem768Priv | Codec::Mlkem1024Pub | Codec::Mlkem1024Priv => { Ok(Box::new(ml_kem::View::try_from(mk)?)) } + #[cfg(feature = "sntrup")] Codec::Sntrup761Pub | Codec::Sntrup761Priv | Codec::Sntrup857Pub @@ -813,6 +975,7 @@ pub(crate) fn dispatch_fingerprint_view<'a>( Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => { Ok(Box::new(classic_mceliece::View::try_from(mk)?)) } + #[cfg(feature = "frodokem")] Codec::FrodoKem640AesPub | Codec::FrodoKem640AesPriv | Codec::FrodoKem976AesPub @@ -825,51 +988,63 @@ pub(crate) fn dispatch_fingerprint_view<'a>( | Codec::FrodoKem976ShakePriv | Codec::FrodoKem1344ShakePub | Codec::FrodoKem1344ShakePriv => Ok(Box::new(frodokem::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(mk)?)), + #[cfg(all(feature = "curve25519", feature = "sntrup"))] Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { Ok(Box::new(x25519_sntrup761::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "frodokem"))] Codec::X25519Frodokem640AesPub | Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePub | Codec::X25519Frodokem640ShakePriv => { Ok(Box::new(x25519_frodokem640::View::try_from(mk)?)) } - #[cfg(feature = "deprecated")] + #[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { Ok(Box::new(x25519_mceliece348864::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-kem"))] Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { Ok(Box::new(x25519_mlkem768::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "mayo"))] Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { Ok(Box::new(ed25519_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-dsa"))] Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { Ok(Box::new(ed25519_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { Ok(Box::new(bls12381_g1_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { Ok(Box::new(bls12381_g1_fndsa512::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { Ok(Box::new(bls12381_g1_mayo1::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { Ok(Box::new(bls12381_g1_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "fn-dsa"))] Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { Ok(Box::new(ed25519_fndsa512::View::try_from(mk)?)) } - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub @@ -961,21 +1136,27 @@ pub(crate) fn dispatch_kdf_view<'a>( mk: &'a Multikey, kdf: &'a Multikey, ) -> Result, Error> { - match kdf.codec { - Codec::BcryptPbkdf => Ok(Box::new(bcrypt::View::new(mk, kdf))), - _ => Err(KdfError::UnsupportedCodec(kdf.codec).into()), + // the tuple carries the parameters into the arms, so a build whose features + // gate every arm out still uses them + match (kdf.codec, mk) { + #[cfg(feature = "bcrypt")] + (Codec::BcryptPbkdf, mk) => Ok(Box::new(bcrypt::View::new(mk, kdf))), + (codec, _) => Err(KdfError::UnsupportedCodec(codec).into()), } } /// Builds the seal view for the viewed Multikey. pub(crate) fn dispatch_seal_view<'a>(mk: &'a Multikey) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1Pub | Codec::Bls12381G1Priv | Codec::Bls12381G2Pub | Codec::Bls12381G2Priv => Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "ml-kem")] Codec::Mlkem768Pub | Codec::Mlkem768Priv | Codec::Mlkem1024Pub | Codec::Mlkem1024Priv => { Ok(Box::new(ml_kem::View::try_from(mk)?)) } + #[cfg(feature = "sntrup")] Codec::Sntrup761Pub | Codec::Sntrup761Priv | Codec::Sntrup857Pub @@ -991,6 +1172,7 @@ pub(crate) fn dispatch_seal_view<'a>(mk: &'a Multikey) -> Result { Ok(Box::new(classic_mceliece::View::try_from(mk)?)) } + #[cfg(feature = "frodokem")] Codec::FrodoKem640AesPub | Codec::FrodoKem640AesPriv | Codec::FrodoKem976AesPub @@ -1003,36 +1185,42 @@ pub(crate) fn dispatch_seal_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(frodokem::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(mk)?)), + #[cfg(all(feature = "curve25519", feature = "sntrup"))] Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { Ok(Box::new(x25519_sntrup761::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "frodokem"))] Codec::X25519Frodokem640AesPub | Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePub | Codec::X25519Frodokem640ShakePriv => { Ok(Box::new(x25519_frodokem640::View::try_from(mk)?)) } - #[cfg(feature = "deprecated")] + #[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { Ok(Box::new(x25519_mceliece348864::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-kem"))] Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { Ok(Box::new(x25519_mlkem768::View::try_from(mk)?)) } + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub | Codec::Rsa3072Priv | Codec::Rsa4096Pub | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(mk)?)), - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), _ => Err(SealError::NotEncryptionKey.into()), } @@ -1040,13 +1228,16 @@ pub(crate) fn dispatch_seal_view<'a>(mk: &'a Multikey) -> Result(mk: &'a Multikey) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1Pub | Codec::Bls12381G1Priv | Codec::Bls12381G2Pub | Codec::Bls12381G2Priv => Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "ml-kem")] Codec::Mlkem768Pub | Codec::Mlkem768Priv | Codec::Mlkem1024Pub | Codec::Mlkem1024Priv => { Ok(Box::new(ml_kem::View::try_from(mk)?)) } + #[cfg(feature = "sntrup")] Codec::Sntrup761Pub | Codec::Sntrup761Priv | Codec::Sntrup857Pub @@ -1062,6 +1253,7 @@ pub(crate) fn dispatch_open_view<'a>(mk: &'a Multikey) -> Result { Ok(Box::new(classic_mceliece::View::try_from(mk)?)) } + #[cfg(feature = "frodokem")] Codec::FrodoKem640AesPub | Codec::FrodoKem640AesPriv | Codec::FrodoKem976AesPub @@ -1074,36 +1266,42 @@ pub(crate) fn dispatch_open_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(frodokem::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(mk)?)), + #[cfg(all(feature = "curve25519", feature = "sntrup"))] Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { Ok(Box::new(x25519_sntrup761::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "frodokem"))] Codec::X25519Frodokem640AesPub | Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePub | Codec::X25519Frodokem640ShakePriv => { Ok(Box::new(x25519_frodokem640::View::try_from(mk)?)) } - #[cfg(feature = "deprecated")] + #[cfg(all(feature = "curve25519", feature = "deprecated"))] #[allow(deprecated)] Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { Ok(Box::new(x25519_mceliece348864::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-kem"))] Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { Ok(Box::new(x25519_mlkem768::View::try_from(mk)?)) } + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub | Codec::Rsa3072Priv | Codec::Rsa4096Pub | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(mk)?)), - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), _ => Err(SealError::NotEncryptionKey.into()), } @@ -1111,6 +1309,7 @@ pub(crate) fn dispatch_open_view<'a>(mk: &'a Multikey) -> Result(mk: &'a Multikey) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1PrivShare | Codec::Bls12381G1Priv | Codec::Bls12381G1Pub @@ -1119,8 +1318,11 @@ pub(crate) fn dispatch_sign_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), + #[cfg(feature = "slh-dsa")] Codec::SlhDsaSha2128FPub | Codec::SlhDsaSha2128SPub | Codec::SlhDsaSha2192FPub @@ -1145,9 +1347,11 @@ pub(crate) fn dispatch_sign_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(slh_dsa::View::try_from(mk)?)), + #[cfg(feature = "ml-dsa")] Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(mk)?)) } + #[cfg(feature = "mayo")] Codec::Mayo1Pub | Codec::Mayo1Priv | Codec::Mayo2Pub @@ -1156,36 +1360,45 @@ pub(crate) fn dispatch_sign_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(mayo::View::try_from(mk)?)), + #[cfg(feature = "fn-dsa")] Codec::FnDsa512Pub | Codec::FnDsa512Priv | Codec::FnDsa1024Pub | Codec::FnDsa1024Priv => { Ok(Box::new(fn_dsa::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "mayo"))] Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { Ok(Box::new(ed25519_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-dsa"))] Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { Ok(Box::new(ed25519_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { Ok(Box::new(bls12381_g1_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { Ok(Box::new(bls12381_g1_fndsa512::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { Ok(Box::new(bls12381_g1_mayo1::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { Ok(Box::new(bls12381_g1_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "fn-dsa"))] Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { Ok(Box::new(ed25519_fndsa512::View::try_from(mk)?)) } - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub @@ -1269,6 +1482,7 @@ pub(crate) fn dispatch_sign_view<'a>(mk: &'a Multikey) -> Result Ok(Box::new(xmss::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(xeddsa::View::try_from(mk)?)), _ => Err(ConversionsError::UnsupportedCodec(mk.codec).into()), } @@ -1278,6 +1492,7 @@ pub(crate) fn dispatch_threshold_view<'a>( mk: &'a Multikey, ) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1Priv | Codec::Bls12381G2Priv => { Ok(Box::new(bls12381::View::try_from(mk)?)) } @@ -1359,6 +1574,7 @@ pub(crate) fn dispatch_verify_view<'a>( mk: &'a Multikey, ) -> Result, Error> { match mk.codec { + #[cfg(feature = "bls12-381")] Codec::Bls12381G1PrivShare | Codec::Bls12381G1Priv | Codec::Bls12381G1Pub @@ -1367,8 +1583,11 @@ pub(crate) fn dispatch_verify_view<'a>( | Codec::Bls12381G2Priv | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(mk)?)), + #[cfg(feature = "secp256k1")] Codec::Secp256K1Pub | Codec::Secp256K1Priv => Ok(Box::new(secp256k1::View::try_from(mk)?)), + #[cfg(feature = "slh-dsa")] Codec::SlhDsaSha2128FPub | Codec::SlhDsaSha2128SPub | Codec::SlhDsaSha2192FPub @@ -1393,9 +1612,11 @@ pub(crate) fn dispatch_verify_view<'a>( | Codec::SlhDsaShake192SPriv | Codec::SlhDsaShake256FPriv | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(mk)?)), + #[cfg(feature = "ml-dsa")] Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(mk)?)) } + #[cfg(feature = "mayo")] Codec::Mayo1Pub | Codec::Mayo1Priv | Codec::Mayo2Pub @@ -1404,36 +1625,45 @@ pub(crate) fn dispatch_verify_view<'a>( | Codec::Mayo3Priv | Codec::Mayo5Pub | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(mk)?)), + #[cfg(feature = "fn-dsa")] Codec::FnDsa512Pub | Codec::FnDsa512Priv | Codec::FnDsa1024Pub | Codec::FnDsa1024Priv => { Ok(Box::new(fn_dsa::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "mayo"))] Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { Ok(Box::new(ed25519_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "ml-dsa"))] Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { Ok(Box::new(ed25519_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "ml-dsa"))] Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { Ok(Box::new(bls12381_g1_mldsa65::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "fn-dsa"))] Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { Ok(Box::new(bls12381_g1_fndsa512::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { Ok(Box::new(bls12381_g1_mayo1::View::try_from(mk)?)) } + #[cfg(all(feature = "bls12-381", feature = "mayo"))] Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { Ok(Box::new(bls12381_g1_mayo2::View::try_from(mk)?)) } + #[cfg(all(feature = "curve25519", feature = "fn-dsa"))] Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { Ok(Box::new(ed25519_fndsa512::View::try_from(mk)?)) } - Codec::P256Pub - | Codec::P256Priv - | Codec::P384Pub - | Codec::P384Priv - | Codec::P521Pub - | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p256")] + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p384")] + Codec::P384Pub | Codec::P384Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "p521")] + Codec::P521Pub | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(mk)?)), + #[cfg(feature = "rsa")] Codec::Rsa2048Pub | Codec::Rsa2048Priv | Codec::Rsa3072Pub @@ -1517,6 +1747,7 @@ pub(crate) fn dispatch_verify_view<'a>( | Codec::XmssSha216256Priv | Codec::XmssSha220256Pub | Codec::XmssSha220256Priv => Ok(Box::new(xmss::View::try_from(mk)?)), + #[cfg(feature = "curve25519")] Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(xeddsa::View::try_from(mk)?)), _ => Err(ConversionsError::UnsupportedCodec(mk.codec).into()), } @@ -1534,8 +1765,8 @@ pub(crate) fn dispatch_disclosure_view<'a>( pub(crate) fn dispatch_merkle_state_view<'a>( mk: &'a Multikey, ) -> Result, Error> { - #[cfg(feature = "lamport")] match mk.codec { + #[cfg(feature = "lamport")] Codec::LamportMerkleSha3256Pub | Codec::LamportMerkleSha3256Priv | Codec::LamportMerkleSha3384Pub @@ -1560,6 +1791,4 @@ pub(crate) fn dispatch_merkle_state_view<'a>( | Codec::LamportMerkleShake256Priv => Ok(Box::new(lamport_merkle::View::try_from(mk)?)), _ => Err(ConversionsError::UnsupportedCodec(mk.codec).into()), } - #[cfg(not(feature = "lamport"))] - Err(ConversionsError::UnsupportedCodec(mk.codec).into()) } diff --git a/src/views/dkg_threshold.rs b/src/views/dkg_threshold.rs index 04275fb..a0d1717 100644 --- a/src/views/dkg_threshold.rs +++ b/src/views/dkg_threshold.rs @@ -3,7 +3,7 @@ //! //! Implements [`ThresholdAttrView`] for the four DKG private-share codecs //! introduced alongside the `bs-dkg` crate. The view reads its values out of -//! the four new [`AttrId::Dkg*`] attributes: +//! the four new `AttrId::Dkg*` attributes: //! //! - [`AttrId::DkgThreshold`] → [`threshold`](ThresholdAttrView::threshold) //! - [`AttrId::DkgLimit`] → [`limit`](ThresholdAttrView::limit) diff --git a/src/views/ed25519.rs b/src/views/ed25519.rs index 0ddcdd4..ae3a3bd 100644 --- a/src/views/ed25519.rs +++ b/src/views/ed25519.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::views::dispatch::{ - dispatch_attr_view, dispatch_conv_view, dispatch_data_view, dispatch_fingerprint_view, + dispatch_attr_view, dispatch_data_view, dispatch_fingerprint_view, public_form, }; use crate::{ AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, @@ -192,10 +192,7 @@ impl<'a> ConvView for View<'a> { /// try to convert a Multikey to an ssh_key::PublicKey fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -212,7 +209,7 @@ impl<'a> ConvView for View<'a> { Ok(ssh_key::PublicKey::new( ssh_key::public::KeyData::Ed25519(ssh_key::public::Ed25519PublicKey(bytes)), - pk.comment, + pk.comment.clone(), )) } @@ -293,14 +290,7 @@ impl<'a> SignView for View<'a> { impl<'a> VerifyView for View<'a> { /// try to verify a Multisig using the Multikey fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; // get the secret key bytes let key_bytes = { diff --git a/src/views/ed25519_fndsa512.rs b/src/views/ed25519_fndsa512.rs index b722d9f..fadd1b8 100644 --- a/src/views/ed25519_fndsa512.rs +++ b/src/views/ed25519_fndsa512.rs @@ -5,7 +5,7 @@ //! Secret layout: ed25519_seed (32) || fn_dsa_signing_key (sign_key_size). //! Public encoding is classical-first: ed25519_pub (32) || fn_dsa_verifying_key. -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -205,13 +205,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/ed25519_mayo2.rs b/src/views/ed25519_mayo2.rs index b2dc238..7e41dc8 100644 --- a/src/views/ed25519_mayo2.rs +++ b/src/views/ed25519_mayo2.rs @@ -3,7 +3,7 @@ //! Sign: s1 = Ed25519(m), s2 = Mayo2(m || s1), sig = s1 || s2 //! Verify: verify Ed25519(m, s1) && verify Mayo2(m || s1, s2) -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -173,7 +173,7 @@ impl<'a> SignView for View<'a> { m2.extend_from_slice(&s1.to_bytes()); let s2 = { - use ml_dsa::signature::Signer; + use signature::Signer; kp.signing_key() .try_sign(&m2) .map_err(|e| SignError::SigningFailed(e.to_string()))? @@ -194,7 +194,7 @@ impl<'a> SignView for View<'a> { impl<'a> VerifyView for View<'a> { fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - use ml_dsa::signature::Verifier as MayoVerifier; + use signature::Verifier as MayoVerifier; let msg_bytes = if let Some(m) = msg { m @@ -204,13 +204,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/ed25519_mldsa65.rs b/src/views/ed25519_mldsa65.rs index e4e1cbe..8b919fa 100644 --- a/src/views/ed25519_mldsa65.rs +++ b/src/views/ed25519_mldsa65.rs @@ -4,7 +4,7 @@ //! Verify: verify Ed25519(m, s1) && verify MlDsa65(m || s1, s2) //! Public encoding is classical-first: ed25519_pub (32) || mldsa65_pub. -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -208,13 +208,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/fn_dsa.rs b/src/views/fn_dsa.rs index 6208bcc..0808d56 100644 --- a/src/views/fn_dsa.rs +++ b/src/views/fn_dsa.rs @@ -2,7 +2,7 @@ //! FN-DSA (Falcon) 512/1024 multikey view; FIPS 206 (draft). use crate::views::dispatch::{ - dispatch_attr_view, dispatch_conv_view, dispatch_data_view, dispatch_fingerprint_view, + dispatch_attr_view, dispatch_data_view, dispatch_fingerprint_view, public_form, }; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, @@ -125,10 +125,7 @@ impl<'a> ConvView for View<'a> { } fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -157,7 +154,7 @@ impl<'a> ConvView for View<'a> { ), key: opaque_key_bytes, }), - pk.comment, + pk.comment.clone(), )) } @@ -259,13 +256,7 @@ impl<'a> SignView for View<'a> { impl<'a> VerifyView for View<'a> { fn verify(&self, multisig: &multi_sig::Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/frodokem.rs b/src/views/frodokem.rs index 7f57f4e..0fe38b5 100644 --- a/src/views/frodokem.rs +++ b/src/views/frodokem.rs @@ -225,15 +225,6 @@ impl<'a> FingerprintView for View<'a> { } } -fn encode_sealed(kem_ct: &[u8], aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(kem_ct.to_vec()).into()); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - fn decode_sealed(data: &[u8]) -> Result { let (kem_ct, ptr) = Varbytes::try_decode_from(data) .map_err(|_| SealError::InvalidFormat("missing kem ciphertext".into()))?; @@ -269,7 +260,10 @@ impl<'a> SealView for View<'a> { let key_len = aead::key_size(aead_codec)?; let aead_key = aead::derive_aead_key(&shared_secret, b"frodokem-seal", key_len)?; let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; - Ok((encode_sealed(&kem_ct, aead_codec, &nonce, &ct_tag), None)) + Ok(( + aead::encode_kem_sealed(&kem_ct, aead_codec, &nonce, &ct_tag), + None, + )) } } diff --git a/src/views/lamport.rs b/src/views/lamport.rs index 42f7276..3fcfc6d 100644 --- a/src/views/lamport.rs +++ b/src/views/lamport.rs @@ -7,7 +7,7 @@ //! single use; the provenance-log / keystore layer must reject a second //! signature by the same Lamport public key. -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, ThresholdView, VerifyView, @@ -596,12 +596,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - dispatch_conv_view(self.mk)?.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; kd.key_bytes()? diff --git a/src/views/lamport_merkle.rs b/src/views/lamport_merkle.rs index c85c5fb..922f955 100644 --- a/src/views/lamport_merkle.rs +++ b/src/views/lamport_merkle.rs @@ -15,7 +15,7 @@ //! and 1 respectively (`[depth, root]`, `[version, depth, next_index, …]`); //! `MtSignature`/`MtSignatureShare` embed depth at byte 0 (`[depth, index, …]`). -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, MerkleStateView, Multikey, SignView, ThresholdView, VerifyView, @@ -720,12 +720,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - dispatch_conv_view(self.mk)?.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; kd.key_bytes()? @@ -1233,7 +1228,7 @@ mod tests { fn test_merkle_depth_mismatch_rejected() { let sk = build_priv(Codec::LamportMerkleSha3256Priv, 2); // tamper the depth attribute: wire says 2, attr now says 3 - let mut tampered = sk.clone(); + let mut tampered = sk; tampered .attributes .insert(AttrId::Depth, Zeroizing::new(vec![3])); diff --git a/src/views/mayo.rs b/src/views/mayo.rs index ec3516b..b103167 100644 --- a/src/views/mayo.rs +++ b/src/views/mayo.rs @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 //! MAYO-1/2/3/5 multikey view; post-quantum multivariate signature. -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{ + dispatch_attr_view, dispatch_conv_view, dispatch_data_view, public_form, +}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -143,10 +145,7 @@ impl<'a> ConvView for View<'a> { } fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -176,7 +175,7 @@ impl<'a> ConvView for View<'a> { ), key: opaque_key_bytes, }), - pk.comment, + pk.comment.clone(), )) } @@ -260,7 +259,7 @@ impl<'a> SignView for View<'a> { combined: bool, _scheme: Option, ) -> Result { - use ml_dsa::signature::Signer; + use signature::Signer; let attr = dispatch_attr_view(self.mk)?; if !attr.is_secret_key() { @@ -330,7 +329,7 @@ impl<'a> SignView for View<'a> { impl<'a> VerifyView for View<'a> { fn verify(&self, sig: &multi_sig::Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - use ml_dsa::signature::Verifier; + use signature::Verifier; let msg_bytes = if let Some(m) = msg { m @@ -340,13 +339,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/ml_dsa.rs b/src/views/ml_dsa.rs index 2f9938e..17da5e4 100644 --- a/src/views/ml_dsa.rs +++ b/src/views/ml_dsa.rs @@ -1,7 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 //! ML-DSA 65/87 multikey view; FIPS 204. -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{ + dispatch_attr_view, dispatch_conv_view, dispatch_data_view, public_form, +}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -130,10 +132,7 @@ impl<'a> ConvView for View<'a> { } fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -162,7 +161,7 @@ impl<'a> ConvView for View<'a> { ), key: opaque_key_bytes, }), - pk.comment, + pk.comment.clone(), )) } @@ -305,13 +304,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/src/views/ml_kem.rs b/src/views/ml_kem.rs index 71cfb7f..da0cdac 100644 --- a/src/views/ml_kem.rs +++ b/src/views/ml_kem.rs @@ -151,16 +151,6 @@ impl<'a> FingerprintView for View<'a> { } } -/// Encode a sealed message: [kem_ct Varbytes][aead_codec Codec][nonce Varbytes][ct+tag Varbytes] -fn encode_sealed(kem_ct: &[u8], aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(kem_ct.to_vec()).into()); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - /// Decode a sealed message: [kem_ct Varbytes][aead_codec Codec][nonce Varbytes][ct+tag Varbytes] fn decode_sealed(data: &[u8]) -> Result { let (kem_ct, ptr) = Varbytes::try_decode_from(data) @@ -229,7 +219,10 @@ impl<'a> SealView for View<'a> { // Encrypt plaintext let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; - Ok((encode_sealed(&kem_ct, aead_codec, &nonce, &ct_tag), None)) + Ok(( + aead::encode_kem_sealed(&kem_ct, aead_codec, &nonce, &ct_tag), + None, + )) } } diff --git a/src/views/nist_p.rs b/src/views/nist_p.rs index b1d6831..8ee9bc4 100644 --- a/src/views/nist_p.rs +++ b/src/views/nist_p.rs @@ -2,7 +2,7 @@ //! NIST P-256, P-384, P-521 ECDSA multikey view. use crate::views::dispatch::{ - dispatch_attr_view, dispatch_conv_view, dispatch_data_view, dispatch_fingerprint_view, + dispatch_attr_view, dispatch_data_view, dispatch_fingerprint_view, public_form, }; use crate::{ AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, @@ -24,10 +24,13 @@ use multi_util::Varuint; use zeroize::Zeroizing; /// P-256: 32-byte secret +#[cfg(feature = "p256")] const P256_SECRET_LEN: usize = 32; /// P-384: 48-byte secret +#[cfg(feature = "p384")] const P384_SECRET_LEN: usize = 48; /// P-521: 66-byte secret +#[cfg(feature = "p521")] const P521_SECRET_LEN: usize = 66; fn pub_codec(codec: Codec) -> Codec { @@ -194,16 +197,19 @@ impl<'a> ConvView for View<'a> { }; let pub_bytes = match self.mk.codec { + #[cfg(feature = "p256")] Codec::P256Priv => { let sk = p256::ecdsa::SigningKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; sk.verifying_key().to_sec1_bytes().to_vec() } + #[cfg(feature = "p384")] Codec::P384Priv => { let sk = p384::ecdsa::SigningKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; sk.verifying_key().to_sec1_bytes().to_vec() } + #[cfg(feature = "p521")] Codec::P521Priv => { let sk = p521::ecdsa::SigningKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; @@ -224,10 +230,7 @@ impl<'a> ConvView for View<'a> { } fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -237,6 +240,7 @@ impl<'a> ConvView for View<'a> { // Build proper SSH ECDSA public key from compressed SEC1 point. // SSH ECDSA requires uncompressed points, so decompress via VerifyingKey. let key_data = match pub_codec(self.mk.codec) { + #[cfg(feature = "p256")] Codec::P256Pub => { let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(&key_bytes) .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; @@ -248,6 +252,7 @@ impl<'a> ConvView for View<'a> { ssh_point, )) } + #[cfg(feature = "p384")] Codec::P384Pub => { let vk = p384::ecdsa::VerifyingKey::from_sec1_bytes(&key_bytes) .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; @@ -259,6 +264,7 @@ impl<'a> ConvView for View<'a> { ssh_point, )) } + #[cfg(feature = "p521")] Codec::P521Pub => { let vk = p521::ecdsa::VerifyingKey::from_sec1_bytes(&key_bytes) .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; @@ -273,7 +279,7 @@ impl<'a> ConvView for View<'a> { _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), }; - Ok(ssh_key::PublicKey::new(key_data, pk.comment)) + Ok(ssh_key::PublicKey::new(key_data, pk.comment.clone())) } fn to_ssh_private_key(&self) -> Result { @@ -284,6 +290,7 @@ impl<'a> ConvView for View<'a> { // Build proper SSH ECDSA keypair let keypair_data = match self.mk.codec { + #[cfg(feature = "p256")] Codec::P256Priv => { let secret_key = p256::SecretKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; @@ -298,6 +305,7 @@ impl<'a> ConvView for View<'a> { private: priv_key, }) } + #[cfg(feature = "p384")] Codec::P384Priv => { let secret_key = p384::SecretKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; @@ -312,6 +320,7 @@ impl<'a> ConvView for View<'a> { private: priv_key, }) } + #[cfg(feature = "p521")] Codec::P521Priv => { let secret_key = p521::SecretKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; @@ -349,6 +358,7 @@ impl<'a> SignView for View<'a> { }; let sig_bytes = match self.mk.codec { + #[cfg(feature = "p256")] Codec::P256Priv => { use p256::ecdsa::{Signature, SigningKey, signature::Signer}; let sk = SigningKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) @@ -358,6 +368,7 @@ impl<'a> SignView for View<'a> { .map_err(|e| SignError::SigningFailed(e.to_string()))?; signature.to_bytes().to_vec() } + #[cfg(feature = "p384")] Codec::P384Priv => { use p384::ecdsa::{Signature, SigningKey, signature::Signer}; let sk = SigningKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) @@ -367,6 +378,7 @@ impl<'a> SignView for View<'a> { .map_err(|e| SignError::SigningFailed(e.to_string()))?; signature.to_bytes().to_vec() } + #[cfg(feature = "p521")] Codec::P521Priv => { use p521::ecdsa::{Signature, SigningKey, signature::Signer}; let sk = SigningKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) @@ -390,13 +402,7 @@ impl<'a> SignView for View<'a> { impl<'a> VerifyView for View<'a> { fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; @@ -415,6 +421,7 @@ impl<'a> VerifyView for View<'a> { }; match pubmk.codec { + #[cfg(feature = "p256")] Codec::P256Pub => { use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = VerifyingKey::from_sec1_bytes(&key_bytes) @@ -424,6 +431,7 @@ impl<'a> VerifyView for View<'a> { vk.verify(msg, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()))?; } + #[cfg(feature = "p384")] Codec::P384Pub => { use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = VerifyingKey::from_sec1_bytes(&key_bytes) @@ -433,6 +441,7 @@ impl<'a> VerifyView for View<'a> { vk.verify(msg, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()))?; } + #[cfg(feature = "p521")] Codec::P521Pub => { use p521::ecdsa::{Signature, VerifyingKey, signature::Verifier}; let vk = VerifyingKey::from_sec1_bytes(&key_bytes) @@ -470,17 +479,7 @@ fn is_nistp_aead_allowed(codec: Codec) -> bool { ) } -/// Encode a sealed message: `[aead_codec Codec][nonce Varbytes][ct+tag Varbytes]`. -/// The ephemeral public key is carried externally (as a Multikey), like X25519. -fn encode_sealed(aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - -/// Decode a sealed message produced by [`encode_sealed`]. +/// Decode a sealed message produced by [`aead::encode_sealed`]. fn decode_sealed(data: &[u8]) -> Result<(Codec, Vec, Vec), SealError> { let (aead_codec, ptr) = Codec::try_decode_from(data) .map_err(|_| SealError::InvalidFormat("missing aead codec".into()))?; @@ -501,7 +500,7 @@ fn seal_from_shared( let key_len = aead::key_size(aead_codec)?; let aead_key = aead::derive_aead_key(shared, NISTP_SEAL_INFO, key_len)?; let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; - Ok(encode_sealed(aead_codec, &nonce, &ct_tag)) + Ok(aead::encode_sealed(aead_codec, &nonce, &ct_tag)) } /// HKDF-expand the ECDH shared secret and AEAD-open the ciphertext. @@ -534,6 +533,7 @@ impl<'a> SealView for View<'a> { let pub_bytes = self.key_bytes()?; let (sealed, eph_pub_bytes) = match self.mk.codec { + #[cfg(feature = "p256")] Codec::P256Pub => { let recipient = p256::PublicKey::from_sec1_bytes(&pub_bytes) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; @@ -551,6 +551,7 @@ impl<'a> SealView for View<'a> { eph_pub.as_affine().to_sec1_point(true).as_bytes().to_vec(), ) } + #[cfg(feature = "p384")] Codec::P384Pub => { let recipient = p384::PublicKey::from_sec1_bytes(&pub_bytes) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; @@ -568,6 +569,7 @@ impl<'a> SealView for View<'a> { eph_pub.as_affine().to_sec1_point(true).as_bytes().to_vec(), ) } + #[cfg(feature = "p521")] Codec::P521Pub => { let recipient = p521::PublicKey::from_sec1_bytes(&pub_bytes) .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; @@ -623,6 +625,7 @@ impl<'a> OpenView for View<'a> { }; let pt = match self.mk.codec { + #[cfg(feature = "p256")] Codec::P256Priv => { let secret = p256::SecretKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; @@ -638,6 +641,7 @@ impl<'a> OpenView for View<'a> { aad, )? } + #[cfg(feature = "p384")] Codec::P384Priv => { let secret = p384::SecretKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; @@ -653,6 +657,7 @@ impl<'a> OpenView for View<'a> { aad, )? } + #[cfg(feature = "p521")] Codec::P521Priv => { let secret = p521::SecretKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; @@ -717,21 +722,25 @@ mod ecies_tests { } } + #[cfg(feature = "p256")] #[test] fn test_p256_seal_open_roundtrip() { seal_open_roundtrip(Codec::P256Priv); } + #[cfg(feature = "p384")] #[test] fn test_p384_seal_open_roundtrip() { seal_open_roundtrip(Codec::P384Priv); } + #[cfg(feature = "p521")] #[test] fn test_p521_seal_open_roundtrip() { seal_open_roundtrip(Codec::P521Priv); } + #[cfg(feature = "p256")] #[test] fn test_p256_wrong_key_fails_to_open() { let mut rng = rand::rng(); diff --git a/src/views/rsa.rs b/src/views/rsa.rs index efe482b..a1231b2 100644 --- a/src/views/rsa.rs +++ b/src/views/rsa.rs @@ -2,7 +2,7 @@ //! RSA-2048/3072/4096 multikey view — signing (PSS SHA-256) + encryption (RSA-OAEP + AEAD). use crate::views::dispatch::{ - dispatch_attr_view, dispatch_conv_view, dispatch_data_view, dispatch_fingerprint_view, + dispatch_attr_view, dispatch_data_view, dispatch_fingerprint_view, public_form, }; use crate::{ AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, @@ -266,10 +266,7 @@ impl<'a> ConvView for View<'a> { } fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -291,7 +288,7 @@ impl<'a> ConvView for View<'a> { ), key: opaque_key_bytes, }), - pk.comment, + pk.comment.clone(), )) } @@ -388,13 +385,7 @@ impl<'a> SignView for View<'a> { impl<'a> VerifyView for View<'a> { fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; @@ -438,16 +429,6 @@ fn is_rsa_aead_allowed(codec: Codec) -> bool { ) } -/// Encode: [rsa_oaep_ciphertext Varbytes][aead_codec Codec][nonce Varbytes][ciphertext Varbytes] -fn encode_sealed(rsa_ct: &[u8], aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(rsa_ct.to_vec()).into()); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - /// RSA-OAEP ciphertext, AEAD codec, nonce, ciphertext+tag type SealedParts = (Vec, Codec, Vec, Vec); @@ -511,7 +492,10 @@ impl<'a> SealView for View<'a> { // Encrypt plaintext with AEAD let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; - Ok((encode_sealed(&rsa_ct, aead_codec, &nonce, &ct_tag), None)) + Ok(( + aead::encode_kem_sealed(&rsa_ct, aead_codec, &nonce, &ct_tag), + None, + )) } } diff --git a/src/views/secp256k1.rs b/src/views/secp256k1.rs index 76772a1..187d720 100644 --- a/src/views/secp256k1.rs +++ b/src/views/secp256k1.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::views::dispatch::{ - dispatch_attr_view, dispatch_conv_view, dispatch_data_view, dispatch_fingerprint_view, + dispatch_attr_view, dispatch_data_view, dispatch_fingerprint_view, public_form, }; use crate::{ AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, @@ -208,10 +208,7 @@ impl<'a> ConvView for View<'a> { /// try to convert a Multikey to an ssh_key::PublicKey fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -234,7 +231,7 @@ impl<'a> ConvView for View<'a> { ), key: opaque_key_bytes, }), - pk.comment, + pk.comment.clone(), )) } @@ -330,14 +327,7 @@ impl<'a> SignView for View<'a> { impl<'a> VerifyView for View<'a> { /// try to verify a Multisig using the Multikey fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; // get the secret key bytes let key_bytes = { @@ -403,16 +393,7 @@ fn is_secp_aead_allowed(codec: Codec) -> bool { ) } -/// Encode a sealed message: `[aead_codec Codec][nonce Varbytes][ct+tag Varbytes]`. -fn encode_sealed(aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - -/// Decode a sealed message produced by [`encode_sealed`]. +/// Decode a sealed message produced by [`aead::encode_sealed`]. fn decode_sealed(data: &[u8]) -> Result<(Codec, Vec, Vec), SealError> { let (aead_codec, ptr) = Codec::try_decode_from(data) .map_err(|_| SealError::InvalidFormat("missing aead codec".into()))?; @@ -459,7 +440,7 @@ impl<'a> SealView for View<'a> { .try_build()?; Ok(( - encode_sealed(aead_codec, &nonce, &ct_tag), + aead::encode_sealed(aead_codec, &nonce, &ct_tag), Some(ephemeral_mk), )) } diff --git a/src/views/slh_dsa.rs b/src/views/slh_dsa.rs index d3adf46..d452d49 100644 --- a/src/views/slh_dsa.rs +++ b/src/views/slh_dsa.rs @@ -3,6 +3,7 @@ use crate::views::dispatch::{ dispatch_attr_view, dispatch_conv_view, dispatch_data_view, dispatch_fingerprint_view, + public_form, }; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, @@ -267,10 +268,7 @@ impl<'a> ConvView for View<'a> { } fn to_ssh_public_key(&self) -> Result { - let mut pk = self.mk.clone(); - if self.is_secret_key() { - pk = self.to_public_key()?; - } + let pk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pk)?; @@ -309,7 +307,7 @@ impl<'a> ConvView for View<'a> { ), key: opaque_key_bytes, }), - pk.comment, + pk.comment.clone(), )) } @@ -513,13 +511,7 @@ impl<'a> VerifyView for View<'a> { } else { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; kd.key_bytes()? diff --git a/src/views/sntrup.rs b/src/views/sntrup.rs index e650221..6177b39 100644 --- a/src/views/sntrup.rs +++ b/src/views/sntrup.rs @@ -300,16 +300,6 @@ impl<'a> FingerprintView for View<'a> { } } -/// Encode a sealed message: [kem_ct Varbytes][aead_codec Codec][nonce Varbytes][ct+tag Varbytes] -fn encode_sealed(kem_ct: &[u8], aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(kem_ct.to_vec()).into()); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - /// Decode a sealed message: [kem_ct Varbytes][aead_codec Codec][nonce Varbytes][ct+tag Varbytes] fn decode_sealed(data: &[u8]) -> Result { let (kem_ct, ptr) = Varbytes::try_decode_from(data) @@ -354,7 +344,10 @@ impl<'a> SealView for View<'a> { // Encrypt plaintext let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; - Ok((encode_sealed(&kem_ct, aead_codec, &nonce, &ct_tag), None)) + Ok(( + aead::encode_kem_sealed(&kem_ct, aead_codec, &nonce, &ct_tag), + None, + )) } } diff --git a/src/views/threshold_marker.rs b/src/views/threshold_marker.rs index 0923dda..53f6e39 100644 --- a/src/views/threshold_marker.rs +++ b/src/views/threshold_marker.rs @@ -141,11 +141,12 @@ impl<'a> MarkerView<'a> { .attributes .get(&AttrId::ThresholdParticipants) .ok_or(AttributesError::MissingThresholdParticipants)?; - let map: BTreeMap, ThresholdParticipant> = ciborium::from_reader(v.as_slice()) + let map: BTreeMap, ThresholdParticipant> = serde_cbor_2::from_slice(v) .map_err(|e| AttributesError::ThresholdMarkerCbor(e.to_string()))?; // CBOR-01: bound the registry size. A t-of-n threshold key has at most // `Limit` participants; reject an oversized map (decode amplification / - // attacker-influenced blob) well below ciborium's generic map cap. + // attacker-influenced blob). The CBOR decoder bounds only nesting depth, + // not map size, so this is the only size cap. const MAX_THRESHOLD_PARTICIPANTS: usize = 1024; if map.len() > MAX_THRESHOLD_PARTICIPANTS { return Err(AttributesError::ThresholdMarkerCbor( @@ -203,8 +204,7 @@ pub fn set_participants( mk: &mut Multikey, participants: &BTreeMap, ThresholdParticipant>, ) -> Result<(), Error> { - let mut bytes = Vec::new(); - ciborium::into_writer(participants, &mut bytes) + let bytes = serde_cbor_2::to_vec(participants) .map_err(|e| AttributesError::ThresholdMarkerCbor(e.to_string()))?; mk.attributes .insert(AttrId::ThresholdParticipants, bytes.into()); @@ -233,10 +233,8 @@ pub fn canonical_marker_bytes(mk: &Multikey) -> Result, Error> { let threshold = mv.threshold()?; let limit = mv.participant_count()?; let payload = (group, participants, threshold, limit); - let mut buf = Vec::new(); - ciborium::into_writer(&payload, &mut buf) - .map_err(|e| Error::Attributes(AttributesError::ThresholdMarkerCbor(e.to_string())))?; - Ok(buf) + serde_cbor_2::to_vec(&payload) + .map_err(|e| Error::Attributes(AttributesError::ThresholdMarkerCbor(e.to_string()))) } /// Authenticate the marker bundle: sign `canonical_marker_bytes(mk)` with @@ -271,7 +269,7 @@ pub fn verify_marker(mk: &Multikey, verifier_pubkey: &Multikey) -> Result<(), Er .map_err(|_| AttributesError::ThresholdMarkerSigInvalid.into()) } -#[cfg(test)] +#[cfg(all(test, feature = "bls12-381"))] mod tests { use super::*; use crate::{Builder, ViewBuilder}; @@ -333,6 +331,7 @@ mod tests { assert_eq!(threshold_params(&mk), Some((2, 3))); } + #[cfg(feature = "curve25519")] #[test] fn classify_non_threshold_none() { // Ed25519 ordinary key diff --git a/src/views/threshold_meta.rs b/src/views/threshold_meta.rs index 6da635c..4bc670e 100644 --- a/src/views/threshold_meta.rs +++ b/src/views/threshold_meta.rs @@ -11,15 +11,14 @@ //! //! - **[`ThresholdDisclosure::Full`]** — t and n are plaintext attributes (default). //! - **[`ThresholdDisclosure::Partial`]** — n is plaintext, t is encrypted. -//! - **[`ThresholdDisclosure::FullConfidentialial`]** — both t and n are encrypted. +//! - **[`ThresholdDisclosure::FullConfidential`]** — both t and n are encrypted. // Re-export the pure crypto types/functions owned by `multi_sig` so that the // historical `multi_key::ThresholdDisclosure` / `multi_key::encrypt_threshold_meta` // public API keeps working for downstream callers. -pub use multi_sig::{ - ThresholdDisclosure, ThresholdMetaCipher, ThresholdMetadata, decrypt_threshold_meta, - encrypt_threshold_meta, generate_meta_key, -}; +pub use multi_sig::{ThresholdDisclosure, ThresholdMetaCipher, ThresholdMetadata}; +#[cfg(feature = "chacha20")] +pub use multi_sig::{decrypt_threshold_meta, encrypt_threshold_meta, generate_meta_key}; use crate::{ AttrId, Error, Multikey, @@ -34,6 +33,7 @@ use zeroize::Zeroizing; /// cipher key (e.g. a ChaCha20-Poly1305 key Multikey). This bridges the /// `Multikey` at-rest encryption infrastructure to the threshold metadata /// encryption helpers in `multi_sig`. +#[cfg(feature = "chacha20")] fn extract_meta_key(meta_key: &Multikey) -> Result>, Error> { let dv = crate::views::dispatch::dispatch_data_view(meta_key)?; let key = dv.key_bytes()?; @@ -46,6 +46,38 @@ fn extract_meta_key(meta_key: &Multikey) -> Result>, Error> { Ok(key) } +/// Without `chacha20` the encrypted disclosure modes are unavailable, so every +/// path that needs the meta key fails here, before any attribute is written; +/// the confidential threshold is never written in plaintext. +#[cfg(not(feature = "chacha20"))] +fn extract_meta_key(_meta_key: &Multikey) -> Result>, Error> { + Err(meta_encryption_unavailable()) +} + +#[cfg(not(feature = "chacha20"))] +fn encrypt_threshold_meta( + _meta: &ThresholdMetadata, + _key: &[u8], +) -> Result<(Vec, ThresholdMetaCipher), Error> { + Err(meta_encryption_unavailable()) +} + +#[cfg(not(feature = "chacha20"))] +fn decrypt_threshold_meta( + _encrypted: &[u8], + _cipher_info: &ThresholdMetaCipher, + _key: &[u8], +) -> Result { + Err(meta_encryption_unavailable()) +} + +#[cfg(not(feature = "chacha20"))] +fn meta_encryption_unavailable() -> Error { + Error::Threshold(ThresholdError::MetaEncryption( + "encrypted threshold metadata needs the chacha20 feature".to_string(), + )) +} + /// Read the disclosure mode from a Multikey. Returns [`ThresholdDisclosure::Full`] /// if no `ThresholdDisclosure` attribute is present (backward compatible). pub fn disclosure_mode(mk: &Multikey) -> Result { @@ -62,9 +94,9 @@ pub fn disclosure_mode(mk: &Multikey) -> Result { /// /// In Full mode, reads plaintext `Threshold`/`Limit` attributes. /// In Partial mode, reads `Limit` from plaintext and decrypts `Threshold`. -/// In FullConfidentialial mode, decrypts both from `EncryptedThresholdMeta`. +/// In FullConfidential mode, decrypts both from `EncryptedThresholdMeta`. /// -/// `meta_key` is required for Partial/FullConfidentialial modes. +/// `meta_key` is required for Partial/FullConfidential modes. pub fn read_threshold_params( mk: &Multikey, meta_key: Option<&Multikey>, @@ -108,7 +140,7 @@ pub fn read_threshold_params( ))? as usize; Ok((t, n)) } - ThresholdDisclosure::FullConfidentialial => { + ThresholdDisclosure::FullConfidential => { let encrypted = mk.attributes.get(&AttrId::EncryptedThresholdMeta).ok_or( ThresholdError::MetaEncryption("missing EncryptedThresholdMeta".to_string()), )?; @@ -185,7 +217,7 @@ pub fn stamp_disclosure_attrs( Zeroizing::new(mode.encode_into()), ); } - ThresholdDisclosure::FullConfidentialial => { + ThresholdDisclosure::FullConfidential => { let meta_key = meta_key.ok_or(ThresholdError::MissingMetaKey)?; let key = extract_meta_key(meta_key)?; @@ -276,10 +308,14 @@ impl<'a> ThresholdDisclosureView for DisclosureView<'a> { #[cfg(test)] mod tests { use super::*; + #[cfg(any(feature = "bls12-381", not(feature = "chacha20")))] use crate::Builder; + #[cfg(feature = "bls12-381")] use crate::ViewBuilder; + #[cfg(any(feature = "bls12-381", not(feature = "chacha20")))] use multi_codec::Codec; + #[cfg(all(feature = "bls12-381", feature = "chacha20"))] fn make_meta_key() -> Multikey { let key = generate_meta_key(); Builder::new(Codec::Chacha20Poly1305) @@ -288,6 +324,7 @@ mod tests { .unwrap() } + #[cfg(feature = "bls12-381")] fn make_share(t: usize, n: usize) -> Multikey { Builder::new(Codec::Bls12381G1PrivShare) .with_threshold(t) @@ -314,7 +351,7 @@ mod tests { ); assert_eq!( ThresholdDisclosure::try_from(2u8).unwrap(), - ThresholdDisclosure::FullConfidentialial + ThresholdDisclosure::FullConfidential ); assert!(ThresholdDisclosure::try_from(3u8).is_err()); } @@ -324,7 +361,7 @@ mod tests { for mode in [ ThresholdDisclosure::Full, ThresholdDisclosure::Partial, - ThresholdDisclosure::FullConfidentialial, + ThresholdDisclosure::FullConfidential, ] { let encoded = mode.encode_into(); let (decoded, rest) = ThresholdDisclosure::try_decode_from(&encoded).unwrap(); @@ -341,6 +378,7 @@ mod tests { assert_eq!(meta, decoded); } + #[cfg(feature = "chacha20")] #[test] fn test_encrypt_decrypt_roundtrip() { let key = generate_meta_key(); @@ -350,6 +388,7 @@ mod tests { assert_eq!(meta, decrypted); } + #[cfg(feature = "chacha20")] #[test] fn test_encrypt_decrypt_wrong_key() { let key1 = generate_meta_key(); @@ -359,6 +398,7 @@ mod tests { assert!(decrypt_threshold_meta(&ct, &info, &key2).is_err()); } + #[cfg(feature = "chacha20")] #[test] fn test_encrypt_decrypt_tampered() { let key = generate_meta_key(); @@ -368,6 +408,7 @@ mod tests { assert!(decrypt_threshold_meta(&ct, &info, &key).is_err()); } + #[cfg(feature = "bls12-381")] #[test] fn test_read_full_mode() { let share = make_share(3, 5); @@ -376,6 +417,7 @@ mod tests { assert_eq!(n, 5); } + #[cfg(all(feature = "bls12-381", feature = "chacha20"))] #[test] fn test_convert_full_to_partial_and_back() { let share = make_share(3, 5); @@ -424,21 +466,18 @@ mod tests { assert_eq!(n, 5); } + #[cfg(all(feature = "bls12-381", feature = "chacha20"))] #[test] - fn test_convert_full_to_full_confidentialial_and_back() { + fn test_convert_full_to_full_confidential_and_back() { let share = make_share(3, 5); let meta_key = make_meta_key(); - // convert to FullConfidentialial + // convert to FullConfidential let encrypted = ViewBuilder::new(&share) .disclosure() .build() .unwrap() - .to_disclosure( - ThresholdDisclosure::FullConfidentialial, - Some(&meta_key), - None, - ) + .to_disclosure(ThresholdDisclosure::FullConfidential, Some(&meta_key), None) .unwrap(); assert_eq!( ViewBuilder::new(&encrypted) @@ -447,7 +486,7 @@ mod tests { .unwrap() .disclosure_mode() .unwrap(), - ThresholdDisclosure::FullConfidentialial + ThresholdDisclosure::FullConfidential ); // read t/n @@ -467,6 +506,7 @@ mod tests { assert_eq!(n, 5); } + #[cfg(all(feature = "bls12-381", feature = "chacha20"))] #[test] fn test_read_encrypted_without_meta_key() { let share = make_share(3, 5); @@ -475,15 +515,12 @@ mod tests { .disclosure() .build() .unwrap() - .to_disclosure( - ThresholdDisclosure::FullConfidentialial, - Some(&meta_key), - None, - ) + .to_disclosure(ThresholdDisclosure::FullConfidential, Some(&meta_key), None) .unwrap(); assert!(read_threshold_params(&encrypted, None).is_err()); } + #[cfg(feature = "bls12-381")] #[test] fn test_convert_to_partial_without_meta_key() { let share = make_share(3, 5); @@ -495,6 +532,49 @@ mod tests { assert!(result.is_err()); } + /// without `chacha20` the encrypted modes fail and leave the threshold + /// out of the attributes rather than writing it in plaintext + #[cfg(not(feature = "chacha20"))] + #[test] + fn test_encrypted_disclosure_needs_chacha20() { + let meta_mk = Builder::new(Codec::Chacha20Poly1305) + .with_key_bytes(&[7u8; 32]) + .try_build() + .unwrap(); + fn unavailable(r: &Result) -> bool { + matches!(r, Err(Error::Threshold(ThresholdError::MetaEncryption(_)))) + } + for mode in [ + ThresholdDisclosure::Partial, + ThresholdDisclosure::FullConfidential, + ] { + let mut attrs = Attributes::new(); + let stamped = stamp_disclosure_attrs(&mut attrs, mode, 2, 3, Some(&meta_mk)); + assert!(unavailable(&stamped)); + assert!(!attrs.contains_key(&AttrId::Threshold)); + assert!(!attrs.contains_key(&AttrId::EncryptedThresholdMeta)); + } + // reading an encrypted share fails the same way + let mut attrs = Attributes::new(); + attrs.insert( + AttrId::ThresholdDisclosure, + Zeroizing::new(ThresholdDisclosure::Partial.encode_into()), + ); + attrs.insert(AttrId::Limit, Zeroizing::new(Varuint(3usize).into())); + attrs.insert(AttrId::EncryptedThresholdMeta, Zeroizing::new(vec![0u8; 8])); + attrs.insert( + AttrId::ThresholdMetaCipher, + ThresholdMetaCipher::new(Codec::Chacha20Poly1305, vec![0u8; 12]) + .to_cbor_bytes() + .unwrap() + .into(), + ); + let mut share = meta_mk.clone(); + share.attributes = attrs; + assert!(unavailable(&read_threshold_params(&share, Some(&meta_mk)))); + } + + #[cfg(feature = "chacha20")] #[test] fn test_generate_meta_key_is_32_bytes() { let key = generate_meta_key(); diff --git a/src/views/x25519.rs b/src/views/x25519.rs index 7096906..3db1b2e 100644 --- a/src/views/x25519.rs +++ b/src/views/x25519.rs @@ -132,16 +132,6 @@ impl<'a> FingerprintView for View<'a> { } } -/// Encode a sealed message: [aead_codec Codec][nonce Varbytes][ct+tag Varbytes] -/// The ephemeral public key is carried externally (in the BsMessage outer format). -fn encode_sealed(aead_codec: Codec, nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut aead_codec.into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - /// Decode a sealed message: [aead_codec Codec][nonce Varbytes][ct+tag Varbytes] fn decode_sealed(data: &[u8]) -> Result { let (aead_codec, ptr) = Codec::try_decode_from(data) @@ -198,7 +188,7 @@ impl<'a> SealView for View<'a> { .try_build()?; Ok(( - encode_sealed(aead_codec, &nonce, &ct_tag), + aead::encode_sealed(aead_codec, &nonce, &ct_tag), Some(ephemeral_mk), )) } diff --git a/src/views/x25519_frodokem640.rs b/src/views/x25519_frodokem640.rs index 17adde1..f0b87dc 100644 --- a/src/views/x25519_frodokem640.rs +++ b/src/views/x25519_frodokem640.rs @@ -233,15 +233,6 @@ fn combine_shared_secrets( *hasher.finalize().as_bytes() } -fn encode_sealed(ephemeral_pub: &[u8], frodo_ct: &[u8], nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(ephemeral_pub.to_vec()).into()); - out.append(&mut Varbytes::new(frodo_ct.to_vec()).into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - fn decode_sealed(data: &[u8]) -> Result { let (ephemeral_pub, ptr) = Varbytes::try_decode_from(data) .map_err(|_| SealError::InvalidFormat("missing ephemeral public key".into()))?; @@ -308,7 +299,12 @@ impl<'a> SealView for View<'a> { let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; Ok(( - encode_sealed(ephemeral_pub.as_bytes(), frodo_ct.as_ref(), &nonce, &ct_tag), + aead::encode_hybrid_sealed( + ephemeral_pub.as_bytes(), + frodo_ct.as_ref(), + &nonce, + &ct_tag, + ), None, )) } diff --git a/src/views/x25519_mceliece348864.rs b/src/views/x25519_mceliece348864.rs index 76854f4..18af2df 100644 --- a/src/views/x25519_mceliece348864.rs +++ b/src/views/x25519_mceliece348864.rs @@ -8,10 +8,13 @@ //! //! # Deprecation //! -//! This view is deprecated. Key-recovery attacks now solve the TII McEliece -//! challenges; see for the -//! recovered keys. It compiles only with the `deprecated` feature. +//! This view is deprecated: 2026 structural key-recovery attacks on binary +//! Goppa codes led Germany's BSI to advise against Classic McEliece in new +//! developments. See the crate-level Deprecations section for the papers and +//! the BSI note. Use the X25519-ML-KEM-768 or X25519-FrodoKEM-640 hybrid +//! instead. It compiles only with the `deprecated` feature. +use crate::views::classic_mceliece::{self, ALGORITHM}; use crate::views::dispatch::dispatch_data_view; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, @@ -23,8 +26,6 @@ use multi_codec::Codec; use multi_hash::{Multihash, mh}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; -use rand_chacha::ChaCha20Rng; -use rand_core_06::SeedableRng; use x25519_dalek::{PublicKey, StaticSecret}; use zeroize::Zeroizing; @@ -36,6 +37,12 @@ const X25519_PUB_LEN: usize = 32; /// Decoded sealed message: (ephemeral_x25519_pub, mceliece_ct, nonce, ciphertext+tag) type SealedParts = (Vec, Vec, Vec, Vec); +/// The Classic McEliece 348864 encapsulation key length, `mt * k / 8` bytes. +const MCELIECE_PUB_LEN: usize = { + let p = ALGORITHM.params(); + p.m * p.t * p.k / 8 +}; + /// Only ChaCha20-Poly1305 allowed (matches the other X-Wing-like hybrid KEMs). fn is_aead_allowed(codec: Codec) -> bool { codec == Codec::Chacha20Poly1305 @@ -109,13 +116,12 @@ impl<'a> ConvView for View<'a> { let mceliece_seed: [u8; 32] = secret_bytes[X25519_SEED_LEN..PRIV_SEED_LEN] .try_into() .map_err(|_| ConversionsError::SecretKeyFailure("invalid mceliece seed".into()))?; - let mut rng = ChaCha20Rng::from_seed(mceliece_seed); - let (pk, _sk) = mceliece348864::keypair_boxed(&mut rng); + let (ek, _dk) = classic_mceliece::keypair_from_seed(mceliece_seed)?; // Concatenate (classical-first): x25519_pub (32) || mceliece_pub - let mut pub_bytes = Vec::with_capacity(X25519_PUB_LEN + pk.as_ref().len()); + let mut pub_bytes = Vec::with_capacity(X25519_PUB_LEN + ek.value().len()); pub_bytes.extend_from_slice(x25519_pub.as_bytes()); - pub_bytes.extend_from_slice(pk.as_ref()); + pub_bytes.extend_from_slice(ek.value()); Builder::new(Codec::X25519Mceliece348864Pub) .with_comment(&self.mk.comment) @@ -170,15 +176,6 @@ fn combine_shared_secrets( *hasher.finalize().as_bytes() } -fn encode_sealed(ephemeral_pub: &[u8], mceliece_ct: &[u8], nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(ephemeral_pub.to_vec()).into()); - out.append(&mut Varbytes::new(mceliece_ct.to_vec()).into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - fn decode_sealed(data: &[u8]) -> Result { let (ephemeral_pub, ptr) = Varbytes::try_decode_from(data) .map_err(|_| SealError::InvalidFormat("missing ephemeral public key".into()))?; @@ -211,7 +208,7 @@ impl<'a> SealView for View<'a> { } let pub_bytes = self.key_bytes()?; - if pub_bytes.len() != X25519_PUB_LEN + mceliece348864::CRYPTO_PUBLICKEYBYTES { + if pub_bytes.len() != X25519_PUB_LEN + MCELIECE_PUB_LEN { return Err( SealError::EncapsulationFailed("invalid hybrid public key length".into()).into(), ); @@ -228,15 +225,13 @@ impl<'a> SealView for View<'a> { let ss_x25519 = ephemeral_secret.diffie_hellman(&recipient_x25519_pub); // McEliece encapsulate - let pk_array: Box<[u8; mceliece348864::CRYPTO_PUBLICKEYBYTES]> = pub_bytes - [X25519_PUB_LEN..] - .to_vec() - .into_boxed_slice() - .try_into() - .map_err(|_| SealError::EncapsulationFailed("invalid mceliece public key".into()))?; - let pk = mceliece348864::PublicKey::from(pk_array); - let (ct, ss_mceliece) = mceliece348864::encapsulate_boxed(&pk, &mut rand_core_06::OsRng); - let mceliece_ct: &[u8] = ct.as_ref(); + let ek = ALGORITHM + .encapsulation_key_from_bytes(&pub_bytes[X25519_PUB_LEN..]) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let (ct, ss_mceliece) = ALGORITHM + .encapsulate(&ek, rand::rng()) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let mceliece_ct = ct.value(); let combined_ss = combine_shared_secrets( ss_mceliece.as_ref(), @@ -253,7 +248,7 @@ impl<'a> SealView for View<'a> { let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; Ok(( - encode_sealed(ephemeral_pub.as_bytes(), mceliece_ct, &nonce, &ct_tag), + aead::encode_hybrid_sealed(ephemeral_pub.as_bytes(), mceliece_ct, &nonce, &ct_tag), None, )) } @@ -304,15 +299,13 @@ impl<'a> OpenView for View<'a> { let mceliece_seed: [u8; 32] = secret_bytes[X25519_SEED_LEN..PRIV_SEED_LEN] .try_into() .map_err(|_| SealError::DecapsulationFailed("invalid mceliece seed".into()))?; - let mut rng = ChaCha20Rng::from_seed(mceliece_seed); - let (_pk, sk) = mceliece348864::keypair_boxed(&mut rng); - - let ct_array: [u8; mceliece348864::CRYPTO_CIPHERTEXTBYTES] = mceliece_ct_bytes - .clone() - .try_into() - .map_err(|_| SealError::DecapsulationFailed("invalid mceliece ciphertext".into()))?; - let ct = mceliece348864::Ciphertext::from(ct_array); - let ss_mceliece = mceliece348864::decapsulate_boxed(&ct, &sk); + let (_ek, dk) = classic_mceliece::keypair_from_seed(mceliece_seed)?; + let ct = ALGORITHM + .ciphertext_from_bytes(&mceliece_ct_bytes) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + let ss_mceliece = ALGORITHM + .decapsulate(&dk, &ct) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; let combined_ss = combine_shared_secrets( ss_mceliece.as_ref(), diff --git a/src/views/x25519_mlkem768.rs b/src/views/x25519_mlkem768.rs index 0bdd668..483d751 100644 --- a/src/views/x25519_mlkem768.rs +++ b/src/views/x25519_mlkem768.rs @@ -168,16 +168,6 @@ fn combine_shared_secrets( out } -/// Encode sealed message: [ephemeral_x25519_pub Varbytes][mlkem_ct Varbytes][nonce Varbytes][ct+tag Varbytes] -fn encode_sealed(ephemeral_pub: &[u8], mlkem_ct: &[u8], nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(ephemeral_pub.to_vec()).into()); - out.append(&mut Varbytes::new(mlkem_ct.to_vec()).into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - /// Decode sealed message fn decode_sealed(data: &[u8]) -> Result { let (ephemeral_pub, ptr) = Varbytes::try_decode_from(data) @@ -255,7 +245,7 @@ impl<'a> SealView for View<'a> { let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; Ok(( - encode_sealed( + aead::encode_hybrid_sealed( ephemeral_pub.as_bytes(), mlkem_ct.as_slice(), &nonce, diff --git a/src/views/x25519_sntrup761.rs b/src/views/x25519_sntrup761.rs index fb0c626..311375b 100644 --- a/src/views/x25519_sntrup761.rs +++ b/src/views/x25519_sntrup761.rs @@ -180,16 +180,6 @@ fn combine_shared_secrets( *hasher.finalize().as_bytes() } -/// Encode sealed message: [ephemeral_x25519_pub Varbytes][sntrup761_ct Varbytes][nonce Varbytes][ct+tag Varbytes] -fn encode_sealed(ephemeral_pub: &[u8], sntrup_ct: &[u8], nonce: &[u8], ct_tag: &[u8]) -> Vec { - let mut out = Vec::new(); - out.append(&mut Varbytes::new(ephemeral_pub.to_vec()).into()); - out.append(&mut Varbytes::new(sntrup_ct.to_vec()).into()); - out.append(&mut Varbytes::new(nonce.to_vec()).into()); - out.append(&mut Varbytes::new(ct_tag.to_vec()).into()); - out -} - /// Decode sealed message fn decode_sealed(data: &[u8]) -> Result { let (ephemeral_pub, ptr) = Varbytes::try_decode_from(data) @@ -263,7 +253,7 @@ impl<'a> SealView for View<'a> { let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; Ok(( - encode_sealed( + aead::encode_hybrid_sealed( ephemeral_pub.as_bytes(), sntrup_ct.as_ref(), &nonce, diff --git a/src/views/xeddsa.rs b/src/views/xeddsa.rs index 164d17a..d513020 100644 --- a/src/views/xeddsa.rs +++ b/src/views/xeddsa.rs @@ -21,7 +21,7 @@ //! 4. The verification equation is standard Ed25519 over the lifted point, //! checked with strict verification. -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -341,12 +341,7 @@ impl<'a> VerifyView for View<'a> { /// non-canonical u encoding, a small-order key, and a mixed-order key. /// Signature verification is strict: a non-canonical R or S is rejected. fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - dispatch_conv_view(self.mk)?.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; if pubmk.codec != Codec::X25519Pub { return Err(AttributesError::UnsupportedCodec(pubmk.codec).into()); diff --git a/src/views/xmss.rs b/src/views/xmss.rs index c4cc52c..8aa2b07 100644 --- a/src/views/xmss.rs +++ b/src/views/xmss.rs @@ -1,10 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 //! XMSS-SHA2_10/16/20_256 stateful hash-based signature multikey view; RFC 8391. -#![allow(dead_code)] //! //! XMSS is stateful: every signature consumes a one-time leaf `index` that must //! never be reused. [`SignView::sign`] embeds the consumed index in the returned -//! [`Multisig`] as [`multi_sig::AttrId::SigIndex`], so the index travels with +//! [`Multisig`](multi_sig::Multisig) as [`multi_sig::AttrId::SigIndex`], so the index travels with //! the signature (e.g. into a provenance log, where monotonicity is enforced). //! //! NOTE: this view signs at whatever index the stored secret key currently holds; @@ -13,7 +12,7 @@ //! [`SignView::sign_advance`] to obtain both the signature and the advanced //! secret key in one step. -use crate::views::dispatch::{dispatch_attr_view, dispatch_conv_view, dispatch_data_view}; +use crate::views::dispatch::{dispatch_attr_view, dispatch_data_view, public_form}; use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, @@ -28,7 +27,7 @@ use zeroize::Zeroizing; mod xmss_wrapper { use super::Zeroizing; - use xmss::{ + use pq_xmss::{ DetachedSignature, KeyPair, SigningKey, VerifyingKey, XmssParameter, XmssSha2_10_256, XmssSha2_16_256, XmssSha2_20_256, }; @@ -380,13 +379,7 @@ impl<'a> VerifyView for View<'a> { return Err(VerifyError::MissingMessage.into()); }; - let attr = dispatch_attr_view(self.mk)?; - let pubmk = if attr.is_secret_key() { - let kc = dispatch_conv_view(self.mk)?; - kc.to_public_key()? - } else { - self.mk.clone() - }; + let pubmk = public_form(self.mk)?; let key_bytes = { let kd = dispatch_data_view(&pubmk)?; diff --git a/tests/custom_codec_tests.rs b/tests/custom_codec_tests.rs index 8b5a580..d26c02c 100644 --- a/tests/custom_codec_tests.rs +++ b/tests/custom_codec_tests.rs @@ -459,8 +459,7 @@ fn test_custom_key_serde_roundtrip() { assert_eq!(back, mk); // binary format - let mut cbor = Vec::new(); - ciborium::into_writer(&mk, &mut cbor).unwrap(); - let back: Multikey = ciborium::from_reader(cbor.as_slice()).unwrap(); + let cbor = serde_cbor_2::to_vec(&mk).unwrap(); + let back: Multikey = serde_cbor_2::from_slice(&cbor).unwrap(); assert_eq!(back, mk); } diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs index e694c2d..d2bc7ed 100644 --- a/tests/edge_case_tests.rs +++ b/tests/edge_case_tests.rs @@ -1,9 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 //! Edge case tests for multi-key -#![allow(clippy::explicit_iter_loop, clippy::uninlined_format_args)] +#[cfg(feature = "curve25519")] use multi_codec::Codec; -use multi_key::{Builder, KEY_CODECS, Multikey}; +#[cfg(all( + feature = "bls12-381", + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" +))] +use multi_key::KEY_CODECS; +use multi_key::{Builder, Multikey}; use multi_trait::Null; /// Test null multikey @@ -15,6 +24,14 @@ fn test_null_multikey() { } /// Test all supported key codecs +#[cfg(all( + feature = "bls12-381", + feature = "curve25519", + feature = "p256", + feature = "p384", + feature = "p521", + feature = "secp256k1" +))] #[test] fn test_all_key_codecs() { let mut rng = rand::rng(); @@ -26,6 +43,7 @@ fn test_all_key_codecs() { } /// Test Clone trait +#[cfg(feature = "curve25519")] #[test] fn test_clone() { let mut rng = rand::rng(); diff --git a/tests/merkle_lamport_tests.rs b/tests/merkle_lamport_tests.rs index b877738..f307f4f 100644 --- a/tests/merkle_lamport_tests.rs +++ b/tests/merkle_lamport_tests.rs @@ -3,7 +3,6 @@ //! threshold split, share-sign, multi-sig accumulate, combine, verify. #![cfg(feature = "lamport")] -#![allow(clippy::uninlined_format_args)] use multi_codec::Codec; use multi_key::{Builder, Multikey, ViewBuilder}; use multi_sig::{Multisig, ViewBuilder as SigViewBuilder}; diff --git a/tests/proptest_tests.rs b/tests/proptest_tests.rs index 751ba20..34ce727 100644 --- a/tests/proptest_tests.rs +++ b/tests/proptest_tests.rs @@ -1,26 +1,30 @@ // SPDX-License-Identifier: Apache-2.0 //! Property-based tests for multi-key +#![cfg(feature = "curve25519")] use multi_codec::Codec; -use multi_key::{Builder, KEY_CODECS, Multikey}; -use multi_trait::TryDecodeFrom; +use multi_key::Builder; use proptest::prelude::*; /// Property: Multikey encoding and decoding should roundtrip +#[cfg(feature = "secp256k1")] #[test] fn test_multikey_roundtrip() { + use multi_key::Multikey; + use multi_trait::TryDecodeFrom; + proptest!(|(_unit in 0..1u8)| { let mut rng = rand::rng(); - for &codec in KEY_CODECS.iter().take(2) { - if let Ok(mk1) = Builder::new_from_random_bytes(codec, &mut rng) - && let Ok(mk1) = mk1.try_build() - { - let bytes: Vec = mk1.clone().into(); - let (mk2, remaining) = Multikey::try_decode_from(&bytes).unwrap(); + for codec in [Codec::Ed25519Priv, Codec::Secp256K1Priv] { + let mk1 = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let bytes = Vec::::from(&mk1); + let (mk2, remaining) = Multikey::try_decode_from(&bytes).unwrap(); - prop_assert_eq!(&mk1, &mk2); - prop_assert!(remaining.is_empty()); - } + prop_assert_eq!(&mk1, &mk2); + prop_assert!(remaining.is_empty()); } }); } diff --git a/tests/security_tests.rs b/tests/security_tests.rs index e501911..1189506 100644 --- a/tests/security_tests.rs +++ b/tests/security_tests.rs @@ -1,8 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 //! Security-focused tests for multi-key -#![allow(clippy::redundant_clone, clippy::doc_markdown)] +#![allow(clippy::redundant_clone)] +#[cfg(feature = "curve25519")] use multi_codec::Codec; -use multi_key::{Builder, Error, Multikey}; +#[cfg(feature = "curve25519")] +use multi_key::Builder; +use multi_key::{Error, Multikey}; /// Test malformed multikey data #[test] @@ -23,11 +26,13 @@ fn test_truncated_data() { /// Test empty bytes #[test] fn test_empty_bytes() { - let result = Multikey::try_from(&[] as &[u8]); + let empty: &[u8] = &[]; + let result = Multikey::try_from(empty); assert!(result.is_err()); } /// Test concurrent key generation +#[cfg(feature = "curve25519")] #[test] fn test_concurrent_generation() { use rand::SeedableRng; @@ -83,7 +88,12 @@ fn test_private_key_zeroization() { /// silently decrypted via the legacy bare-ChaCha20 fallback. Without the /// `legacy_chacha20_fallback` feature, AEAD failure must surface as a hard /// error so an attacker can never downgrade authentication. -#[cfg(not(feature = "legacy_chacha20_fallback"))] +#[cfg(all( + feature = "bcrypt", + feature = "chacha20", + feature = "curve25519", + not(feature = "legacy_chacha20_fallback") +))] #[test] fn test_chacha20_aead_tamper_rejected() { use multi_key::{Builder, ViewBuilder, cipher, kdf}; @@ -160,7 +170,12 @@ fn test_chacha20_aead_tamper_rejected() { /// `legacy_chacha20_fallback` feature enabled, tampered AEAD ciphertext falls /// back to bare ChaCha20 and "succeeds" (producing garbage). This documents /// the downgrade risk the feature carries — it must stay off by default. -#[cfg(feature = "legacy_chacha20_fallback")] +#[cfg(all( + feature = "bcrypt", + feature = "chacha20", + feature = "curve25519", + feature = "legacy_chacha20_fallback" +))] #[test] fn test_chacha20_legacy_fallback_downgrades_on_tamper() { use multi_key::{Builder, ViewBuilder, cipher, kdf};