From 73d83512f8ba8c0dc1e7a894e62a3c7ae6169ee6 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 13:19:06 -0600 Subject: [PATCH 1/5] update from bettersign Signed-off-by: Dave Grantham --- CHANGELOG.md | 25 + Cargo.toml | 102 +- benches/multikey_bench.rs | 85 + src/attrid.rs | 76 +- src/cipher.rs | 10 +- src/error.rs | 155 +- src/kdf.rs | 10 +- src/keysplit.rs | 757 +++++++++ src/lib.rs | 109 +- src/mk.rs | 2356 ++++++++++++++++++++++++++-- src/nonce.rs | 31 +- src/serde/de.rs | 6 +- src/serde/mod.rs | 171 +- src/serde/ser.rs | 4 +- src/types.rs | 224 +++ src/views.rs | 100 +- src/views/aead.rs | 233 +++ src/views/bcrypt.rs | 17 +- src/views/bls12381.rs | 377 ++++- src/views/bls12381_g1_fndsa512.rs | 270 ++++ src/views/bls12381_g1_mayo1.rs | 257 +++ src/views/bls12381_g1_mayo2.rs | 257 +++ src/views/bls12381_g1_mldsa65.rs | 294 ++++ src/views/bls12381_hybrid.rs | 54 + src/views/chacha20.rs | 84 +- src/views/classic_mceliece.rs | 435 +++++ src/views/dkg_threshold.rs | 148 ++ src/views/ed25519.rs | 28 +- src/views/ed25519_fndsa512.rs | 294 ++++ src/views/ed25519_mayo2.rs | 423 +++++ src/views/ed25519_mldsa65.rs | 316 ++++ src/views/fn_dsa.rs | 300 ++++ src/views/frodokem.rs | 282 ++++ src/views/frodokem_helper.rs | 203 +++ src/views/mayo.rs | 435 +++++ src/views/ml_dsa.rs | 353 +++++ src/views/ml_kem.rs | 482 ++++++ src/views/nist_p.rs | 746 +++++++++ src/views/rsa.rs | 489 ++++++ src/views/secp256k1.rs | 228 ++- src/views/slh_dsa.rs | 661 ++++++++ src/views/sntrup.rs | 580 +++++++ src/views/threshold_marker.rs | 355 +++++ src/views/x25519.rs | 423 +++++ src/views/x25519_frodokem640.rs | 412 +++++ src/views/x25519_mceliece348864.rs | 363 +++++ src/views/x25519_mlkem768.rs | 516 ++++++ src/views/x25519_sntrup761.rs | 505 ++++++ tests/edge_case_tests.rs | 48 + tests/proptest_tests.rs | 40 + tests/security_tests.rs | 80 + 51 files changed, 14713 insertions(+), 496 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 benches/multikey_bench.rs create mode 100644 src/keysplit.rs create mode 100644 src/types.rs create mode 100644 src/views/aead.rs create mode 100644 src/views/bls12381_g1_fndsa512.rs create mode 100644 src/views/bls12381_g1_mayo1.rs create mode 100644 src/views/bls12381_g1_mayo2.rs create mode 100644 src/views/bls12381_g1_mldsa65.rs create mode 100644 src/views/bls12381_hybrid.rs create mode 100644 src/views/classic_mceliece.rs create mode 100644 src/views/dkg_threshold.rs create mode 100644 src/views/ed25519_fndsa512.rs create mode 100644 src/views/ed25519_mayo2.rs create mode 100644 src/views/ed25519_mldsa65.rs create mode 100644 src/views/fn_dsa.rs create mode 100644 src/views/frodokem.rs create mode 100644 src/views/frodokem_helper.rs create mode 100644 src/views/mayo.rs create mode 100644 src/views/ml_dsa.rs create mode 100644 src/views/ml_kem.rs create mode 100644 src/views/nist_p.rs create mode 100644 src/views/rsa.rs create mode 100644 src/views/slh_dsa.rs create mode 100644 src/views/sntrup.rs create mode 100644 src/views/threshold_marker.rs create mode 100644 src/views/x25519.rs create mode 100644 src/views/x25519_frodokem640.rs create mode 100644 src/views/x25519_mceliece348864.rs create mode 100644 src/views/x25519_mlkem768.rs create mode 100644 src/views/x25519_sntrup761.rs create mode 100644 tests/edge_case_tests.rs create mode 100644 tests/proptest_tests.rs create mode 100644 tests/security_tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4611432 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-07-13 + +### Changed +- Synced from bettersign workspace (bs-multikey 0.7.0) +- Renamed crate from `bs-multikey` to `multi-key` +- Added PQC key views: ML-DSA, ML-KEM, SLH-DSA, FN-DSA, MAYO, SNTRUP, FrodoKEM, Classic McEliece +- Added NIST curve views: P-256, P-384, P-521 +- Added RSA views: RSA-2048, RSA-3072, RSA-4096 +- Added X25519 views and hybrid KEMs: X25519+SNTRUP761, X25519+ML-KEM-768, X25519+FrodoKEM-640, X25519+McEliece +- Added hybrid signature views: Ed25519+MAYO2, Ed25519+ML-DSA-65, Ed25519+FN-DSA-512, BLS12-381-G1+ML-DSA-65, +FN-DSA-512, +MAYO1, +MAYO2 +- Added `SealView`/`OpenView` traits for KEM-based encryption +- Added `ThresholdKeyView` trait for DKG metadata +- Added `keysplit.rs` module for verifiable threshold key splitting (Feldman VSS, gf256, dual) +- Added `types.rs` module with type-safe wrappers (`PublicKeyBytes`, `PrivateKeyBytes`, `KeyScheme`) +- Added `frodokem_helper.rs` module (inlined from former bs-frodokem wrapper) +- Added comprehensive test suite (edge cases, proptest, security) +- Major dependency updates: ed25519-dalek 3, blsful 4, elliptic-curve 0.14, vsss-rs 6, ssh-key 0.7 +- Initial published release on crates.io as `multi-key` \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 7d9eead..3297396 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,57 +1,93 @@ [package] -name = "multikey" -version = "1.0.7" +name = "multi-key" +version = "1.0.0" edition = "2021" authors = ["Dave Grantham "] description = "Multikey self-describing cryptographic key data" -repository = "https://github.com/cryptidtech/multikey.git" +repository = "https://github.com/cryptidtech/multi-key.git" readme = "README.md" license = "Apache-2.0" +keywords = ["multiformats", "multikey", "crypto", "post-quantum"] +categories = ["cryptography", "encoding"] [features] default = ["serde"] -wasm = ["getrandom/js"] # needed for CI testing on wasm32-unknown-unknown +wasm = ["getrandom/wasm_js"] [dependencies] +aes-gcm = "0.10" bcrypt-pbkdf = "0.10" -blsful = "2.5" -ed25519-dalek = { version = "2.1", features = ["rand_core"] } -elliptic-curve = "0.13" -hex = "0.4" -k256 = "0.13" -multibase = { version = "1.0", git = "https://github.com/cryptidtech/rust-multibase.git" } -multicodec = { version = "1.0", git = "https://github.com/cryptidtech/rust-multicodec.git" } -multihash = { version = "1.0", git = "https://github.com/cryptidtech/multihash.git" } -multisig = { version = "^1.0", git = "https://github.com/cryptidtech/multisig.git" } -multitrait = { version = "1.0", git = "https://github.com/cryptidtech/multitrait.git" } -multiutil = { version = "1.0", git = "https://github.com/cryptidtech/multiutil.git" } -rand = "0.8" -sec1 = "0.7" -serde = { version = "1.0", default-features = false, features = [ - "alloc", - "derive", -], optional = true } +blake3 = { version = "1.5.1", features = ["traits-preview", "zeroize"] } +# blsful configured per-target below (blst for native, rust for wasm) +mceliece348864 = { version = "1.0" } +frodo-kem-rs = { version = "0.7", default-features = false, features = ["frodo640aes", "frodo976aes", "frodo1344aes", "frodo640shake", "frodo976shake", "frodo1344shake"] } +multi-base = { version = "1.0", path = "../multi-base" } +multi-codec = { version = "1.0", path = "../multi-codec" } +multi-hash = { version = "1.0", path = "../multi-hash" } +multi-sig = { version = "1.0", path = "../multi-sig" } +multi-trait = { version = "1.0", path = "../multi-trait" } +multi-util = { version = "1.0", path = "../multi-util" } chacha20 = "0.9" +chacha20poly1305 = "0.10" +ed25519-dalek = { version = "3.0", features = ["pkcs8", "rand_core", "alloc"] } +elliptic-curve = "0.14" +fn-dsa = "0.3" +getrandom = { version = "0.4", features = ["sys_rng"] } +hex = "0.4" +hkdf = "0.12" +k256 = { version = "0.14", features = ["ecdh"] } +ml-dsa = "0.1.1" +ml-kem = { version = "0.2", features = ["deterministic"] } +p256 = { version = "0.14", features = ["ecdsa", "ecdh"] } +p384 = { version = "0.14", features = ["ecdsa", "ecdh"] } +p521 = { version = "0.14.0-rc.15", features = ["ecdsa", "ecdh"] } poly1305 = "0.8" -thiserror = "1.0" +pq-mayo = "0.5.0" +# RustCrypto 0.14 stack (vsss-rs 6, elliptic-curve 0.14, ed25519-dalek 3) is on +# rand_core 0.10; pin the rand ecosystem to the matching majors so RNGs thread +# through cleanly (overrides the workspace rand 0.8 used by not-yet-migrated crates). +rand = "0.10" +# rand_chacha stays 0.3 (rand_core 0.6): the only ChaCha use is deterministic +# PQC keygen (classic-mceliece), which is on the rand_core 0.6 island below. +rand_chacha = "0.3" +rand_core = "0.10" +# The PQC crates (fn-dsa, ml-dsa, ml-kem, slh-dsa, sntrup) are still on rand_core +# 0.6 (fn-dsa exposes its own RngCore/CryptoRng traits); keep a 0.6 OsRng for +# those sites while the curve stack above uses rand_core 0.10. +rand_core_06 = { package = "rand_core", version = "0.6", features = ["getrandom"] } +rsa = { version = "0.10.0-rc.18", features = ["sha2"] } +sec1 = "0.8" +serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } +sha2 = "0.10" +signature = "3" +slh-dsa = "0.2.0-rc.5" +sntrup = "0.3.0" +ssh-encoding = "0.3" +thiserror = { version = "2.0" } typenum = "1.17" unsigned-varint = { version = "0.8", features = ["std"] } -vsss-rs = "3.4" -zeroize = "1.7" -ssh-encoding = { version = "0.2" } +serde_cbor = "0.11" +vsss-rs = "6.0.0-rc2" +curve25519-dalek = { version = "5.0.0", features = ["group"] } +x25519-dalek = { version = "3.0", features = ["static_secrets", "getrandom"] } +zeroize = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -ssh-key = { version = "0.6", default-features = false, features = [ - "alloc", - "ecdsa", - "ed25519", -] } -getrandom = { version = "0.2", features = ["js"], optional = true } +blsful = { version = "4.0.0-rc1", default-features = false, features = ["rust"] } +ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["alloc", "ecdsa", "ed25519", "p256", "p384", "p521"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -ssh-key = { version = "0.6", features = ["alloc", "crypto", "ed25519"] } +blsful = { version = "4.0.0-rc1", default-features = false, features = ["blst"] } +ssh-key = { version = "0.7.0-rc.11", features = ["alloc", "crypto", "ed25519"] } [dev-dependencies] -serde_test = "1.0" +criterion = { version = "0.8", features = ["html_reports"] } +proptest = "1.4" serde_cbor = "0.11" serde_json = "1.0" +serde_test = "1.0" + +[[bench]] +name = "multikey_bench" +harness = false +path = "benches/multikey_bench.rs" \ No newline at end of file diff --git a/benches/multikey_bench.rs b/benches/multikey_bench.rs new file mode 100644 index 0000000..0017685 --- /dev/null +++ b/benches/multikey_bench.rs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Performance benchmarks for multi-key + +use multi_codec::Codec; +use multi_key::{Builder, Multikey}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use std::hint::black_box; + +/// Benchmark key generation +fn bench_key_generation(c: &mut Criterion) { + let mut group = c.benchmark_group("key_generation"); + + let mut rng = rand::rng(); + let algorithms = vec![ + ("Ed25519", Codec::Ed25519Priv), + ("Secp256k1", Codec::Secp256K1Priv), + ]; + + for (name, codec) in algorithms { + group.bench_with_input(BenchmarkId::new("generate", name), &codec, |b, &codec| { + b.iter(|| { + Builder::new_from_random_bytes(black_box(codec), &mut rng) + .unwrap() + .try_build() + }) + }); + } + + group.finish(); +} + +/// Benchmark encoding +fn bench_encoding(c: &mut Criterion) { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + c.bench_function("multikey_to_bytes", |b| { + b.iter(|| { + let _bytes: Vec = black_box(mk.clone()).into(); + }) + }); +} + +/// Benchmark decoding +fn bench_decoding(c: &mut Criterion) { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let bytes: Vec = mk.into(); + + c.bench_function("multikey_from_bytes", |b| { + b.iter(|| Multikey::try_from(black_box(bytes.as_ref()))) + }); +} + +/// Benchmark roundtrip +fn bench_roundtrip(c: &mut Criterion) { + let mut rng = rand::rng(); + + c.bench_function("roundtrip_ed25519", |b| { + b.iter(|| { + let mk1 = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let bytes: Vec = mk1.into(); + let _mk2 = Multikey::try_from(bytes.as_ref()).unwrap(); + }) + }); +} + +criterion_group!( + benches, + bench_key_generation, + bench_encoding, + bench_decoding, + bench_roundtrip +); + +criterion_main!(benches); diff --git a/src/attrid.rs b/src/attrid.rs index 89a5835..6731ad8 100644 --- a/src/attrid.rs +++ b/src/attrid.rs @@ -1,6 +1,6 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{error::AttributesError, Error}; -use multitrait::{EncodeInto, TryDecodeFrom}; +use multi_trait::{EncodeInto, TryDecodeFrom}; use std::fmt; /// enum of attribute identifiers. this is here to avoid collisions between @@ -33,6 +33,42 @@ pub enum AttrId { ShareIdentifier, /// codec-specific threshold key data ThresholdData, + /// DKG: the VLAD bytes of the session owner + DkgOwnerId, + /// DKG: the CBOR-encoded list of participant VLADs + DkgParticipants, + /// DKG: the t-of-n threshold value (u16 little-endian) + DkgThreshold, + /// DKG: the t-of-n limit value (u16 little-endian) + DkgLimit, + /// DKG: the group public key bytes + DkgGroupPublicKey, + /// DKG: this participant's frost-dkg Identifier bytes + DkgIdentifier, + /// DKG: the session UUID bytes + DkgSessionId, + /// Feldman verifier commitment set for a verifiable key share (CBOR list of + /// group-element byte strings). Present on Feldman shares of split keys. + ShareVerifiers, + /// Secondary dual share for seed-based keys (Ed25519/X25519): the CBOR + /// encoding of the Feldman scalar share carried alongside the gf256 seed + /// share. Lets these keys mirror the ECC shares without losing exact restore. + ShareDual, + /// Split (Shamir) threshold key: the group/combined BLS public key bytes. + /// This is the split-key analogue of [`AttrId::DkgGroupPublicKey`]; it is + /// stamped on a controller Multikey so that the combined threshold + /// signature can be verified against the group public key. + ThresholdGroupPublicKey, + /// Split (Shamir) threshold key: a CBOR-encoded map from share-identifier + /// bytes (the Shamir x-coordinate, exactly as stored in + /// [`AttrId::ShareIdentifier`]) to a participant record carrying that + /// participant's VLAD and its BLS public key share v_i bytes. + ThresholdParticipants, + /// Split (Shamir) threshold key: a `Multisig` (signature) over the canonical + /// encoding of the marker bundle (group public key ‖ participants ‖ threshold + /// ‖ limit), produced by the controller's signing key. Lets a verifier reject + /// a tampered marker before trusting it (defeats TSIG-1 marker forgery). + ThresholdMarkerSig, } impl AttrId { @@ -56,6 +92,18 @@ impl AttrId { AttrId::Limit => "limit", AttrId::ShareIdentifier => "share-identifier", AttrId::ThresholdData => "threshold-data", + AttrId::DkgOwnerId => "dkg-owner-id", + AttrId::DkgParticipants => "dkg-participants", + AttrId::DkgThreshold => "dkg-threshold", + AttrId::DkgLimit => "dkg-limit", + AttrId::DkgGroupPublicKey => "dkg-group-public-key", + AttrId::DkgIdentifier => "dkg-identifier", + AttrId::DkgSessionId => "dkg-session-id", + AttrId::ShareVerifiers => "share-verifiers", + AttrId::ShareDual => "share-dual", + AttrId::ThresholdGroupPublicKey => "threshold-group-public-key", + AttrId::ThresholdParticipants => "threshold-participants", + AttrId::ThresholdMarkerSig => "threshold-marker-sig", } } } @@ -83,6 +131,18 @@ impl TryFrom for AttrId { 9 => Ok(AttrId::Limit), 10 => Ok(AttrId::ShareIdentifier), 11 => Ok(AttrId::ThresholdData), + 12 => Ok(AttrId::DkgOwnerId), + 13 => Ok(AttrId::DkgParticipants), + 14 => Ok(AttrId::DkgThreshold), + 15 => Ok(AttrId::DkgLimit), + 16 => Ok(AttrId::DkgGroupPublicKey), + 17 => Ok(AttrId::DkgIdentifier), + 18 => Ok(AttrId::DkgSessionId), + 19 => Ok(AttrId::ShareVerifiers), + 20 => Ok(AttrId::ShareDual), + 21 => Ok(AttrId::ThresholdGroupPublicKey), + 22 => Ok(AttrId::ThresholdParticipants), + 23 => Ok(AttrId::ThresholdMarkerSig), _ => Err(AttributesError::InvalidAttributeValue(c).into()), } } @@ -130,6 +190,18 @@ impl TryFrom<&str> for AttrId { "limit" => Ok(AttrId::Limit), "share-identifier" => Ok(AttrId::ShareIdentifier), "threshold-data" => Ok(AttrId::ThresholdData), + "dkg-owner-id" => Ok(AttrId::DkgOwnerId), + "dkg-participants" => Ok(AttrId::DkgParticipants), + "dkg-threshold" => Ok(AttrId::DkgThreshold), + "dkg-limit" => Ok(AttrId::DkgLimit), + "dkg-group-public-key" => Ok(AttrId::DkgGroupPublicKey), + "dkg-identifier" => Ok(AttrId::DkgIdentifier), + "dkg-session-id" => Ok(AttrId::DkgSessionId), + "share-verifiers" => Ok(AttrId::ShareVerifiers), + "share-dual" => Ok(AttrId::ShareDual), + "threshold-group-public-key" => Ok(AttrId::ThresholdGroupPublicKey), + "threshold-participants" => Ok(AttrId::ThresholdParticipants), + "threshold-marker-sig" => Ok(AttrId::ThresholdMarkerSig), _ => Err(AttributesError::InvalidAttributeName(s.to_string()).into()), } } diff --git a/src/cipher.rs b/src/cipher.rs index 2e5447f..463aea6 100644 --- a/src/cipher.rs +++ b/src/cipher.rs @@ -1,7 +1,7 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{mk::Attributes, AttrId, Error, Multikey}; -use multicodec::Codec; -use rand::{CryptoRng, RngCore}; +use multi_codec::Codec; +use rand_core::CryptoRng; use zeroize::Zeroizing; /// Builder for creating a Multikey intended for encryption/decryption of other @@ -49,7 +49,7 @@ impl Builder { } /// add a random nonce for cipher - pub fn with_random_nonce(mut self, len: usize, rng: &mut (impl RngCore + CryptoRng)) -> Self { + pub fn with_random_nonce(mut self, len: usize, rng: &mut impl CryptoRng) -> Self { // heap allocate a buffer to receive the random nonce let mut buf: Zeroizing> = vec![0; len].into(); rng.fill_bytes(buf.as_mut_slice()); @@ -111,7 +111,7 @@ mod tests { .unwrap(); // generate a random secret key - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = mk::Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) .unwrap() .with_comment("test key") diff --git a/src/error.rs b/src/error.rs index 82644e4..05c1784 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 /// Errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { /// Attributes error @@ -18,6 +18,9 @@ pub enum Error { /// Nonce error #[error(transparent)] Nonce(#[from] NonceError), + /// Seal error + #[error(transparent)] + Seal(#[from] SealError), /// Sign error #[error(transparent)] Sign(#[from] SignError), @@ -27,25 +30,28 @@ pub enum Error { /// Verify error #[error(transparent)] Verify(#[from] VerifyError), + /// Key split/combine error + #[error("key split error: {0}")] + KeySplit(String), /// Multibase conversion error #[error(transparent)] - Multibase(#[from] multibase::Error), + Multibase(#[from] multi_base::Error), /// Multicodec decoding error #[error(transparent)] - Multicodec(#[from] multicodec::Error), + Multicodec(#[from] multi_codec::Error), /// Multiutil error #[error(transparent)] - Multiutil(#[from] multiutil::Error), + Multiutil(#[from] multi_util::Error), /// Multisig error #[error(transparent)] - Multisig(#[from] multisig::Error), + Multisig(#[from] multi_sig::Error), /// Multitrait error #[error(transparent)] - Multitrait(#[from] multitrait::Error), + Multitrait(#[from] multi_trait::Error), /// Multihash error #[error(transparent)] - Multihash(#[from] multihash::Error), + Multihash(#[from] multi_hash::Error), /// Utf8 error #[error(transparent)] @@ -62,18 +68,18 @@ pub enum Error { } /// Attributes errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum AttributesError { /// Error with the key codec #[error("Unsupported key codec: {0}")] - UnsupportedCodec(multicodec::Codec), + UnsupportedCodec(multi_codec::Codec), /// No key data attribute #[error("Key data unit missing")] MissingKey, /// Not a secret key #[error("Not a secret key {0}")] - NotSecretKey(multicodec::codec::Codec), + NotSecretKey(multi_codec::codec::Codec), /// Key is encrypted #[error("Key is encrypted")] EncryptedKey, @@ -95,10 +101,25 @@ pub enum AttributesError { /// No threshold data #[error("Missing threshold data")] MissingThresholdData, + /// No group public key + #[error("Missing group public key")] + MissingGroupPublicKey, + /// No participant map + #[error("Missing threshold participants")] + MissingThresholdParticipants, + /// CBOR (de)serialization error for a threshold marker attribute + #[error("Threshold marker CBOR error: {0}")] + ThresholdMarkerCbor(String), + /// The threshold marker bundle carries no authenticating signature. + #[error("Missing threshold marker signature")] + MissingThresholdMarkerSig, + /// The threshold marker signature failed verification (tampered marker). + #[error("Threshold marker signature verification failed")] + ThresholdMarkerSigInvalid, } /// Conversions errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum ConversionsError { /// Ssh key error @@ -115,12 +136,12 @@ pub enum ConversionsError { UnsupportedAlgorithm(String), /// Error with the key codec #[error("Unsupported key codec: {0}")] - UnsupportedCodec(multicodec::Codec), + UnsupportedCodec(multi_codec::Codec), } /// SSH Encoding Errors that cannot be handled by thiserror since they may not use the std feature /// in the case of wasm32 target. -#[derive(Clone, Debug)] +#[derive(Debug)] pub enum SshErrors { /// Error from [ssh_key::Error] Key(ssh_key::Error), @@ -162,12 +183,12 @@ impl From for SshErrors { } /// Cipher errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum CipherError { /// Error with the cipher codec #[error("Unsupported cipher codec: {0}")] - UnsupportedCodec(multicodec::Codec), + UnsupportedCodec(multi_codec::Codec), /// Missing codec #[error("Missing cipher codec")] MissingCodec, @@ -198,7 +219,7 @@ pub enum CipherError { } /// Kdf errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum KdfError { /// Bcrypt PBKDF error @@ -206,7 +227,7 @@ pub enum KdfError { Bcrypt(#[from] bcrypt_pbkdf::Error), /// Error with the KDF codec #[error("Unsupported KDF codec: {0}")] - UnsupportedCodec(multicodec::Codec), + UnsupportedCodec(multi_codec::Codec), /// Missing codec #[error("Missing KDF codec")] MissingCodec, @@ -222,7 +243,7 @@ pub enum KdfError { } /// Nonce errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum NonceError { /// Missing sigil @@ -233,8 +254,44 @@ pub enum NonceError { MissingBytes, } +/// Seal/Open (encryption) errors created by this library +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum SealError { + /// Not an encryption key + #[error("Not an encryption key")] + NotEncryptionKey, + /// Not a decapsulation (private) key + #[error("Not a decapsulation key")] + NotDecapsulationKey, + /// Not an encapsulation (public) key + #[error("Not an encapsulation key")] + NotEncapsulationKey, + /// Unsupported AEAD codec + #[error("Unsupported AEAD codec: {0}")] + UnsupportedAeadCodec(multi_codec::Codec), + /// Encapsulation failed + #[error("Encapsulation failed: {0}")] + EncapsulationFailed(String), + /// Decapsulation failed + #[error("Decapsulation failed: {0}")] + DecapsulationFailed(String), + /// AEAD seal failed + #[error("AEAD seal failed: {0}")] + AeadSealFailed(String), + /// AEAD open failed + #[error("AEAD open failed")] + AeadOpenFailed, + /// Invalid format + #[error("Invalid sealed message format: {0}")] + InvalidFormat(String), + /// Key derivation failed + #[error("Key derivation failed: {0}")] + KeyDerivationFailed(String), +} + /// Sign errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum SignError { /// Not a signing key @@ -249,7 +306,7 @@ pub enum SignError { } /// Threshold errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum ThresholdError { /// Bls error @@ -273,7 +330,7 @@ pub enum ThresholdError { } /// Verify errors created by this library -#[derive(Clone, Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum VerifyError { /// Missing signature @@ -286,3 +343,57 @@ pub enum VerifyError { #[error("Bad signature: {0}")] BadSignature(String), } + +impl Error { + /// Get the error kind as a string + pub fn kind(&self) -> &str { + match self { + Self::Attributes(_) => "Attributes", + Self::Conversions(_) => "Conversions", + Self::Cipher(_) => "Cipher", + Self::Kdf(_) => "Kdf", + Self::Nonce(_) => "Nonce", + Self::Seal(_) => "Seal", + Self::Sign(_) => "Sign", + Self::Threshold(_) => "Threshold", + Self::Verify(_) => "Verify", + Self::KeySplit(_) => "KeySplit", + Self::Multibase(_) => "Multibase", + Self::Multicodec(_) => "Multicodec", + Self::Multiutil(_) => "Multiutil", + Self::Multisig(_) => "Multisig", + Self::Multitrait(_) => "Multitrait", + Self::Multihash(_) => "Multihash", + Self::Utf8(_) => "Utf8", + Self::DuplicateAttribute(_) => "DuplicateAttribute", + Self::MissingSigil => "MissingSigil", + Self::UnsupportedAlgorithm(_) => "UnsupportedAlgorithm", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_kind() { + let err = Error::MissingSigil; + assert_eq!(err.kind(), "MissingSigil"); + } + + #[test] + fn test_error_display() { + let err = Error::MissingSigil; + assert!(!err.to_string().is_empty()); + } + + #[test] + fn test_error_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + } +} diff --git a/src/kdf.rs b/src/kdf.rs index d9daab5..eea760c 100644 --- a/src/kdf.rs +++ b/src/kdf.rs @@ -1,8 +1,8 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{mk::Attributes, AttrId, Error, Multikey}; -use multicodec::Codec; -use multiutil::Varuint; -use rand::{CryptoRng, RngCore}; +use multi_codec::Codec; +use multi_util::Varuint; +use rand_core::CryptoRng; use zeroize::Zeroizing; /// Multikey builder constructs kdf multikeys. @@ -49,7 +49,7 @@ impl Builder { } /// add a random salt for the kdf - pub fn with_random_salt(mut self, len: usize, rng: &mut (impl RngCore + CryptoRng)) -> Self { + pub fn with_random_salt(mut self, len: usize, rng: &mut impl CryptoRng) -> Self { // heap allocate a buffer to receive the random salt let mut buf: Zeroizing> = vec![0; len].into(); rng.fill_bytes(buf.as_mut_slice()); diff --git a/src/keysplit.rs b/src/keysplit.rs new file mode 100644 index 0000000..ef77448 --- /dev/null +++ b/src/keysplit.rs @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Split a private [`Multikey`] into `n` threshold-`t` verifiable shares and +//! recombine them. This only splits/recombines static key material — it does +//! not perform threshold signing. +//! +//! Scheme selection per key family: +//! * **Feldman VSS over the curve scalar field** (verifiable, with commitments) +//! for single-scalar keys: secp256k1, P-256, P-384, P-521, and BLS12-381 +//! G1/G2 — so every elliptic-curve key, BLS included, produces commitment- +//! verifiable shares uniformly. +//! * **gf256 byte-sharing** for RSA and the post-quantum families (ML-DSA, +//! ML-KEM, SLH-DSA, FN-DSA, MAYO, SNTRUP, FrodoKEM, Classic McEliece) and the +//! hybrids — opaque byte secrets that round-trip exactly. +//! * **Dual** for Ed25519/X25519: a gf256 share of the 32-byte seed (exact, +//! functional restore) plus a Feldman scalar share with commitments (mirrors +//! the ECC keys, threshold-signing-ready). Both are stored per share. +//! +//! A share is a [`Codec::KeySplitShare`] `Multikey` whose [`AttrId::KeyData`] is +//! the CBOR-encoded share payload, so shares serialize to CBOR/JSON through the +//! normal Multikey encoders. + +use crate::mk::Attributes; +use crate::{AttrId, Builder, Error, Multikey, Views}; +use blsful::inner_types::{G1Projective, G2Projective, Scalar as BlsScalar}; +use multi_codec::Codec; +use multi_util::CodecInfo; +use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar as DalekScalar}; +use elliptic_curve::ff::PrimeField; +use elliptic_curve::group::GroupEncoding; +use rand_core::CryptoRng; +use serde::{Deserialize, Serialize}; +use vsss_rs::{ + feldman, DefaultShare, FeldmanVerifierSet, Gf256, IdentifierPrimeField, ReadableShareSet, + ShareVerifierGroup, ValueGroup, +}; +use zeroize::Zeroizing; + +type Ds = DefaultShare, IdentifierPrimeField>; + +fn err(e: E) -> Error { + Error::KeySplit(e.to_string()) +} + +/// Sharing scheme tag stored in each share payload. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +enum Scheme { + Feldman, + Gf256, +} + +/// The Feldman scalar share carried alongside an Ed25519/X25519 gf256 seed share. +#[derive(Clone, Serialize, Deserialize)] +struct DualPayload { + identifier: Vec, + value: Vec, + verifiers: Vec>, +} + +/// CBOR payload stored in a [`Codec::KeySplitShare`] Multikey's key data. +#[derive(Clone, Serialize, Deserialize)] +struct SharePayload { + /// the original (split) key's codec, as its multicodec code + codec: u64, + threshold: u16, + limit: u16, + scheme: Scheme, + identifier: Vec, + value: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + verifiers: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + 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)) + } + + 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()) + } + + 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")) + } + }; +} + +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, + Codec::Secp256K1Priv + | Codec::P256Priv + | Codec::P384Priv + | Codec::P521Priv + | Codec::Ed25519Priv + | Codec::X25519Priv + | Codec::Bls12381G1Priv + | Codec::Bls12381G2Priv + ) +} + +fn ecc_split( + codec: Codec, + secret: &[u8], + t: usize, + 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) + } + 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), + _ => 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), + _ => Err(err("unsupported curve codec")), + } +} + +// ---- gf256 byte path -------------------------------------------------------- + +fn gf256_split( + secret: &[u8], + t: usize, + n: usize, + rng: impl CryptoRng, +) -> Result)>, Error> { + let raw = Gf256::split_array(t, n, secret, rng).map_err(err)?; + raw.into_iter() + .map(|inner| { + let (id, val) = inner + .split_first() + .ok_or_else(|| err("empty gf256 share"))?; + Ok((*id, val.to_vec())) + }) + .collect() +} + +fn gf256_combine(rows: &[(u8, Vec)]) -> Result, Error> { + let full: Vec> = rows + .iter() + .map(|(id, val)| { + let mut r = Vec::with_capacity(1 + val.len()); + r.push(*id); + r.extend_from_slice(val); + r + }) + .collect(); + Gf256::combine_array(&full).map_err(err) +} + +// ---- payload construction / share wrapping ---------------------------------- + +fn build_payloads( + codec: Codec, + secret: &[u8], + t: usize, + n: usize, + mut rng: impl CryptoRng, +) -> Result, Error> { + let ccode: u64 = codec.into(); + if matches!(codec, Codec::Ed25519Priv | Codec::X25519Priv) { + // dual: gf256 seed share (exact restore) + Feldman scalar share + let seed = gf256_split(secret, t, n, &mut rng)?; + let (pairs, verifiers) = ecc_split(codec, secret, t, n, &mut rng)?; + Ok(seed + .into_iter() + .zip(pairs) + .map(|((id, val), (fid, fval))| SharePayload { + codec: ccode, + threshold: t as u16, + limit: n as u16, + scheme: Scheme::Gf256, + identifier: vec![id], + value: val, + verifiers: None, + dual: Some(DualPayload { + identifier: fid, + value: fval, + verifiers: verifiers.clone(), + }), + }) + .collect()) + } else if is_feldman_codec(codec) { + let (pairs, verifiers) = ecc_split(codec, secret, t, n, rng)?; + Ok(pairs + .into_iter() + .map(|(id, val)| SharePayload { + codec: ccode, + threshold: t as u16, + limit: n as u16, + scheme: Scheme::Feldman, + identifier: id, + value: val, + verifiers: Some(verifiers.clone()), + dual: None, + }) + .collect()) + } else { + let rows = gf256_split(secret, t, n, rng)?; + Ok(rows + .into_iter() + .map(|(id, val)| SharePayload { + codec: ccode, + threshold: t as u16, + limit: n as u16, + scheme: Scheme::Gf256, + identifier: vec![id], + value: val, + verifiers: None, + dual: None, + }) + .collect()) + } +} + +fn wrap_share(orig: &Multikey, payload: &SharePayload) -> Result { + let cbor = serde_cbor::to_vec(payload).map_err(err)?; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, Zeroizing::new(cbor)); + Ok(Multikey { + codec: Codec::KeySplitShare, + comment: orig.comment.clone(), + attributes, + }) +} + +fn unwrap_share(mk: &Multikey) -> Result { + if mk.codec() != Codec::KeySplitShare { + return Err(err("not a key-split share")); + } + let kd = mk + .attributes + .get(&AttrId::KeyData) + .ok_or_else(|| err("share missing key data"))?; + serde_cbor::from_slice(kd.as_slice()).map_err(err) +} + +// ---- public API ------------------------------------------------------------- + +/// Split a private [`Multikey`] into `limit` shares, any `threshold` of which +/// recombine it. Returns one [`Codec::KeySplitShare`] Multikey per share. +pub fn split( + mk: &Multikey, + threshold: usize, + limit: usize, + rng: impl CryptoRng, +) -> Result, Error> { + if threshold < 2 || threshold > limit || limit > 255 { + return Err(err("need 2 <= threshold <= limit <= 255")); + } + let codec = mk.codec(); + let secret = mk.data_view()?.secret_bytes()?; + let payloads = build_payloads(codec, &secret, threshold, limit, rng)?; + payloads.iter().map(|p| wrap_share(mk, p)).collect() +} + +/// Verify a single share's Feldman commitments (Feldman scheme, or the dual of +/// an Ed25519/X25519 seed share). gf256-only shares have nothing to verify. +pub fn verify_share(share: &Multikey) -> Result<(), Error> { + let p = unwrap_share(share)?; + let codec = Codec::try_from(p.codec).map_err(err)?; + match p.scheme { + Scheme::Feldman => { + let vb = p + .verifiers + .as_deref() + .ok_or_else(|| err("missing verifiers"))?; + ecc_verify(codec, &p.identifier, &p.value, vb) + } + Scheme::Gf256 => { + if let Some(d) = &p.dual { + ecc_verify(codec, &d.identifier, &d.value, &d.verifiers)?; + } + Ok(()) + } + } +} + +/// Recombine [`Codec::KeySplitShare`] shares into the original private key. At +/// least `threshold` shares for the same key are required; Feldman shares are +/// verified before combining. +pub fn combine(shares: &[Multikey]) -> Result { + if shares.is_empty() { + return Err(err("no shares supplied")); + } + let payloads: Vec = shares.iter().map(unwrap_share).collect::>()?; + let first = &payloads[0]; + if payloads + .iter() + .any(|p| p.codec != first.codec || p.scheme != first.scheme) + { + return Err(err("shares describe different keys")); + } + let codec = Codec::try_from(first.codec).map_err(err)?; + + let secret: Zeroizing> = match first.scheme { + Scheme::Feldman => { + for p in &payloads { + let vb = p + .verifiers + .as_deref() + .ok_or_else(|| err("missing verifiers"))?; + ecc_verify(codec, &p.identifier, &p.value, vb)?; + } + let pairs: Vec<(Vec, Vec)> = payloads + .iter() + .map(|p| (p.identifier.clone(), p.value.clone())) + .collect(); + Zeroizing::new(ecc_combine(codec, &pairs)?) + } + Scheme::Gf256 => { + for p in &payloads { + if let Some(d) = &p.dual { + ecc_verify(codec, &d.identifier, &d.value, &d.verifiers)?; + } + } + let rows: Vec<(u8, Vec)> = payloads + .iter() + .map(|p| { + Ok::<_, Error>(( + *p.identifier.first().ok_or_else(|| err("bad gf256 id"))?, + p.value.clone(), + )) + }) + .collect::>()?; + Zeroizing::new(gf256_combine(&rows)?) + } + }; + + Builder::new(codec) + .with_key_bytes(&secret.as_slice()) + .try_build() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk; + + fn gen(codec: Codec) -> Multikey { + Builder::new_from_random_bytes(codec, &mut rand::rng()) + .unwrap_or_else(|e| panic!("{codec:?} keygen: {e}")) + .try_build() + .unwrap_or_else(|e| panic!("{codec:?} build: {e}")) + } + + fn secret(mk: &Multikey) -> Vec { + mk.data_view().unwrap().secret_bytes().unwrap().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(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"); + } + + 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); + } + + #[test] + fn roundtrip_x25519() { + assert_all(&mk::X25519_KEY_CODECS); + } + + // ── RSA ── + #[test] + fn roundtrip_rsa() { + assert_all(&mk::RSA_KEY_CODECS); + } + + // ── Post-quantum signatures ── + #[test] + fn roundtrip_ml_dsa() { + assert_all(&mk::ML_DSA_KEY_CODECS); + } + + #[test] + fn roundtrip_fn_dsa() { + assert_all(&mk::FN_DSA_KEY_CODECS); + } + + #[test] + fn roundtrip_mayo() { + assert_all(&mk::MAYO_KEY_CODECS); + } + + #[test] + fn roundtrip_slh_dsa() { + assert_all(&mk::SLH_DSA_KEY_CODECS); + } + + // ── Post-quantum KEMs ── + #[test] + fn roundtrip_ml_kem() { + assert_all(&mk::ML_KEM_KEY_CODECS); + } + + #[test] + fn roundtrip_sntrup() { + assert_all(&mk::SNTRUP_KEY_CODECS); + } + + #[test] + fn roundtrip_mceliece() { + assert_all(&mk::MCELIECE_KEY_CODECS); + } + + #[test] + fn roundtrip_frodokem() { + assert_all(&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); + } + + // ── Scheme/property checks ── + #[test] + fn ed25519_x25519_carry_verifiable_dual() { + for &codec in &[Codec::Ed25519Priv, Codec::X25519Priv] { + let shares = split(&gen(codec), 2, 4, rand::rng()).unwrap(); + for s in &shares { + let p = unwrap_share(s).unwrap(); + assert_eq!(p.scheme, Scheme::Gf256, "{codec:?} primary is gf256 seed"); + let d = p.dual.as_ref().expect("dual present"); + assert!(!d.verifiers.is_empty(), "{codec:?} dual has commitments"); + } + } + } + + #[test] + fn feldman_curves_emit_commitments() { + for &codec in &[ + Codec::Secp256K1Priv, + Codec::P256Priv, + Codec::P384Priv, + Codec::P521Priv, + Codec::Bls12381G1Priv, + Codec::Bls12381G2Priv, + ] { + let shares = split(&gen(codec), 2, 3, rand::rng()).unwrap(); + let p = unwrap_share(&shares[0]).unwrap(); + assert_eq!(p.scheme, Scheme::Feldman, "{codec:?}"); + assert!(p.verifiers.is_some(), "{codec:?} has Feldman commitments"); + } + } + + // ── Serialization: a KeySplitShare is a Multikey; CBOR + JSON round-trip ── + #[test] + fn share_survives_cbor_and_json() { + let mk = gen(Codec::P256Priv); + let original = secret(&mk); + let shares = split(&mk, 2, 3, rand::rng()).unwrap(); + + let cbor: Vec> = shares + .iter() + .map(|s| serde_cbor::to_vec(s).unwrap()) + .collect(); + let from_cbor: Vec = cbor + .iter() + .map(|b| serde_cbor::from_slice(b).unwrap()) + .collect(); + assert_eq!( + secret(&combine(&from_cbor[0..2]).unwrap()), + original, + "cbor roundtrip" + ); + + let json: Vec> = shares + .iter() + .map(|s| serde_json::to_vec(s).unwrap()) + .collect(); + let from_json: Vec = json + .iter() + .map(|b| serde_json::from_slice(b).unwrap()) + .collect(); + assert_eq!( + secret(&combine(&from_json[0..2]).unwrap()), + original, + "json roundtrip" + ); + } + + // ── Negative tests ── + #[test] + fn tampered_feldman_share_fails_verify() { + let mk = gen(Codec::P256Priv); + let shares = split(&mk, 2, 3, rand::rng()).unwrap(); + let mut p = unwrap_share(&shares[0]).unwrap(); + p.value[0] ^= 0xff; + let bad = wrap_share(&mk, &p).unwrap(); + assert!( + verify_share(&bad).is_err(), + "tampered Feldman share must fail" + ); + } + + #[test] + fn tampered_dual_fails_verify() { + let mk = gen(Codec::Ed25519Priv); + let shares = split(&mk, 2, 3, rand::rng()).unwrap(); + let mut p = unwrap_share(&shares[0]).unwrap(); + p.dual.as_mut().unwrap().value[0] ^= 0xff; + let bad = wrap_share(&mk, &p).unwrap(); + assert!(verify_share(&bad).is_err(), "tampered dual must fail"); + } + + #[test] + fn below_threshold_does_not_recover() { + let mk = gen(Codec::P256Priv); + let original = secret(&mk); + let shares = split(&mk, 3, 5, rand::rng()).unwrap(); + if let Ok(got) = combine(&shares[0..2]) { + assert_ne!(secret(&got), original, "under-threshold leaked the secret"); + } + } + + #[test] + fn mixed_and_empty_and_public_rejected() { + let a = split(&gen(Codec::P256Priv), 2, 3, rand::rng()).unwrap(); + let b = split(&gen(Codec::Secp256K1Priv), 2, 3, rand::rng()).unwrap(); + assert!( + combine(&[a[0].clone(), b[0].clone()]).is_err(), + "mixed codecs" + ); + assert!(combine(&[]).is_err(), "empty set"); + let pk = gen(Codec::P256Priv) + .conv_view() + .unwrap() + .to_public_key() + .unwrap(); + assert!(split(&pk, 2, 3, rand::rng()).is_err(), "public key"); + } + + #[test] + fn invalid_params_rejected() { + let mk = gen(Codec::P256Priv); + assert!(split(&mk, 1, 3, rand::rng()).is_err(), "threshold < 2"); + assert!(split(&mk, 4, 3, rand::rng()).is_err(), "threshold > limit"); + } +} diff --git a/src/lib.rs b/src/lib.rs index fc05d27..d25222d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,70 @@ -// SPDX-License-Idnetifier: Apache-2.0 -//! Multikey Crate +// SPDX-License-Identifier: Apache-2.0 +//! # multi-key +//! +//! Self-describing cryptographic key implementation supporting multiple key types +//! and formats including public keys, private keys, encrypted keys, and threshold keys. +//! +//! ## Overview +//! +//! This crate provides multikey functionality for creating and managing self-describing +//! cryptographic keys with support for: +//! - Ed25519, Secp256k1, and BLS12-381 key schemes +//! - Public and private key pairs +//! - Key encryption and decryption +//! - Threshold key sharing (Shamir Secret Sharing) +//! - SSH key format conversion +//! - Nonce generation +//! +//! ## Quick Start +//! +//! ### Generating a Key Pair +//! +//! ```rust +//! use multi_key::Builder; +//! use multi_codec::Codec; +//! +//! let mut rng = rand::rng(); +//! let multikey = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) +//! .unwrap() +//! .try_build() +//! .unwrap(); +//! ``` +//! +//! ### Encoding and Decoding +//! +//! ```rust +//! use multi_key::{Builder, Multikey}; +//! use multi_codec::Codec; +//! +//! let mut rng = rand::rng(); +//! let mk1 = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) +//! .unwrap() +//! .try_build() +//! .unwrap(); +//! +//! // Encode to bytes +//! let bytes: Vec = mk1.clone().into(); +//! +//! // Decode from bytes +//! let mk2 = Multikey::try_from(bytes.as_ref()).unwrap(); +//! assert_eq!(mk1, mk2); +//! ``` +//! +//! ## Features +//! +//! - **`serde`** (default): Enables serde serialization +//! - **`wasm`**: Enables WebAssembly support with getrandom/js +//! +//! ## Security +//! +//! - Private keys are automatically zeroized on drop +//! - Debug output redacts sensitive key material +//! - Thread-safe for concurrent operations +//! +//! ## Supported Key Types +//! +//! See [`KEY_CODECS`] for the complete list of supported key algorithms. + #![warn(missing_docs)] #![deny( trivial_casts, @@ -24,28 +89,56 @@ pub mod kdf; /// Key views pub mod views; +pub use views::threshold_marker::{ + self, group_public_key, participants, set_group_public_key, set_participants, threshold_kind, + threshold_params, MarkerView, ThresholdParticipant, ThresholdScheme, +}; pub use views::{ AttrView, CipherAttrView, CipherView, ConvView, DataView, FingerprintView, KdfAttrView, - KdfView, SignView, ThresholdAttrView, ThresholdView, VerifyView, Views, + KdfView, OpenView, SealView, SignView, ThresholdAttrView, ThresholdKeyView, ThresholdView, + VerifyView, Views, }; +/// Key splitting / recombination (verifiable threshold shares) +pub mod keysplit; + /// Multikey type and functions pub mod mk; -pub use mk::{Builder, EncodedMultikey, Multikey, KEY_CODECS, KEY_SHARE_CODECS}; +pub use mk::{ + Builder, EncodedMultikey, Multikey, FN_DSA_KEY_CODECS, KEY_CODECS, KEY_SHARE_CODECS, + ML_DSA_KEY_CODECS, ML_KEM_KEY_CODECS, SLH_DSA_KEY_CODECS, X25519_KEY_CODECS, +}; /// Nonce type pub mod nonce; pub use nonce::{EncodedNonce, Nonce}; +// Re-export EncodingInfo for consumers of Nonce who need the encoding() method +pub use multi_util::EncodingInfo; + +/// Type-safe wrappers for key components +pub mod types; +pub use types::{KeyScheme, PrivateKeyBytes, PublicKeyBytes}; + /// Serde serialization #[cfg(feature = "serde")] pub mod serde; -/// ...and in the darkness bind them +/// Commonly used items +/// +/// ``` +/// use multi_key::prelude::*; +/// +/// let mut rng = rand::rng(); +/// let mk = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) +/// .unwrap() +/// .try_build() +/// .unwrap(); +/// ``` pub mod prelude { pub use super::*; /// re-exports - pub use multibase::Base; - pub use multicodec::Codec; - pub use multiutil::BaseEncoded; + pub use multi_base::Base; + pub use multi_codec::Codec; + pub use multi_util::BaseEncoded; } diff --git a/src/mk.rs b/src/mk.rs index 3da507e..f664eff 100644 --- a/src/mk.rs +++ b/src/mk.rs @@ -1,16 +1,29 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ - error::{AttributesError, CipherError, ConversionsError, KdfError}, - views::{bcrypt, bls12381, chacha20, ed25519, secp256k1}, + error::{AttributesError, CipherError, ConversionsError, KdfError, SealError}, + views::{ + bcrypt, bls12381, bls12381_g1_fndsa512, bls12381_g1_mayo1, bls12381_g1_mayo2, + bls12381_g1_mldsa65, chacha20, classic_mceliece, ed25519, ed25519_fndsa512, ed25519_mayo2, + ed25519_mldsa65, fn_dsa, frodokem, mayo, ml_dsa, ml_kem, nist_p, rsa, secp256k1, slh_dsa, + sntrup, x25519, x25519_frodokem640, x25519_mceliece348864, x25519_mlkem768, + x25519_sntrup761, + }, AttrId, AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, KdfView, SignView, ThresholdAttrView, ThresholdView, VerifyView, Views, + KdfAttrView, KdfView, OpenView, SealView, SignView, ThresholdAttrView, ThresholdKeyView, + ThresholdView, VerifyView, Views, }; -use multibase::Base; -use multicodec::Codec; -use multitrait::{Null, TryDecodeFrom}; -use multiutil::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, Varuint}; -use rand::{CryptoRng, RngCore}; +use ::fn_dsa::{ + sign_key_size, vrfy_key_size, KeyPairGenerator, KeyPairGeneratorStandard, FN_DSA_LOGN_1024, + FN_DSA_LOGN_512, +}; +use multi_base::Base; +use multi_codec::Codec; +use multi_trait::{Null, TryDecodeFrom}; +use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, Varuint}; +use elliptic_curve::sec1::ToSec1Point; +use elliptic_curve::Generate; +use rand_core::CryptoRng; use ssh_key::{ private::{EcdsaKeypair, KeypairData}, public::{EcdsaPublicKey, KeyData}, @@ -20,21 +33,86 @@ use std::{collections::BTreeMap, fmt}; use zeroize::Zeroizing; /// the list of key codecs supported for key generation -pub const KEY_CODECS: [Codec; 4] = [ +pub const KEY_CODECS: [Codec; 7] = [ Codec::Ed25519Priv, - /* - Codec::LamportSha3256Priv, - Codec::LamportSha3384Priv, - Codec::LamportSha3512Priv, - Codec::P256Priv, - Codec::P384Priv, - Codec::P521Priv, - */ Codec::Secp256K1Priv, Codec::Bls12381G1Priv, Codec::Bls12381G2Priv, + Codec::P256Priv, + Codec::P384Priv, + Codec::P521Priv, +]; + +/// the list of RSA key codecs supported for key generation +pub const RSA_KEY_CODECS: [Codec; 3] = [Codec::Rsa2048Priv, Codec::Rsa3072Priv, Codec::Rsa4096Priv]; + +/// the list of FN-DSA key codecs supported for key generation +pub const FN_DSA_KEY_CODECS: [Codec; 2] = [Codec::FnDsa512Priv, Codec::FnDsa1024Priv]; + +/// the list of ML-DSA key codecs supported for key generation +pub const ML_DSA_KEY_CODECS: [Codec; 2] = [Codec::MlDsa65Priv, Codec::MlDsa87Priv]; + +/// the list of MAYO key codecs supported for key generation +pub const MAYO_KEY_CODECS: [Codec; 4] = [ + Codec::Mayo1Priv, + Codec::Mayo2Priv, + Codec::Mayo3Priv, + Codec::Mayo5Priv, +]; + +/// the list of SLH-DSA key codecs (all 12 parameter sets) +pub const SLH_DSA_KEY_CODECS: [Codec; 12] = [ + Codec::SlhDsaSha2128FPriv, + Codec::SlhDsaSha2128SPriv, + Codec::SlhDsaSha2192FPriv, + Codec::SlhDsaSha2192SPriv, + Codec::SlhDsaSha2256FPriv, + Codec::SlhDsaSha2256SPriv, + Codec::SlhDsaShake128FPriv, + Codec::SlhDsaShake128SPriv, + Codec::SlhDsaShake192FPriv, + Codec::SlhDsaShake192SPriv, + Codec::SlhDsaShake256FPriv, + Codec::SlhDsaShake256SPriv, +]; + +/// the list of ML-KEM key codecs supported for key generation +pub const ML_KEM_KEY_CODECS: [Codec; 2] = [Codec::Mlkem768Priv, Codec::Mlkem1024Priv]; + +/// the list of sntrup key codecs supported for key generation (all sizes) +pub const SNTRUP_KEY_CODECS: [Codec; 5] = [ + Codec::Sntrup761Priv, + Codec::Sntrup857Priv, + Codec::Sntrup953Priv, + Codec::Sntrup1013Priv, + Codec::Sntrup1277Priv, +]; + +/// the list of Classic McEliece key codecs supported for key generation +pub const MCELIECE_KEY_CODECS: [Codec; 1] = [Codec::Mceliece348864Priv]; + +/// the list of FrodoKEM key codecs supported for key generation +pub const FRODOKEM_KEY_CODECS: [Codec; 6] = [ + Codec::FrodoKem640AesPriv, + Codec::FrodoKem976AesPriv, + Codec::FrodoKem1344AesPriv, + Codec::FrodoKem640ShakePriv, + Codec::FrodoKem976ShakePriv, + Codec::FrodoKem1344ShakePriv, ]; +/// the list of X25519 key codecs supported for key generation +pub const X25519_KEY_CODECS: [Codec; 1] = [Codec::X25519Priv]; + +/// the list of X25519-sntrup761 hybrid KEM key codecs +pub const X25519_SNTRUP761_KEY_CODECS: [Codec; 1] = [Codec::X25519Sntrup761Priv]; + +/// the list of X25519-ML-KEM-768 hybrid KEM key codecs +pub const X25519_MLKEM768_KEY_CODECS: [Codec; 1] = [Codec::X25519Mlkem768Priv]; + +/// the list of Ed25519-MAYO2 hybrid signing key codecs +pub const ED25519_MAYO2_KEY_CODECS: [Codec; 1] = [Codec::Ed25519Mayo2Priv]; + /// the list of key share codecs supported pub const KEY_SHARE_CODECS: [Codec; 4] = [ Codec::Bls12381G1PubShare, @@ -99,13 +177,13 @@ impl From for Vec { // add in the key codec v.append(&mut mk.codec.into()); // add in the comment - v.append(&mut Varbytes(mk.comment.as_bytes().to_vec()).into()); + v.append(&mut Varbytes::new(mk.comment.as_bytes().to_vec()).into()); // add in the number of codec-specific attributes v.append(&mut Varuint(mk.attributes.len()).into()); // add in the codec-specific attributes mk.attributes.iter().for_each(|(id, attr)| { v.append(&mut (*id).into()); - v.append(&mut Varbytes(attr.to_vec()).into()); + v.append(&mut Varbytes::new(attr.to_vec()).into()); }); v } @@ -206,6 +284,123 @@ impl Views for Multikey { Ok(Box::new(secp256k1::View::try_from(self)?)) } Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(self)?)), + Codec::SlhDsaSha2128FPub + | Codec::SlhDsaSha2128SPub + | Codec::SlhDsaSha2192FPub + | Codec::SlhDsaSha2192SPub + | Codec::SlhDsaSha2256FPub + | Codec::SlhDsaSha2256SPub + | Codec::SlhDsaShake128FPub + | Codec::SlhDsaShake128SPub + | Codec::SlhDsaShake192FPub + | Codec::SlhDsaShake192SPub + | Codec::SlhDsaShake256FPub + | Codec::SlhDsaShake256SPub + | Codec::SlhDsaSha2128FPriv + | Codec::SlhDsaSha2128SPriv + | Codec::SlhDsaSha2192FPriv + | Codec::SlhDsaSha2192SPriv + | Codec::SlhDsaSha2256FPriv + | Codec::SlhDsaSha2256SPriv + | Codec::SlhDsaShake128FPriv + | Codec::SlhDsaShake128SPriv + | Codec::SlhDsaShake192FPriv + | Codec::SlhDsaShake192SPriv + | Codec::SlhDsaShake256FPriv + | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Ok(Box::new(ml_dsa::View::try_from(self)?)) + } + Codec::Mayo1Pub + | Codec::Mayo1Priv + | Codec::Mayo2Pub + | Codec::Mayo2Priv + | Codec::Mayo3Pub + | Codec::Mayo3Priv + | Codec::Mayo5Pub + | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(self)?)), + Codec::FnDsa512Pub + | Codec::FnDsa512Priv + | Codec::FnDsa1024Pub + | Codec::FnDsa1024Priv => Ok(Box::new(fn_dsa::View::try_from(self)?)), + Codec::Mlkem768Pub + | Codec::Mlkem768Priv + | Codec::Mlkem1024Pub + | Codec::Mlkem1024Priv => Ok(Box::new(ml_kem::View::try_from(self)?)), + Codec::Sntrup761Pub + | Codec::Sntrup761Priv + | Codec::Sntrup857Pub + | Codec::Sntrup857Priv + | Codec::Sntrup953Pub + | Codec::Sntrup953Priv + | Codec::Sntrup1013Pub + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Pub + | Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)), + Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => { + Ok(Box::new(classic_mceliece::View::try_from(self)?)) + } + Codec::FrodoKem640AesPub + | Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPub + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPub + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePub + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePub + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePub + | Codec::FrodoKem1344ShakePriv => Ok(Box::new(frodokem::View::try_from(self)?)), + Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(self)?)), + Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { + Ok(Box::new(x25519_sntrup761::View::try_from(self)?)) + } + Codec::X25519Frodokem640AesPub + | Codec::X25519Frodokem640AesPriv + | Codec::X25519Frodokem640ShakePub + | Codec::X25519Frodokem640ShakePriv => { + Ok(Box::new(x25519_frodokem640::View::try_from(self)?)) + } + Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { + Ok(Box::new(x25519_mceliece348864::View::try_from(self)?)) + } + Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { + Ok(Box::new(x25519_mlkem768::View::try_from(self)?)) + } + Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { + Ok(Box::new(ed25519_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { + Ok(Box::new(ed25519_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { + Ok(Box::new(bls12381_g1_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { + Ok(Box::new(bls12381_g1_fndsa512::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { + Ok(Box::new(bls12381_g1_mayo1::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { + Ok(Box::new(bls12381_g1_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { + Ok(Box::new(ed25519_fndsa512::View::try_from(self)?)) + } + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), _ => Err(AttributesError::UnsupportedCodec(self.codec).into()), } } @@ -239,6 +434,123 @@ impl Views for Multikey { Ok(Box::new(secp256k1::View::try_from(self)?)) } Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(self)?)), + Codec::SlhDsaSha2128FPub + | Codec::SlhDsaSha2128SPub + | Codec::SlhDsaSha2192FPub + | Codec::SlhDsaSha2192SPub + | Codec::SlhDsaSha2256FPub + | Codec::SlhDsaSha2256SPub + | Codec::SlhDsaShake128FPub + | Codec::SlhDsaShake128SPub + | Codec::SlhDsaShake192FPub + | Codec::SlhDsaShake192SPub + | Codec::SlhDsaShake256FPub + | Codec::SlhDsaShake256SPub + | Codec::SlhDsaSha2128FPriv + | Codec::SlhDsaSha2128SPriv + | Codec::SlhDsaSha2192FPriv + | Codec::SlhDsaSha2192SPriv + | Codec::SlhDsaSha2256FPriv + | Codec::SlhDsaSha2256SPriv + | Codec::SlhDsaShake128FPriv + | Codec::SlhDsaShake128SPriv + | Codec::SlhDsaShake192FPriv + | Codec::SlhDsaShake192SPriv + | Codec::SlhDsaShake256FPriv + | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Ok(Box::new(ml_dsa::View::try_from(self)?)) + } + Codec::Mayo1Pub + | Codec::Mayo1Priv + | Codec::Mayo2Pub + | Codec::Mayo2Priv + | Codec::Mayo3Pub + | Codec::Mayo3Priv + | Codec::Mayo5Pub + | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(self)?)), + Codec::FnDsa512Pub + | Codec::FnDsa512Priv + | Codec::FnDsa1024Pub + | Codec::FnDsa1024Priv => Ok(Box::new(fn_dsa::View::try_from(self)?)), + Codec::Mlkem768Pub + | Codec::Mlkem768Priv + | Codec::Mlkem1024Pub + | Codec::Mlkem1024Priv => Ok(Box::new(ml_kem::View::try_from(self)?)), + Codec::Sntrup761Pub + | Codec::Sntrup761Priv + | Codec::Sntrup857Pub + | Codec::Sntrup857Priv + | Codec::Sntrup953Pub + | Codec::Sntrup953Priv + | Codec::Sntrup1013Pub + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Pub + | Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)), + Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => { + Ok(Box::new(classic_mceliece::View::try_from(self)?)) + } + Codec::FrodoKem640AesPub + | Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPub + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPub + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePub + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePub + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePub + | Codec::FrodoKem1344ShakePriv => Ok(Box::new(frodokem::View::try_from(self)?)), + Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(self)?)), + Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { + Ok(Box::new(x25519_sntrup761::View::try_from(self)?)) + } + Codec::X25519Frodokem640AesPub + | Codec::X25519Frodokem640AesPriv + | Codec::X25519Frodokem640ShakePub + | Codec::X25519Frodokem640ShakePriv => { + Ok(Box::new(x25519_frodokem640::View::try_from(self)?)) + } + Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { + Ok(Box::new(x25519_mceliece348864::View::try_from(self)?)) + } + Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { + Ok(Box::new(x25519_mlkem768::View::try_from(self)?)) + } + Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { + Ok(Box::new(ed25519_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { + Ok(Box::new(ed25519_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { + Ok(Box::new(bls12381_g1_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { + Ok(Box::new(bls12381_g1_fndsa512::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { + Ok(Box::new(bls12381_g1_mayo1::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { + Ok(Box::new(bls12381_g1_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { + Ok(Box::new(ed25519_fndsa512::View::try_from(self)?)) + } + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), } } @@ -267,6 +579,29 @@ impl Views for Multikey { | Codec::Bls12381G2Priv | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(self)?)), + Codec::Ed25519ThreshPrivShare + | Codec::P256ThreshPrivShare + | Codec::P384ThreshPrivShare + | Codec::Secp256K1ThreshPrivShare + | Codec::Bls12381ThreshPrivShare + | Codec::Ed448ThreshPrivShare => { + Ok(Box::new(crate::views::dkg_threshold::View::try_from(self)?)) + } + _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), + } + } + + /// Provide a read-only view of higher-level threshold key metadata in the viewed Multikey + fn threshold_key_view<'a>(&'a self) -> Result, Error> { + match self.codec { + Codec::Ed25519ThreshPrivShare + | Codec::P256ThreshPrivShare + | Codec::P384ThreshPrivShare + | Codec::Secp256K1ThreshPrivShare + | Codec::Bls12381ThreshPrivShare + | Codec::Ed448ThreshPrivShare => { + Ok(Box::new(crate::views::dkg_threshold::View::try_from(self)?)) + } _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), } } @@ -294,6 +629,123 @@ impl Views for Multikey { Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } + Codec::SlhDsaSha2128FPub + | Codec::SlhDsaSha2128SPub + | Codec::SlhDsaSha2192FPub + | Codec::SlhDsaSha2192SPub + | Codec::SlhDsaSha2256FPub + | Codec::SlhDsaSha2256SPub + | Codec::SlhDsaShake128FPub + | Codec::SlhDsaShake128SPub + | Codec::SlhDsaShake192FPub + | Codec::SlhDsaShake192SPub + | Codec::SlhDsaShake256FPub + | Codec::SlhDsaShake256SPub + | Codec::SlhDsaSha2128FPriv + | Codec::SlhDsaSha2128SPriv + | Codec::SlhDsaSha2192FPriv + | Codec::SlhDsaSha2192SPriv + | Codec::SlhDsaSha2256FPriv + | Codec::SlhDsaSha2256SPriv + | Codec::SlhDsaShake128FPriv + | Codec::SlhDsaShake128SPriv + | Codec::SlhDsaShake192FPriv + | Codec::SlhDsaShake192SPriv + | Codec::SlhDsaShake256FPriv + | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Ok(Box::new(ml_dsa::View::try_from(self)?)) + } + Codec::Mayo1Pub + | Codec::Mayo1Priv + | Codec::Mayo2Pub + | Codec::Mayo2Priv + | Codec::Mayo3Pub + | Codec::Mayo3Priv + | Codec::Mayo5Pub + | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(self)?)), + Codec::FnDsa512Pub + | Codec::FnDsa512Priv + | Codec::FnDsa1024Pub + | Codec::FnDsa1024Priv => Ok(Box::new(fn_dsa::View::try_from(self)?)), + Codec::Mlkem768Pub + | Codec::Mlkem768Priv + | Codec::Mlkem1024Pub + | Codec::Mlkem1024Priv => Ok(Box::new(ml_kem::View::try_from(self)?)), + Codec::Sntrup761Pub + | Codec::Sntrup761Priv + | Codec::Sntrup857Pub + | Codec::Sntrup857Priv + | Codec::Sntrup953Pub + | Codec::Sntrup953Priv + | Codec::Sntrup1013Pub + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Pub + | Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)), + Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => { + Ok(Box::new(classic_mceliece::View::try_from(self)?)) + } + Codec::FrodoKem640AesPub + | Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPub + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPub + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePub + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePub + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePub + | Codec::FrodoKem1344ShakePriv => Ok(Box::new(frodokem::View::try_from(self)?)), + Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(self)?)), + Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { + Ok(Box::new(x25519_sntrup761::View::try_from(self)?)) + } + Codec::X25519Frodokem640AesPub + | Codec::X25519Frodokem640AesPriv + | Codec::X25519Frodokem640ShakePub + | Codec::X25519Frodokem640ShakePriv => { + Ok(Box::new(x25519_frodokem640::View::try_from(self)?)) + } + Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { + Ok(Box::new(x25519_mceliece348864::View::try_from(self)?)) + } + Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { + Ok(Box::new(x25519_mlkem768::View::try_from(self)?)) + } + Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { + Ok(Box::new(ed25519_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { + Ok(Box::new(ed25519_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { + Ok(Box::new(bls12381_g1_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { + Ok(Box::new(bls12381_g1_fndsa512::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { + Ok(Box::new(bls12381_g1_mayo1::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { + Ok(Box::new(bls12381_g1_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { + Ok(Box::new(ed25519_fndsa512::View::try_from(self)?)) + } + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), } } @@ -314,6 +766,123 @@ impl Views for Multikey { Ok(Box::new(secp256k1::View::try_from(self)?)) } Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(self)?)), + Codec::SlhDsaSha2128FPub + | Codec::SlhDsaSha2128SPub + | Codec::SlhDsaSha2192FPub + | Codec::SlhDsaSha2192SPub + | Codec::SlhDsaSha2256FPub + | Codec::SlhDsaSha2256SPub + | Codec::SlhDsaShake128FPub + | Codec::SlhDsaShake128SPub + | Codec::SlhDsaShake192FPub + | Codec::SlhDsaShake192SPub + | Codec::SlhDsaShake256FPub + | Codec::SlhDsaShake256SPub + | Codec::SlhDsaSha2128FPriv + | Codec::SlhDsaSha2128SPriv + | Codec::SlhDsaSha2192FPriv + | Codec::SlhDsaSha2192SPriv + | Codec::SlhDsaSha2256FPriv + | Codec::SlhDsaSha2256SPriv + | Codec::SlhDsaShake128FPriv + | Codec::SlhDsaShake128SPriv + | Codec::SlhDsaShake192FPriv + | Codec::SlhDsaShake192SPriv + | Codec::SlhDsaShake256FPriv + | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Ok(Box::new(ml_dsa::View::try_from(self)?)) + } + Codec::Mayo1Pub + | Codec::Mayo1Priv + | Codec::Mayo2Pub + | Codec::Mayo2Priv + | Codec::Mayo3Pub + | Codec::Mayo3Priv + | Codec::Mayo5Pub + | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(self)?)), + Codec::FnDsa512Pub + | Codec::FnDsa512Priv + | Codec::FnDsa1024Pub + | Codec::FnDsa1024Priv => Ok(Box::new(fn_dsa::View::try_from(self)?)), + Codec::Mlkem768Pub + | Codec::Mlkem768Priv + | Codec::Mlkem1024Pub + | Codec::Mlkem1024Priv => Ok(Box::new(ml_kem::View::try_from(self)?)), + Codec::Sntrup761Pub + | Codec::Sntrup761Priv + | Codec::Sntrup857Pub + | Codec::Sntrup857Priv + | Codec::Sntrup953Pub + | Codec::Sntrup953Priv + | Codec::Sntrup1013Pub + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Pub + | Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)), + Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => { + Ok(Box::new(classic_mceliece::View::try_from(self)?)) + } + Codec::FrodoKem640AesPub + | Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPub + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPub + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePub + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePub + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePub + | Codec::FrodoKem1344ShakePriv => Ok(Box::new(frodokem::View::try_from(self)?)), + Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(self)?)), + Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { + Ok(Box::new(x25519_sntrup761::View::try_from(self)?)) + } + Codec::X25519Frodokem640AesPub + | Codec::X25519Frodokem640AesPriv + | Codec::X25519Frodokem640ShakePub + | Codec::X25519Frodokem640ShakePriv => { + Ok(Box::new(x25519_frodokem640::View::try_from(self)?)) + } + Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { + Ok(Box::new(x25519_mceliece348864::View::try_from(self)?)) + } + Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { + Ok(Box::new(x25519_mlkem768::View::try_from(self)?)) + } + Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { + Ok(Box::new(ed25519_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { + Ok(Box::new(ed25519_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { + Ok(Box::new(bls12381_g1_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { + Ok(Box::new(bls12381_g1_fndsa512::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { + Ok(Box::new(bls12381_g1_mayo1::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { + Ok(Box::new(bls12381_g1_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { + Ok(Box::new(ed25519_fndsa512::View::try_from(self)?)) + } + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), } } @@ -326,6 +895,148 @@ impl Views for Multikey { } } + /// Provide an interface to seal (encrypt) plaintext + fn seal_view<'a>(&'a self) -> Result, Error> { + match self.codec { + Codec::Bls12381G1Pub + | Codec::Bls12381G1Priv + | Codec::Bls12381G2Pub + | Codec::Bls12381G2Priv => Ok(Box::new(bls12381::View::try_from(self)?)), + Codec::Mlkem768Pub + | Codec::Mlkem768Priv + | Codec::Mlkem1024Pub + | Codec::Mlkem1024Priv => Ok(Box::new(ml_kem::View::try_from(self)?)), + Codec::Sntrup761Pub + | Codec::Sntrup761Priv + | Codec::Sntrup857Pub + | Codec::Sntrup857Priv + | Codec::Sntrup953Pub + | Codec::Sntrup953Priv + | Codec::Sntrup1013Pub + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Pub + | Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)), + Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => { + Ok(Box::new(classic_mceliece::View::try_from(self)?)) + } + Codec::FrodoKem640AesPub + | Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPub + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPub + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePub + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePub + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePub + | Codec::FrodoKem1344ShakePriv => Ok(Box::new(frodokem::View::try_from(self)?)), + Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(self)?)), + Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { + Ok(Box::new(x25519_sntrup761::View::try_from(self)?)) + } + Codec::X25519Frodokem640AesPub + | Codec::X25519Frodokem640AesPriv + | Codec::X25519Frodokem640ShakePub + | Codec::X25519Frodokem640ShakePriv => { + Ok(Box::new(x25519_frodokem640::View::try_from(self)?)) + } + Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { + Ok(Box::new(x25519_mceliece348864::View::try_from(self)?)) + } + Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { + Ok(Box::new(x25519_mlkem768::View::try_from(self)?)) + } + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Secp256K1Pub | Codec::Secp256K1Priv => { + Ok(Box::new(secp256k1::View::try_from(self)?)) + } + _ => Err(SealError::NotEncryptionKey.into()), + } + } + + /// Provide an interface to open (decrypt) sealed data + fn open_view<'a>(&'a self) -> Result, Error> { + match self.codec { + Codec::Bls12381G1Pub + | Codec::Bls12381G1Priv + | Codec::Bls12381G2Pub + | Codec::Bls12381G2Priv => Ok(Box::new(bls12381::View::try_from(self)?)), + Codec::Mlkem768Pub + | Codec::Mlkem768Priv + | Codec::Mlkem1024Pub + | Codec::Mlkem1024Priv => Ok(Box::new(ml_kem::View::try_from(self)?)), + Codec::Sntrup761Pub + | Codec::Sntrup761Priv + | Codec::Sntrup857Pub + | Codec::Sntrup857Priv + | Codec::Sntrup953Pub + | Codec::Sntrup953Priv + | Codec::Sntrup1013Pub + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Pub + | Codec::Sntrup1277Priv => Ok(Box::new(sntrup::View::try_from(self)?)), + Codec::Mceliece348864Pub | Codec::Mceliece348864Priv => { + Ok(Box::new(classic_mceliece::View::try_from(self)?)) + } + Codec::FrodoKem640AesPub + | Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPub + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPub + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePub + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePub + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePub + | Codec::FrodoKem1344ShakePriv => Ok(Box::new(frodokem::View::try_from(self)?)), + Codec::X25519Pub | Codec::X25519Priv => Ok(Box::new(x25519::View::try_from(self)?)), + Codec::X25519Sntrup761Pub | Codec::X25519Sntrup761Priv => { + Ok(Box::new(x25519_sntrup761::View::try_from(self)?)) + } + Codec::X25519Frodokem640AesPub + | Codec::X25519Frodokem640AesPriv + | Codec::X25519Frodokem640ShakePub + | Codec::X25519Frodokem640ShakePriv => { + Ok(Box::new(x25519_frodokem640::View::try_from(self)?)) + } + Codec::X25519Mceliece348864Pub | Codec::X25519Mceliece348864Priv => { + Ok(Box::new(x25519_mceliece348864::View::try_from(self)?)) + } + Codec::X25519Mlkem768Pub | Codec::X25519Mlkem768Priv => { + Ok(Box::new(x25519_mlkem768::View::try_from(self)?)) + } + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Secp256K1Pub | Codec::Secp256K1Priv => { + Ok(Box::new(secp256k1::View::try_from(self)?)) + } + _ => Err(SealError::NotEncryptionKey.into()), + } + } + /// Provide an interface to sign a message and return a Multisig fn sign_view<'a>(&'a self) -> Result, Error> { match self.codec { @@ -341,6 +1052,78 @@ impl Views for Multikey { Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } + Codec::SlhDsaSha2128FPub + | Codec::SlhDsaSha2128SPub + | Codec::SlhDsaSha2192FPub + | Codec::SlhDsaSha2192SPub + | Codec::SlhDsaSha2256FPub + | Codec::SlhDsaSha2256SPub + | Codec::SlhDsaShake128FPub + | Codec::SlhDsaShake128SPub + | Codec::SlhDsaShake192FPub + | Codec::SlhDsaShake192SPub + | Codec::SlhDsaShake256FPub + | Codec::SlhDsaShake256SPub + | Codec::SlhDsaSha2128FPriv + | Codec::SlhDsaSha2128SPriv + | Codec::SlhDsaSha2192FPriv + | Codec::SlhDsaSha2192SPriv + | Codec::SlhDsaSha2256FPriv + | Codec::SlhDsaSha2256SPriv + | Codec::SlhDsaShake128FPriv + | Codec::SlhDsaShake128SPriv + | Codec::SlhDsaShake192FPriv + | Codec::SlhDsaShake192SPriv + | Codec::SlhDsaShake256FPriv + | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Ok(Box::new(ml_dsa::View::try_from(self)?)) + } + Codec::Mayo1Pub + | Codec::Mayo1Priv + | Codec::Mayo2Pub + | Codec::Mayo2Priv + | Codec::Mayo3Pub + | Codec::Mayo3Priv + | Codec::Mayo5Pub + | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(self)?)), + Codec::FnDsa512Pub + | Codec::FnDsa512Priv + | Codec::FnDsa1024Pub + | Codec::FnDsa1024Priv => Ok(Box::new(fn_dsa::View::try_from(self)?)), + Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { + Ok(Box::new(ed25519_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { + Ok(Box::new(ed25519_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { + Ok(Box::new(bls12381_g1_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { + Ok(Box::new(bls12381_g1_fndsa512::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { + Ok(Box::new(bls12381_g1_mayo1::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { + Ok(Box::new(bls12381_g1_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { + Ok(Box::new(ed25519_fndsa512::View::try_from(self)?)) + } + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), } } @@ -370,6 +1153,78 @@ impl Views for Multikey { Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } + Codec::SlhDsaSha2128FPub + | Codec::SlhDsaSha2128SPub + | Codec::SlhDsaSha2192FPub + | Codec::SlhDsaSha2192SPub + | Codec::SlhDsaSha2256FPub + | Codec::SlhDsaSha2256SPub + | Codec::SlhDsaShake128FPub + | Codec::SlhDsaShake128SPub + | Codec::SlhDsaShake192FPub + | Codec::SlhDsaShake192SPub + | Codec::SlhDsaShake256FPub + | Codec::SlhDsaShake256SPub + | Codec::SlhDsaSha2128FPriv + | Codec::SlhDsaSha2128SPriv + | Codec::SlhDsaSha2192FPriv + | Codec::SlhDsaSha2192SPriv + | Codec::SlhDsaSha2256FPriv + | Codec::SlhDsaSha2256SPriv + | Codec::SlhDsaShake128FPriv + | Codec::SlhDsaShake128SPriv + | Codec::SlhDsaShake192FPriv + | Codec::SlhDsaShake192SPriv + | Codec::SlhDsaShake256FPriv + | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Ok(Box::new(ml_dsa::View::try_from(self)?)) + } + Codec::Mayo1Pub + | Codec::Mayo1Priv + | Codec::Mayo2Pub + | Codec::Mayo2Priv + | Codec::Mayo3Pub + | Codec::Mayo3Priv + | Codec::Mayo5Pub + | Codec::Mayo5Priv => Ok(Box::new(mayo::View::try_from(self)?)), + Codec::FnDsa512Pub + | Codec::FnDsa512Priv + | Codec::FnDsa1024Pub + | Codec::FnDsa1024Priv => Ok(Box::new(fn_dsa::View::try_from(self)?)), + Codec::Ed25519Mayo2Pub | Codec::Ed25519Mayo2Priv => { + Ok(Box::new(ed25519_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Mldsa65Pub | Codec::Ed25519Mldsa65Priv => { + Ok(Box::new(ed25519_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Mldsa65Pub | Codec::Bls12381G1Mldsa65Priv => { + Ok(Box::new(bls12381_g1_mldsa65::View::try_from(self)?)) + } + Codec::Bls12381G1Fndsa512Pub | Codec::Bls12381G1Fndsa512Priv => { + Ok(Box::new(bls12381_g1_fndsa512::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo1Pub | Codec::Bls12381G1Mayo1Priv => { + Ok(Box::new(bls12381_g1_mayo1::View::try_from(self)?)) + } + Codec::Bls12381G1Mayo2Pub | Codec::Bls12381G1Mayo2Priv => { + Ok(Box::new(bls12381_g1_mayo2::View::try_from(self)?)) + } + Codec::Ed25519Fndsa512Pub | Codec::Ed25519Fndsa512Priv => { + Ok(Box::new(ed25519_fndsa512::View::try_from(self)?)) + } + Codec::P256Pub + | Codec::P256Priv + | Codec::P384Pub + | Codec::P384Priv + | Codec::P521Pub + | Codec::P521Priv => Ok(Box::new(nist_p::View::try_from(self)?)), + Codec::Rsa2048Pub + | Codec::Rsa2048Priv + | Codec::Rsa3072Pub + | Codec::Rsa3072Priv + | Codec::Rsa4096Pub + | Codec::Rsa4096Priv => Ok(Box::new(rsa::View::try_from(self)?)), _ => Err(ConversionsError::UnsupportedCodec(self.codec).into()), } } @@ -396,10 +1251,7 @@ impl Builder { } /// new builder from random bytes source - pub fn new_from_random_bytes( - codec: Codec, - rng: &mut (impl RngCore + CryptoRng), - ) -> Result { + 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) @@ -414,7 +1266,7 @@ impl Builder { .map_err(|e| ConversionsError::Ssh(e.into()))? .private_key_bytes() .to_vec(), - Codec::Secp256K1Priv => k256::SecretKey::random(rng).to_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() @@ -423,6 +1275,203 @@ impl Builder { .to_be_bytes() .as_slice() .to_vec(), + 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)]; + kg.keygen( + FN_DSA_LOGN_512, + &mut rand_core_06::OsRng, + &mut sign_key, + &mut vrfy_key, + ); + sign_key + } + 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)]; + kg.keygen( + FN_DSA_LOGN_1024, + &mut rand_core_06::OsRng, + &mut sign_key, + &mut vrfy_key, + ); + sign_key + } + Codec::MlDsa65Priv | Codec::MlDsa87Priv => { + let mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::Mayo1Priv | Codec::Mayo2Priv => { + let mut seed = [0u8; 24]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::Mayo3Priv => { + let mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::Mayo5Priv => { + let mut seed = [0u8; 40]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::Mlkem768Priv | Codec::Mlkem1024Priv => { + // 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 => { + let mut seed = [0u8; 32]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::Mceliece348864Priv => { + 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 => { + let mut secret = [0u8; 32]; + rng.fill_bytes(&mut secret); + secret.to_vec() + } + Codec::X25519Sntrup761Priv => { + // x25519_seed (32) || sntrup761_seed (32) = 64 bytes + let mut seed = [0u8; 64]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::X25519Mceliece348864Priv => { + // 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 => { + // x25519_seed (32) || frodokem full secret key + x25519_frodokem640::generate_private_key(codec) + } + Codec::X25519Mlkem768Priv => { + // 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 => { + // ed25519_seed (32) || mayo2_seed (24) = 56 bytes + let mut seed = [0u8; 56]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::Ed25519Mldsa65Priv => { + // ed25519_seed (32) || ml_dsa_seed (32) = 64 bytes + let mut seed = [0u8; 64]; + rng.fill_bytes(&mut seed); + seed.to_vec() + } + Codec::Bls12381G1Mldsa65Priv => { + // 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()); + let mut mldsa_seed = [0u8; 32]; + rng.fill_bytes(&mut mldsa_seed); + out.extend_from_slice(&mldsa_seed); + out + } + 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()); + 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)]; + kg.keygen( + FN_DSA_LOGN_512, + &mut rand_core_06::OsRng, + &mut sign_key, + &mut vrfy_key, + ); + out.extend_from_slice(&sign_key); + out + } + Codec::Bls12381G1Mayo1Priv | Codec::Bls12381G1Mayo2Priv => { + // 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()); + let mut mayo_seed = [0u8; 24]; + rng.fill_bytes(&mut mayo_seed); + out.extend_from_slice(&mayo_seed); + out + } + Codec::Ed25519Fndsa512Priv => { + // ed25519_seed (32) || fn_dsa_512_signing_key + let mut ed_seed = [0u8; 32]; + rng.fill_bytes(&mut ed_seed); + 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)]; + kg.keygen( + FN_DSA_LOGN_512, + &mut rand_core_06::OsRng, + &mut sign_key, + &mut vrfy_key, + ); + let mut out = Vec::with_capacity(32 + sign_key.len()); + out.extend_from_slice(&ed_seed); + out.extend_from_slice(&sign_key); + out + } + Codec::Rsa2048Priv => { + let key = ::rsa::RsaPrivateKey::new(&mut rsa::OsRng, 2048) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + ::rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))? + .as_bytes() + .to_vec() + } + Codec::Rsa3072Priv => { + let key = ::rsa::RsaPrivateKey::new(&mut rsa::OsRng, 3072) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + ::rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))? + .as_bytes() + .to_vec() + } + Codec::Rsa4096Priv => { + let key = ::rsa::RsaPrivateKey::new(&mut rsa::OsRng, 4096) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + ::rsa::pkcs1::EncodeRsaPrivateKey::to_pkcs1_der(&key) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))? + .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>(), _ => return Err(ConversionsError::UnsupportedCodec(codec).into()), }; let mut attributes = Attributes::new(); @@ -440,10 +1489,15 @@ impl Builder { match sshkey.algorithm() { Ecdsa { curve } => { use EcdsaCurve::*; + // SSH stores uncompressed SEC1 points; we store compressed internally let (key_bytes, codec) = match curve { NistP256 => { if let KeyData::Ecdsa(EcdsaPublicKey::NistP256(point)) = sshkey.key_data() { - (point.as_bytes().to_vec(), Codec::P256Pub) + let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(point.as_bytes()) + .map_err(|e| { + ConversionsError::PublicKeyFailure(e.to_string()) + })?; + (vk.to_sec1_bytes().to_vec(), Codec::P256Pub) } else { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -453,7 +1507,11 @@ impl Builder { } NistP384 => { if let KeyData::Ecdsa(EcdsaPublicKey::NistP384(point)) = sshkey.key_data() { - (point.as_bytes().to_vec(), Codec::P384Pub) + let vk = p384::ecdsa::VerifyingKey::from_sec1_bytes(point.as_bytes()) + .map_err(|e| { + ConversionsError::PublicKeyFailure(e.to_string()) + })?; + (vk.to_sec1_bytes().to_vec(), Codec::P384Pub) } else { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -463,7 +1521,12 @@ impl Builder { } NistP521 => { if let KeyData::Ecdsa(EcdsaPublicKey::NistP521(point)) = sshkey.key_data() { - (point.as_bytes().to_vec(), Codec::P521Pub) + let vk = p521::ecdsa::VerifyingKey::from_sec1_bytes(point.as_bytes()) + .map_err(|e| { + ConversionsError::PublicKeyFailure(e.to_string()) + })?; + let ep = vk.as_affine().to_sec1_point(true); + (ep.as_bytes().to_vec(), Codec::P521Pub) } else { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -531,7 +1594,7 @@ impl Builder { } }; let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = Varuint(key_share.0).into(); + let identifier: Vec = key_share.0 .0.to_be_bytes().into(); let threshold: Vec = Varuint(key_share.1).into(); let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); @@ -576,7 +1639,7 @@ impl Builder { } }; let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = Varuint(key_share.0).into(); + let identifier: Vec = key_share.0 .0.to_be_bytes().into(); let threshold: Vec = Varuint(key_share.1).into(); let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); @@ -585,94 +1648,16 @@ impl Builder { attributes.insert(AttrId::Limit, limit.into()); attributes.insert(AttrId::KeyData, key_share.3.into()); Ok(Builder { - codec: Codec::Bls12381G1PubShare, + codec: Codec::Bls12381G2PubShare, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() }) } - s => Err(ConversionsError::UnsupportedAlgorithm(s.to_string()).into()), - }, - Ed25519 => { - let key_bytes = match sshkey.key_data() { - KeyData::Ed25519(e) => e.0.to_vec(), - _ => { - return Err(ConversionsError::UnsupportedAlgorithm( - sshkey.algorithm().to_string(), - ) - .into()) - } - }; - let mut attributes = Attributes::new(); - attributes.insert(AttrId::KeyData, key_bytes.into()); - Ok(Builder { - codec: Codec::Ed25519Pub, - comment: Some(sshkey.comment().to_string()), - attributes: Some(attributes), - ..Default::default() - }) - } - _ => Err(ConversionsError::UnsupportedAlgorithm(sshkey.algorithm().to_string()).into()), - } - } - - /// new builder from ssh_key::PrivateKey source - pub fn new_from_ssh_private_key(sshkey: &PrivateKey) -> Result { - use ssh_key::Algorithm::*; - match sshkey.algorithm() { - Ecdsa { curve } => { - use EcdsaCurve::*; - let (key_bytes, codec) = match curve { - NistP256 => { - if let KeypairData::Ecdsa(EcdsaKeypair::NistP256 { private, .. }) = - sshkey.key_data() - { - (private.as_slice().to_vec(), Codec::P256Priv) - } else { - return Err(ConversionsError::UnsupportedAlgorithm( - sshkey.algorithm().to_string(), - ) - .into()); - } - } - NistP384 => { - if let KeypairData::Ecdsa(EcdsaKeypair::NistP384 { private, .. }) = - sshkey.key_data() - { - (private.as_slice().to_vec(), Codec::P384Priv) - } else { - return Err(ConversionsError::UnsupportedAlgorithm( - sshkey.algorithm().to_string(), - ) - .into()); - } - } - NistP521 => { - if let KeypairData::Ecdsa(EcdsaKeypair::NistP521 { private, .. }) = - sshkey.key_data() - { - (private.as_slice().to_vec(), Codec::P521Priv) - } else { - return Err(ConversionsError::UnsupportedAlgorithm( - sshkey.algorithm().to_string(), - ) - .into()); - } - } - }; - let mut attributes = Attributes::new(); - attributes.insert(AttrId::KeyData, key_bytes.into()); - Ok(Builder { - codec, - comment: Some(sshkey.comment().to_string()), - attributes: Some(attributes), - ..Default::default() - }) - } - Other(name) => match name.as_str() { - secp256k1::ALGORITHM_NAME => { + // ML-DSA public keys + ml_dsa::ALGORITHM_NAME_65 => { let key_bytes = match sshkey.key_data() { - KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + KeyData::Other(pk) => pk.key.as_ref().to_vec(), _ => { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -683,15 +1668,15 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::Secp256K1Priv, + codec: Codec::MlDsa65Pub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() }) } - bls12381::ALGORITHM_NAME_G1 => { + ml_dsa::ALGORITHM_NAME_87 => { let key_bytes = match sshkey.key_data() { - KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + KeyData::Other(pk) => pk.key.as_ref().to_vec(), _ => { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -702,15 +1687,16 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::Bls12381G1Priv, + codec: Codec::MlDsa87Pub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() }) } - bls12381::ALGORITHM_NAME_G1_SHARE => { + // FN-DSA public keys + fn_dsa::ALGORITHM_NAME_512 => { let key_bytes = match sshkey.key_data() { - KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + KeyData::Other(pk) => pk.key.as_ref().to_vec(), _ => { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -718,25 +1704,18 @@ impl Builder { .into()) } }; - let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = Varuint(key_share.0).into(); - let threshold: Vec = Varuint(key_share.1).into(); - let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); - attributes.insert(AttrId::ShareIdentifier, identifier.into()); - attributes.insert(AttrId::Threshold, threshold.into()); - attributes.insert(AttrId::Limit, limit.into()); - attributes.insert(AttrId::KeyData, key_share.3.into()); + attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::Bls12381G1PrivShare, + codec: Codec::FnDsa512Pub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() }) } - bls12381::ALGORITHM_NAME_G2 => { + fn_dsa::ALGORITHM_NAME_1024 => { let key_bytes = match sshkey.key_data() { - KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + KeyData::Other(pk) => pk.key.as_ref().to_vec(), _ => { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -747,15 +1726,406 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::Bls12381G2Priv, + codec: Codec::FnDsa1024Pub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() }) } - bls12381::ALGORITHM_NAME_G2_SHARE => { + // SLH-DSA public keys + slh_dsa::ALGORITHM_NAME_SHA2_128F => { let key_bytes = match sshkey.key_data() { - KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2128FPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_128S => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2128SPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_192F => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2192FPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_192S => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2192SPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_256F => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2256FPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_256S => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2256SPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_128F => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake128FPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_128S => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake128SPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_192F => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake192FPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_192S => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake192SPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_256F => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake256FPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_256S => { + let key_bytes = match sshkey.key_data() { + KeyData::Other(pk) => pk.key.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake256SPub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + s => Err(ConversionsError::UnsupportedAlgorithm(s.to_string()).into()), + }, + Ed25519 => { + let key_bytes = match sshkey.key_data() { + KeyData::Ed25519(e) => e.0.to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::Ed25519Pub, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + _ => Err(ConversionsError::UnsupportedAlgorithm(sshkey.algorithm().to_string()).into()), + } + } + + /// new builder from ssh_key::PrivateKey source + pub fn new_from_ssh_private_key(sshkey: &PrivateKey) -> Result { + use ssh_key::Algorithm::*; + match sshkey.algorithm() { + Ecdsa { curve } => { + use EcdsaCurve::*; + let (key_bytes, codec) = match curve { + NistP256 => { + if let KeypairData::Ecdsa(EcdsaKeypair::NistP256 { private, .. }) = + sshkey.key_data() + { + (private.as_slice().to_vec(), Codec::P256Priv) + } else { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()); + } + } + NistP384 => { + if let KeypairData::Ecdsa(EcdsaKeypair::NistP384 { private, .. }) = + sshkey.key_data() + { + (private.as_slice().to_vec(), Codec::P384Priv) + } else { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()); + } + } + NistP521 => { + if let KeypairData::Ecdsa(EcdsaKeypair::NistP521 { private, .. }) = + sshkey.key_data() + { + (private.as_slice().to_vec(), Codec::P521Priv) + } else { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()); + } + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + Other(name) => match name.as_str() { + secp256k1::ALGORITHM_NAME => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::Secp256K1Priv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + bls12381::ALGORITHM_NAME_G1 => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::Bls12381G1Priv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + bls12381::ALGORITHM_NAME_G1_SHARE => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; + let identifier: Vec = key_share.0 .0.to_be_bytes().into(); + let threshold: Vec = Varuint(key_share.1).into(); + let limit: Vec = Varuint(key_share.2).into(); + let mut attributes = Attributes::new(); + attributes.insert(AttrId::ShareIdentifier, identifier.into()); + attributes.insert(AttrId::Threshold, threshold.into()); + attributes.insert(AttrId::Limit, limit.into()); + attributes.insert(AttrId::KeyData, key_share.3.into()); + Ok(Builder { + codec: Codec::Bls12381G1PrivShare, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + bls12381::ALGORITHM_NAME_G2 => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::Bls12381G2Priv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + bls12381::ALGORITHM_NAME_G2_SHARE => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), _ => { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), @@ -764,16 +2134,323 @@ impl Builder { } }; let key_share = bls12381::KeyShare::try_from(key_bytes.as_ref())?; - let identifier: Vec = Varuint(key_share.0).into(); + let identifier: Vec = key_share.0 .0.to_be_bytes().into(); let threshold: Vec = Varuint(key_share.1).into(); let limit: Vec = Varuint(key_share.2).into(); let mut attributes = Attributes::new(); - attributes.insert(AttrId::ShareIdentifier, identifier.into()); - attributes.insert(AttrId::Threshold, threshold.into()); - attributes.insert(AttrId::Limit, limit.into()); - attributes.insert(AttrId::KeyData, key_share.3.into()); + attributes.insert(AttrId::ShareIdentifier, identifier.into()); + attributes.insert(AttrId::Threshold, threshold.into()); + attributes.insert(AttrId::Limit, limit.into()); + attributes.insert(AttrId::KeyData, key_share.3.into()); + Ok(Builder { + codec: Codec::Bls12381G2PrivShare, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + // ML-DSA private keys + ml_dsa::ALGORITHM_NAME_65 => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::MlDsa65Priv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + ml_dsa::ALGORITHM_NAME_87 => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::MlDsa87Priv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + // FN-DSA private keys + fn_dsa::ALGORITHM_NAME_512 => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::FnDsa512Priv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + fn_dsa::ALGORITHM_NAME_1024 => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::FnDsa1024Priv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + // SLH-DSA private keys + slh_dsa::ALGORITHM_NAME_SHA2_128F => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2128FPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_128S => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2128SPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_192F => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2192FPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_192S => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2192SPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_256F => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2256FPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHA2_256S => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaSha2256SPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_128F => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake128FPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_128S => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake128SPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_192F => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake192FPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_192S => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::Bls12381G2PrivShare, + codec: Codec::SlhDsaShake192SPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_256F => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake256FPriv, + comment: Some(sshkey.comment().to_string()), + attributes: Some(attributes), + ..Default::default() + }) + } + slh_dsa::ALGORITHM_NAME_SHAKE_256S => { + let key_bytes = match sshkey.key_data() { + KeypairData::Other(kp) => kp.private.as_ref().to_vec(), + _ => { + return Err(ConversionsError::UnsupportedAlgorithm( + sshkey.algorithm().to_string(), + ) + .into()) + } + }; + let mut attributes = Attributes::new(); + attributes.insert(AttrId::KeyData, key_bytes.into()); + Ok(Builder { + codec: Codec::SlhDsaShake256SPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -859,8 +2536,8 @@ impl Builder { } /// add in the share identifier value - pub fn with_identifier(self, identifier: u8) -> Self { - self.with_attribute(AttrId::ShareIdentifier, &Varuint(identifier).into()) + pub fn with_identifier(self, identifier: &impl AsRef<[u8]>) -> Self { + self.with_attribute(AttrId::ShareIdentifier, &identifier.as_ref().to_vec()) } /// add in the threshold data @@ -913,13 +2590,13 @@ impl Builder { mod tests { use super::*; use crate::{cipher, kdf}; - use multisig::EncodedMultisig; + use multi_sig::EncodedMultisig; use ssh_key::private::Ed25519Keypair; #[test] fn test_random() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_comment("test key") @@ -949,7 +2626,7 @@ mod tests { #[test] fn test_encoded_random() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_base_encoding(Base::Base32Lower) @@ -965,7 +2642,7 @@ mod tests { #[test] fn test_random_public_ssh_key_roundtrip() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_comment("test key") @@ -985,7 +2662,7 @@ mod tests { #[test] fn test_random_private_ssh_key_roundtrip() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_comment("test key") @@ -1004,7 +2681,7 @@ mod tests { #[test] fn test_ssh_key_roundtrip() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let sk1 = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_comment("test key") @@ -1034,7 +2711,7 @@ mod tests { #[test] fn test_encryption_roundtrip() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk1 = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_comment("test key") @@ -1123,7 +2800,7 @@ mod tests { #[test] fn test_signing_detached_roundtrip() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_comment("test key") @@ -1160,7 +2837,7 @@ mod tests { #[test] fn test_signing_merged_roundtrip() { for codec in KEY_CODECS { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(codec, &mut rng) .unwrap() .with_comment("test key") @@ -1194,9 +2871,382 @@ mod tests { } } + #[test] + fn test_fn_dsa_random() { + for codec in FN_DSA_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 = mk.conv_view().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_fn_dsa_encoded_random() { + for codec in FN_DSA_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_fn_dsa_signing_detached_roundtrip() { + for codec in FN_DSA_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 = mk.attr_view().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + let conv = mk.conv_view().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 = mk.sign_view().unwrap(); + let signature = signmk.sign(msg.as_slice(), false, None).unwrap(); + let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); + println!("signature: {}", sig); + + let verifymk = mk.verify_view().unwrap(); + assert!(verifymk.verify(&signature, Some(msg.as_slice())).is_ok()); + } + } + + #[test] + fn test_fn_dsa_signing_merged_roundtrip() { + for codec in FN_DSA_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 = mk.attr_view().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + let msg = + hex::decode("8bb78be51ac7cc98f44e38947ff8a128764ec039b89687a790dfa8444ba97682") + .unwrap(); + + let signmk = mk.sign_view().unwrap(); + let signature = signmk.sign(&msg, true, None).unwrap(); + + // make sure the message is stored correctly in the signature + assert_eq!(signature.message, msg); + + let verifymk = mk.verify_view().unwrap(); + assert!(verifymk.verify(&signature, None).is_ok()); + } + } + + #[test] + fn test_ml_dsa_random() { + for codec in ML_DSA_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 = mk.conv_view().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_ml_dsa_encoded_random() { + for codec in ML_DSA_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_ml_dsa_signing_detached_roundtrip() { + // ML-DSA signing allocates large key/signature buffers on the stack; + // run on a 16 MiB stack to avoid overflowing the default test-thread + // stack (matches `test_slh_dsa_signing_codec`). + std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(|| { + for codec in ML_DSA_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 = mk.attr_view().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + let conv = mk.conv_view().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 = mk.sign_view().unwrap(); + let signature = signmk.sign(msg.as_slice(), false, None).unwrap(); + let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); + println!("signature: {}", sig); + + let verifymk = mk.verify_view().unwrap(); + assert!(verifymk.verify(&signature, Some(msg.as_slice())).is_ok()); + } + }) + .unwrap() + .join() + .unwrap(); + } + + #[test] + fn test_ml_dsa_signing_merged_roundtrip() { + // ML-DSA signing allocates large key/signature buffers on the stack; + // run on a 16 MiB stack to avoid overflowing the default test-thread + // stack (matches `test_slh_dsa_signing_codec`). + std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(|| { + for codec in ML_DSA_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 = mk.attr_view().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + let msg = hex::decode( + "8bb78be51ac7cc98f44e38947ff8a128764ec039b89687a790dfa8444ba97682", + ) + .unwrap(); + + let signmk = mk.sign_view().unwrap(); + let signature = signmk.sign(&msg, true, None).unwrap(); + + // make sure the message is stored correctly in the signature + assert_eq!(signature.message, msg); + + let verifymk = mk.verify_view().unwrap(); + assert!(verifymk.verify(&signature, None).is_ok()); + } + }) + .unwrap() + .join() + .unwrap(); + } + + #[test] + fn test_slh_dsa_random() { + for codec in SLH_DSA_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 = mk.conv_view().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_slh_dsa_encoded_random() { + for codec in SLH_DSA_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_slh_dsa_signing_detached_roundtrip() { + // SLH-DSA signing uses large stack buffers internally (Merkle trees, WOTS+). + // Spawn a thread with a larger stack to avoid overflow. + std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(|| { + for codec in SLH_DSA_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 = mk.attr_view().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + let conv = mk.conv_view().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 = mk.sign_view().unwrap(); + let signature = signmk.sign(msg.as_slice(), false, None).unwrap(); + let sig = EncodedMultisig::new(Base::Base16Lower, signature.clone()); + println!("signature: {}", sig); + + let verifymk = mk.verify_view().unwrap(); + assert!(verifymk.verify(&signature, Some(msg.as_slice())).is_ok()); + } + }) + .unwrap() + .join() + .unwrap(); + } + + #[test] + fn test_slh_dsa_signing_merged_roundtrip() { + // SLH-DSA signing uses large stack buffers internally (Merkle trees, WOTS+). + // Spawn a thread with a larger stack to avoid overflow. + std::thread::Builder::new() + .stack_size(16 * 1024 * 1024) + .spawn(|| { + for codec in SLH_DSA_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 = mk.attr_view().unwrap(); + assert!(!attr.is_encrypted()); + assert!(!attr.is_public_key()); + assert!(attr.is_secret_key()); + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + let msg = hex::decode( + "8bb78be51ac7cc98f44e38947ff8a128764ec039b89687a790dfa8444ba97682", + ) + .unwrap(); + + let signmk = mk.sign_view().unwrap(); + let signature = signmk.sign(&msg, true, None).unwrap(); + + // make sure the message is stored correctly in the signature + assert_eq!(signature.message, msg); + + let verifymk = mk.verify_view().unwrap(); + assert!(verifymk.verify(&signature, None).is_ok()); + } + }) + .unwrap() + .join() + .unwrap(); + } + #[test] fn test_bls_key_combine() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk1 = Builder::new_from_random_bytes(Codec::Bls12381G1Priv, &mut rng) .unwrap() .with_comment("test key") @@ -1240,7 +3290,7 @@ mod tests { #[test] fn test_bls_share_ssh_key_roundtrip() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = Builder::new_from_random_bytes(Codec::Bls12381G1Priv, &mut rng) .unwrap() .with_comment("test key") @@ -1272,7 +3322,7 @@ mod tests { #[test] fn test_from_ssh_pubkey() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let kp = KeypairData::Ed25519(Ed25519Keypair::random(&mut rng)); let sk = PrivateKey::new(kp, "test key").unwrap(); @@ -1295,7 +3345,7 @@ mod tests { #[test] fn test_from_ssh_privkey() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let kp = KeypairData::Ed25519(Ed25519Keypair::random(&mut rng)); let sk = PrivateKey::new(kp, "test key").unwrap(); diff --git a/src/nonce.rs b/src/nonce.rs index e471223..29a4e87 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -1,11 +1,11 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{error::NonceError, Error}; +use multi_base::Base; +use multi_codec::Codec; +use multi_trait::{Null, TryDecodeFrom}; +use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes}; use core::fmt; -use multibase::Base; -use multicodec::Codec; -use multitrait::{Null, TryDecodeFrom}; -use multiutil::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes}; -use rand::{CryptoRng, RngCore}; +use rand_core::CryptoRng; /// the Nonce multicodec sigil pub const SIGIL: Codec = Codec::Nonce; @@ -14,7 +14,7 @@ pub const SIGIL: Codec = Codec::Nonce; pub type EncodedNonce = BaseEncoded; /// a multicodec Nonce type -#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct Nonce { /// the random nonce bytes pub(crate) nonce: Vec, @@ -25,6 +25,11 @@ impl Nonce { pub fn len(&self) -> usize { self.nonce.len() } + + /// check if the nonce is empty + pub fn is_empty(&self) -> bool { + self.nonce.is_empty() + } } impl CodecInfo for Nonce { @@ -61,7 +66,7 @@ impl From for Vec { // add the sigil v.append(&mut SIGIL.into()); // add the nonce bytes - v.append(&mut Varbytes(val.nonce.clone()).into()); + v.append(&mut Varbytes::new(val.nonce.clone()).into()); v } } @@ -120,7 +125,7 @@ pub struct Builder { impl Builder { /// build from random source - pub fn new_from_random_bytes(size: usize, rng: &mut (impl RngCore + CryptoRng)) -> Self { + pub fn new_from_random_bytes(size: usize, rng: &mut impl CryptoRng) -> Self { let mut bytes = vec![0; size]; bytes.resize(size, 0u8); rng.fill_bytes(bytes.as_mut()); @@ -167,7 +172,7 @@ mod tests { #[test] fn test_random() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let n = Builder::new_from_random_bytes(32, &mut rng) .try_build() .unwrap(); @@ -178,7 +183,7 @@ mod tests { #[test] fn test_binary_roundtrip() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let n = Builder::new_from_random_bytes(32, &mut rng) .try_build() .unwrap(); @@ -188,7 +193,7 @@ mod tests { #[test] fn test_encoded_roundtrip() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let n = Builder::new_from_random_bytes(32, &mut rng) .try_build_encoded() .unwrap(); @@ -200,7 +205,7 @@ mod tests { #[test] fn test_nonce_multisig_roundtrip() { - let mut rng = rand::rngs::OsRng; + let mut rng = rand::rng(); let mk = mk::Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) .unwrap() .with_comment("test key") diff --git a/src/serde/de.rs b/src/serde/de.rs index f66d307..408efcf 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -1,11 +1,11 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ mk::{self, Attributes}, nonce, AttrId, Multikey, Nonce, }; +use multi_codec::Codec; +use multi_util::EncodedVarbytes; use core::fmt; -use multicodec::Codec; -use multiutil::EncodedVarbytes; use serde::{ de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 66e3d71..c96a0b9 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -1,4 +1,4 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 //! Serde (de)serialization for [`crate::Multikey`]. mod de; mod ser; @@ -6,11 +6,11 @@ mod ser; #[cfg(test)] mod tests { use crate::{cipher, kdf, nonce, Builder, EncodedMultikey, Multikey, Views}; - use multibase::Base; - use multicodec::Codec; - use multihash::EncodedMultihash; - use multitrait::Null; - use multiutil::BaseEncoded; + use multi_base::Base; + use multi_codec::Codec; + use multi_hash::EncodedMultihash; + use multi_trait::Null; + use multi_util::BaseEncoded; use serde::{Deserialize, Serialize}; use serde_test::{assert_tokens, Configure, Token}; use std::collections::BTreeMap; @@ -67,7 +67,7 @@ mod tests { // try to get the associated public key let mk = { let conv = sk.conv_view().unwrap(); - + conv.to_public_key().unwrap() }; @@ -123,7 +123,7 @@ mod tests { let mk = { let conv = sk.conv_view().unwrap(); - + conv.to_public_key().unwrap() }; @@ -205,41 +205,14 @@ mod tests { println!(""); */ - assert_tokens( - &mk2.compact(), - &[Token::BorrowedBytes(&[ - 0xba, 0x24, // Multikey sigil - 0x80, 0x26, // Ed25519 private codec as varuint - 0x08, // comment of 8 bytes - // comment - 0x74, 0x65, 0x73, 0x74, 0x20, 0x6b, 0x65, 0x79, 0x08, // 8 bytes of attributes - // key is encrypted - 0x00, 0x01, 0x01, // 3 bytes - // key data of 32 bytes - 0x01, 0x20, // 0x20 = 32 byte key - 0xef, 0x7c, 0xf7, 0x8f, 0x3e, 0x0e, 0x58, 0x82, // 8 bytes - 0x32, 0xa4, 0x23, 0xdb, 0x1f, 0xdf, 0x02, 0xe2, // 16 bytes - 0x18, 0xc4, 0x94, 0x4c, 0x35, 0xba, 0x4e, 0xb4, // 24 bytes - 0x54, 0x96, 0xb5, 0x27, 0x42, 0x53, 0x9d, 0x78, // 32 bytes - // cipher codec - 0x02, 0x02, 0xa5, 0x01, // codec (Chacha20Poly1305) - // cipher key len (32) - 0x03, 0x01, 0x20, // 3 bytes codec - // 12 byte cipher nonce - 0x04, 0x0c, // 0x0c = 12 bytes of nonce - 0x71, 0x4e, 0x5a, 0xbf, 0x0f, // 6 bytes - 0x7b, 0xea, 0xe8, 0xaa, 0xbb, 0xcc, 0xdd, // 12 bytes - // kdf codec - 0x05, 0x03, 0x8d, 0xa0, 0x03, // kdf salt - 0x06, 0x20, // 2 bytes salt codec - 0x62, 0x1f, 0x20, 0xcf, 0xda, 0x14, 0x0b, 0xd8, // 8 bytes salt - 0xbf, 0x83, 0xa8, 0x99, 0x16, 0x74, 0x28, 0x46, // 16 - 0x29, 0x29, 0xa4, 0x1e, 0x9b, 0x68, 0xa8, 0x46, // 24 - 0x7b, 0xfc, 0x24, 0x55, 0xe9, 0xf9, 0x84, 0x06, // 32 - // kdf rounds (10) - 0x07, 0x01, 0x0a, // 3 bytes rounds - ])], - ); + // The encrypted key round-trips through serde unchanged. We no longer + // assert exact ciphertext bytes: ChaCha20Poly1305 appends a 16-byte + // Poly1305 tag, so the encrypted form differs from the legacy bare-stream + // output (see test_chacha20_aead_roundtrip for the crypto itself). + assert!(mk2.attr_view().unwrap().is_encrypted()); + let json = serde_json::to_string(&mk2).unwrap(); + let mk3: Multikey = serde_json::from_str(&json).unwrap(); + assert_eq!(mk2, mk3); } #[test] @@ -287,55 +260,12 @@ mod tests { cipher.encrypt().unwrap() }; - assert_tokens( - &mk2.readable(), - &[ - Token::Struct { - name: "multikey", - len: 3, - }, - Token::Str("codec"), - Token::Str("ed25519-priv"), - Token::Str("comment"), - Token::Str("test key"), - Token::Str("attributes"), - Token::Seq { len: Some(8) }, - Token::Tuple { len: 2 }, - Token::Str("key-is-encrypted"), - Token::Str("f0101"), - Token::TupleEnd, - Token::Tuple { len: 2 }, - Token::Str("key-data"), - Token::Str("f20ef7cf78f3e0e588232a423db1fdf02e218c4944c35ba4eb45496b52742539d78"), - Token::TupleEnd, - Token::Tuple { len: 2 }, - Token::Str("cipher-codec"), - Token::Str("f02a501"), - Token::TupleEnd, - Token::Tuple { len: 2 }, - Token::Str("cipher-key-len"), - Token::Str("f0120"), - Token::TupleEnd, - Token::Tuple { len: 2 }, - Token::Str("cipher-nonce"), - Token::Str("f0c714e5abf0f7beae8aabbccdd"), - Token::TupleEnd, - Token::Tuple { len: 2 }, - Token::Str("kdf-codec"), - Token::Str("f038da003"), - Token::TupleEnd, - Token::Tuple { len: 2 }, - Token::Str("kdf-salt"), - Token::Str("f20621f20cfda140bd8bf83a899167428462929a41e9b68a8467bfc2455e9f98406"), - Token::TupleEnd, - Token::Tuple { len: 2 }, - Token::Str("kdf-rounds"), - Token::Str("f010a"), - Token::TupleEnd, - Token::SeqEnd, - Token::StructEnd, - ], - ); + // No exact-ciphertext assertion (ChaCha20Poly1305 tag); assert the + // encrypted key round-trips through serde unchanged. + assert!(mk2.attr_view().unwrap().is_encrypted()); + let json = serde_json::to_string(&mk2).unwrap(); + let mk3: Multikey = serde_json::from_str(&json).unwrap(); + assert_eq!(mk2, mk3); } #[test] @@ -384,9 +314,10 @@ mod tests { cipher.encrypt().unwrap() }; + // No exact-ciphertext assertion: ChaCha20Poly1305 appends a 16-byte tag, + // so the encrypted bytes intentionally differ from the legacy format. We + // assert the encrypted key round-trips through JSON unchanged. let s = serde_json::to_string(&mk2).unwrap(); - assert_eq!(s, "{\"codec\":\"ed25519-priv\",\"comment\":\"test key\",\"attributes\":[[\"key-is-encrypted\",\"f0101\"],[\"key-data\",\"f20ef7cf78f3e0e588232a423db1fdf02e218c4944c35ba4eb45496b52742539d78\"],[\"cipher-codec\",\"f02a501\"],[\"cipher-key-len\",\"f0120\"],[\"cipher-nonce\",\"f0c714e5abf0f7beae8aabbccdd\"],[\"kdf-codec\",\"f038da003\"],[\"kdf-salt\",\"f20621f20cfda140bd8bf83a899167428462929a41e9b68a8467bfc2455e9f98406\"],[\"kdf-rounds\",\"f010a\"]]}".to_string()); - let mk3: Multikey = serde_json::from_str(&s).unwrap(); assert_eq!(mk2, mk3); } @@ -448,14 +379,60 @@ mod tests { cipher.encrypt().unwrap() }; + // No exact-ciphertext assertion (ChaCha20Poly1305 tag); assert round-trip. let s = serde_json::to_string(&mk2).unwrap(); - println!("{}", s); - assert_eq!(s, "{\"codec\":\"bls12_381-g1-priv\",\"comment\":\"test key\",\"attributes\":[[\"key-is-encrypted\",\"f0101\"],[\"key-data\",\"f20da4d0758cd8d3debfbf143b6813c94ed6bd40a0ddb0971f3caf51daeec3a3acd\"],[\"cipher-codec\",\"f02a501\"],[\"cipher-key-len\",\"f0120\"],[\"cipher-nonce\",\"f0c714e5abf0f7beae8aabbccdd\"],[\"kdf-codec\",\"f038da003\"],[\"kdf-salt\",\"f20621f20cfda140bd8bf83a899167428462929a41e9b68a8467bfc2455e9f98406\"],[\"kdf-rounds\",\"f010a\"]]}".to_string()); - let mk3: Multikey = serde_json::from_str(&s).unwrap(); assert_eq!(mk2, mk3); } + #[test] + fn test_chacha20_aead_roundtrip() { + // A secret key encrypted with ChaCha20Poly1305 must decrypt back to the + // exact plaintext, and the ciphertext must be 16 bytes longer than the + // plaintext (the appended Poly1305 authentication tag) — proving the AEAD + // path is active rather than the legacy bare ChaCha20 stream. + let plain = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") + .unwrap(); + let mk1 = Builder::new(Codec::Ed25519Priv) + .with_comment("test key") + .with_key_bytes(&plain) + .try_build() + .unwrap(); + + let salt = hex::decode("621f20cfda140bd8bf83a899167428462929a41e9b68a8467bfc2455e9f98406") + .unwrap(); + let kdfmk = kdf::Builder::new(Codec::BcryptPbkdf) + .with_salt(&salt) + .with_rounds(10) + .try_build() + .unwrap(); + let nonce = hex::decode("714e5abf0f7beae8aabbccdd").unwrap(); + let ciphermk = cipher::Builder::new(Codec::Chacha20Poly1305) + .with_nonce(&nonce) + .try_build() + .unwrap(); + let ciphermk = ciphermk + .kdf_view(&kdfmk) + .unwrap() + .derive_key(b"for great justice, move every zig!") + .unwrap(); + + // encrypt → must be longer by the 16-byte tag + let enc = mk1.cipher_view(&ciphermk).unwrap().encrypt().unwrap(); + assert!(enc.attr_view().unwrap().is_encrypted()); + assert_eq!( + enc.data_view().unwrap().key_bytes().unwrap().len(), + plain.len() + 16 + ); + + // decrypt → recovers the original plaintext + let dec = enc.cipher_view(&ciphermk).unwrap().decrypt().unwrap(); + assert_eq!( + dec.data_view().unwrap().secret_bytes().unwrap().as_slice(), + plain.as_slice() + ); + } + #[test] fn test_encoded_public_key() { let bytes = hex::decode("7e48467029ffb9f6282b56e9ce131cead6e4bd061a3500697c57ac7034cf86f2") diff --git a/src/serde/ser.rs b/src/serde/ser.rs index 9f84c27..6ebab92 100644 --- a/src/serde/ser.rs +++ b/src/serde/ser.rs @@ -1,6 +1,6 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{mk, nonce, AttrId, Multikey, Nonce}; -use multiutil::{EncodedVarbytes, EncodingInfo, Varbytes}; +use multi_util::{EncodedVarbytes, EncodingInfo, Varbytes}; use serde::ser::{self, SerializeStruct}; /// Serialize instance of [`crate::Nonce`] diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..87972f1 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Type-safe wrappers for cryptographic key components + +use multi_codec::Codec; +use core::fmt; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +/// Public key bytes +/// +/// Type-safe wrapper for public key data. +/// +/// # Examples +/// +/// ``` +/// use multi_key::types::PublicKeyBytes; +/// +/// let pubkey = PublicKeyBytes::new(vec![0u8; 32]); +/// assert_eq!(pubkey.len(), 32); +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublicKeyBytes(Vec); + +impl PublicKeyBytes { + /// Create new PublicKeyBytes + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + /// Get bytes + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Get length + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Convert to inner bytes + pub fn into_bytes(self) -> Vec { + self.0 + } +} + +impl From> for PublicKeyBytes { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl AsRef<[u8]> for PublicKeyBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// Private key bytes with automatic zeroization +/// +/// Sensitive key material that is automatically zeroized on drop. +/// +/// # Examples +/// +/// ``` +/// use multi_key::types::PrivateKeyBytes; +/// +/// let privkey = PrivateKeyBytes::new(vec![0u8; 32]); +/// assert_eq!(privkey.len(), 32); +/// // Automatically zeroized when dropped +/// ``` +#[derive(Clone, Zeroize, ZeroizeOnDrop)] +pub struct PrivateKeyBytes(Vec); + +impl PrivateKeyBytes { + /// Create new PrivateKeyBytes + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + /// Get bytes (careful - exposes sensitive data) + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Get length + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Get a copy of the bytes (careful - caller must handle zeroization) + pub fn to_vec(&self) -> Vec { + self.0.clone() + } +} + +impl From> for PrivateKeyBytes { + fn from(bytes: Vec) -> Self { + Self(bytes) + } +} + +impl AsRef<[u8]> for PrivateKeyBytes { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Debug for PrivateKeyBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PrivateKeyBytes([REDACTED {} bytes])", self.0.len()) + } +} + +/// Key scheme identifier +/// +/// Type-safe wrapper for key algorithm codecs. +/// +/// # Examples +/// +/// ``` +/// use multi_key::types::KeyScheme; +/// use multi_codec::Codec; +/// +/// let scheme = KeyScheme::new(Codec::Ed25519Pub); +/// assert_eq!(scheme.codec(), Codec::Ed25519Pub); +/// ``` +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct KeyScheme(Codec); + +impl KeyScheme { + /// Create new KeyScheme + pub const fn new(codec: Codec) -> Self { + Self(codec) + } + + /// Get codec + pub const fn codec(self) -> Codec { + self.0 + } + + /// Get name + pub fn name(self) -> &'static str { + self.0.into() + } + + /// Get code + pub fn code(self) -> u64 { + self.0.code() + } +} + +impl From for KeyScheme { + fn from(codec: Codec) -> Self { + Self(codec) + } +} + +impl From for Codec { + fn from(scheme: KeyScheme) -> Codec { + scheme.0 + } +} + +impl fmt::Display for KeyScheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_public_key_bytes() { + let pubkey = PublicKeyBytes::new(vec![1, 2, 3]); + assert_eq!(pubkey.len(), 3); + assert_eq!(pubkey.as_bytes(), &[1, 2, 3]); + } + + #[test] + fn test_private_key_bytes_zeroization() { + let privkey = PrivateKeyBytes::new(vec![1, 2, 3]); + assert_eq!(privkey.len(), 3); + // Automatically zeroized on drop + } + + #[test] + fn test_private_key_debug_redacted() { + let privkey = PrivateKeyBytes::new(vec![1, 2, 3]); + let debug = format!("{:?}", privkey); + assert!(debug.contains("REDACTED")); + assert!(!debug.contains("1")); + } + + #[test] + fn test_key_scheme() { + let scheme = KeyScheme::new(Codec::Ed25519Pub); + assert_eq!(scheme.codec(), Codec::Ed25519Pub); + assert_eq!(scheme.name(), "ed25519-pub"); + } + + #[test] + fn test_newtypes_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + assert_send::(); + assert_sync::(); + assert_send::(); + assert_sync::(); + } +} diff --git a/src/views.rs b/src/views.rs index b533500..e1568eb 100644 --- a/src/views.rs +++ b/src/views.rs @@ -1,22 +1,51 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{Error, Multikey}; -use multicodec::Codec; -use multihash::Multihash; -use multisig::Multisig; +use multi_codec::Codec; +use multi_hash::Multihash; +use multi_sig::Multisig; use zeroize::Zeroizing; // algorithms implement different sets of view pub(crate) mod bcrypt; pub(crate) mod bls12381; +pub(crate) mod bls12381_g1_fndsa512; +pub(crate) mod bls12381_g1_mayo1; +pub(crate) mod bls12381_g1_mayo2; +pub(crate) mod bls12381_g1_mldsa65; +pub(crate) mod bls12381_hybrid; pub(crate) mod chacha20; +pub(crate) mod classic_mceliece; +pub(crate) mod dkg_threshold; pub(crate) mod ed25519; +pub(crate) mod ed25519_fndsa512; +pub(crate) mod ed25519_mayo2; +pub(crate) mod ed25519_mldsa65; +pub(crate) mod fn_dsa; +pub(crate) mod frodokem; +pub(crate) mod frodokem_helper; +pub(crate) mod mayo; +pub(crate) mod ml_dsa; +pub(crate) mod ml_kem; +pub(crate) mod nist_p; +pub(crate) mod rsa; pub(crate) mod secp256k1; +pub(crate) mod slh_dsa; +pub(crate) mod sntrup; +/// Split (Shamir) threshold marker attributes and scheme classifier. +pub mod threshold_marker; +pub(crate) mod x25519; +pub(crate) mod x25519_frodokem640; +pub(crate) mod x25519_mceliece348864; +pub(crate) mod x25519_mlkem768; +pub(crate) mod x25519_sntrup761; + +// shared AEAD helper used by ml_kem, sntrup, classic_mceliece, x25519, and hybrid KEM views +pub(crate) mod aead; /// /// Attributes views let you inquire about the Multikey and retrieve data /// associated with the particular view. /// - /// trait for returning basic, general attributes about a Multikey pub trait AttrView { /// is this key encrypted @@ -70,7 +99,7 @@ pub trait ThresholdAttrView { /// get the limit value for the multikey fn limit(&self) -> Result; /// get the share identifier for the multikey - fn identifier(&self) -> Result; + fn identifier(&self) -> Result<&[u8], Error>; /// get the codec-specific threshold data fn threshold_data(&self) -> Result<&[u8], Error>; } @@ -79,7 +108,6 @@ pub trait ThresholdAttrView { /// The following key operations views are functions that generate new /// Multikeys, Multihashes, or Multisigs from the viewed Multikey (self) /// - /// trait for encrypting and decrypting Multikeys pub trait CipherView { /// decrypt the secret_bytes from the viewed Multikey using the codec and @@ -125,6 +153,39 @@ pub trait KdfView { fn derive_key(&self, passphrase: &[u8]) -> Result; } +/// trait for sealing (encrypting) data using a public encryption key +pub trait SealView { + /// Seal (encrypt) plaintext using the public key and the specified AEAD codec. + /// + /// Returns `(sealed_bytes, optional_ephemeral_pub)`. For ECDH-based schemes + /// (e.g. X25519) the second element is the sender's ephemeral public key, + /// which the caller should transmit alongside the sealed blob. For + /// unidirectional PQ KEMs (ML-KEM, sntrup761, McEliece, BLS TimeCrypt, and + /// hybrid schemes) it is `None` because the ephemeral material is already + /// self-contained inside `sealed_bytes`. + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error>; +} + +/// trait for opening (decrypting) data using a private encryption key +pub trait OpenView { + /// Open (decrypt) a sealed message using the private key. + /// + /// `ephemeral` must be `Some(sender_ephemeral_pub)` for ECDH-based schemes + /// (e.g. X25519) and `None` for PQ KEMs that embed their ephemeral material + /// inside `sealed_msg`. + fn open( + &self, + sealed_msg: &[u8], + ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error>; +} + /// trait for digially signing data using a multikey pub trait SignView { /// try to create a Multisig by siging the passed-in data with the Multikey @@ -141,6 +202,25 @@ pub trait ThresholdView { fn combine(&self) -> Result; } +/// trait exposing higher-level DKG-shaped read-only metadata on a Multikey +/// that participates in a threshold (t-of-n) key. +/// +/// This is intentionally narrower than [`ThresholdAttrView`]: it returns +/// already-decoded values (u16, owning Vec) suitable for direct consumption +/// by application code, and includes [`is_threshold_key`] for cheap discrimination. +pub trait ThresholdKeyView { + /// Get the group public key bytes for the t-of-n key. + fn group_pubkey(&self) -> Result, Error>; + /// Returns `true` if the underlying Multikey is part of a threshold key. + fn is_threshold_key(&self) -> bool; + /// Number of participants `n` in the t-of-n scheme. + fn participant_count(&self) -> Result; + /// Threshold `t` in the t-of-n scheme. + fn threshold(&self) -> Result; + /// VLAD bytes of the session owner. + fn owner_vlad(&self) -> Result, Error>; +} + /// trait for verifying digial signatures using a multikey pub trait VerifyView { /// try to verify a Multisig using the Multikey @@ -159,6 +239,8 @@ pub trait Views { 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 @@ -167,6 +249,10 @@ pub trait Views { 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 diff --git a/src/views/aead.rs b/src/views/aead.rs new file mode 100644 index 0000000..d9cf9a2 --- /dev/null +++ b/src/views/aead.rs @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Shared AEAD helper functions for ML-KEM and X25519 seal/open operations. + +use crate::error::SealError; +use aes_gcm::{ + aead::{Aead, KeyInit, Payload}, + Aes128Gcm, Aes256Gcm, +}; +use multi_codec::Codec; +use chacha20poly1305::{ChaCha20Poly1305, XChaCha20Poly1305}; +use hkdf::Hkdf; +use sha2::Sha512; +use zeroize::Zeroizing; + +/// Derive an AEAD key from a shared secret using HKDF-SHA512. +pub(crate) fn derive_aead_key( + shared_secret: &[u8], + info: &[u8], + key_len: usize, +) -> Result>, SealError> { + let hk = 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()))?; + Ok(okm) +} + +/// Return the symmetric key size in bytes for the given AEAD codec. +pub(crate) fn key_size(codec: Codec) -> Result { + match codec { + Codec::Chacha20Poly1305 => Ok(32), + Codec::Xchacha20Poly1305 => Ok(32), + Codec::AesGcm256 => Ok(32), + Codec::AesGcm128 => Ok(16), + _ => Err(SealError::UnsupportedAeadCodec(codec)), + } +} + +/// Return the nonce size in bytes for the given AEAD codec. +pub(crate) fn nonce_size(codec: Codec) -> Result { + match codec { + Codec::Chacha20Poly1305 => Ok(12), + Codec::Xchacha20Poly1305 => Ok(24), + Codec::AesGcm256 => Ok(12), + Codec::AesGcm128 => Ok(12), + _ => Err(SealError::UnsupportedAeadCodec(codec)), + } +} + +/// Encrypt plaintext with the given AEAD codec and key. +/// Returns `(nonce, ciphertext_with_tag)`. +pub(crate) fn aead_seal( + codec: Codec, + key: &[u8], + plaintext: &[u8], + aad: &[u8], +) -> Result<(Vec, Vec), SealError> { + use rand::Rng; + let ns = nonce_size(codec)?; + let mut nonce_bytes = vec![0u8; ns]; + rand::rng().fill_bytes(&mut nonce_bytes); + + let ct = match codec { + Codec::Chacha20Poly1305 => { + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))?; + cipher + .encrypt( + chacha20poly1305::Nonce::from_slice(&nonce_bytes), + Payload { + msg: plaintext, + aad, + }, + ) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))? + } + Codec::Xchacha20Poly1305 => { + let cipher = XChaCha20Poly1305::new_from_slice(key) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))?; + cipher + .encrypt( + chacha20poly1305::XNonce::from_slice(&nonce_bytes), + Payload { + msg: plaintext, + aad, + }, + ) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))? + } + Codec::AesGcm256 => { + let cipher = Aes256Gcm::new_from_slice(key) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))?; + cipher + .encrypt( + aes_gcm::Nonce::from_slice(&nonce_bytes), + Payload { + msg: plaintext, + aad, + }, + ) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))? + } + Codec::AesGcm128 => { + let cipher = Aes128Gcm::new_from_slice(key) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))?; + cipher + .encrypt( + aes_gcm::Nonce::from_slice(&nonce_bytes), + Payload { + msg: plaintext, + aad, + }, + ) + .map_err(|e| SealError::AeadSealFailed(e.to_string()))? + } + _ => return Err(SealError::UnsupportedAeadCodec(codec)), + }; + + Ok((nonce_bytes, ct)) +} + +/// Decrypt ciphertext with the given AEAD codec, key, and nonce. +pub(crate) fn aead_open( + codec: Codec, + key: &[u8], + nonce: &[u8], + ct_tag: &[u8], + aad: &[u8], +) -> Result>, SealError> { + let pt = match codec { + Codec::Chacha20Poly1305 => { + let cipher = + ChaCha20Poly1305::new_from_slice(key).map_err(|_| SealError::AeadOpenFailed)?; + cipher + .decrypt( + chacha20poly1305::Nonce::from_slice(nonce), + Payload { msg: ct_tag, aad }, + ) + .map_err(|_| SealError::AeadOpenFailed)? + } + Codec::Xchacha20Poly1305 => { + let cipher = + XChaCha20Poly1305::new_from_slice(key).map_err(|_| SealError::AeadOpenFailed)?; + cipher + .decrypt( + chacha20poly1305::XNonce::from_slice(nonce), + Payload { msg: ct_tag, aad }, + ) + .map_err(|_| SealError::AeadOpenFailed)? + } + Codec::AesGcm256 => { + let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| SealError::AeadOpenFailed)?; + cipher + .decrypt( + aes_gcm::Nonce::from_slice(nonce), + Payload { msg: ct_tag, aad }, + ) + .map_err(|_| SealError::AeadOpenFailed)? + } + Codec::AesGcm128 => { + let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| SealError::AeadOpenFailed)?; + cipher + .decrypt( + aes_gcm::Nonce::from_slice(nonce), + Payload { msg: ct_tag, aad }, + ) + .map_err(|_| SealError::AeadOpenFailed)? + } + _ => return Err(SealError::UnsupportedAeadCodec(codec)), + }; + + Ok(Zeroizing::new(pt)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_aead_roundtrip_chacha20() { + let key = [0x42u8; 32]; + let plaintext = b"hello world"; + let (nonce, ct) = aead_seal(Codec::Chacha20Poly1305, &key, plaintext, b"").unwrap(); + let pt = aead_open(Codec::Chacha20Poly1305, &key, &nonce, &ct, b"").unwrap(); + assert_eq!(plaintext.as_slice(), pt.as_slice()); + } + + #[test] + fn test_aead_roundtrip_xchacha20() { + let key = [0x42u8; 32]; + let plaintext = b"hello world"; + let (nonce, ct) = aead_seal(Codec::Xchacha20Poly1305, &key, plaintext, b"").unwrap(); + let pt = aead_open(Codec::Xchacha20Poly1305, &key, &nonce, &ct, b"").unwrap(); + assert_eq!(plaintext.as_slice(), pt.as_slice()); + } + + #[test] + fn test_aead_roundtrip_aes256() { + let key = [0x42u8; 32]; + let plaintext = b"hello world"; + let (nonce, ct) = aead_seal(Codec::AesGcm256, &key, plaintext, b"").unwrap(); + let pt = aead_open(Codec::AesGcm256, &key, &nonce, &ct, b"").unwrap(); + assert_eq!(plaintext.as_slice(), pt.as_slice()); + } + + #[test] + fn test_aead_roundtrip_aes128() { + let key = [0x42u8; 16]; + let plaintext = b"hello world"; + let (nonce, ct) = aead_seal(Codec::AesGcm128, &key, plaintext, b"").unwrap(); + let pt = aead_open(Codec::AesGcm128, &key, &nonce, &ct, b"").unwrap(); + assert_eq!(plaintext.as_slice(), pt.as_slice()); + } + + #[test] + fn test_aead_tampered_ciphertext_fails() { + let key = [0x42u8; 32]; + let plaintext = b"hello world"; + let (nonce, mut ct) = aead_seal(Codec::Chacha20Poly1305, &key, plaintext, b"").unwrap(); + ct[0] ^= 0xff; // tamper + assert!(aead_open(Codec::Chacha20Poly1305, &key, &nonce, &ct, b"").is_err()); + } + + #[test] + fn test_hkdf_derive() { + let secret = [0xabu8; 32]; + let key = derive_aead_key(&secret, b"test", 32).unwrap(); + assert_eq!(key.len(), 32); + // derive again with same inputs => same output + let key2 = derive_aead_key(&secret, b"test", 32).unwrap(); + assert_eq!(key.as_slice(), key2.as_slice()); + } +} diff --git a/src/views/bcrypt.rs b/src/views/bcrypt.rs index f1692f6..ab393a2 100644 --- a/src/views/bcrypt.rs +++ b/src/views/bcrypt.rs @@ -1,7 +1,7 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{error::KdfError, AttrId, Error, KdfAttrView, KdfView, Multikey, Views}; -use multicodec::Codec; -use multiutil::Varuint; +use multi_codec::Codec; +use multi_util::Varuint; use zeroize::Zeroizing; /// constants for Bcrypt @@ -42,7 +42,7 @@ impl<'a> KdfView for View<'a> { // get the key length from the viewed Multikey let key_length = { let cattr = self.mk.cipher_attr_view()?; - + cattr.key_length()? }; @@ -50,13 +50,8 @@ impl<'a> KdfView for View<'a> { let mut key: Zeroizing> = vec![0; key_length].into(); // derive the key - bcrypt_pbkdf::bcrypt_pbkdf( - passphrase, - &salt, - rounds as u32, - key.as_mut_slice(), - ) - .map_err(KdfError::Bcrypt)?; + bcrypt_pbkdf::bcrypt_pbkdf(passphrase, &salt, rounds as u32, key.as_mut_slice()) + .map_err(KdfError::Bcrypt)?; // prepare the attributes let kdf_codec: Vec = kdf.codec.into(); diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index 19d6b0c..d1ad75d 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -1,24 +1,26 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::{ - AttributesError, CipherError, ConversionsError, KdfError, SignError, ThresholdError, - VerifyError, + AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, + ThresholdError, VerifyError, }, AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, Multikey, SignView, ThresholdAttrView, ThresholdView, VerifyView, Views, + KdfAttrView, Multikey, OpenView, SealView, SignView, ThresholdAttrView, ThresholdView, + VerifyView, Views, }; use blsful::{ - inner_types::{G1Projective, G2Projective}, - vsss_rs::Share, + inner_types::{G1Projective, G2Projective, Scalar}, + vsss_rs::{IdentifierPrimeField, Share, ValueGroup}, Bls12381G1Impl, Bls12381G2Impl, PublicKey, PublicKeyShare, SecretKey, SecretKeyShare, - Signature, SignatureSchemes, SignatureShare, SECRET_KEY_BYTES, + Signature, SignatureSchemes, SignatureShare, TimeCryptCiphertext, SECRET_KEY_BYTES, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, views::bls12381::SchemeTypeId, Multisig, Views as SigViews}; +use multi_trait::TryDecodeFrom; +use multi_util::{Varbytes, Varuint}; use elliptic_curve::group::GroupEncoding; -use multicodec::Codec; -use multihash::{mh, Multihash}; -use multisig::{ms, views::bls12381::SchemeTypeId, Multisig, Views as SigViews}; -use multitrait::TryDecodeFrom; -use multiutil::{Varbytes, Varuint}; +use rand_core::Rng; use ssh_encoding::{Decode, Encode}; use std::{array::TryFromSliceError, collections::BTreeMap}; use zeroize::Zeroizing; @@ -33,11 +35,39 @@ pub const ALGORITHM_NAME_G2_SHARE: &str = "bls12_381-g2-share@multikey"; pub const G1_PUBLIC_KEY_BYTES: usize = 48; pub const G2_PUBLIC_KEY_BYTES: usize = 96; +/// Parse 32 bytes to identifier for BLS key/sig shares. +fn bytes_to_identifier(bytes: &[u8]) -> Result, Error> { + let arr: [u8; SECRET_KEY_BYTES] = bytes.try_into().map_err(|_| { + Error::Conversions(ConversionsError::SecretKeyFailure( + "Invalid share identifier length".to_string(), + )) + })?; + Ok(IdentifierPrimeField( + Option::from(Scalar::from_be_bytes(&arr)).ok_or(Error::Conversions( + ConversionsError::SecretKeyFailure("Invalid share identifier bytes".to_string()), + ))?, + )) +} + +/// Parse 32 bytes to value (scalar) for BLS secret key shares. +fn bytes_to_value(bytes: &[u8]) -> Result, Error> { + let arr: [u8; SECRET_KEY_BYTES] = bytes.try_into().map_err(|_| { + Error::Conversions(ConversionsError::SecretKeyFailure( + "Invalid share value length".to_string(), + )) + })?; + Ok(IdentifierPrimeField( + Option::from(Scalar::from_be_bytes(&arr)).ok_or(Error::Conversions( + ConversionsError::SecretKeyFailure("Invalid share value bytes".to_string()), + ))?, + )) +} + /// tuple of the key share data with threshold attributes #[derive(Clone)] pub struct KeyShare( /// identifier - pub u8, + pub IdentifierPrimeField, /// threshold, pub usize, /// limit @@ -50,13 +80,13 @@ impl From for Vec { fn from(val: KeyShare) -> Self { let mut v = Vec::default(); // add in the share identifier - v.append(&mut Varuint(val.0).into()); + v.extend_from_slice(&val.0 .0.to_be_bytes()); // add in the threshold v.append(&mut Varuint(val.1).into()); // add in the limit v.append(&mut Varuint(val.2).into()); // add in the key share data - v.append(&mut Varbytes(val.3.clone()).into()); + v.append(&mut Varbytes::new(val.3.clone()).into()); v } } @@ -75,7 +105,8 @@ impl<'a> TryDecodeFrom<'a> for KeyShare { fn try_decode_from(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> { // try to decode the identifier - let (id, ptr) = Varuint::::try_decode_from(bytes)?; + let (id_bytes, ptr) = Varuint::<[u8; 32]>::try_decode_from(bytes)?; + let identifier = bytes_to_identifier(id_bytes.0.as_slice())?; // try to decode the threshold let (threshold, ptr) = Varuint::::try_decode_from(ptr)?; // try to decode the limit @@ -84,7 +115,7 @@ impl<'a> TryDecodeFrom<'a> for KeyShare { let (key_data, ptr) = Varbytes::try_decode_from(ptr)?; Ok(( Self( - id.to_inner(), + identifier, threshold.to_inner(), limit.to_inner(), key_data.to_inner(), @@ -95,7 +126,7 @@ impl<'a> TryDecodeFrom<'a> for KeyShare { } #[derive(Clone, Default)] -pub(crate) struct ThresholdData(pub(crate) BTreeMap); +pub(crate) struct ThresholdData(pub(crate) BTreeMap, KeyShare>); impl From for Vec { fn from(val: ThresholdData) -> Self { @@ -208,13 +239,13 @@ impl<'a> ThresholdAttrView for View<'a> { Ok(*Varuint::::try_from(v.as_slice())?) } /// get the share identifier for the multikey - fn identifier(&self) -> Result { + fn identifier(&self) -> Result<&[u8], Error> { let v = self .mk .attributes .get(&AttrId::ShareIdentifier) .ok_or(AttributesError::MissingShareIdentifier)?; - Ok(*Varuint::::try_from(v.as_slice())?) + Ok(v.as_slice()) } /// get the threshold data fn threshold_data(&self) -> Result<&[u8], Error> { @@ -374,23 +405,23 @@ impl<'a> ConvView for View<'a> { let av = self.mk.threshold_attr_view()?; let threshold = av.threshold()?; let limit = av.limit()?; - let identifier = av.identifier()?; + let identifier = bytes_to_identifier(av.identifier()?)?; + let value = bytes_to_value(secret_bytes.as_slice())?; - let secret_key: SecretKeyShare = SecretKeyShare( - Share::with_identifier_and_value(identifier, secret_bytes.as_slice()), - ); + let secret_key: SecretKeyShare = + SecretKeyShare(Share::with_identifier_and_value(identifier, value)); // get the public key and build a Multikey out of it let public_key = secret_key .public_key() .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; - let key_bytes = public_key.0 .0.value_vec(); + let key_bytes = public_key.0 .0.value().0.to_bytes().as_ref().to_vec(); Builder::new(Codec::Bls12381G1PubShare) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) .with_threshold(threshold) .with_limit(limit) - .with_identifier(identifier) + .with_identifier(&public_key.0 .0.identifier().0.to_be_bytes()) .try_build() } Codec::Bls12381G2Priv => { @@ -419,23 +450,23 @@ impl<'a> ConvView for View<'a> { let av = self.mk.threshold_attr_view()?; let threshold = av.threshold()?; let limit = av.limit()?; - let identifier = av.identifier()?; + let identifier = bytes_to_identifier(av.identifier()?)?; + let value = bytes_to_value(secret_bytes.as_slice())?; - let secret_key: SecretKeyShare = SecretKeyShare( - Share::with_identifier_and_value(identifier, secret_bytes.as_slice()), - ); + let secret_key: SecretKeyShare = + SecretKeyShare(Share::with_identifier_and_value(identifier, value)); // get the public key and build a Multikey out of it let public_key = secret_key .public_key() .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; - let key_bytes = public_key.0 .0.value_vec(); - Builder::new(Codec::Bls12381G1PubShare) + let key_bytes = public_key.0 .0.value().0.to_bytes().as_ref().to_vec(); + Builder::new(Codec::Bls12381G2PubShare) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) .with_threshold(threshold) .with_limit(limit) - .with_identifier(identifier) + .with_identifier(&public_key.0 .0.identifier().0.to_be_bytes()) .try_build() } _ => Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), @@ -467,7 +498,7 @@ impl<'a> ConvView for View<'a> { Codec::Bls12381G1PubShare => { let tav = pk.threshold_attr_view()?; let key_share: Vec = KeyShare( - tav.identifier()?, + bytes_to_identifier(tav.identifier()?)?, tav.threshold()?, tav.limit()?, key_bytes.to_vec(), @@ -487,7 +518,7 @@ impl<'a> ConvView for View<'a> { Codec::Bls12381G2PubShare => { let tav = pk.threshold_attr_view()?; let key_share: Vec = KeyShare( - tav.identifier()?, + bytes_to_identifier(tav.identifier()?)?, tav.threshold()?, tav.limit()?, key_bytes.to_vec(), @@ -547,7 +578,7 @@ impl<'a> ConvView for View<'a> { Codec::Bls12381G1PrivShare => { let sav = self.mk.threshold_attr_view()?; let secret_key_share: Vec = KeyShare( - sav.identifier()?, + bytes_to_identifier(sav.identifier()?)?, sav.threshold()?, sav.limit()?, secret_bytes.to_vec(), @@ -555,7 +586,7 @@ impl<'a> ConvView for View<'a> { .into(); let pav = pk.threshold_attr_view()?; let public_key_share: Vec = KeyShare( - pav.identifier()?, + bytes_to_identifier(pav.identifier()?)?, pav.threshold()?, pav.limit()?, key_bytes.to_vec(), @@ -581,7 +612,7 @@ impl<'a> ConvView for View<'a> { Codec::Bls12381G2PrivShare => { let sav = self.mk.threshold_attr_view()?; let secret_key_share: Vec = KeyShare( - sav.identifier()?, + bytes_to_identifier(sav.identifier()?)?, sav.threshold()?, sav.limit()?, secret_bytes.to_vec(), @@ -589,7 +620,7 @@ impl<'a> ConvView for View<'a> { .into(); let pav = pk.threshold_attr_view()?; let public_key_share: Vec = KeyShare( - pav.identifier()?, + bytes_to_identifier(pav.identifier()?)?, pav.threshold()?, pav.limit()?, key_bytes.to_vec(), @@ -681,11 +712,11 @@ impl<'a> SignView for View<'a> { let av = self.mk.threshold_attr_view()?; let threshold = av.threshold()?; let limit = av.limit()?; - let identifier = av.identifier()?; + let identifier = bytes_to_identifier(av.identifier()?)?; + let value = bytes_to_value(secret_bytes.as_slice())?; - let secret_key: SecretKeyShare = SecretKeyShare( - Share::with_identifier_and_value(identifier, secret_bytes.as_slice()), - ); + let secret_key: SecretKeyShare = + SecretKeyShare(Share::with_identifier_and_value(identifier, value)); // sign the data let signature = secret_key @@ -728,11 +759,11 @@ impl<'a> SignView for View<'a> { let av = self.mk.threshold_attr_view()?; let threshold = av.threshold()?; let limit = av.limit()?; - let identifier = av.identifier()?; + let identifier = bytes_to_identifier(av.identifier()?)?; + let value = bytes_to_value(secret_bytes.as_slice())?; - let secret_key: SecretKeyShare = SecretKeyShare( - Share::with_identifier_and_value(identifier, secret_bytes.as_slice()), - ); + let secret_key: SecretKeyShare = + SecretKeyShare(Share::with_identifier_and_value(identifier, value)); // sign the data let signature = secret_key @@ -793,15 +824,16 @@ impl<'a> ThresholdView for View<'a> { key_shares .iter() .try_for_each(|share| -> Result<(), Error> { - let key_bytes = share.as_raw_value().value_vec(); - let identifier = share.as_raw_value().identifier(); + let raw = share.as_raw_value(); + let key_bytes = raw.value().0.to_be_bytes(); + let identifier = raw.identifier().0.to_be_bytes(); let mk = Builder::new(Codec::Bls12381G1PrivShare) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) .with_threshold(threshold) .with_limit(limit) - .with_identifier(identifier) + .with_identifier(&identifier) .try_build()?; shares.push(mk); Ok(()) @@ -833,15 +865,16 @@ impl<'a> ThresholdView for View<'a> { key_shares .iter() .try_for_each(|share| -> Result<(), Error> { - let key_bytes = share.as_raw_value().value_vec(); - let identifier = share.as_raw_value().identifier(); + let raw = share.as_raw_value(); + let key_bytes = raw.value().0.to_be_bytes(); + let identifier = raw.identifier().0.to_be_bytes(); let mk = Builder::new(Codec::Bls12381G2PrivShare) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) .with_threshold(threshold) .with_limit(limit) - .with_identifier(identifier) + .with_identifier(&identifier) .try_build()?; shares.push(mk); Ok(()) @@ -871,7 +904,7 @@ impl<'a> ThresholdView for View<'a> { let (key_share, identifier, threshold, limit) = { // get the share attributes let av = share.threshold_attr_view()?; - let identifier = av.identifier()?; + let identifier = bytes_to_identifier(av.identifier()?)?; let threshold = av.threshold()?; let limit = av.limit()?; // get the key data @@ -943,13 +976,14 @@ impl<'a> ThresholdView for View<'a> { .0 .iter() .try_for_each(|(id, share)| -> Result<(), Error> { - let vsss = Share::with_identifier_and_value(*id, share.3.as_slice()); + let value = bytes_to_value(share.3.as_slice())?; + let vsss = Share::with_identifier_and_value(*id, value); shares.push(SecretKeyShare::(vsss)); Ok(()) })?; let key = SecretKey::combine(shares.as_slice()) .map_err(|e| ThresholdError::ShareCombineFailed(e.to_string()))?; - let key_bytes = key.to_be_bytes().as_ref().to_vec(); + let key_bytes = key.to_be_bytes().to_vec(); Builder::new(Codec::Bls12381G1Priv) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) @@ -961,13 +995,14 @@ impl<'a> ThresholdView for View<'a> { .0 .iter() .try_for_each(|(id, share)| -> Result<(), Error> { - let vsss = Share::with_identifier_and_value(*id, share.3.as_slice()); + let value = bytes_to_value(share.3.as_slice())?; + let vsss = Share::with_identifier_and_value(*id, value); shares.push(SecretKeyShare::(vsss)); Ok(()) })?; let key = SecretKey::combine(shares.as_slice()) .map_err(|e| ThresholdError::ShareCombineFailed(e.to_string()))?; - let key_bytes = key.to_be_bytes().as_ref().to_vec(); + let key_bytes = key.to_be_bytes().to_vec(); Builder::new(Codec::Bls12381G2Priv) .with_comment(&self.mk.comment) .with_key_bytes(&key_bytes) @@ -1045,19 +1080,43 @@ impl<'a> VerifyView for View<'a> { .map_err(|e| VerifyError::BadSignature(e.to_string()))?) } Codec::Bls12381G1PubShare => { - // build a blsful::PublicKeyShare from the bytes - let public_key = PublicKeyShare::try_from(&key_bytes) - .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + // Rebuild the blsful::PublicKeyShare from the stored public + // point + this key's own share identifier. `key_bytes` is the + // 96-byte compressed G2 public-key-share point (see + // `to_public_key` for Bls12381G1PrivShare). The single-share + // pairing verify below does not use the identifier, but blsful + // requires a well-formed share to verify against, so we + // reconstruct it rather than calling `PublicKeyShare::try_from` + // on a bare point (which expects the full share serialization). + let pk_bytes: [u8; 96] = key_bytes.as_slice().try_into().map_err(|_| { + ConversionsError::PublicKeyFailure("Invalid public key share bytes".to_string()) + })?; + let pk_point: G2Projective = Option::from(G2Projective::from_compressed(&pk_bytes)) + .ok_or(ConversionsError::PublicKeyFailure( + "Invalid public key share point".to_string(), + ))?; + let pk_identifier = { + let pav = pubmk.threshold_attr_view()?; + bytes_to_identifier(pav.identifier()?)? + }; + let public_key: PublicKeyShare = PublicKeyShare( + Share::with_identifier_and_value(pk_identifier, ValueGroup(pk_point)), + ); // get the share identifier let av = multisig.threshold_attr_view()?; - let identifier = av.identifier()?; + let identifier = bytes_to_identifier(av.identifier()?)?; // get the signature data let sv = multisig.data_view()?; let value = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; - - let share = Share::with_identifier_and_value(identifier, &value); + let bytes: [u8; 48] = value.as_slice().try_into().map_err(|_| { + VerifyError::BadSignature("Invalid signature share bytes".to_string()) + })?; + let point = Option::from(G1Projective::from_compressed(&bytes)).ok_or( + VerifyError::BadSignature("Invalid signature share bytes".to_string()), + )?; + let share = Share::with_identifier_and_value(identifier, ValueGroup(point)); let sig = match sig_scheme { SchemeTypeId::Basic => SignatureShare::::Basic(share), @@ -1125,27 +1184,49 @@ impl<'a> VerifyView for View<'a> { .map_err(|e| VerifyError::BadSignature(e.to_string()))?) } Codec::Bls12381G2PubShare => { - // build a blsful::PublicKeyShare from the bytes - let public_key = PublicKeyShare::try_from(&key_bytes) - .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + // Rebuild the blsful::PublicKeyShare from the stored public + // point + this key's own share identifier. `key_bytes` is the + // 48-byte compressed G1 public-key-share point (see + // `to_public_key` for Bls12381G2PrivShare). See the G1PubShare + // arm above for why we reconstruct rather than `try_from`. + let pk_bytes: [u8; 48] = key_bytes.as_slice().try_into().map_err(|_| { + ConversionsError::PublicKeyFailure("Invalid public key share bytes".to_string()) + })?; + let pk_point: G1Projective = Option::from(G1Projective::from_compressed(&pk_bytes)) + .ok_or(ConversionsError::PublicKeyFailure( + "Invalid public key share point".to_string(), + ))?; + let pk_identifier = { + let pav = pubmk.threshold_attr_view()?; + bytes_to_identifier(pav.identifier()?)? + }; + let public_key: PublicKeyShare = PublicKeyShare( + Share::with_identifier_and_value(pk_identifier, ValueGroup(pk_point)), + ); // get the share identifier let av = multisig.threshold_attr_view()?; - let identifier = av.identifier()?; + let identifier = bytes_to_identifier(av.identifier()?)?; // get the signature data let sv = multisig.data_view()?; let value = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; - - let share = Share::with_identifier_and_value(identifier, &value); + let bytes: [u8; 96] = value.as_slice().try_into().map_err(|_| { + VerifyError::BadSignature("Invalid signature share bytes".to_string()) + })?; + let point: G2Projective = Option::from(G2Projective::from_compressed(&bytes)) + .ok_or(VerifyError::BadSignature( + "Invalid signature share bytes".to_string(), + ))?; + let share = Share::with_identifier_and_value(identifier, ValueGroup(point)); let sig = match sig_scheme { - SchemeTypeId::Basic => SignatureShare::::Basic(share), + SchemeTypeId::Basic => SignatureShare::::Basic(share), SchemeTypeId::MessageAugmentation => { - SignatureShare::::MessageAugmentation(share) + SignatureShare::::MessageAugmentation(share) } SchemeTypeId::ProofOfPossession => { - SignatureShare::::ProofOfPossession(share) + SignatureShare::::ProofOfPossession(share) } }; @@ -1166,3 +1247,153 @@ impl<'a> VerifyView for View<'a> { } } } + +impl<'a> SealView for View<'a> { + /// Encrypt plaintext using BLS12-381 TimeCrypt encryption. + /// + /// ## How it works + /// + /// TimeCrypt is built on a BLS pairing-based KEM: + /// + /// 1. A fresh random 32-byte `id` is generated per message. This is the + /// **commitment**: whoever can produce `sign(sk, id)` (the "decryption + /// token") may recover the shared secret and decrypt. + /// 2. `encrypt_time_lock(scheme, plaintext, id)` embeds the commitment into + /// the ciphertext using the public key and a random blinding factor. + /// 3. The `id` is prepended to the sealed blob so the recipient knows what + /// to sign. + /// + /// ## Sealed blob format + /// + /// ```text + /// [id Varbytes] per-message random identifier (32 bytes) + /// [ciphertext bytes ] serde_bare-encoded TimeCryptCiphertext + /// ``` + /// + /// The `aead_codec` parameter is unused — TimeCrypt handles all encryption + /// internally via the BLS pairing. + fn seal( + &self, + plaintext: &[u8], + _aead_codec: Codec, + _aad: &[u8], + ) -> Result<(Vec, Option), Error> { + let attr = self.mk.attr_view()?; + if !attr.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + + let key_bytes = self.mk.data_view()?.key_bytes()?; + + // Generate a fresh per-message identifier. The recipient proves key + // ownership by signing this id; the resulting signature is the + // decryption token. Using a random id ensures each ciphertext is + // bound to a unique commitment, preventing token reuse across messages. + let mut id = [0u8; 32]; + rand::rng().fill_bytes(&mut id); + + match self.mk.codec { + Codec::Bls12381G1Pub => { + 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) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let mut out = Vec::new(); + out.append(&mut Varbytes::new(id.to_vec()).into()); + out.extend_from_slice(&Vec::from(&ct)); + Ok((out, None)) + } + Codec::Bls12381G2Pub => { + 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) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let mut out = Vec::new(); + out.append(&mut Varbytes::new(id.to_vec()).into()); + out.extend_from_slice(&Vec::from(&ct)); + Ok((out, None)) + } + _ => Err(SealError::NotEncapsulationKey.into()), + } + } +} + +impl<'a> OpenView for View<'a> { + /// Decrypt a BLS12-381 TimeCrypt sealed message. + /// + /// Parses the `id` from the sealed blob, signs it with the secret key to + /// produce the decryption token (`sign(sk, id)`), then uses that token to + /// recover the plaintext via the BLS pairing. + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + _aad: &[u8], + ) -> Result>, Error> { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let secret_bytes = self.mk.data_view()?.secret_bytes()?; + + // Parse the sealed blob: [id Varbytes][ciphertext bytes (remainder)] + let (id_vb, ct_bytes) = Varbytes::try_decode_from(sealed_msg) + .map_err(|_| SealError::InvalidFormat("missing TimeCrypt id".into()))?; + let id = id_vb.to_inner(); + + match self.mk.codec { + Codec::Bls12381G1Priv => { + let bytes: [u8; SECRET_KEY_BYTES] = secret_bytes.as_slice()[..SECRET_KEY_BYTES] + .try_into() + .map_err(|_| { + SealError::DecapsulationFailed("invalid BLS G1 secret key length".into()) + })?; + let secret_key: SecretKey = + Option::from(SecretKey::from_be_bytes(&bytes)).ok_or_else(|| { + SealError::DecapsulationFailed("invalid BLS G1 secret key bytes".into()) + })?; + let ct = TimeCryptCiphertext::::try_from(ct_bytes) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + // Produce the decryption token: sign(sk, id) + let sig = secret_key + .sign(ct.scheme, &id) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + ct.decrypt(&sig) + .into_option() + .map(Zeroizing::new) + .ok_or_else(|| { + SealError::DecapsulationFailed("BLS TimeCrypt decryption failed".into()) + .into() + }) + } + Codec::Bls12381G2Priv => { + let bytes: [u8; SECRET_KEY_BYTES] = secret_bytes.as_slice()[..SECRET_KEY_BYTES] + .try_into() + .map_err(|_| { + SealError::DecapsulationFailed("invalid BLS G2 secret key length".into()) + })?; + let secret_key: SecretKey = + Option::from(SecretKey::from_be_bytes(&bytes)).ok_or_else(|| { + SealError::DecapsulationFailed("invalid BLS G2 secret key bytes".into()) + })?; + let ct = TimeCryptCiphertext::::try_from(ct_bytes) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + // Produce the decryption token: sign(sk, id) + let sig = secret_key + .sign(ct.scheme, &id) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + ct.decrypt(&sig) + .into_option() + .map(Zeroizing::new) + .ok_or_else(|| { + SealError::DecapsulationFailed("BLS TimeCrypt decryption failed".into()) + .into() + }) + } + _ => Err(SealError::NotDecapsulationKey.into()), + } + } +} diff --git a/src/views/bls12381_g1_fndsa512.rs b/src/views/bls12381_g1_fndsa512.rs new file mode 100644 index 0000000..4f727e0 --- /dev/null +++ b/src/views/bls12381_g1_fndsa512.rs @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 +//! BLS12-381-G1-FN-DSA-512 hybrid signing multikey view (Birds-of-Prey-1); +//! combines BLS12-381 G1 with FN-DSA-512 (Falcon) post-quantum signatures. +//! Sign: s1 = BLS_G1(m), s2 = FnDsa512(m || s1), sig = s1 || s2 +//! Verify: verify BLS_G1(m, s1) && verify FnDsa512(m || s1, s2) +//! Secret layout: bls_g1_secret (32) || fn_dsa_signing_key. +//! Public encoding is classical-first: bls_g1_pub (96) || fn_dsa_verifying_key. + +use super::bls12381_hybrid as bls; +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use fn_dsa::{ + sign_key_size, signature_size, vrfy_key_size, SigningKey as _, SigningKeyStandard, + VerifyingKey as _, VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, +}; +use zeroize::Zeroizing; + +const LOGN: u32 = FN_DSA_LOGN_512; + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Fndsa512Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Fndsa512Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != bls::BLS_G1_SECRET_LEN + sign_key_size(LOGN) { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid secret length".into()).into(), + ); + } + + // BLS-G1 public key + let bls_pub = bls::public_from_secret(&secret_bytes[..bls::BLS_G1_SECRET_LEN])?; + + // FN-DSA verifying key + let fndsa_sk = SigningKeyStandard::decode(&secret_bytes[bls::BLS_G1_SECRET_LEN..]).ok_or( + ConversionsError::SecretKeyFailure("invalid fn-dsa signing key".into()), + )?; + let mut fndsa_vk = vec![0u8; vrfy_key_size(LOGN)]; + fndsa_sk.to_verifying_key(&mut fndsa_vk); + + // Concatenate (classical-first): bls_g1_pub (96) || fndsa_vk + let mut pub_bytes = Vec::with_capacity(bls::BLS_G1_PUB_LEN + fndsa_vk.len()); + pub_bytes.extend_from_slice(&bls_pub); + pub_bytes.extend_from_slice(&fndsa_vk); + + Builder::new(Codec::Bls12381G1Fndsa512Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-FN-DSA-512 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-FN-DSA-512 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() != bls::BLS_G1_SECRET_LEN + sign_key_size(LOGN) { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid secret length".into()).into(), + ); + } + + // Step 1: BLS-G1 sign + let s1 = bls::sign(&secret_bytes[..bls::BLS_G1_SECRET_LEN], msg)?; + + // Step 2: FN-DSA-512 sign(m || s1) + let mut fndsa_sk = SigningKeyStandard::decode(&secret_bytes[bls::BLS_G1_SECRET_LEN..]) + .ok_or(ConversionsError::SecretKeyFailure( + "invalid fn-dsa signing key".into(), + ))?; + + let mut m2 = Vec::with_capacity(msg.len() + s1.len()); + m2.extend_from_slice(msg); + m2.extend_from_slice(&s1); + + let mut s2 = vec![0u8; signature_size(LOGN)]; + fndsa_sk.sign( + &mut rand_core_06::OsRng, + &DOMAIN_NONE, + &HASH_ID_RAW, + &m2, + &mut s2, + ); + + // Step 3: sig = s1 || s2 + let mut sig_bytes = Vec::with_capacity(s1.len() + s2.len()); + sig_bytes.extend_from_slice(&s1); + sig_bytes.extend_from_slice(&s2); + + let mut ms = + ms::Builder::new(Codec::Bls12381G1Fndsa512Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + if key_bytes.len() != bls::BLS_G1_PUB_LEN + vrfy_key_size(LOGN) { + return Err(ConversionsError::PublicKeyFailure( + "invalid hybrid public key length".into(), + ) + .into()); + } + + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + if sig_bytes.len() != bls::BLS_G1_SIG_LEN + signature_size(LOGN) { + return Err(VerifyError::BadSignature("invalid hybrid signature length".into()).into()); + } + + let bls_pub_bytes = &key_bytes[..bls::BLS_G1_PUB_LEN]; + let fndsa_vk_bytes = &key_bytes[bls::BLS_G1_PUB_LEN..]; + + let s1_bytes = &sig_bytes[..bls::BLS_G1_SIG_LEN]; + let s2_bytes = &sig_bytes[bls::BLS_G1_SIG_LEN..]; + + // Verify BLS-G1 + bls::verify(bls_pub_bytes, s1_bytes, msg_bytes)?; + + // Verify FN-DSA-512: verify(m || s1, s2) + let fndsa_vk = VerifyingKeyStandard::decode(fndsa_vk_bytes).ok_or( + ConversionsError::PublicKeyFailure("invalid fn-dsa verifying key".into()), + )?; + + let mut m2 = Vec::with_capacity(msg_bytes.len() + s1_bytes.len()); + m2.extend_from_slice(msg_bytes); + m2.extend_from_slice(s1_bytes); + + if fndsa_vk.verify(s2_bytes, &DOMAIN_NONE, &HASH_ID_RAW, &m2) { + Ok(()) + } else { + Err(VerifyError::BadSignature("FN-DSA-512 verify failed".into()).into()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_sign_verify_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Bls12381G1Fndsa512Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello BLS12-381-G1-FN-DSA-512 hybrid (Birds of Prey 1)!"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err()); + } +} diff --git a/src/views/bls12381_g1_mayo1.rs b/src/views/bls12381_g1_mayo1.rs new file mode 100644 index 0000000..50aca57 --- /dev/null +++ b/src/views/bls12381_g1_mayo1.rs @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: Apache-2.0 +//! BLS12-381-G1-MAYO-1 hybrid signing multikey view (Birds-of-Prey-1); +//! combines BLS12-381 G1 with MAYO-1 post-quantum multivariate signatures. +//! Sign: s1 = BLS_G1(m), s2 = Mayo1(m || s1), sig = s1 || s2 +//! Verify: verify BLS_G1(m, s1) && verify Mayo1(m || s1, s2) +//! Secret layout: bls_g1_secret (32) || mayo1_seed (24) = 56 bytes. +//! Public encoding is classical-first: bls_g1_pub (96) || mayo1_pub. + +use super::bls12381_hybrid as bls; +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use ml_dsa::signature::{Signer, Verifier}; +use pq_mayo::{KeyPair, Mayo1, Signature, VerifyingKey}; +use zeroize::Zeroizing; + +const MAYO1_SEED_LEN: usize = 24; +const PRIV_SEED_LEN: usize = bls::BLS_G1_SECRET_LEN + MAYO1_SEED_LEN; // 56 +const MAYO1_PUB_LEN: usize = 1420; +const PUB_KEY_LEN: usize = bls::BLS_G1_PUB_LEN + MAYO1_PUB_LEN; // 1516 +const MAYO1_SIG_LEN: usize = 454; +const HYBRID_SIG_LEN: usize = bls::BLS_G1_SIG_LEN + MAYO1_SIG_LEN; // 502 + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Mayo1Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Mayo1Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + let bls_pub = bls::public_from_secret(&secret_bytes[..bls::BLS_G1_SECRET_LEN])?; + + let kp = KeyPair::::from_seed(&secret_bytes[bls::BLS_G1_SECRET_LEN..]) + .map_err(|e| ConversionsError::SecretKeyFailure(format!("MAYO-1 seed error: {}", e)))?; + + let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); + pub_bytes.extend_from_slice(&bls_pub); + pub_bytes.extend_from_slice(kp.verifying_key().as_ref()); + + Builder::new(Codec::Bls12381G1Mayo1Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-MAYO-1 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-MAYO-1 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // Step 1: BLS-G1 sign + let s1 = bls::sign(&secret_bytes[..bls::BLS_G1_SECRET_LEN], msg)?; + + // Step 2: MAYO-1 sign(m || s1) + let kp = KeyPair::::from_seed(&secret_bytes[bls::BLS_G1_SECRET_LEN..]) + .map_err(|e| ConversionsError::SecretKeyFailure(format!("MAYO-1 seed error: {}", e)))?; + + let mut m2 = Vec::with_capacity(msg.len() + s1.len()); + m2.extend_from_slice(msg); + m2.extend_from_slice(&s1); + let s2 = kp + .signing_key() + .try_sign(&m2) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + + // Step 3: sig = s1 || s2 + let mut sig_bytes = Vec::with_capacity(HYBRID_SIG_LEN); + sig_bytes.extend_from_slice(&s1); + sig_bytes.extend_from_slice(s2.as_ref()); + + let mut ms = ms::Builder::new(Codec::Bls12381G1Mayo1Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + if key_bytes.len() != PUB_KEY_LEN { + return Err(ConversionsError::PublicKeyFailure( + "invalid hybrid public key length".into(), + ) + .into()); + } + + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + if sig_bytes.len() != HYBRID_SIG_LEN { + return Err(VerifyError::BadSignature("invalid hybrid signature length".into()).into()); + } + + let bls_pub_bytes = &key_bytes[..bls::BLS_G1_PUB_LEN]; + let mayo_pub_bytes = &key_bytes[bls::BLS_G1_PUB_LEN..PUB_KEY_LEN]; + let s1_bytes = &sig_bytes[..bls::BLS_G1_SIG_LEN]; + let s2_bytes = &sig_bytes[bls::BLS_G1_SIG_LEN..HYBRID_SIG_LEN]; + + // Verify BLS-G1 + bls::verify(bls_pub_bytes, s1_bytes, msg_bytes)?; + + // Verify MAYO-1: verify(m || s1, s2) + let vk = VerifyingKey::::try_from(mayo_pub_bytes) + .map_err(|_| ConversionsError::PublicKeyFailure("invalid MAYO-1 public key".into()))?; + let signature = Signature::::try_from(s2_bytes) + .map_err(|_| VerifyError::BadSignature("invalid MAYO-1 signature".into()))?; + + let mut m2 = Vec::with_capacity(msg_bytes.len() + s1_bytes.len()); + m2.extend_from_slice(msg_bytes); + m2.extend_from_slice(s1_bytes); + + vk.verify(&m2, &signature) + .map_err(|e| VerifyError::BadSignature(format!("MAYO-1 verify failed: {}", e)))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_sign_verify_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Bls12381G1Mayo1Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello BLS12-381-G1-MAYO-1 hybrid (Birds of Prey 1)!"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err()); + } +} diff --git a/src/views/bls12381_g1_mayo2.rs b/src/views/bls12381_g1_mayo2.rs new file mode 100644 index 0000000..b1a985f --- /dev/null +++ b/src/views/bls12381_g1_mayo2.rs @@ -0,0 +1,257 @@ +// SPDX-License-Identifier: Apache-2.0 +//! BLS12-381-G1-MAYO-2 hybrid signing multikey view (Birds-of-Prey-1); +//! combines BLS12-381 G1 with MAYO-2 post-quantum multivariate signatures. +//! Sign: s1 = BLS_G1(m), s2 = Mayo2(m || s1), sig = s1 || s2 +//! Verify: verify BLS_G1(m, s1) && verify Mayo2(m || s1, s2) +//! Secret layout: bls_g1_secret (32) || mayo2_seed (24) = 56 bytes. +//! Public encoding is classical-first: bls_g1_pub (96) || mayo2_pub. + +use super::bls12381_hybrid as bls; +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use ml_dsa::signature::{Signer, Verifier}; +use pq_mayo::{KeyPair, Mayo2, Signature, VerifyingKey}; +use zeroize::Zeroizing; + +const MAYO2_SEED_LEN: usize = 24; +const PRIV_SEED_LEN: usize = bls::BLS_G1_SECRET_LEN + MAYO2_SEED_LEN; // 56 +const MAYO2_PUB_LEN: usize = 4368; +const PUB_KEY_LEN: usize = bls::BLS_G1_PUB_LEN + MAYO2_PUB_LEN; // 4464 +const MAYO2_SIG_LEN: usize = 216; +const HYBRID_SIG_LEN: usize = bls::BLS_G1_SIG_LEN + MAYO2_SIG_LEN; // 264 + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Mayo2Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Mayo2Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + let bls_pub = bls::public_from_secret(&secret_bytes[..bls::BLS_G1_SECRET_LEN])?; + + let kp = KeyPair::::from_seed(&secret_bytes[bls::BLS_G1_SECRET_LEN..]) + .map_err(|e| ConversionsError::SecretKeyFailure(format!("MAYO-2 seed error: {}", e)))?; + + let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); + pub_bytes.extend_from_slice(&bls_pub); + pub_bytes.extend_from_slice(kp.verifying_key().as_ref()); + + Builder::new(Codec::Bls12381G1Mayo2Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-MAYO-2 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-MAYO-2 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // Step 1: BLS-G1 sign + let s1 = bls::sign(&secret_bytes[..bls::BLS_G1_SECRET_LEN], msg)?; + + // Step 2: MAYO-2 sign(m || s1) + let kp = KeyPair::::from_seed(&secret_bytes[bls::BLS_G1_SECRET_LEN..]) + .map_err(|e| ConversionsError::SecretKeyFailure(format!("MAYO-2 seed error: {}", e)))?; + + let mut m2 = Vec::with_capacity(msg.len() + s1.len()); + m2.extend_from_slice(msg); + m2.extend_from_slice(&s1); + let s2 = kp + .signing_key() + .try_sign(&m2) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + + // Step 3: sig = s1 || s2 + let mut sig_bytes = Vec::with_capacity(HYBRID_SIG_LEN); + sig_bytes.extend_from_slice(&s1); + sig_bytes.extend_from_slice(s2.as_ref()); + + let mut ms = ms::Builder::new(Codec::Bls12381G1Mayo2Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + if key_bytes.len() != PUB_KEY_LEN { + return Err(ConversionsError::PublicKeyFailure( + "invalid hybrid public key length".into(), + ) + .into()); + } + + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + if sig_bytes.len() != HYBRID_SIG_LEN { + return Err(VerifyError::BadSignature("invalid hybrid signature length".into()).into()); + } + + let bls_pub_bytes = &key_bytes[..bls::BLS_G1_PUB_LEN]; + let mayo_pub_bytes = &key_bytes[bls::BLS_G1_PUB_LEN..PUB_KEY_LEN]; + let s1_bytes = &sig_bytes[..bls::BLS_G1_SIG_LEN]; + let s2_bytes = &sig_bytes[bls::BLS_G1_SIG_LEN..HYBRID_SIG_LEN]; + + // Verify BLS-G1 + bls::verify(bls_pub_bytes, s1_bytes, msg_bytes)?; + + // Verify MAYO-2: verify(m || s1, s2) + let vk = VerifyingKey::::try_from(mayo_pub_bytes) + .map_err(|_| ConversionsError::PublicKeyFailure("invalid MAYO-2 public key".into()))?; + let signature = Signature::::try_from(s2_bytes) + .map_err(|_| VerifyError::BadSignature("invalid MAYO-2 signature".into()))?; + + let mut m2 = Vec::with_capacity(msg_bytes.len() + s1_bytes.len()); + m2.extend_from_slice(msg_bytes); + m2.extend_from_slice(s1_bytes); + + vk.verify(&m2, &signature) + .map_err(|e| VerifyError::BadSignature(format!("MAYO-2 verify failed: {}", e)))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_sign_verify_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Bls12381G1Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello BLS12-381-G1-MAYO-2 hybrid (Birds of Prey 1)!"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err()); + } +} diff --git a/src/views/bls12381_g1_mldsa65.rs b/src/views/bls12381_g1_mldsa65.rs new file mode 100644 index 0000000..8598553 --- /dev/null +++ b/src/views/bls12381_g1_mldsa65.rs @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 +//! BLS12-381-G1-ML-DSA-65 hybrid signing multikey view (Birds-of-Prey-1); +//! combines BLS12-381 G1 with ML-DSA-65 post-quantum signatures. +//! Sign: s1 = BLS_G1(m), s2 = MlDsa65(m || s1), sig = s1 || s2 +//! Verify: verify BLS_G1(m, s1) && verify MlDsa65(m || s1, s2) +//! Public encoding is classical-first: bls_g1_pub (96) || mldsa65_pub. + +use super::bls12381_hybrid as bls; +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use ml_dsa::{ + signature::{Keypair, Signer as MlDsaSigner, Verifier as MlDsaVerifier}, + EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MlDsaSeed, Signature as MlDsaSig, + SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, +}; +use zeroize::Zeroizing; + +const MLDSA65_SEED_LEN: usize = 32; +const PRIV_SEED_LEN: usize = bls::BLS_G1_SECRET_LEN + MLDSA65_SEED_LEN; // 64 + +const MLDSA65_PUB_LEN: usize = 1952; +const PUB_KEY_LEN: usize = bls::BLS_G1_PUB_LEN + MLDSA65_PUB_LEN; // 2048 + +const MLDSA65_SIG_LEN: usize = 3309; +const HYBRID_SIG_LEN: usize = bls::BLS_G1_SIG_LEN + MLDSA65_SIG_LEN; // 3357 + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Mldsa65Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::Bls12381G1Mldsa65Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // BLS-G1 public key + let bls_pub = bls::public_from_secret(&secret_bytes[..bls::BLS_G1_SECRET_LEN])?; + + // ML-DSA-65 public key + let mldsa_seed: [u8; 32] = secret_bytes[bls::BLS_G1_SECRET_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-dsa-65 seed".into()))?; + let mldsa_seed = MlDsaSeed::from(mldsa_seed); + let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + + // Concatenate (classical-first): bls_g1_pub (96) || mldsa65_pub (1952) + let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); + pub_bytes.extend_from_slice(&bls_pub); + pub_bytes.extend_from_slice(kp.verifying_key().encode().as_slice()); + + Builder::new(Codec::Bls12381G1Mldsa65Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-ML-DSA-65 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "BLS12-381-G1-ML-DSA-65 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // Step 1: BLS-G1 sign + let s1 = bls::sign(&secret_bytes[..bls::BLS_G1_SECRET_LEN], msg)?; + + // Step 2: ML-DSA-65 sign(m || s1) + let mldsa_seed: [u8; 32] = secret_bytes[bls::BLS_G1_SECRET_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-dsa-65 seed".into()))?; + let mldsa_seed = MlDsaSeed::from(mldsa_seed); + let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + + let mut m2 = Vec::with_capacity(msg.len() + s1.len()); + m2.extend_from_slice(msg); + m2.extend_from_slice(&s1); + let s2 = MlDsaSigner::sign(&kp, &m2); + + // Step 3: sig = s1 || s2 + let mut sig_bytes = Vec::with_capacity(HYBRID_SIG_LEN); + sig_bytes.extend_from_slice(&s1); + sig_bytes.extend_from_slice(s2.encode().as_slice()); + + let mut ms = + ms::Builder::new(Codec::Bls12381G1Mldsa65Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + if key_bytes.len() != PUB_KEY_LEN { + return Err(ConversionsError::PublicKeyFailure( + "invalid hybrid public key length".into(), + ) + .into()); + } + + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + if sig_bytes.len() != HYBRID_SIG_LEN { + return Err(VerifyError::BadSignature("invalid hybrid signature length".into()).into()); + } + + // Split public key + let bls_pub_bytes = &key_bytes[..bls::BLS_G1_PUB_LEN]; + let mldsa_pub_bytes = &key_bytes[bls::BLS_G1_PUB_LEN..PUB_KEY_LEN]; + + // Split signature + let s1_bytes = &sig_bytes[..bls::BLS_G1_SIG_LEN]; + let s2_bytes = &sig_bytes[bls::BLS_G1_SIG_LEN..HYBRID_SIG_LEN]; + + // Verify BLS-G1: verify(m, s1) + bls::verify(bls_pub_bytes, s1_bytes, msg_bytes)?; + + // Verify ML-DSA-65: verify(m || s1, s2) + let encoded_vk = + EncodedVerifyingKey::::try_from(mldsa_pub_bytes).map_err(|_| { + ConversionsError::PublicKeyFailure("invalid ML-DSA-65 public key".into()) + })?; + let mldsa_vk = MlDsaVerifyingKey::::decode(&encoded_vk); + let encoded_sig = EncodedSignature::::try_from(s2_bytes) + .map_err(|_| VerifyError::BadSignature("invalid ML-DSA-65 signature".into()))?; + let s2 = MlDsaSig::decode(&encoded_sig).ok_or(VerifyError::BadSignature( + "invalid ML-DSA-65 signature".into(), + ))?; + + let mut m2 = Vec::with_capacity(msg_bytes.len() + s1_bytes.len()); + m2.extend_from_slice(msg_bytes); + m2.extend_from_slice(s1_bytes); + + mldsa_vk + .verify(&m2, &s2) + .map_err(|e| VerifyError::BadSignature(format!("ML-DSA-65 verify failed: {}", e)))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_sign_verify_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Bls12381G1Mldsa65Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello BLS12-381-G1-ML-DSA-65 hybrid (Birds of Prey 1)!"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + // wrong message must fail + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err()); + } + + #[test] + fn test_public_key_length() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Bls12381G1Mldsa65Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + assert_eq!( + pk.data_view().unwrap().key_bytes().unwrap().len(), + PUB_KEY_LEN + ); + } +} diff --git a/src/views/bls12381_hybrid.rs b/src/views/bls12381_hybrid.rs new file mode 100644 index 0000000..3878b3f --- /dev/null +++ b/src/views/bls12381_hybrid.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Shared BLS12-381 G1 helpers for Birds-of-Prey-1 hybrid signatures. +//! +//! BLS-G1 (blsful `Bls12381G1Impl`, "minimal-signature-size"): secret = 32 bytes, +//! public key = 96 bytes (G2 point), signature = 48 bytes (G1 point). The hybrid +//! schemes use a fixed `Basic` signature scheme. + +use crate::error::{ConversionsError, SignError, VerifyError}; +use crate::Error; +use blsful::inner_types::{G1Projective, GroupEncoding}; +use blsful::{Bls12381G1Impl, PublicKey, SecretKey, Signature, SignatureSchemes}; + +pub(crate) const BLS_G1_SECRET_LEN: usize = 32; +pub(crate) const BLS_G1_PUB_LEN: usize = 96; +pub(crate) const BLS_G1_SIG_LEN: usize = 48; + +fn secret_from_bytes(secret: &[u8]) -> Result, Error> { + let arr: [u8; BLS_G1_SECRET_LEN] = secret + .get(..BLS_G1_SECRET_LEN) + .and_then(|s| s.try_into().ok()) + .ok_or_else(|| ConversionsError::SecretKeyFailure("invalid BLS-G1 secret".to_string()))?; + Option::from(SecretKey::from_be_bytes(&arr)).ok_or_else(|| { + ConversionsError::SecretKeyFailure("invalid BLS-G1 secret".to_string()).into() + }) +} + +/// Derive the 96-byte BLS-G1 public key (G2 point) from the secret. +pub(crate) fn public_from_secret(secret: &[u8]) -> Result, Error> { + let sk = secret_from_bytes(secret)?; + Ok(sk.public_key().0.to_bytes().as_ref().to_vec()) +} + +/// Sign `msg` with the BLS-G1 secret, returning the 48-byte signature. +pub(crate) fn sign(secret: &[u8], msg: &[u8]) -> Result, Error> { + let sk = secret_from_bytes(secret)?; + let sig = sk + .sign(SignatureSchemes::Basic, msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + Ok(sig.as_raw_value().to_bytes().as_ref().to_vec()) +} + +/// Verify a 48-byte BLS-G1 signature against the 96-byte public key. +pub(crate) fn verify(public: &[u8], sig_bytes: &[u8], msg: &[u8]) -> Result<(), Error> { + let pk = PublicKey::::try_from(&public.to_vec()) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let arr: [u8; BLS_G1_SIG_LEN] = sig_bytes + .try_into() + .map_err(|_| VerifyError::BadSignature("invalid BLS-G1 signature length".to_string()))?; + let group = Option::from(G1Projective::from_compressed(&arr)) + .ok_or_else(|| VerifyError::BadSignature("invalid BLS-G1 signature point".to_string()))?; + let sig = Signature::::Basic(group); + sig.verify(&pk, msg) + .map_err(|e| VerifyError::BadSignature(format!("BLS-G1 verify failed: {}", e)).into()) +} diff --git a/src/views/chacha20.rs b/src/views/chacha20.rs index c148e56..66c8a6a 100644 --- a/src/views/chacha20.rs +++ b/src/views/chacha20.rs @@ -1,15 +1,15 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::{AttributesError, CipherError, KdfError}, AttrId, AttrView, CipherAttrView, CipherView, DataView, Error, FingerprintView, KdfAttrView, Multikey, Views, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varuint; use chacha20::cipher::{KeyIvInit, StreamCipher}; use chacha20::{ChaCha20, Nonce}; -use multicodec::Codec; -use multihash::{mh, Multihash}; -use multitrait::TryDecodeFrom; -use multiutil::Varuint; use zeroize::Zeroizing; use super::bcrypt::SALT_LENGTH; @@ -17,7 +17,7 @@ use super::bcrypt::SALT_LENGTH; pub const KEY_SIZE: usize = poly1305::KEY_SIZE; /// Return the length of the [Nonce] -#[allow(dead_code)] +#[cfg(test)] pub(crate) fn nonce_length() -> usize { Nonce::default().len() } @@ -92,6 +92,7 @@ impl<'a> CipherAttrView for View<'a> { Ok(Codec::Chacha20Poly1305) } + #[allow(deprecated)] fn nonce_bytes(&self) -> Result>, Error> { // try to look up the salt in the multikey attributes let nonce = self @@ -100,8 +101,7 @@ impl<'a> CipherAttrView for View<'a> { .get(&AttrId::CipherNonce) .ok_or(CipherError::MissingNonce)?; - let nonce = - Nonce::from_exact_iter(nonce.iter().copied()).ok_or(CipherError::InvalidNonce)?; + let nonce = Nonce::clone_from_slice(nonce); Ok(nonce.to_vec().into()) } @@ -148,6 +148,7 @@ impl<'a> KdfAttrView for View<'a> { } impl<'a> CipherView for View<'a> { + #[allow(deprecated)] fn decrypt(&self) -> Result { let cipher = self.cipher.ok_or(CipherError::MissingCodec)?; // make sure the viewed key is an encrypted secret key @@ -163,7 +164,7 @@ impl<'a> CipherView for View<'a> { }; // create the chacha nonce from the data - let n = Nonce::from_exact_iter(nonce.iter().copied()).ok_or(CipherError::InvalidNonce)?; + let n = Nonce::clone_from_slice(&nonce); // get the key data from the passed-in Multikey let key = { @@ -176,8 +177,7 @@ impl<'a> CipherView for View<'a> { }; // create the chacha key from the data - let k = - chacha20::Key::from_exact_iter(key.iter().copied()).ok_or(CipherError::InvalidKey)?; + let k = chacha20::Key::clone_from_slice(&key); // get the encrypted key bytes from the viewed Multikey (self) let msg = { @@ -185,15 +185,33 @@ impl<'a> CipherView for View<'a> { attr.key_bytes()? }; - // // decrypt the key bytes - // let dec = chacha20poly1305::open(msg.as_slice(), None, &n, &k) - // .map_err(|_| CipherError::DecryptionFailed)?; - - let mut chacha = ChaCha20::new(&k, &n); - - let mut dec = msg.clone(); - - chacha.apply_keystream(&mut dec); + // Authenticated decryption (ChaCha20Poly1305): the stored ciphertext is + // `plaintext || 16-byte Poly1305 tag`, so tampering or a wrong passphrase + // is rejected rather than silently yielding garbage key bytes. + // + // Legacy fallback: keys encrypted before authentication was added used + // bare ChaCha20 (no tag). If AEAD verification fails we retry the legacy + // path so existing keystores keep opening; re-encrypting a key upgrades + // it to the authenticated format. + let dec = { + use chacha20poly1305::aead::{Aead, KeyInit}; + use chacha20poly1305::{ChaCha20Poly1305, Nonce as AeadNonce}; + let opened = ChaCha20Poly1305::new_from_slice(&key) + .ok() + .and_then(|aead| { + aead.decrypt(AeadNonce::from_slice(&nonce), msg.as_slice()) + .ok() + }); + match opened { + Some(pt) => Zeroizing::new(pt), + None => { + let mut chacha = ChaCha20::new(&k, &n); + let mut legacy = msg.clone(); + chacha.apply_keystream(&mut legacy); + legacy + } + } + }; // create a new Multikey from the viewed Multikey (self) with the // decrypted key and none of the kdf or cipher attributes @@ -209,6 +227,7 @@ impl<'a> CipherView for View<'a> { Ok(res) } + #[allow(deprecated)] fn encrypt(&self) -> Result { let cipher = self.cipher.ok_or(CipherError::MissingCodec)?; // make sure the viewed key is not encrypted @@ -225,8 +244,6 @@ impl<'a> CipherView for View<'a> { cattr.nonce_bytes()? }; - let n = Nonce::from_exact_iter(nonce.iter().copied()).ok_or(CipherError::InvalidNonce)?; - // get the key data from the passed-in Multikey let key = { let kd = cipher.data_view()?; @@ -237,21 +254,26 @@ impl<'a> CipherView for View<'a> { key }; - let k = - chacha20::Key::from_exact_iter(key.iter().copied()).ok_or(CipherError::InvalidKey)?; - // get the secret bytes from the viewed Multikey let msg = { let kd = self.mk.data_view()?; kd.secret_bytes()? }; - let mut chacha = ChaCha20::new(&k, &n); - - let mut enc = msg.clone(); - - // apply keystream (encrypt) - chacha.apply_keystream(&mut enc); + // Authenticated encryption (ChaCha20Poly1305): output is + // `ciphertext || 16-byte Poly1305 tag` so integrity is verified on + // decrypt. Same 32-byte key / 12-byte nonce as the bare cipher. + let enc = { + use chacha20poly1305::aead::{Aead, KeyInit}; + use chacha20poly1305::{ChaCha20Poly1305, Nonce as AeadNonce}; + let aead = ChaCha20Poly1305::new_from_slice(&key).map_err(|_| { + CipherError::EncryptionFailed("invalid cipher key length".to_string()) + })?; + let ct = aead + .encrypt(AeadNonce::from_slice(&nonce), msg.as_slice()) + .map_err(|_| CipherError::EncryptionFailed("AEAD seal failed".to_string()))?; + Zeroizing::new(ct) + }; // prepare the cipher attributes let cattr = cipher.cipher_attr_view()?; diff --git a/src/views/classic_mceliece.rs b/src/views/classic_mceliece.rs new file mode 100644 index 0000000..e3173f0 --- /dev/null +++ b/src/views/classic_mceliece.rs @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Classic McEliece multikey view; post-quantum KEM (348864 variant). +//! +//! 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. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use rand_chacha::ChaCha20Rng; +use rand_core_06::SeedableRng; +use zeroize::Zeroizing; + +const MCELIECE_SEED_LENGTH: usize = 32; + +/// Decoded sealed message: (kem_ct, aead_codec, nonce, ciphertext+tag) +type SealedParts = (Vec, Codec, Vec, Vec); + +fn is_mceliece_priv(codec: Codec) -> bool { + codec == Codec::Mceliece348864Priv +} + +fn is_mceliece_pub(codec: Codec) -> bool { + codec == Codec::Mceliece348864Pub +} + +/// Classic McEliece AEAD codecs allowed (256-bit symmetric for PQ safety) +fn is_mceliece_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::Xchacha20Poly1305 | Codec::Chacha20Poly1305 | Codec::AesGcm256 + ) +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_mceliece_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_mceliece_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != MCELIECE_SEED_LENGTH { + return Err(ConversionsError::SecretKeyFailure("invalid seed length".into()).into()); + } + + let seed: [u8; 32] = secret_bytes[..32] + .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(); + + Builder::new(Codec::Mceliece348864Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "classic mceliece not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "classic mceliece not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// 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) + .map_err(|_| SealError::InvalidFormat("missing kem ciphertext".into()))?; + let (aead_codec, ptr) = Codec::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing aead codec".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + kem_ct.to_inner(), + aead_codec, + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_mceliece_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + 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(); + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(shared_secret, b"mceliece-seal", key_len)?; + + // 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)) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (kem_ct, aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if !is_mceliece_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != MCELIECE_SEED_LENGTH { + return Err(ConversionsError::SecretKeyFailure("invalid seed length".into()).into()); + } + + let seed: [u8; 32] = secret_bytes[..32] + .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(); + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(shared_secret, b"mceliece-seal", key_len)?; + + // Decrypt ciphertext + Ok(aead::aead_open( + aead_codec, &aead_key, &nonce, &ct_tag, aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk::MCELIECE_KEY_CODECS; + use crate::views::Views; + + #[test] + fn test_mceliece_key_gen_roundtrip() { + for codec in MCELIECE_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test kem key") + .try_build() + .unwrap(); + + let attr = mk.attr_view().unwrap(); + assert!(attr.is_secret_key()); + assert!(!attr.is_public_key()); + + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + // serialize/deserialize roundtrip + let bytes: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } + } + + #[test] + fn test_mceliece_public_key_derivation() { + for codec in MCELIECE_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let conv = mk.conv_view().unwrap(); + let pk = conv.to_public_key().unwrap(); + + let attr = pk.attr_view().unwrap(); + assert!(attr.is_public_key()); + assert!(!attr.is_secret_key()); + + // derive again and check same result + let pk2 = conv.to_public_key().unwrap(); + assert_eq!(pk, pk2); + } + } + + #[test] + fn test_mceliece_fingerprint() { + for codec in MCELIECE_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let fp = pk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + let fp_bytes: Vec = fp.into(); + assert!(!fp_bytes.is_empty()); + } + } + + #[test] + fn test_mceliece_seal_open_roundtrip() { + let aead_codecs = [ + Codec::Xchacha20Poly1305, + Codec::Chacha20Poly1305, + Codec::AesGcm256, + ]; + + for key_codec in MCELIECE_KEY_CODECS { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(key_codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + for aead_codec in &aead_codecs { + let plaintext = b"hello classic mceliece world!"; + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(plaintext, *aead_codec, b"") + .unwrap(); + + let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + } + } + + #[test] + fn test_mceliece_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::Mceliece348864Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let sk2 = Builder::new_from_random_bytes(Codec::Mceliece348864Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let (sealed, _) = pk1 + .seal_view() + .unwrap() + .seal(b"secret data", Codec::Xchacha20Poly1305, b"") + .unwrap(); + + // Opening with wrong key should fail + assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_mceliece_seal_requires_public_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Mceliece348864Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + // seal with private key should fail + assert!(sk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err()); + } + + #[test] + fn test_mceliece_open_requires_private_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Mceliece348864Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .unwrap(); + + // open with public key should fail + assert!(pk.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_mceliece_unsupported_aead_codec() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Mceliece348864Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + // AES-128-GCM is not allowed for mceliece (not PQ-safe) + assert!(pk + .seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err()); + } +} diff --git a/src/views/dkg_threshold.rs b/src/views/dkg_threshold.rs new file mode 100644 index 0000000..691c423 --- /dev/null +++ b/src/views/dkg_threshold.rs @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +//! DKG threshold attribute view. +//! +//! 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: +//! +//! - [`AttrId::DkgThreshold`] → [`threshold`](ThresholdAttrView::threshold) +//! - [`AttrId::DkgLimit`] → [`limit`](ThresholdAttrView::limit) +//! - [`AttrId::DkgIdentifier`] → [`identifier`](ThresholdAttrView::identifier) +//! - [`AttrId::DkgGroupPublicKey`] → [`threshold_data`](ThresholdAttrView::threshold_data) + +use crate::error::AttributesError; +use crate::Error; +use crate::{AttrId, AttrView, Multikey, ThresholdAttrView, ThresholdKeyView}; + +/// Read-only DKG threshold view over a [`Multikey`]. +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + // Encryption status is recorded on the multikey itself, not on the view; + // a DKG share is conceptually a secret-bearing share, so report false + // here and let the multikey's own AttrView impl be authoritative. + false + } + + fn is_public_key(&self) -> bool { + false + } + + fn is_secret_key(&self) -> bool { + false + } + + fn is_secret_key_share(&self) -> bool { + true + } +} + +impl<'a> ThresholdAttrView for View<'a> { + fn threshold(&self) -> Result { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgThreshold) + .ok_or(AttributesError::MissingThreshold)?; + if bytes.len() < 2 { + return Err(AttributesError::MissingThreshold.into()); + } + let v = u16::from_le_bytes([bytes[0], bytes[1]]); + Ok(v as usize) + } + + fn limit(&self) -> Result { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgLimit) + .ok_or(AttributesError::MissingLimit)?; + if bytes.len() < 2 { + return Err(AttributesError::MissingLimit.into()); + } + let v = u16::from_le_bytes([bytes[0], bytes[1]]); + Ok(v as usize) + } + + fn identifier(&self) -> Result<&[u8], Error> { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgIdentifier) + .ok_or(AttributesError::MissingShareIdentifier)?; + Ok(bytes.as_slice()) + } + + fn threshold_data(&self) -> Result<&[u8], Error> { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgGroupPublicKey) + .ok_or(AttributesError::MissingThresholdData)?; + Ok(bytes.as_slice()) + } +} + +impl<'a> ThresholdKeyView for View<'a> { + /// Get the group public key bytes for the t-of-n key. + fn group_pubkey(&self) -> Result, Error> { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgGroupPublicKey) + .ok_or(AttributesError::MissingThresholdData)?; + Ok(bytes.to_vec()) + } + + /// Returns `true` if the underlying Multikey is part of a threshold key. + fn is_threshold_key(&self) -> bool { + true + } + + /// Number of participants `n` in the t-of-n scheme. + fn participant_count(&self) -> Result { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgLimit) + .ok_or(AttributesError::MissingLimit)?; + if bytes.len() < 2 { + return Err(AttributesError::MissingLimit.into()); + } + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } + + /// Threshold `t` in the t-of-n scheme. + fn threshold(&self) -> Result { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgThreshold) + .ok_or(AttributesError::MissingThreshold)?; + if bytes.len() < 2 { + return Err(AttributesError::MissingThreshold.into()); + } + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } + + /// VLAD bytes of the session owner. + fn owner_vlad(&self) -> Result, Error> { + let bytes = self + .mk + .attributes + .get(&AttrId::DkgOwnerId) + .ok_or(AttributesError::MissingShareIdentifier)?; + Ok(bytes.to_vec()) + } +} diff --git a/src/views/ed25519.rs b/src/views/ed25519.rs index b2403a5..5fbdc64 100644 --- a/src/views/ed25519.rs +++ b/src/views/ed25519.rs @@ -1,17 +1,17 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::{AttributesError, CipherError, ConversionsError, KdfError, SignError, VerifyError}, AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, KdfAttrView, Multikey, SignView, VerifyView, Views, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_trait::TryDecodeFrom; +use multi_util::Varuint; use ed25519_dalek::{ Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, }; -use multicodec::Codec; -use multihash::{mh, Multihash}; -use multisig::{ms, Multisig, Views as SigViews}; -use multitrait::TryDecodeFrom; -use multiutil::Varuint; use zeroize::Zeroizing; pub(crate) struct View<'a> { @@ -150,7 +150,7 @@ impl<'a> FingerprintView for View<'a> { // get the key bytes let bytes = { let kd = self.mk.data_view()?; - + kd.key_bytes()? }; // hash the key bytes using the given codec @@ -165,7 +165,7 @@ impl<'a> ConvView for View<'a> { // get the secret key bytes let secret_bytes = { let kd = self.mk.data_view()?; - + kd.secret_bytes()? }; @@ -193,7 +193,7 @@ impl<'a> ConvView for View<'a> { let key_bytes = { let kd = pk.data_view()?; - + kd.key_bytes()? }; @@ -214,7 +214,7 @@ impl<'a> ConvView for View<'a> { fn to_ssh_private_key(&self) -> Result { let secret_bytes = { let kd = self.mk.data_view()?; - + kd.secret_bytes()? }; @@ -256,7 +256,7 @@ impl<'a> SignView for View<'a> { // get the secret key bytes let secret_bytes = { let kd = self.mk.data_view()?; - + kd.secret_bytes()? }; @@ -267,7 +267,7 @@ impl<'a> SignView for View<'a> { .map_err(|_| { ConversionsError::SecretKeyFailure("failed to get secret key bytes".to_string()) })?; - + SigningKey::from_bytes(&bytes) }; @@ -290,7 +290,7 @@ impl<'a> VerifyView for View<'a> { let attr = self.mk.attr_view()?; let pubmk = if attr.is_secret_key() { let kc = self.mk.conv_view()?; - + kc.to_public_key()? } else { self.mk.clone() @@ -299,7 +299,7 @@ impl<'a> VerifyView for View<'a> { // get the secret key bytes let key_bytes = { let kd = pubmk.data_view()?; - + kd.key_bytes()? }; diff --git a/src/views/ed25519_fndsa512.rs b/src/views/ed25519_fndsa512.rs new file mode 100644 index 0000000..ef4a36f --- /dev/null +++ b/src/views/ed25519_fndsa512.rs @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Ed25519-FN-DSA-512 hybrid signing multikey view; combines Ed25519 with FN-DSA-512 (Falcon) post-quantum signatures. +//! Sign: s1 = Ed25519(m), s2 = FnDsa512(m || s1), sig = s1 || s2 +//! Verify: verify Ed25519(m, s1) && verify FnDsa512(m || s1, s2) +//! 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::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; +use fn_dsa::{ + sign_key_size, signature_size, vrfy_key_size, SigningKey as _, SigningKeyStandard, + VerifyingKey as _, VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, +}; +use zeroize::Zeroizing; + +const LOGN: u32 = FN_DSA_LOGN_512; + +const ED25519_SEED_LEN: usize = 32; +const ED25519_PUB_LEN: usize = 32; +const ED25519_SIG_LEN: usize = 64; + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::Ed25519Fndsa512Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::Ed25519Fndsa512Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != ED25519_SEED_LEN + sign_key_size(LOGN) { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid secret length".into()).into(), + ); + } + + // Ed25519 public key + let ed_seed: [u8; 32] = secret_bytes[..ED25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ed25519 seed".into()))?; + let ed_signing_key = SigningKey::from_bytes(&ed_seed); + let ed_pub = ed_signing_key.verifying_key(); + + // FN-DSA verifying key + let fndsa_sk = SigningKeyStandard::decode(&secret_bytes[ED25519_SEED_LEN..]).ok_or( + ConversionsError::SecretKeyFailure("invalid fn-dsa signing key".into()), + )?; + let mut fndsa_vk = vec![0u8; vrfy_key_size(LOGN)]; + fndsa_sk.to_verifying_key(&mut fndsa_vk); + + // Concatenate (classical-first): ed25519_pub (32) || fndsa_vk + let mut pub_bytes = Vec::with_capacity(ED25519_PUB_LEN + fndsa_vk.len()); + pub_bytes.extend_from_slice(ed_pub.as_bytes()); + pub_bytes.extend_from_slice(&fndsa_vk); + + Builder::new(Codec::Ed25519Fndsa512Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Ed25519-FN-DSA-512 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Ed25519-FN-DSA-512 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != ED25519_SEED_LEN + sign_key_size(LOGN) { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid secret length".into()).into(), + ); + } + + // Step 1: Ed25519 sign + let ed_seed: [u8; 32] = secret_bytes[..ED25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("failed to get ed25519 seed".into()))?; + let ed_signing_key = SigningKey::from_bytes(&ed_seed); + let s1: Ed25519Sig = ed25519_dalek::Signer::sign(&ed_signing_key, msg); + + // Step 2: FN-DSA-512 sign(m || s1) + let mut fndsa_sk = SigningKeyStandard::decode(&secret_bytes[ED25519_SEED_LEN..]).ok_or( + ConversionsError::SecretKeyFailure("invalid fn-dsa signing key".into()), + )?; + + let mut m2 = Vec::with_capacity(msg.len() + ED25519_SIG_LEN); + m2.extend_from_slice(msg); + m2.extend_from_slice(&s1.to_bytes()); + + let mut s2 = vec![0u8; signature_size(LOGN)]; + fndsa_sk.sign( + &mut rand_core_06::OsRng, + &DOMAIN_NONE, + &HASH_ID_RAW, + &m2, + &mut s2, + ); + + // Step 3: sig = s1 || s2 + let mut sig_bytes = Vec::with_capacity(ED25519_SIG_LEN + s2.len()); + sig_bytes.extend_from_slice(&s1.to_bytes()); + sig_bytes.extend_from_slice(&s2); + + let mut ms = ms::Builder::new(Codec::Ed25519Fndsa512Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + if key_bytes.len() != ED25519_PUB_LEN + vrfy_key_size(LOGN) { + return Err(ConversionsError::PublicKeyFailure( + "invalid hybrid public key length".into(), + ) + .into()); + } + + // Get signature bytes + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + + if sig_bytes.len() != ED25519_SIG_LEN + signature_size(LOGN) { + return Err(VerifyError::BadSignature("invalid hybrid signature length".into()).into()); + } + + // Split public key + let ed_pub_bytes: [u8; 32] = key_bytes[..ED25519_PUB_LEN] + .try_into() + .map_err(|_| ConversionsError::PublicKeyFailure("invalid ed25519 public key".into()))?; + let fndsa_vk_bytes = &key_bytes[ED25519_PUB_LEN..]; + + // Split signature + let s1_bytes = &sig_bytes[..ED25519_SIG_LEN]; + let s2_bytes = &sig_bytes[ED25519_SIG_LEN..]; + + // Verify Ed25519: verify(m, s1) + let ed_verifying_key = VerifyingKey::from_bytes(&ed_pub_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let s1 = Ed25519Sig::from_slice(s1_bytes) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + ed_verifying_key + .verify_strict(msg_bytes, &s1) + .map_err(|e| VerifyError::BadSignature(format!("Ed25519 verify failed: {}", e)))?; + + // Verify FN-DSA-512: verify(m || s1, s2) + let fndsa_vk = VerifyingKeyStandard::decode(fndsa_vk_bytes).ok_or( + ConversionsError::PublicKeyFailure("invalid fn-dsa verifying key".into()), + )?; + + let mut m2 = Vec::with_capacity(msg_bytes.len() + ED25519_SIG_LEN); + m2.extend_from_slice(msg_bytes); + m2.extend_from_slice(s1_bytes); + + if fndsa_vk.verify(s2_bytes, &DOMAIN_NONE, &HASH_ID_RAW, &m2) { + Ok(()) + } else { + Err(VerifyError::BadSignature("FN-DSA-512 verify failed".into()).into()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_sign_verify_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Ed25519Fndsa512Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello Ed25519-FN-DSA-512 hybrid signing!"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + // wrong message must fail + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err()); + } +} diff --git a/src/views/ed25519_mayo2.rs b/src/views/ed25519_mayo2.rs new file mode 100644 index 0000000..8007eea --- /dev/null +++ b/src/views/ed25519_mayo2.rs @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Ed25519-MAYO2 hybrid signing multikey view; combines Ed25519 with MAYO-2 post-quantum signatures. +//! Sign: s1 = Ed25519(m), s2 = Mayo2(m || s1), sig = s1 || s2 +//! Verify: verify Ed25519(m, s1) && verify Mayo2(m || s1, s2) + +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; +use pq_mayo::{KeyPair, Mayo2}; +use zeroize::Zeroizing; + +const ED25519_SEED_LEN: usize = 32; +const MAYO2_SEED_LEN: usize = 24; +const PRIV_SEED_LEN: usize = ED25519_SEED_LEN + MAYO2_SEED_LEN; // 56 + +const ED25519_PUB_LEN: usize = 32; +const MAYO2_PUB_LEN: usize = 4368; +const PUB_KEY_LEN: usize = ED25519_PUB_LEN + MAYO2_PUB_LEN; // 4400 + +const ED25519_SIG_LEN: usize = 64; +const MAYO2_SIG_LEN: usize = 216; +const HYBRID_SIG_LEN: usize = ED25519_SIG_LEN + MAYO2_SIG_LEN; // 280 + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::Ed25519Mayo2Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::Ed25519Mayo2Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // Ed25519 public key + let ed_seed: [u8; 32] = secret_bytes[..ED25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ed25519 seed".into()))?; + let ed_signing_key = SigningKey::from_bytes(&ed_seed); + let ed_pub = ed_signing_key.verifying_key(); + + // MAYO-2 public key + let mayo_seed = &secret_bytes[ED25519_SEED_LEN..PRIV_SEED_LEN]; + let kp = KeyPair::::from_seed(mayo_seed) + .map_err(|e| ConversionsError::SecretKeyFailure(format!("MAYO-2 seed error: {}", e)))?; + + // Concatenate: ed25519_pub (32) || mayo2_pub (4368) + let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); + pub_bytes.extend_from_slice(ed_pub.as_bytes()); + pub_bytes.extend_from_slice(kp.verifying_key().as_ref()); + + Builder::new(Codec::Ed25519Mayo2Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Ed25519-MAYO2 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Ed25519-MAYO2 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // Step 1: Ed25519 sign + let ed_seed: [u8; 32] = secret_bytes[..ED25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("failed to get ed25519 seed".into()))?; + let ed_signing_key = SigningKey::from_bytes(&ed_seed); + let s1: Ed25519Sig = ed25519_dalek::Signer::sign(&ed_signing_key, msg); + + // Step 2: MAYO-2 sign(m || s1) + let mayo_seed = &secret_bytes[ED25519_SEED_LEN..PRIV_SEED_LEN]; + let kp = KeyPair::::from_seed(mayo_seed) + .map_err(|e| ConversionsError::SecretKeyFailure(format!("MAYO-2 seed error: {}", e)))?; + + let mut m2 = Vec::with_capacity(msg.len() + ED25519_SIG_LEN); + m2.extend_from_slice(msg); + m2.extend_from_slice(&s1.to_bytes()); + + let s2 = { + use ml_dsa::signature::Signer; + kp.signing_key() + .try_sign(&m2) + .map_err(|e| SignError::SigningFailed(e.to_string()))? + }; + + // Step 3: sig = s1 || s2 + let mut sig_bytes = Vec::with_capacity(HYBRID_SIG_LEN); + sig_bytes.extend_from_slice(&s1.to_bytes()); + sig_bytes.extend_from_slice(s2.as_ref()); + + let mut ms = ms::Builder::new(Codec::Ed25519Mayo2Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + use ml_dsa::signature::Verifier as MayoVerifier; + + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + if key_bytes.len() != PUB_KEY_LEN { + return Err(ConversionsError::PublicKeyFailure( + "invalid hybrid public key length".into(), + ) + .into()); + } + + // Get signature bytes + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + + if sig_bytes.len() != HYBRID_SIG_LEN { + return Err(VerifyError::BadSignature("invalid hybrid signature length".into()).into()); + } + + // Split public key + let ed_pub_bytes: [u8; 32] = key_bytes[..ED25519_PUB_LEN] + .try_into() + .map_err(|_| ConversionsError::PublicKeyFailure("invalid ed25519 public key".into()))?; + let mayo_pub_bytes = &key_bytes[ED25519_PUB_LEN..PUB_KEY_LEN]; + + // Split signature + let s1_bytes = &sig_bytes[..ED25519_SIG_LEN]; + let s2_bytes = &sig_bytes[ED25519_SIG_LEN..HYBRID_SIG_LEN]; + + // Verify Ed25519: verify(m, s1) + let ed_verifying_key = VerifyingKey::from_bytes(&ed_pub_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let s1 = Ed25519Sig::from_slice(s1_bytes) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + ed_verifying_key + .verify_strict(msg_bytes, &s1) + .map_err(|e| VerifyError::BadSignature(format!("Ed25519 verify failed: {}", e)))?; + + // Verify MAYO-2: verify(m || s1, s2) + let mayo_vk = pq_mayo::VerifyingKey::::try_from(mayo_pub_bytes) + .map_err(|_| ConversionsError::PublicKeyFailure("invalid MAYO-2 public key".into()))?; + let s2 = pq_mayo::Signature::::try_from(s2_bytes) + .map_err(|_| VerifyError::BadSignature("invalid MAYO-2 signature".into()))?; + + let mut m2 = Vec::with_capacity(msg_bytes.len() + ED25519_SIG_LEN); + m2.extend_from_slice(msg_bytes); + m2.extend_from_slice(s1_bytes); + + mayo_vk + .verify(&m2, &s2) + .map_err(|e| VerifyError::BadSignature(format!("MAYO-2 verify failed: {}", e)))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk::ED25519_MAYO2_KEY_CODECS; + use crate::views::Views; + + #[test] + fn test_key_gen_roundtrip() { + for codec in ED25519_MAYO2_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test hybrid signing key") + .try_build() + .unwrap(); + + let attr = mk.attr_view().unwrap(); + assert!(attr.is_secret_key()); + assert!(!attr.is_public_key()); + + // serialize/deserialize roundtrip + let bytes: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } + } + + #[test] + fn test_public_key_derivation() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::Ed25519Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let conv = mk.conv_view().unwrap(); + let pk = conv.to_public_key().unwrap(); + + let attr = pk.attr_view().unwrap(); + assert!(attr.is_public_key()); + assert!(!attr.is_secret_key()); + + // derive again => same result + let pk2 = conv.to_public_key().unwrap(); + assert_eq!(pk, pk2); + + // check public key length + let dv = pk.data_view().unwrap(); + assert_eq!(dv.key_bytes().unwrap().len(), PUB_KEY_LEN); + } + + #[test] + fn test_fingerprint() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::Ed25519Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + // Fingerprint from private key (derives public key internally) + let fp1 = mk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + + // Fingerprint from public key + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let fp2 = pk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + + let fp1_bytes: Vec = fp1.into(); + let fp2_bytes: Vec = fp2.into(); + assert_eq!(fp1_bytes, fp2_bytes); + assert!(!fp1_bytes.is_empty()); + } + + #[test] + fn test_sign_verify_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Ed25519Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello Ed25519-MAYO2 hybrid signing!"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + + // Verify with public key + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + // Verify with private key (auto-derives public key) + sk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + } + + #[test] + fn test_sign_verify_combined() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Ed25519Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"combined message test"; + let sig = sk.sign_view().unwrap().sign(msg, true, None).unwrap(); + + // Verify without explicit message (uses embedded message) + pk.verify_view().unwrap().verify(&sig, None).unwrap(); + } + + #[test] + fn test_tampered_signature_fails() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Ed25519Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"tamper test"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + + // Tamper with message + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err()); + } + + #[test] + fn test_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::Ed25519Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let sk2 = Builder::new_from_random_bytes(Codec::Ed25519Mayo2Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk2 = sk2.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"wrong key test"; + let sig = sk1.sign_view().unwrap().sign(msg, false, None).unwrap(); + + // Verify with wrong key should fail + assert!(pk2.verify_view().unwrap().verify(&sig, Some(msg)).is_err()); + } +} diff --git a/src/views/ed25519_mldsa65.rs b/src/views/ed25519_mldsa65.rs new file mode 100644 index 0000000..e1335a8 --- /dev/null +++ b/src/views/ed25519_mldsa65.rs @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Ed25519-ML-DSA-65 hybrid signing multikey view; combines Ed25519 with ML-DSA-65 post-quantum signatures. +//! Sign: s1 = Ed25519(m), s2 = MlDsa65(m || s1), sig = s1 || s2 +//! Verify: verify Ed25519(m, s1) && verify MlDsa65(m || s1, s2) +//! Public encoding is classical-first: ed25519_pub (32) || mldsa65_pub. + +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; +use ml_dsa::{ + signature::{Keypair, Signer as MlDsaSigner, Verifier as MlDsaVerifier}, + EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MlDsaSeed, Signature as MlDsaSig, + SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, +}; +use zeroize::Zeroizing; + +const ED25519_SEED_LEN: usize = 32; +const MLDSA65_SEED_LEN: usize = 32; +const PRIV_SEED_LEN: usize = ED25519_SEED_LEN + MLDSA65_SEED_LEN; // 64 + +const ED25519_PUB_LEN: usize = 32; +const MLDSA65_PUB_LEN: usize = 1952; +const PUB_KEY_LEN: usize = ED25519_PUB_LEN + MLDSA65_PUB_LEN; // 1984 + +const ED25519_SIG_LEN: usize = 64; +const MLDSA65_SIG_LEN: usize = 3309; +const HYBRID_SIG_LEN: usize = ED25519_SIG_LEN + MLDSA65_SIG_LEN; // 3373 + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::Ed25519Mldsa65Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::Ed25519Mldsa65Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // Ed25519 public key + let ed_seed: [u8; 32] = secret_bytes[..ED25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ed25519 seed".into()))?; + let ed_signing_key = SigningKey::from_bytes(&ed_seed); + let ed_pub = ed_signing_key.verifying_key(); + + // ML-DSA-65 public key + let mldsa_seed: [u8; 32] = secret_bytes[ED25519_SEED_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-dsa-65 seed".into()))?; + let mldsa_seed = MlDsaSeed::from(mldsa_seed); + let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + + // Concatenate (classical-first): ed25519_pub (32) || mldsa65_pub (1952) + let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); + pub_bytes.extend_from_slice(ed_pub.as_bytes()); + pub_bytes.extend_from_slice(kp.verifying_key().encode().as_slice()); + + Builder::new(Codec::Ed25519Mldsa65Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Ed25519-ML-DSA-65 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "Ed25519-ML-DSA-65 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // Step 1: Ed25519 sign + let ed_seed: [u8; 32] = secret_bytes[..ED25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("failed to get ed25519 seed".into()))?; + let ed_signing_key = SigningKey::from_bytes(&ed_seed); + let s1: Ed25519Sig = ed25519_dalek::Signer::sign(&ed_signing_key, msg); + + // Step 2: ML-DSA-65 sign(m || s1) + let mldsa_seed: [u8; 32] = secret_bytes[ED25519_SEED_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| { + ConversionsError::SecretKeyFailure("failed to get ml-dsa-65 seed".into()) + })?; + let mldsa_seed = MlDsaSeed::from(mldsa_seed); + let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + + let mut m2 = Vec::with_capacity(msg.len() + ED25519_SIG_LEN); + m2.extend_from_slice(msg); + m2.extend_from_slice(&s1.to_bytes()); + + let s2 = MlDsaSigner::sign(&kp, &m2); + + // Step 3: sig = s1 || s2 + let mut sig_bytes = Vec::with_capacity(HYBRID_SIG_LEN); + sig_bytes.extend_from_slice(&s1.to_bytes()); + sig_bytes.extend_from_slice(s2.encode().as_slice()); + + let mut ms = ms::Builder::new(Codec::Ed25519Mldsa65Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + if key_bytes.len() != PUB_KEY_LEN { + return Err(ConversionsError::PublicKeyFailure( + "invalid hybrid public key length".into(), + ) + .into()); + } + + // Get signature bytes + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + + if sig_bytes.len() != HYBRID_SIG_LEN { + return Err(VerifyError::BadSignature("invalid hybrid signature length".into()).into()); + } + + // Split public key + let ed_pub_bytes: [u8; 32] = key_bytes[..ED25519_PUB_LEN] + .try_into() + .map_err(|_| ConversionsError::PublicKeyFailure("invalid ed25519 public key".into()))?; + let mldsa_pub_bytes = &key_bytes[ED25519_PUB_LEN..PUB_KEY_LEN]; + + // Split signature + let s1_bytes = &sig_bytes[..ED25519_SIG_LEN]; + let s2_bytes = &sig_bytes[ED25519_SIG_LEN..HYBRID_SIG_LEN]; + + // Verify Ed25519: verify(m, s1) + let ed_verifying_key = VerifyingKey::from_bytes(&ed_pub_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let s1 = Ed25519Sig::from_slice(s1_bytes) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + ed_verifying_key + .verify_strict(msg_bytes, &s1) + .map_err(|e| VerifyError::BadSignature(format!("Ed25519 verify failed: {}", e)))?; + + // Verify ML-DSA-65: verify(m || s1, s2) + let encoded_vk = + EncodedVerifyingKey::::try_from(mldsa_pub_bytes).map_err(|_| { + ConversionsError::PublicKeyFailure("invalid ML-DSA-65 public key".into()) + })?; + let mldsa_vk = MlDsaVerifyingKey::::decode(&encoded_vk); + let encoded_sig = EncodedSignature::::try_from(s2_bytes) + .map_err(|_| VerifyError::BadSignature("invalid ML-DSA-65 signature".into()))?; + let s2 = MlDsaSig::decode(&encoded_sig).ok_or(VerifyError::BadSignature( + "invalid ML-DSA-65 signature".into(), + ))?; + + let mut m2 = Vec::with_capacity(msg_bytes.len() + ED25519_SIG_LEN); + m2.extend_from_slice(msg_bytes); + m2.extend_from_slice(s1_bytes); + + mldsa_vk + .verify(&m2, &s2) + .map_err(|e| VerifyError::BadSignature(format!("ML-DSA-65 verify failed: {}", e)))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_sign_verify_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Ed25519Mldsa65Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello Ed25519-ML-DSA-65 hybrid signing!"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + // wrong message must fail + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"wrong message")) + .is_err()); + } + + #[test] + fn test_public_key_length() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Ed25519Mldsa65Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + let dv = pk.data_view().unwrap(); + assert_eq!(dv.key_bytes().unwrap().len(), PUB_KEY_LEN); + } +} diff --git a/src/views/fn_dsa.rs b/src/views/fn_dsa.rs new file mode 100644 index 0000000..ab84136 --- /dev/null +++ b/src/views/fn_dsa.rs @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: Apache-2.0 +//! FN-DSA (Falcon) 512/1024 multikey view; FIPS 206 (draft). + +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Views as _}; +use fn_dsa::{ + signature_size, vrfy_key_size, SigningKey, SigningKeyStandard, VerifyingKey, + VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_1024, FN_DSA_LOGN_512, HASH_ID_RAW, +}; +use ssh_encoding::{Decode, Encode}; +use zeroize::Zeroizing; + +/// SSH algorithm name for FN-DSA-512 +pub const ALGORITHM_NAME_512: &str = "fn-dsa-512@multikey"; +/// SSH algorithm name for FN-DSA-1024 +pub const ALGORITHM_NAME_1024: &str = "fn-dsa-1024@multikey"; + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::FnDsa512Priv || self.mk.codec == Codec::FnDsa1024Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::FnDsa512Pub || self.mk.codec == Codec::FnDsa1024Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + if self.is_encrypted() { + return Err(AttributesError::EncryptedKey.into()); + } + self.key_bytes() + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let attr = self.mk.attr_view()?; + if attr.is_secret_key() { + // convert to a public key Multikey + let pk = self.to_public_key()?; + // get a conversions view on the public key + let fp = pk.fingerprint_view()?; + // get the fingerprint + let f = fp.fingerprint(codec)?; + Ok(f) + } else { + // get the key bytes + let bytes = { + let kd = self.mk.data_view()?; + + kd.key_bytes()? + }; + // hash the key bytes using the given codec + Ok(mh::Builder::new_from_bytes(codec, bytes)?.try_build()?) + } + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let key = SigningKeyStandard::decode(secret_bytes.as_slice()).ok_or( + ConversionsError::SecretKeyFailure("invalid secret key length".into()), + )?; + + let logn = key.get_logn(); + let mut verifying_key = vec![0u8; vrfy_key_size(logn)]; + key.to_verifying_key(&mut verifying_key); + + let pub_codec = if logn == FN_DSA_LOGN_512 { + Codec::FnDsa512Pub + } else { + Codec::FnDsa1024Pub + }; + Builder::new(pub_codec) + .with_comment(&self.mk.comment) + .with_key_bytes(&verifying_key) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + let mut pk = self.mk.clone(); + if self.is_secret_key() { + pk = self.to_public_key()?; + } + + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + // Determine algorithm name based on codec + let algorithm_name = match pk.codec { + Codec::FnDsa512Pub => ALGORITHM_NAME_512, + Codec::FnDsa1024Pub => ALGORITHM_NAME_1024, + _ => return Err(ConversionsError::UnsupportedCodec(pk.codec).into()), + }; + + let mut buff: Vec = Vec::new(); + key_bytes + .encode(&mut buff) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_key_bytes = ssh_key::public::OpaquePublicKeyBytes::decode(&mut buff.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PublicKey::new( + ssh_key::public::KeyData::Other(ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_key_bytes, + }), + pk.comment, + )) + } + + fn to_ssh_private_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + // Determine algorithm name based on codec + let algorithm_name = match self.mk.codec { + Codec::FnDsa512Priv => ALGORITHM_NAME_512, + Codec::FnDsa1024Priv => ALGORITHM_NAME_1024, + _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), + }; + + let mut buf: Vec = Vec::new(); + secret_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_private_key_bytes = + ssh_key::private::OpaquePrivateKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + let pk = self.to_public_key()?; + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + buf.clear(); + key_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_public_key_bytes = + ssh_key::public::OpaquePublicKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Other(ssh_key::private::OpaqueKeypair { + public: ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_public_key_bytes, + }, + private: opaque_private_key_bytes, + }), + self.mk.comment.clone(), + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} + +impl<'a> SignView for View<'a> { + fn sign( + &self, + msg: &[u8], + combined: bool, + _scheme: Option, + ) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let mut key = SigningKeyStandard::decode(secret_bytes.as_slice()).ok_or( + ConversionsError::SecretKeyFailure("invalid secret key length".into()), + )?; + + let logn = key.get_logn(); + let mut sig = vec![0u8; signature_size(logn)]; + key.sign( + &mut rand_core_06::OsRng, + &DOMAIN_NONE, + &HASH_ID_RAW, + msg, + &mut sig, + ); + + let msig_codec = if logn == FN_DSA_LOGN_512 { + Codec::FnDsa512Msig + } else { + Codec::FnDsa1024Msig + }; + let mut ms = ms::Builder::new(msig_codec).with_signature_bytes(&sig); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &multi_sig::Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + let key = VerifyingKeyStandard::decode(key_bytes.as_slice()).ok_or( + ConversionsError::PublicKeyFailure("invalid public key length".into()), + )?; + + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes()?; + + let expected_sig_len = if key_bytes.len() == vrfy_key_size(FN_DSA_LOGN_512) { + signature_size(FN_DSA_LOGN_512) + } else { + signature_size(FN_DSA_LOGN_1024) + }; + if sig_bytes.len() != expected_sig_len { + return Err(VerifyError::BadSignature("invalid signature length".into()).into()); + } + + let msg_bytes = if let Some(m) = msg { + m + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + if key.verify(sig_bytes.as_slice(), &DOMAIN_NONE, &HASH_ID_RAW, msg_bytes) { + Ok(()) + } else { + Err(VerifyError::BadSignature("invalid signature".into()).into()) + } + } +} diff --git a/src/views/frodokem.rs b/src/views/frodokem.rs new file mode 100644 index 0000000..37dc742 --- /dev/null +++ b/src/views/frodokem.rs @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 +//! FrodoKEM AES/SHAKE multikey view. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use zeroize::Zeroizing; + +type SealedParts = (Vec, Codec, Vec, Vec); + +fn is_frodokem_priv(codec: Codec) -> bool { + matches!( + codec, + Codec::FrodoKem640AesPriv + | Codec::FrodoKem976AesPriv + | Codec::FrodoKem1344AesPriv + | Codec::FrodoKem640ShakePriv + | Codec::FrodoKem976ShakePriv + | Codec::FrodoKem1344ShakePriv + ) +} + +fn is_frodokem_pub(codec: Codec) -> bool { + matches!( + codec, + Codec::FrodoKem640AesPub + | Codec::FrodoKem976AesPub + | Codec::FrodoKem1344AesPub + | Codec::FrodoKem640ShakePub + | Codec::FrodoKem976ShakePub + | Codec::FrodoKem1344ShakePub + ) +} + +fn is_frodokem_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::Xchacha20Poly1305 | Codec::Chacha20Poly1305 | Codec::AesGcm256 + ) +} + +fn public_codec(codec: Codec) -> Result { + match codec { + Codec::FrodoKem640AesPriv => Ok(Codec::FrodoKem640AesPub), + Codec::FrodoKem976AesPriv => Ok(Codec::FrodoKem976AesPub), + Codec::FrodoKem1344AesPriv => Ok(Codec::FrodoKem1344AesPub), + Codec::FrodoKem640ShakePriv => Ok(Codec::FrodoKem640ShakePub), + Codec::FrodoKem976ShakePriv => Ok(Codec::FrodoKem976ShakePub), + Codec::FrodoKem1344ShakePriv => Ok(Codec::FrodoKem1344ShakePub), + _ => Err(ConversionsError::SecretKeyFailure("not a FrodoKEM private key".into()).into()), + } +} + +fn public_from_private(codec: Codec, secret_bytes: &[u8]) -> Result, Error> { + match codec { + Codec::FrodoKem640AesPriv => super::frodokem_helper::public_from_private_640aes(secret_bytes), + Codec::FrodoKem976AesPriv => super::frodokem_helper::public_from_private_976aes(secret_bytes), + Codec::FrodoKem1344AesPriv => super::frodokem_helper::public_from_private_1344aes(secret_bytes), + Codec::FrodoKem640ShakePriv => super::frodokem_helper::public_from_private_640shake(secret_bytes), + Codec::FrodoKem976ShakePriv => super::frodokem_helper::public_from_private_976shake(secret_bytes), + Codec::FrodoKem1344ShakePriv => super::frodokem_helper::public_from_private_1344shake(secret_bytes), + _ => { + return Err( + ConversionsError::SecretKeyFailure("not a FrodoKEM private key".into()).into(), + ) + } + } + .map_err(|e| ConversionsError::SecretKeyFailure(e).into()) +} + +fn keypair(codec: Codec) -> Result<(Vec, Zeroizing>), Error> { + match codec { + Codec::FrodoKem640AesPriv => Ok(super::frodokem_helper::keypair_640aes()), + Codec::FrodoKem976AesPriv => Ok(super::frodokem_helper::keypair_976aes()), + Codec::FrodoKem1344AesPriv => Ok(super::frodokem_helper::keypair_1344aes()), + Codec::FrodoKem640ShakePriv => Ok(super::frodokem_helper::keypair_640shake()), + Codec::FrodoKem976ShakePriv => Ok(super::frodokem_helper::keypair_976shake()), + Codec::FrodoKem1344ShakePriv => Ok(super::frodokem_helper::keypair_1344shake()), + _ => Err(ConversionsError::SecretKeyFailure("not a FrodoKEM private key".into()).into()), + } +} + +fn encap(codec: Codec, pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), Error> { + match codec { + Codec::FrodoKem640AesPub => super::frodokem_helper::encap_640aes(pub_key_bytes), + Codec::FrodoKem976AesPub => super::frodokem_helper::encap_976aes(pub_key_bytes), + Codec::FrodoKem1344AesPub => super::frodokem_helper::encap_1344aes(pub_key_bytes), + Codec::FrodoKem640ShakePub => super::frodokem_helper::encap_640shake(pub_key_bytes), + Codec::FrodoKem976ShakePub => super::frodokem_helper::encap_976shake(pub_key_bytes), + Codec::FrodoKem1344ShakePub => super::frodokem_helper::encap_1344shake(pub_key_bytes), + _ => return Err(SealError::NotEncapsulationKey.into()), + } + .map_err(|e| SealError::EncapsulationFailed(e).into()) +} + +fn decap( + codec: Codec, + secret_bytes: &[u8], + ciphertext: &[u8], +) -> Result>, Error> { + match codec { + Codec::FrodoKem640AesPriv => super::frodokem_helper::decap_640aes(secret_bytes, ciphertext), + Codec::FrodoKem976AesPriv => super::frodokem_helper::decap_976aes(secret_bytes, ciphertext), + Codec::FrodoKem1344AesPriv => super::frodokem_helper::decap_1344aes(secret_bytes, ciphertext), + Codec::FrodoKem640ShakePriv => super::frodokem_helper::decap_640shake(secret_bytes, ciphertext), + Codec::FrodoKem976ShakePriv => super::frodokem_helper::decap_976shake(secret_bytes, ciphertext), + Codec::FrodoKem1344ShakePriv => super::frodokem_helper::decap_1344shake(secret_bytes, ciphertext), + _ => return Err(SealError::NotDecapsulationKey.into()), + } + .map_err(|e| SealError::DecapsulationFailed(e).into()) +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_frodokem_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_frodokem_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let pub_bytes = public_from_private(self.mk.codec, secret_bytes.as_slice())?; + Builder::new(public_codec(self.mk.codec)?) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "FrodoKEM not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "FrodoKEM not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +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()))?; + let (aead_codec, ptr) = Codec::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing aead codec".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + kem_ct.to_inner(), + aead_codec, + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_frodokem_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + let pub_bytes = self.key_bytes()?; + let (kem_ct, shared_secret) = encap(self.mk.codec, pub_bytes.as_slice())?; + 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)) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + let (kem_ct, aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + if !is_frodokem_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let shared_secret = decap(self.mk.codec, secret_bytes.as_slice(), &kem_ct)?; + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(&shared_secret, b"frodokem-seal", key_len)?; + Ok(aead::aead_open( + aead_codec, &aead_key, &nonce, &ct_tag, aad, + )?) + } +} + +pub(crate) fn generate_private_key(codec: Codec) -> Result>, Error> { + let (_pk, sk) = keypair(codec)?; + Ok(sk) +} diff --git a/src/views/frodokem_helper.rs b/src/views/frodokem_helper.rs new file mode 100644 index 0000000..6a19162 --- /dev/null +++ b/src/views/frodokem_helper.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 +//! FrodoKEM AES/SHAKE helper — inlined from the former bs-frodokem wrapper. +//! +//! Keys and ciphertexts are passed as raw byte slices; shared secrets are +//! returned as `Vec` wrapped in [`zeroize::Zeroizing`]. +use frodo_kem_rs::Algorithm; +use getrandom::rand_core::UnwrapErr; +use getrandom::SysRng; +use zeroize::Zeroizing; + +fn rng() -> UnwrapErr { + UnwrapErr(SysRng) +} + +fn keypair(alg: Algorithm) -> (Vec, Zeroizing>) { + let (ek, dk) = alg.generate_keypair(rng()); + (ek.as_ref().to_vec(), Zeroizing::new(dk.as_ref().to_vec())) +} + +fn encap(alg: Algorithm, pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), String> { + let ek = alg + .encryption_key_from_bytes(pub_key_bytes) + .map_err(|e| e.to_string())?; + let (ct, ss) = alg + .encapsulate_with_rng(&ek, rng()) + .map_err(|e| e.to_string())?; + Ok((ct.as_ref().to_vec(), Zeroizing::new(ss.as_ref().to_vec()))) +} + +fn decap( + alg: Algorithm, + secret_key_bytes: &[u8], + ciphertext_bytes: &[u8], +) -> Result>, String> { + let dk = alg + .decryption_key_from_bytes(secret_key_bytes) + .map_err(|e| e.to_string())?; + let ct = alg + .ciphertext_from_bytes(ciphertext_bytes) + .map_err(|e| e.to_string())?; + let (ss, _msg) = alg.decapsulate(&dk, &ct).map_err(|e| e.to_string())?; + Ok(Zeroizing::new(ss.as_ref().to_vec())) +} + +pub fn public_from_private(alg: Algorithm, secret_key_bytes: &[u8]) -> Result, String> { + let dk = alg + .decryption_key_from_bytes(secret_key_bytes) + .map_err(|e| e.to_string())?; + Ok(alg + .encryption_key_from_decryption_key(&dk) + .as_ref() + .to_vec()) +} + +// ── FrodoKEM-640-AES ──────────────────────────────────────────────────────── + +pub fn keypair_640aes() -> (Vec, Zeroizing>) { + keypair(Algorithm::FrodoKem640Aes) +} + +pub fn encap_640aes(pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), String> { + encap(Algorithm::FrodoKem640Aes, pub_key_bytes) +} + +pub fn decap_640aes( + secret_key_bytes: &[u8], + ciphertext_bytes: &[u8], +) -> Result>, String> { + decap( + Algorithm::FrodoKem640Aes, + secret_key_bytes, + ciphertext_bytes, + ) +} + +pub fn public_from_private_640aes(secret_key_bytes: &[u8]) -> Result, String> { + public_from_private(Algorithm::FrodoKem640Aes, secret_key_bytes) +} + +// ── FrodoKEM-976-AES ──────────────────────────────────────────────────────── + +pub fn keypair_976aes() -> (Vec, Zeroizing>) { + keypair(Algorithm::FrodoKem976Aes) +} + +pub fn encap_976aes(pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), String> { + encap(Algorithm::FrodoKem976Aes, pub_key_bytes) +} + +pub fn decap_976aes( + secret_key_bytes: &[u8], + ciphertext_bytes: &[u8], +) -> Result>, String> { + decap( + Algorithm::FrodoKem976Aes, + secret_key_bytes, + ciphertext_bytes, + ) +} + +pub fn public_from_private_976aes(secret_key_bytes: &[u8]) -> Result, String> { + public_from_private(Algorithm::FrodoKem976Aes, secret_key_bytes) +} + +// ── FrodoKEM-1344-AES ─────────────────────────────────────────────────────── + +pub fn keypair_1344aes() -> (Vec, Zeroizing>) { + keypair(Algorithm::FrodoKem1344Aes) +} + +pub fn encap_1344aes(pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), String> { + encap(Algorithm::FrodoKem1344Aes, pub_key_bytes) +} + +pub fn decap_1344aes( + secret_key_bytes: &[u8], + ciphertext_bytes: &[u8], +) -> Result>, String> { + decap( + Algorithm::FrodoKem1344Aes, + secret_key_bytes, + ciphertext_bytes, + ) +} + +pub fn public_from_private_1344aes(secret_key_bytes: &[u8]) -> Result, String> { + public_from_private(Algorithm::FrodoKem1344Aes, secret_key_bytes) +} + +// ── FrodoKEM-640-SHAKE ────────────────────────────────────────────────────── + +pub fn keypair_640shake() -> (Vec, Zeroizing>) { + keypair(Algorithm::FrodoKem640Shake) +} + +pub fn encap_640shake(pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), String> { + encap(Algorithm::FrodoKem640Shake, pub_key_bytes) +} + +pub fn decap_640shake( + secret_key_bytes: &[u8], + ciphertext_bytes: &[u8], +) -> Result>, String> { + decap( + Algorithm::FrodoKem640Shake, + secret_key_bytes, + ciphertext_bytes, + ) +} + +pub fn public_from_private_640shake(secret_key_bytes: &[u8]) -> Result, String> { + public_from_private(Algorithm::FrodoKem640Shake, secret_key_bytes) +} + +// ── FrodoKEM-976-SHAKE ────────────────────────────────────────────────────── + +pub fn keypair_976shake() -> (Vec, Zeroizing>) { + keypair(Algorithm::FrodoKem976Shake) +} + +pub fn encap_976shake(pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), String> { + encap(Algorithm::FrodoKem976Shake, pub_key_bytes) +} + +pub fn decap_976shake( + secret_key_bytes: &[u8], + ciphertext_bytes: &[u8], +) -> Result>, String> { + decap( + Algorithm::FrodoKem976Shake, + secret_key_bytes, + ciphertext_bytes, + ) +} + +pub fn public_from_private_976shake(secret_key_bytes: &[u8]) -> Result, String> { + public_from_private(Algorithm::FrodoKem976Shake, secret_key_bytes) +} + +// ── FrodoKEM-1344-SHAKE ───────────────────────────────────────────────────── + +pub fn keypair_1344shake() -> (Vec, Zeroizing>) { + keypair(Algorithm::FrodoKem1344Shake) +} + +pub fn encap_1344shake(pub_key_bytes: &[u8]) -> Result<(Vec, Zeroizing>), String> { + encap(Algorithm::FrodoKem1344Shake, pub_key_bytes) +} + +pub fn decap_1344shake( + secret_key_bytes: &[u8], + ciphertext_bytes: &[u8], +) -> Result>, String> { + decap( + Algorithm::FrodoKem1344Shake, + secret_key_bytes, + ciphertext_bytes, + ) +} + +pub fn public_from_private_1344shake(secret_key_bytes: &[u8]) -> Result, String> { + public_from_private(Algorithm::FrodoKem1344Shake, secret_key_bytes) +} \ No newline at end of file diff --git a/src/views/mayo.rs b/src/views/mayo.rs new file mode 100644 index 0000000..a63fe6a --- /dev/null +++ b/src/views/mayo.rs @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: Apache-2.0 +//! MAYO-1/2/3/5 multikey view; post-quantum multivariate signature. + +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Views as _}; +use pq_mayo::{KeyPair, Mayo1, Mayo2, Mayo3, Mayo5, Signature, VerifyingKey}; +use ssh_encoding::{Decode, Encode}; +use zeroize::Zeroizing; + +/// SSH algorithm name for MAYO-1 +pub const ALGORITHM_NAME_1: &str = "mayo-1@multikey"; +/// SSH algorithm name for MAYO-2 +pub const ALGORITHM_NAME_2: &str = "mayo-2@multikey"; +/// SSH algorithm name for MAYO-3 +pub const ALGORITHM_NAME_3: &str = "mayo-3@multikey"; +/// SSH algorithm name for MAYO-5 +pub const ALGORITHM_NAME_5: &str = "mayo-5@multikey"; + +fn is_mayo_priv(codec: Codec) -> bool { + matches!( + codec, + Codec::Mayo1Priv | Codec::Mayo2Priv | Codec::Mayo3Priv | Codec::Mayo5Priv + ) +} + +fn is_mayo_pub(codec: Codec) -> bool { + matches!( + codec, + Codec::Mayo1Pub | Codec::Mayo2Pub | Codec::Mayo3Pub | Codec::Mayo5Pub + ) +} + +/// Compact-secret-key (seed) lengths per MAYO parameter set. +const MAYO_SEED_LENGTH: usize = 24; // MAYO-1 and MAYO-2 +const MAYO_3_SEED_LENGTH: usize = 32; +const MAYO_5_SEED_LENGTH: usize = 40; + +const MAYO_1_PUBLIC_KEY_LENGTH: usize = 1420; +const MAYO_2_PUBLIC_KEY_LENGTH: usize = 4368; +const MAYO_3_PUBLIC_KEY_LENGTH: usize = 2986; +const MAYO_5_PUBLIC_KEY_LENGTH: usize = 5554; + +const MAYO_1_SIGNATURE_LENGTH: usize = 454; +const MAYO_2_SIGNATURE_LENGTH: usize = 216; +const MAYO_3_SIGNATURE_LENGTH: usize = 681; +const MAYO_5_SIGNATURE_LENGTH: usize = 964; + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_mayo_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_mayo_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let (public_key, codec) = match (self.mk.codec, secret_bytes.len()) { + (Codec::Mayo1Priv, MAYO_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-1 seed error: {}", e)) + })?; + (kp.verifying_key().as_ref().to_vec(), Codec::Mayo1Pub) + } + (Codec::Mayo2Priv, MAYO_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-2 seed error: {}", e)) + })?; + (kp.verifying_key().as_ref().to_vec(), Codec::Mayo2Pub) + } + (Codec::Mayo3Priv, MAYO_3_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-3 seed error: {}", e)) + })?; + (kp.verifying_key().as_ref().to_vec(), Codec::Mayo3Pub) + } + (Codec::Mayo5Priv, MAYO_5_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-5 seed error: {}", e)) + })?; + (kp.verifying_key().as_ref().to_vec(), Codec::Mayo5Pub) + } + _ => { + return Err(ConversionsError::SecretKeyFailure( + "invalid secret key or seed length".into(), + ) + .into()); + } + }; + + Builder::new(codec) + .with_comment(&self.mk.comment) + .with_key_bytes(&public_key) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + let mut pk = self.mk.clone(); + if self.is_secret_key() { + pk = self.to_public_key()?; + } + + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + let algorithm_name = match pk.codec { + Codec::Mayo1Pub => ALGORITHM_NAME_1, + Codec::Mayo2Pub => ALGORITHM_NAME_2, + Codec::Mayo3Pub => ALGORITHM_NAME_3, + Codec::Mayo5Pub => ALGORITHM_NAME_5, + _ => return Err(ConversionsError::UnsupportedCodec(pk.codec).into()), + }; + + let mut buff: Vec = Vec::new(); + key_bytes + .encode(&mut buff) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_key_bytes = ssh_key::public::OpaquePublicKeyBytes::decode(&mut buff.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PublicKey::new( + ssh_key::public::KeyData::Other(ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_key_bytes, + }), + pk.comment, + )) + } + + fn to_ssh_private_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let algorithm_name = match self.mk.codec { + Codec::Mayo1Priv => ALGORITHM_NAME_1, + Codec::Mayo2Priv => ALGORITHM_NAME_2, + Codec::Mayo3Priv => ALGORITHM_NAME_3, + Codec::Mayo5Priv => ALGORITHM_NAME_5, + _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), + }; + + let mut buf: Vec = Vec::new(); + secret_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_private_key_bytes = + ssh_key::private::OpaquePrivateKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + let pk = self.to_public_key()?; + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + buf.clear(); + key_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_public_key_bytes = + ssh_key::public::OpaquePublicKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Other(ssh_key::private::OpaqueKeypair { + public: ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_public_key_bytes, + }, + private: opaque_private_key_bytes, + }), + self.mk.comment.clone(), + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + if self.is_secret_key() { + return Err(ConversionsError::SecretKeyFailure( + "MAYO public key derivation not yet implemented".into(), + ) + .into()); + } + let bytes = self.key_bytes()?; + Ok(mh::Builder::new_from_bytes(codec, bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign( + &self, + msg: &[u8], + combined: bool, + _scheme: Option, + ) -> Result { + use ml_dsa::signature::Signer; + + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let (signature, codec) = match (self.mk.codec, secret_bytes.len()) { + (Codec::Mayo1Priv, MAYO_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-1 seed error: {}", e)) + })?; + let sig = kp + .signing_key() + .try_sign(msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + (sig.as_ref().to_vec(), Codec::Mayo1Msig) + } + (Codec::Mayo2Priv, MAYO_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-2 seed error: {}", e)) + })?; + let sig = kp + .signing_key() + .try_sign(msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + (sig.as_ref().to_vec(), Codec::Mayo2Msig) + } + (Codec::Mayo3Priv, MAYO_3_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-3 seed error: {}", e)) + })?; + let sig = kp + .signing_key() + .try_sign(msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + (sig.as_ref().to_vec(), Codec::Mayo3Msig) + } + (Codec::Mayo5Priv, MAYO_5_SEED_LENGTH) => { + let kp = KeyPair::::from_seed(secret_bytes.as_slice()).map_err(|e| { + ConversionsError::SecretKeyFailure(format!("MAYO-5 seed error: {}", e)) + })?; + let sig = kp + .signing_key() + .try_sign(msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + (sig.as_ref().to_vec(), Codec::Mayo5Msig) + } + _ => { + return Err(ConversionsError::SecretKeyFailure( + "invalid secret key or seed length".into(), + ) + .into()); + } + }; + + let mut ms = ms::Builder::new(codec).with_signature_bytes(&signature); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, sig: &multi_sig::Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + use ml_dsa::signature::Verifier; + + let msg_bytes = if let Some(m) = msg { + m + } else if !sig.message.is_empty() { + sig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + let sv = sig.data_view()?; + let sig_bytes = sv.sig_bytes()?; + + match (key_bytes.len(), sig_bytes.len()) { + (MAYO_1_PUBLIC_KEY_LENGTH, MAYO_1_SIGNATURE_LENGTH) => { + let vk = VerifyingKey::::try_from(key_bytes.as_slice()).map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + vk.verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + Ok(()) + } + (MAYO_2_PUBLIC_KEY_LENGTH, MAYO_2_SIGNATURE_LENGTH) => { + let vk = VerifyingKey::::try_from(key_bytes.as_slice()).map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + vk.verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + Ok(()) + } + (MAYO_3_PUBLIC_KEY_LENGTH, MAYO_3_SIGNATURE_LENGTH) => { + let vk = VerifyingKey::::try_from(key_bytes.as_slice()).map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + vk.verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + Ok(()) + } + (MAYO_5_PUBLIC_KEY_LENGTH, MAYO_5_SIGNATURE_LENGTH) => { + let vk = VerifyingKey::::try_from(key_bytes.as_slice()).map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + vk.verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + Ok(()) + } + _ => Err( + VerifyError::BadSignature("invalid public key or signature length".into()).into(), + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + fn sign_verify_roundtrip(priv_codec: Codec) { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(priv_codec, &mut rng) + .unwrap() + .with_comment("mayo test") + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let msg = b"hello MAYO multivariate signature"; + let sig = sk.sign_view().unwrap().sign(msg, false, None).unwrap(); + pk.verify_view().unwrap().verify(&sig, Some(msg)).unwrap(); + + // wrong message must fail + assert!(pk + .verify_view() + .unwrap() + .verify(&sig, Some(b"tampered")) + .is_err()); + } + + #[test] + fn test_mayo3_sign_verify_roundtrip() { + sign_verify_roundtrip(Codec::Mayo3Priv); + } + + #[test] + fn test_mayo5_sign_verify_roundtrip() { + sign_verify_roundtrip(Codec::Mayo5Priv); + } +} diff --git a/src/views/ml_dsa.rs b/src/views/ml_dsa.rs new file mode 100644 index 0000000..5d61864 --- /dev/null +++ b/src/views/ml_dsa.rs @@ -0,0 +1,353 @@ +// SPDX-License-Identifier: Apache-2.0 +//! ML-DSA 65/87 multikey view; FIPS 204. + +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + views::Views, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Views as _}; +use ml_dsa::{ + signature::{Keypair, Signer, Verifier}, + EncodedSignature, EncodedVerifyingKey, MlDsa65, MlDsa87, Seed, Signature, SigningKey, + VerifyingKey, +}; +use ssh_encoding::{Decode, Encode}; +use zeroize::Zeroizing; + +/// SSH algorithm name for ML-DSA-65 +pub const ALGORITHM_NAME_65: &str = "ml-dsa-65@multikey"; +/// SSH algorithm name for ML-DSA-87 +pub const ALGORITHM_NAME_87: &str = "ml-dsa-87@multikey"; + +fn is_ml_dsa_priv(codec: Codec) -> bool { + codec == Codec::MlDsa65Priv || codec == Codec::MlDsa87Priv +} + +fn is_ml_dsa_pub(codec: Codec) -> bool { + codec == Codec::MlDsa65Pub || codec == Codec::MlDsa87Pub +} + +const ML_DSA_SEED_LENGTH: usize = 32; + +const ML_DSA_65_PUBLIC_KEY_LENGTH: usize = 1952; +const ML_DSA_87_PUBLIC_KEY_LENGTH: usize = 2592; + +const ML_DSA_65_SIGNATURE_LENGTH: usize = 3309; +const ML_DSA_87_SIGNATURE_LENGTH: usize = 4627; + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_ml_dsa_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_ml_dsa_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let (public_key, codec) = match (self.mk.codec, secret_bytes.len()) { + (Codec::MlDsa65Priv, ML_DSA_SEED_LENGTH) => { + let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = + secret_bytes.as_slice().try_into().map_err(|_| { + ConversionsError::SecretKeyFailure("invalid seed length".into()) + })?; + let seed = Seed::from(seed_bytes); + let kp = SigningKey::::from_seed(&seed); + ( + kp.verifying_key().encode().as_slice().to_vec(), + Codec::MlDsa65Pub, + ) + } + (Codec::MlDsa87Priv, ML_DSA_SEED_LENGTH) => { + let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = + secret_bytes.as_slice().try_into().map_err(|_| { + ConversionsError::SecretKeyFailure("invalid seed length".into()) + })?; + let seed = Seed::from(seed_bytes); + let kp = SigningKey::::from_seed(&seed); + ( + kp.verifying_key().encode().as_slice().to_vec(), + Codec::MlDsa87Pub, + ) + } + _ => { + return Err(ConversionsError::SecretKeyFailure( + "invalid secret key or seed length".into(), + ) + .into()); + } + }; + + Builder::new(codec) + .with_comment(&self.mk.comment) + .with_key_bytes(&public_key) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + let mut pk = self.mk.clone(); + if self.is_secret_key() { + pk = self.to_public_key()?; + } + + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + // Determine algorithm name based on codec + let algorithm_name = match pk.codec { + Codec::MlDsa65Pub => ALGORITHM_NAME_65, + Codec::MlDsa87Pub => ALGORITHM_NAME_87, + _ => return Err(ConversionsError::UnsupportedCodec(pk.codec).into()), + }; + + let mut buff: Vec = Vec::new(); + key_bytes + .encode(&mut buff) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_key_bytes = ssh_key::public::OpaquePublicKeyBytes::decode(&mut buff.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PublicKey::new( + ssh_key::public::KeyData::Other(ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_key_bytes, + }), + pk.comment, + )) + } + + fn to_ssh_private_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + // Determine algorithm name based on codec + let algorithm_name = match self.mk.codec { + Codec::MlDsa65Priv => ALGORITHM_NAME_65, + Codec::MlDsa87Priv => ALGORITHM_NAME_87, + _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), + }; + + let mut buf: Vec = Vec::new(); + secret_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_private_key_bytes = + ssh_key::private::OpaquePrivateKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + let pk = self.to_public_key()?; + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + buf.clear(); + key_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_public_key_bytes = + ssh_key::public::OpaquePublicKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Other(ssh_key::private::OpaqueKeypair { + public: ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_public_key_bytes, + }, + private: opaque_private_key_bytes, + }), + self.mk.comment.clone(), + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + if self.is_secret_key() { + return Err(ConversionsError::SecretKeyFailure( + "ML-DSA public key derivation not yet implemented".into(), + ) + .into()); + } + let bytes = self.key_bytes()?; + Ok(mh::Builder::new_from_bytes(codec, bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign( + &self, + msg: &[u8], + combined: bool, + _scheme: Option, + ) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let (signature, codec) = match (self.mk.codec, secret_bytes.len()) { + (Codec::MlDsa65Priv, ML_DSA_SEED_LENGTH) => { + let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = + secret_bytes.as_slice().try_into().map_err(|_| { + ConversionsError::SecretKeyFailure("invalid seed length".into()) + })?; + let seed = Seed::from(seed_bytes); + let kp = SigningKey::::from_seed(&seed); + ( + kp.sign(msg).encode().as_slice().to_vec(), + Codec::MlDsa65Msig, + ) + } + (Codec::MlDsa87Priv, ML_DSA_SEED_LENGTH) => { + let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = + secret_bytes.as_slice().try_into().map_err(|_| { + ConversionsError::SecretKeyFailure("invalid seed length".into()) + })?; + let seed = Seed::from(seed_bytes); + let kp = SigningKey::::from_seed(&seed); + ( + kp.sign(msg).encode().as_slice().to_vec(), + Codec::MlDsa87Msig, + ) + } + _ => { + return Err(ConversionsError::SecretKeyFailure( + "invalid secret key or seed length".into(), + ) + .into()); + } + }; + + let mut ms = ms::Builder::new(codec).with_signature_bytes(&signature); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, sig: &multi_sig::Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !sig.message.is_empty() { + sig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + let sv = sig.data_view()?; + let sig_bytes = sv.sig_bytes()?; + + match (key_bytes.len(), sig_bytes.len()) { + (ML_DSA_65_PUBLIC_KEY_LENGTH, ML_DSA_65_SIGNATURE_LENGTH) => { + let encoded_verifying_key = + EncodedVerifyingKey::::try_from(key_bytes.as_slice()).map_err( + |_| ConversionsError::PublicKeyFailure("invalid public key length".into()), + )?; + let verifying_key = VerifyingKey::::decode(&encoded_verifying_key); + let encoded_signature = EncodedSignature::::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + let signature = Signature::decode(&encoded_signature) + .ok_or(VerifyError::BadSignature("invalid signature".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + Ok(()) + } + (ML_DSA_87_PUBLIC_KEY_LENGTH, ML_DSA_87_SIGNATURE_LENGTH) => { + let encoded_verifying_key = + EncodedVerifyingKey::::try_from(key_bytes.as_slice()).map_err( + |_| ConversionsError::PublicKeyFailure("invalid public key length".into()), + )?; + let verifying_key = VerifyingKey::::decode(&encoded_verifying_key); + let encoded_signature = EncodedSignature::::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + let signature = Signature::decode(&encoded_signature) + .ok_or(VerifyError::BadSignature("invalid signature".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + Ok(()) + } + _ => Err( + VerifyError::BadSignature("invalid public key or signature length".into()).into(), + ), + } + } +} diff --git a/src/views/ml_kem.rs b/src/views/ml_kem.rs new file mode 100644 index 0000000..ce0381a --- /dev/null +++ b/src/views/ml_kem.rs @@ -0,0 +1,482 @@ +// SPDX-License-Identifier: Apache-2.0 +//! ML-KEM 768/1024 multikey view; FIPS 203 key encapsulation. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use ml_kem::{ + kem::{Decapsulate, Encapsulate}, + EncodedSizeUser, KemCore, MlKem1024, MlKem768, +}; +use zeroize::Zeroizing; + +const ML_KEM_SEED_LENGTH: usize = 64; // d (32) || z (32) + +/// Decoded sealed message: (kem_ct, aead_codec, nonce, ciphertext+tag) +type SealedParts = (Vec, Codec, Vec, Vec); + +fn is_ml_kem_priv(codec: Codec) -> bool { + codec == Codec::Mlkem768Priv || codec == Codec::Mlkem1024Priv +} + +fn is_ml_kem_pub(codec: Codec) -> bool { + codec == Codec::Mlkem768Pub || codec == Codec::Mlkem1024Pub +} + +/// ML-KEM AEAD codecs allowed (256-bit symmetric for PQ safety) +fn is_ml_kem_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::Xchacha20Poly1305 | Codec::Chacha20Poly1305 | Codec::AesGcm256 + ) +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_ml_kem_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_ml_kem_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != ML_KEM_SEED_LENGTH { + return Err(ConversionsError::SecretKeyFailure("invalid seed length".into()).into()); + } + + let d: [u8; 32] = secret_bytes[..32] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; + let z: [u8; 32] = secret_bytes[32..64] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; + + let (public_key, codec) = match self.mk.codec { + Codec::Mlkem768Priv => { + let (_dk, ek) = MlKem768::generate_deterministic(&d.into(), &z.into()); + (ek.as_bytes().to_vec(), Codec::Mlkem768Pub) + } + Codec::Mlkem1024Priv => { + let (_dk, ek) = MlKem1024::generate_deterministic(&d.into(), &z.into()); + (ek.as_bytes().to_vec(), Codec::Mlkem1024Pub) + } + _ => { + return Err( + ConversionsError::SecretKeyFailure("not an ML-KEM private key".into()).into(), + ); + } + }; + + Builder::new(codec) + .with_comment(&self.mk.comment) + .with_key_bytes(&public_key) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err( + ConversionsError::UnsupportedAlgorithm("ML-KEM not supported in SSH key format".into()) + .into(), + ) + } + fn to_ssh_private_key(&self) -> Result { + Err( + ConversionsError::UnsupportedAlgorithm("ML-KEM not supported in SSH key format".into()) + .into(), + ) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// 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) + .map_err(|_| SealError::InvalidFormat("missing kem ciphertext".into()))?; + let (aead_codec, ptr) = Codec::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing aead codec".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + kem_ct.to_inner(), + aead_codec, + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_ml_kem_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + let mut rng = rand_core_06::OsRng; + + let (kem_ct, shared_secret) = + match self.mk.codec { + Codec::Mlkem768Pub => { + let ek = ::EncapsulationKey::from_bytes( + pub_bytes.as_slice().try_into().map_err(|_| { + SealError::EncapsulationFailed("invalid key size".into()) + })?, + ); + let (ct, ss) = ek.encapsulate(&mut rng).map_err(|_| { + SealError::EncapsulationFailed("encapsulation failed".into()) + })?; + (ct.to_vec(), ss.to_vec()) + } + Codec::Mlkem1024Pub => { + let ek = ::EncapsulationKey::from_bytes( + pub_bytes.as_slice().try_into().map_err(|_| { + SealError::EncapsulationFailed("invalid key size".into()) + })?, + ); + let (ct, ss) = ek.encapsulate(&mut rng).map_err(|_| { + SealError::EncapsulationFailed("encapsulation failed".into()) + })?; + (ct.to_vec(), ss.to_vec()) + } + _ => return Err(SealError::NotEncapsulationKey.into()), + }; + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(&shared_secret, b"ml-kem-seal", key_len)?; + + // 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)) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (kem_ct, aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if !is_ml_kem_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != ML_KEM_SEED_LENGTH { + return Err(ConversionsError::SecretKeyFailure("invalid seed length".into()).into()); + } + + let d: [u8; 32] = secret_bytes[..32] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; + let z: [u8; 32] = secret_bytes[32..64] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; + + let shared_secret = match self.mk.codec { + Codec::Mlkem768Priv => { + let (dk, _ek) = MlKem768::generate_deterministic(&d.into(), &z.into()); + let ct = kem_ct.as_slice().try_into().map_err(|_| { + SealError::DecapsulationFailed("invalid ciphertext size".into()) + })?; + let ss = dk + .decapsulate(&ct) + .map_err(|_| SealError::DecapsulationFailed("decapsulation failed".into()))?; + ss.to_vec() + } + Codec::Mlkem1024Priv => { + let (dk, _ek) = MlKem1024::generate_deterministic(&d.into(), &z.into()); + let ct = kem_ct.as_slice().try_into().map_err(|_| { + SealError::DecapsulationFailed("invalid ciphertext size".into()) + })?; + let ss = dk + .decapsulate(&ct) + .map_err(|_| SealError::DecapsulationFailed("decapsulation failed".into()))?; + ss.to_vec() + } + _ => return Err(SealError::NotDecapsulationKey.into()), + }; + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(&shared_secret, b"ml-kem-seal", key_len)?; + + // Decrypt ciphertext + Ok(aead::aead_open( + aead_codec, &aead_key, &nonce, &ct_tag, aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk::ML_KEM_KEY_CODECS; + use crate::views::Views; + + #[test] + fn test_ml_kem_key_gen_roundtrip() { + for codec in ML_KEM_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test kem key") + .try_build() + .unwrap(); + + let attr = mk.attr_view().unwrap(); + assert!(attr.is_secret_key()); + assert!(!attr.is_public_key()); + + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + // serialize/deserialize roundtrip + let bytes: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } + } + + #[test] + fn test_ml_kem_public_key_derivation() { + for codec in ML_KEM_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let conv = mk.conv_view().unwrap(); + let pk = conv.to_public_key().unwrap(); + + let attr = pk.attr_view().unwrap(); + assert!(attr.is_public_key()); + assert!(!attr.is_secret_key()); + + // derive again and check same result + let pk2 = conv.to_public_key().unwrap(); + assert_eq!(pk, pk2); + } + } + + #[test] + fn test_ml_kem_fingerprint() { + for codec in ML_KEM_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let fp = pk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + let fp_bytes: Vec = fp.into(); + assert!(!fp_bytes.is_empty()); + } + } + + #[test] + fn test_ml_kem_seal_open_roundtrip() { + let aead_codecs = [ + Codec::Xchacha20Poly1305, + Codec::Chacha20Poly1305, + Codec::AesGcm256, + ]; + + for key_codec in ML_KEM_KEY_CODECS { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(key_codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + for aead_codec in &aead_codecs { + let plaintext = b"hello ML-KEM world!"; + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(plaintext, *aead_codec, b"") + .unwrap(); + + let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + } + } + + #[test] + fn test_ml_kem_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let sk2 = Builder::new_from_random_bytes(Codec::Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let (sealed, _) = pk1 + .seal_view() + .unwrap() + .seal(b"secret data", Codec::Xchacha20Poly1305, b"") + .unwrap(); + + // Opening with wrong key should fail (decapsulation will produce different shared secret + // and AEAD open will fail) + assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_ml_kem_seal_requires_public_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + // seal with private key should fail + assert!(sk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err()); + } + + #[test] + fn test_ml_kem_open_requires_private_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .unwrap(); + + // open with public key should fail + assert!(pk.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_ml_kem_unsupported_aead_codec() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + // AES-128-GCM is not allowed for ML-KEM (not PQ-safe) + assert!(pk + .seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err()); + } +} diff --git a/src/views/nist_p.rs b/src/views/nist_p.rs new file mode 100644 index 0000000..0f78c52 --- /dev/null +++ b/src/views/nist_p.rs @@ -0,0 +1,746 @@ +// SPDX-License-Identifier: Apache-2.0 +//! NIST P-256, P-384, P-521 ECDSA multikey view. + +use crate::{ + error::{ + AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, VerifyError, + }, + views::aead, + AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, +}; + +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use multi_util::Varuint; +use elliptic_curve::sec1::ToSec1Point; +use elliptic_curve::Generate; +use zeroize::Zeroizing; + +/// P-256: 32-byte secret +const P256_SECRET_LEN: usize = 32; +/// P-384: 48-byte secret +const P384_SECRET_LEN: usize = 48; +/// P-521: 66-byte secret +const P521_SECRET_LEN: usize = 66; + +fn pub_codec(codec: Codec) -> Codec { + match codec { + Codec::P256Pub | Codec::P256Priv => Codec::P256Pub, + Codec::P384Pub | Codec::P384Priv => Codec::P384Pub, + Codec::P521Pub | Codec::P521Priv => Codec::P521Pub, + _ => codec, + } +} + +fn msig_codec(codec: Codec) -> Codec { + match codec { + Codec::P256Pub | Codec::P256Priv => Codec::Es256Msig, + Codec::P384Pub | Codec::P384Priv => Codec::Es384Msig, + Codec::P521Pub | Codec::P521Priv => Codec::Es521Msig, + _ => Codec::Es256Msig, + } +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + if let Some(v) = self.mk.attributes.get(&AttrId::KeyIsEncrypted) { + if let Ok((b, _)) = Varuint::::try_decode_from(v.as_slice()) { + return b.to_inner(); + } + } + false + } + + fn is_secret_key(&self) -> bool { + matches!( + self.mk.codec, + Codec::P256Priv | Codec::P384Priv | Codec::P521Priv + ) + } + + fn is_public_key(&self) -> bool { + matches!( + self.mk.codec, + Codec::P256Pub | Codec::P384Pub | Codec::P521Pub + ) + } + + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + if self.is_encrypted() { + return Err(AttributesError::EncryptedKey.into()); + } + self.key_bytes() + } +} + +impl<'a> CipherAttrView for View<'a> { + fn cipher_codec(&self) -> Result { + let codec = self + .mk + .attributes + .get(&AttrId::CipherCodec) + .ok_or(CipherError::MissingCodec)?; + Ok(Codec::try_from(codec.as_slice())?) + } + + fn nonce_bytes(&self) -> Result>, Error> { + self.mk + .attributes + .get(&AttrId::CipherNonce) + .ok_or(CipherError::MissingNonce.into()) + .cloned() + } + + fn key_length(&self) -> Result { + let key_length = self + .mk + .attributes + .get(&AttrId::CipherKeyLen) + .ok_or(CipherError::MissingKeyLen)?; + Ok(Varuint::::try_from(key_length.as_slice())?.to_inner()) + } +} + +impl<'a> KdfAttrView for View<'a> { + fn kdf_codec(&self) -> Result { + let codec = self + .mk + .attributes + .get(&AttrId::KdfCodec) + .ok_or(KdfError::MissingCodec)?; + Ok(Codec::try_from(codec.as_slice())?) + } + + fn salt_bytes(&self) -> Result>, Error> { + self.mk + .attributes + .get(&AttrId::KdfSalt) + .ok_or(KdfError::MissingSalt.into()) + .cloned() + } + + fn rounds(&self) -> Result { + let rounds = self + .mk + .attributes + .get(&AttrId::KdfRounds) + .ok_or(KdfError::MissingRounds)?; + Ok(Varuint::::try_from(rounds.as_slice())?.to_inner()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let attr = self.mk.attr_view()?; + if attr.is_secret_key() { + let pk = self.to_public_key()?; + let fp = pk.fingerprint_view()?; + fp.fingerprint(codec) + } else { + let bytes = { + let kd = self.mk.data_view()?; + kd.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, bytes)?.try_build()?) + } + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let pub_bytes = match self.mk.codec { + 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() + } + 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() + } + Codec::P521Priv => { + let sk = p521::ecdsa::SigningKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let vk = p521::ecdsa::VerifyingKey::from(&sk); + vk.as_affine().to_sec1_point(true).as_bytes().to_vec() + } + _ => { + return Err( + ConversionsError::SecretKeyFailure("not a NIST-P secret key".into()).into(), + ) + } + }; + + Builder::new(pub_codec(self.mk.codec)) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + let mut pk = self.mk.clone(); + if self.is_secret_key() { + pk = self.to_public_key()?; + } + + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + // 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) { + Codec::P256Pub => { + let vk = p256::ecdsa::VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let uncompressed = vk.as_affine().to_sec1_point(false); + let ssh_point = + sec1::EncodedPoint::::from_bytes(uncompressed.as_bytes()) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + ssh_key::public::KeyData::Ecdsa(ssh_key::public::EcdsaPublicKey::NistP256( + ssh_point, + )) + } + Codec::P384Pub => { + let vk = p384::ecdsa::VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let uncompressed = vk.as_affine().to_sec1_point(false); + let ssh_point = + sec1::EncodedPoint::::from_bytes(uncompressed.as_bytes()) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + ssh_key::public::KeyData::Ecdsa(ssh_key::public::EcdsaPublicKey::NistP384( + ssh_point, + )) + } + Codec::P521Pub => { + let vk = p521::ecdsa::VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let uncompressed = vk.as_affine().to_sec1_point(false); + let ssh_point = + sec1::EncodedPoint::::from_bytes(uncompressed.as_bytes()) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + ssh_key::public::KeyData::Ecdsa(ssh_key::public::EcdsaPublicKey::NistP521( + ssh_point, + )) + } + _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), + }; + + Ok(ssh_key::PublicKey::new(key_data, pk.comment)) + } + + fn to_ssh_private_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + // Build proper SSH ECDSA keypair + let keypair_data = match self.mk.codec { + Codec::P256Priv => { + let secret_key = p256::SecretKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let vk = secret_key.public_key(); + let uncompressed = vk.as_affine().to_sec1_point(false); + let ssh_point = + sec1::EncodedPoint::::from_bytes(uncompressed.as_bytes()) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let priv_key: ssh_key::private::EcdsaPrivateKey<32> = secret_key.into(); + ssh_key::private::KeypairData::Ecdsa(ssh_key::private::EcdsaKeypair::NistP256 { + public: ssh_point, + private: priv_key, + }) + } + Codec::P384Priv => { + let secret_key = p384::SecretKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let vk = secret_key.public_key(); + let uncompressed = vk.as_affine().to_sec1_point(false); + let ssh_point = + sec1::EncodedPoint::::from_bytes(uncompressed.as_bytes()) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let priv_key: ssh_key::private::EcdsaPrivateKey<48> = secret_key.into(); + ssh_key::private::KeypairData::Ecdsa(ssh_key::private::EcdsaKeypair::NistP384 { + public: ssh_point, + private: priv_key, + }) + } + Codec::P521Priv => { + let secret_key = p521::SecretKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let vk = secret_key.public_key(); + let uncompressed = vk.as_affine().to_sec1_point(false); + let ssh_point = + sec1::EncodedPoint::::from_bytes(uncompressed.as_bytes()) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let priv_key: ssh_key::private::EcdsaPrivateKey<66> = secret_key.into(); + ssh_key::private::KeypairData::Ecdsa(ssh_key::private::EcdsaKeypair::NistP521 { + public: ssh_point, + private: priv_key, + }) + } + _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), + }; + + Ok( + ssh_key::PrivateKey::new(keypair_data, self.mk.comment.clone()) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let sig_bytes = match self.mk.codec { + Codec::P256Priv => { + use p256::ecdsa::{signature::Signer, Signature, SigningKey}; + let sk = SigningKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let signature: Signature = sk + .try_sign(msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + signature.to_bytes().to_vec() + } + Codec::P384Priv => { + use p384::ecdsa::{signature::Signer, Signature, SigningKey}; + let sk = SigningKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let signature: Signature = sk + .try_sign(msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + signature.to_bytes().to_vec() + } + Codec::P521Priv => { + use p521::ecdsa::{signature::Signer, Signature, SigningKey}; + let sk = SigningKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let signature: Signature = sk + .try_sign(msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + signature.to_bytes().to_vec() + } + _ => return Err(SignError::NotSigningKey.into()), + }; + + let sig_codec = msig_codec(self.mk.codec); + let mut ms = ms::Builder::new(sig_codec).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + let sv = multisig.data_view()?; + let sig = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + + let msg = if let Some(msg) = msg { + msg + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + match pubmk.codec { + Codec::P256Pub => { + use p256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + let vk = VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let signature = Signature::from_slice(&sig) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + vk.verify(msg, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + } + Codec::P384Pub => { + use p384::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + let vk = VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let signature = Signature::from_slice(&sig) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + vk.verify(msg, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + } + Codec::P521Pub => { + use p521::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + let vk = VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + let signature = Signature::from_slice(&sig) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + vk.verify(msg, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + } + _ => return Err(ConversionsError::UnsupportedCodec(pubmk.codec).into()), + } + + Ok(()) + } +} + +// ---------------------------------------------------------------------------- +// NIST-P ECIES (ECDH + HKDF + AEAD) — P-256 and P-384. +// +// Mirrors the X25519 ECIES scheme: an ephemeral keypair is generated per seal, +// ECDH against the recipient's public key yields a shared secret, HKDF-SHA512 +// expands it into an AEAD key, and the ephemeral public key is carried +// externally as a Multikey (like X25519). P-521 is intentionally not wired here +// (sign/verify only). +// ---------------------------------------------------------------------------- + +/// HKDF info string binding the derived key to the NIST-P ECIES scheme. +const NISTP_SEAL_INFO: &[u8] = b"nistp-ecies-seal"; + +/// AEAD codecs allowed for NIST-P ECIES sealing. +fn is_nistp_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::AesGcm128 | Codec::AesGcm256 | Codec::Chacha20Poly1305 | Codec::Xchacha20Poly1305 + ) +} + +/// 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`]. +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()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok((aead_codec, nonce.to_inner(), ct_tag.to_inner())) +} + +/// HKDF-expand the ECDH shared secret and AEAD-seal the plaintext. +fn seal_from_shared( + aead_codec: Codec, + shared: &[u8], + plaintext: &[u8], + aad: &[u8], +) -> Result, SealError> { + 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)) +} + +/// HKDF-expand the ECDH shared secret and AEAD-open the ciphertext. +fn open_from_shared( + aead_codec: Codec, + shared: &[u8], + nonce: &[u8], + ct_tag: &[u8], + aad: &[u8], +) -> Result>, SealError> { + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(shared, NISTP_SEAL_INFO, key_len)?; + aead::aead_open(aead_codec, &aead_key, nonce, ct_tag, aad) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_nistp_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + + let (sealed, eph_pub_bytes) = match self.mk.codec { + Codec::P256Pub => { + let recipient = p256::PublicKey::from_sec1_bytes(&pub_bytes) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let eph = p256::ecdh::EphemeralSecret::generate_from_rng(&mut rand::rng()); + let eph_pub = eph.public_key(); + let shared = eph.diffie_hellman(&recipient); + let sealed = seal_from_shared( + aead_codec, + shared.raw_secret_bytes().as_slice(), + plaintext, + aad, + )?; + ( + sealed, + eph_pub.as_affine().to_sec1_point(true).as_bytes().to_vec(), + ) + } + Codec::P384Pub => { + let recipient = p384::PublicKey::from_sec1_bytes(&pub_bytes) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let eph = p384::ecdh::EphemeralSecret::generate_from_rng(&mut rand::rng()); + let eph_pub = eph.public_key(); + let shared = eph.diffie_hellman(&recipient); + let sealed = seal_from_shared( + aead_codec, + shared.raw_secret_bytes().as_slice(), + plaintext, + aad, + )?; + ( + sealed, + eph_pub.as_affine().to_sec1_point(true).as_bytes().to_vec(), + ) + } + Codec::P521Pub => { + let recipient = p521::PublicKey::from_sec1_bytes(&pub_bytes) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + let eph = p521::ecdh::EphemeralSecret::generate_from_rng(&mut rand::rng()); + let eph_pub = eph.public_key(); + let shared = eph.diffie_hellman(&recipient); + let sealed = seal_from_shared( + aead_codec, + shared.raw_secret_bytes().as_slice(), + plaintext, + aad, + )?; + ( + sealed, + eph_pub.as_affine().to_sec1_point(true).as_bytes().to_vec(), + ) + } + _ => return Err(SealError::NotEncapsulationKey.into()), + }; + + let ephemeral_mk = Builder::new(pub_codec(self.mk.codec)) + .with_key_bytes(&eph_pub_bytes) + .try_build()?; + + Ok((sealed, Some(ephemeral_mk))) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let ephemeral_mk = ephemeral.ok_or_else(|| { + SealError::InvalidFormat("NIST-P open requires an ephemeral public key".into()) + })?; + + let (aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + if !is_nistp_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let eph_bytes = ephemeral_mk.data_view()?.key_bytes()?; + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let pt = match self.mk.codec { + Codec::P256Priv => { + let secret = p256::SecretKey::from_slice(&secret_bytes[..P256_SECRET_LEN]) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + let eph_pub = p256::PublicKey::from_sec1_bytes(&eph_bytes) + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let shared = + p256::ecdh::diffie_hellman(secret.to_nonzero_scalar(), eph_pub.as_affine()); + open_from_shared( + aead_codec, + shared.raw_secret_bytes().as_slice(), + &nonce, + &ct_tag, + aad, + )? + } + Codec::P384Priv => { + let secret = p384::SecretKey::from_slice(&secret_bytes[..P384_SECRET_LEN]) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + let eph_pub = p384::PublicKey::from_sec1_bytes(&eph_bytes) + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let shared = + p384::ecdh::diffie_hellman(secret.to_nonzero_scalar(), eph_pub.as_affine()); + open_from_shared( + aead_codec, + shared.raw_secret_bytes().as_slice(), + &nonce, + &ct_tag, + aad, + )? + } + Codec::P521Priv => { + let secret = p521::SecretKey::from_slice(&secret_bytes[..P521_SECRET_LEN]) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + let eph_pub = p521::PublicKey::from_sec1_bytes(&eph_bytes) + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let shared = + p521::ecdh::diffie_hellman(secret.to_nonzero_scalar(), eph_pub.as_affine()); + open_from_shared( + aead_codec, + shared.raw_secret_bytes().as_slice(), + &nonce, + &ct_tag, + aad, + )? + } + _ => return Err(SealError::NotDecapsulationKey.into()), + }; + + Ok(pt) + } +} + +#[cfg(test)] +mod ecies_tests { + use super::*; + use crate::views::Views; + + fn seal_open_roundtrip(priv_codec: Codec) { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(priv_codec, &mut rng) + .unwrap() + .with_comment("nistp ecies test") + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let plaintext = b"the quick brown fox jumps over the lazy dog"; + for aead_codec in [ + Codec::Chacha20Poly1305, + Codec::Xchacha20Poly1305, + Codec::AesGcm128, + Codec::AesGcm256, + ] { + let (sealed, ephemeral) = pk + .seal_view() + .unwrap() + .seal(plaintext, aead_codec, b"") + .unwrap(); + let opened = sk + .open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + } + + #[test] + fn test_p256_seal_open_roundtrip() { + seal_open_roundtrip(Codec::P256Priv); + } + + #[test] + fn test_p384_seal_open_roundtrip() { + seal_open_roundtrip(Codec::P384Priv); + } + + #[test] + fn test_p521_seal_open_roundtrip() { + seal_open_roundtrip(Codec::P521Priv); + } + + #[test] + fn test_p256_wrong_key_fails_to_open() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::P256Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let sk2 = Builder::new_from_random_bytes(Codec::P256Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, ephemeral) = pk1 + .seal_view() + .unwrap() + .seal(b"secret", Codec::Chacha20Poly1305, b"") + .unwrap(); + assert!(sk2 + .open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err()); + } +} diff --git a/src/views/rsa.rs b/src/views/rsa.rs new file mode 100644 index 0000000..0f861e7 --- /dev/null +++ b/src/views/rsa.rs @@ -0,0 +1,489 @@ +// SPDX-License-Identifier: Apache-2.0 +//! RSA-2048/3072/4096 multikey view — signing (PSS SHA-256) + encryption (RSA-OAEP + AEAD). + +use crate::{ + error::{ + AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, VerifyError, + }, + views::{aead, Views}, + AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, +}; + +use ::rsa::sha2::Sha256; +use ::rsa::{ + pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPublicKey}, + pss, Oaep, RsaPrivateKey, RsaPublicKey, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_trait::TryDecodeFrom; +use multi_util::{Varbytes, Varuint}; +use ssh_encoding::{Decode, Encode}; +use zeroize::Zeroizing; + +/// OsRng compatible with rsa 0.10 (rand_core 0.10) using getrandom 0.4 +pub(crate) struct OsRng; + +impl ::rsa::rand_core::TryRng for OsRng { + type Error = core::convert::Infallible; + + fn try_next_u32(&mut self) -> Result { + let mut buf = [0u8; 4]; + getrandom::fill(&mut buf).expect("getrandom failed"); + Ok(u32::from_le_bytes(buf)) + } + + fn try_next_u64(&mut self) -> Result { + let mut buf = [0u8; 8]; + getrandom::fill(&mut buf).expect("getrandom failed"); + Ok(u64::from_le_bytes(buf)) + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> { + getrandom::fill(dest).expect("getrandom failed"); + Ok(()) + } +} + +impl ::rsa::rand_core::TryCryptoRng for OsRng {} + +fn pub_codec(codec: Codec) -> Codec { + match codec { + Codec::Rsa2048Pub | Codec::Rsa2048Priv => Codec::Rsa2048Pub, + Codec::Rsa3072Pub | Codec::Rsa3072Priv => Codec::Rsa3072Pub, + Codec::Rsa4096Pub | Codec::Rsa4096Priv => Codec::Rsa4096Pub, + _ => codec, + } +} + +fn is_priv(codec: Codec) -> bool { + matches!( + codec, + Codec::Rsa2048Priv | Codec::Rsa3072Priv | Codec::Rsa4096Priv + ) +} + +fn is_pub(codec: Codec) -> bool { + matches!( + codec, + Codec::Rsa2048Pub | Codec::Rsa3072Pub | Codec::Rsa4096Pub + ) +} + +const ALGORITHM_NAME: &str = "rsa-sha256@multikey"; + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + if let Some(v) = self.mk.attributes.get(&AttrId::KeyIsEncrypted) { + if let Ok((b, _)) = Varuint::::try_decode_from(v.as_slice()) { + return b.to_inner(); + } + } + false + } + + fn is_secret_key(&self) -> bool { + is_priv(self.mk.codec) + } + + fn is_public_key(&self) -> bool { + is_pub(self.mk.codec) + } + + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + if self.is_encrypted() { + return Err(AttributesError::EncryptedKey.into()); + } + self.key_bytes() + } +} + +impl<'a> CipherAttrView for View<'a> { + fn cipher_codec(&self) -> Result { + let codec = self + .mk + .attributes + .get(&AttrId::CipherCodec) + .ok_or(CipherError::MissingCodec)?; + Ok(Codec::try_from(codec.as_slice())?) + } + + fn nonce_bytes(&self) -> Result>, Error> { + self.mk + .attributes + .get(&AttrId::CipherNonce) + .ok_or(CipherError::MissingNonce.into()) + .cloned() + } + + fn key_length(&self) -> Result { + let key_length = self + .mk + .attributes + .get(&AttrId::CipherKeyLen) + .ok_or(CipherError::MissingKeyLen)?; + Ok(Varuint::::try_from(key_length.as_slice())?.to_inner()) + } +} + +impl<'a> KdfAttrView for View<'a> { + fn kdf_codec(&self) -> Result { + let codec = self + .mk + .attributes + .get(&AttrId::KdfCodec) + .ok_or(KdfError::MissingCodec)?; + Ok(Codec::try_from(codec.as_slice())?) + } + + fn salt_bytes(&self) -> Result>, Error> { + self.mk + .attributes + .get(&AttrId::KdfSalt) + .ok_or(KdfError::MissingSalt.into()) + .cloned() + } + + fn rounds(&self) -> Result { + let rounds = self + .mk + .attributes + .get(&AttrId::KdfRounds) + .ok_or(KdfError::MissingRounds)?; + Ok(Varuint::::try_from(rounds.as_slice())?.to_inner()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let attr = self.mk.attr_view()?; + if attr.is_secret_key() { + let pk = self.to_public_key()?; + let fp = pk.fingerprint_view()?; + fp.fingerprint(codec) + } else { + let bytes = { + let kd = self.mk.data_view()?; + kd.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, bytes)?.try_build()?) + } + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let private_key = RsaPrivateKey::from_pkcs1_der(&secret_bytes) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + let public_key = private_key.to_public_key(); + let pub_der = public_key + .to_pkcs1_der() + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + + Builder::new(pub_codec(self.mk.codec)) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_der) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + let mut pk = self.mk.clone(); + if self.is_secret_key() { + pk = self.to_public_key()?; + } + + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + let mut buff: Vec = Vec::new(); + key_bytes + .encode(&mut buff) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_key_bytes = ssh_key::public::OpaquePublicKeyBytes::decode(&mut buff.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PublicKey::new( + ssh_key::public::KeyData::Other(ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(ALGORITHM_NAME) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_key_bytes, + }), + pk.comment, + )) + } + + fn to_ssh_private_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let mut buf: Vec = Vec::new(); + secret_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_private_key_bytes = + ssh_key::private::OpaquePrivateKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + let pk = self.to_public_key()?; + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + buf.clear(); + key_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_public_key_bytes = + ssh_key::public::OpaquePublicKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Other(ssh_key::private::OpaqueKeypair { + public: ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(ALGORITHM_NAME) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_public_key_bytes, + }, + private: opaque_private_key_bytes, + }), + self.mk.comment.clone(), + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} + +impl<'a> SignView for View<'a> { + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let private_key = RsaPrivateKey::from_pkcs1_der(&secret_bytes) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + + use ::rsa::signature::RandomizedSigner; + let signing_key = pss::SigningKey::::new(private_key); + let signature = signing_key + .try_sign_with_rng(&mut OsRng, msg) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + + use ::rsa::signature::SignatureEncoding; + let sig_bytes = signature.to_bytes(); + + let mut ms = ms::Builder::new(Codec::Rs256Msig).with_signature_bytes(&sig_bytes); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + let public_key = RsaPublicKey::from_pkcs1_der(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + + let sv = multisig.data_view()?; + let sig = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + + let msg = if let Some(msg) = msg { + msg + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + use ::rsa::signature::Verifier; + let verifying_key = pss::VerifyingKey::::new(public_key); + let signature = pss::Signature::try_from(sig.as_slice()) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + + verifying_key + .verify(msg, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + + Ok(()) + } +} + +// --- Seal / Open: RSA-OAEP + AEAD hybrid encryption --- + +/// Allowed AEAD codecs for RSA hybrid encryption +fn is_rsa_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::AesGcm128 | Codec::AesGcm256 | Codec::Chacha20Poly1305 | Codec::Xchacha20Poly1305 + ) +} + +/// 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); + +/// Decode sealed message +fn decode_sealed(data: &[u8]) -> Result { + let (rsa_ct, ptr) = Varbytes::try_decode_from(data) + .map_err(|_| SealError::InvalidFormat("missing RSA-OAEP ciphertext".into()))?; + let (aead_codec, ptr) = Codec::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing AEAD codec".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + rsa_ct.to_inner(), + aead_codec, + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_rsa_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + let public_key = RsaPublicKey::from_pkcs1_der(&pub_bytes) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + + // Generate random AEAD key + let key_len = aead::key_size(aead_codec)?; + let mut aead_key = vec![0u8; key_len]; + use ::rsa::rand_core::Rng; + OsRng.fill_bytes(&mut aead_key); + + // Encrypt AEAD key with RSA-OAEP + let padding = Oaep::::new(); + let rsa_ct = public_key + .encrypt(&mut OsRng, padding, &aead_key) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + + // 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)) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (rsa_ct, aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if !is_rsa_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let private_key = RsaPrivateKey::from_pkcs1_der(&secret_bytes) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + + // Decrypt AEAD key with RSA-OAEP + let padding = Oaep::::new(); + let aead_key = private_key + .decrypt(padding, &rsa_ct) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + + // Decrypt ciphertext with AEAD + Ok(aead::aead_open( + aead_codec, &aead_key, &nonce, &ct_tag, aad, + )?) + } +} diff --git a/src/views/secp256k1.rs b/src/views/secp256k1.rs index d55ec41..27df66b 100644 --- a/src/views/secp256k1.rs +++ b/src/views/secp256k1.rs @@ -1,19 +1,24 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ - error::{AttributesError, CipherError, ConversionsError, KdfError, SignError, VerifyError}, + error::{ + AttributesError, CipherError, ConversionsError, KdfError, SealError, SignError, VerifyError, + }, + views::aead, AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, - KdfAttrView, Multikey, SignView, VerifyView, Views, + KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_trait::TryDecodeFrom; +use multi_util::{Varbytes, Varuint}; +use elliptic_curve::sec1::ToSec1Point; +use elliptic_curve::Generate; use k256::ecdsa::{ signature::{Signer, Verifier}, Signature, SigningKey, VerifyingKey, }; -use multicodec::Codec; -use multihash::{mh, Multihash}; -use multisig::{ms, Multisig, Views as SigViews}; -use multitrait::TryDecodeFrom; -use multiutil::Varuint; use ssh_encoding::{Decode, Encode}; use zeroize::Zeroizing; @@ -160,7 +165,7 @@ impl<'a> FingerprintView for View<'a> { // get the key bytes let bytes = { let kd = self.mk.data_view()?; - + kd.key_bytes()? }; // hash the key bytes using the given codec @@ -175,7 +180,7 @@ impl<'a> ConvView for View<'a> { // get the secret key bytes let secret_bytes = { let kd = self.mk.data_view()?; - + kd.secret_bytes()? }; @@ -204,7 +209,7 @@ impl<'a> ConvView for View<'a> { let key_bytes = { let kd = pk.data_view()?; - + kd.key_bytes()? }; @@ -231,7 +236,7 @@ impl<'a> ConvView for View<'a> { fn to_ssh_private_key(&self) -> Result { let secret_bytes = { let kd = self.mk.data_view()?; - + kd.secret_bytes()? }; @@ -246,7 +251,7 @@ impl<'a> ConvView for View<'a> { let pk = self.to_public_key()?; let key_bytes = { let kd = pk.data_view()?; - + kd.key_bytes()? }; @@ -286,7 +291,7 @@ impl<'a> SignView for View<'a> { // get the secret key bytes let secret_bytes = { let kd = self.mk.data_view()?; - + kd.secret_bytes()? }; @@ -297,7 +302,7 @@ impl<'a> SignView for View<'a> { .map_err(|_| { ConversionsError::SecretKeyFailure("failed to get secret key bytes".to_string()) })?; - + SigningKey::from_bytes(&bytes.into()) .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))? }; @@ -322,7 +327,7 @@ impl<'a> VerifyView for View<'a> { let attr = self.mk.attr_view()?; let pubmk = if attr.is_secret_key() { let kc = self.mk.conv_view()?; - + kc.to_public_key()? } else { self.mk.clone() @@ -331,7 +336,7 @@ impl<'a> VerifyView for View<'a> { // get the secret key bytes let key_bytes = { let kd = pubmk.data_view()?; - + kd.key_bytes()? }; @@ -371,3 +376,192 @@ impl<'a> VerifyView for View<'a> { Ok(()) } } + +// ---------------------------------------------------------------------------- +// secp256k1 ECIES (ECDH + HKDF + AEAD). +// +// Same scheme as the NIST-P and X25519 ECIES paths: an ephemeral keypair is +// generated per seal, ECDH against the recipient's public key yields a shared +// secret, HKDF-SHA512 expands it into an AEAD key, and the ephemeral public key +// is carried externally as a Multikey. +// ---------------------------------------------------------------------------- + +/// HKDF info string binding the derived key to the secp256k1 ECIES scheme. +const SECP256K1_SEAL_INFO: &[u8] = b"secp256k1-ecies-seal"; + +/// AEAD codecs allowed for secp256k1 ECIES sealing. +fn is_secp_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::AesGcm128 | Codec::AesGcm256 | Codec::Chacha20Poly1305 | Codec::Xchacha20Poly1305 + ) +} + +/// 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`]. +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()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok((aead_codec, nonce.to_inner(), ct_tag.to_inner())) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_secp_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + let recipient = k256::PublicKey::from_sec1_bytes(&pub_bytes) + .map_err(|e| SealError::EncapsulationFailed(e.to_string()))?; + + let eph = k256::ecdh::EphemeralSecret::generate_from_rng(&mut rand::rng()); + let eph_pub = eph.public_key(); + let shared = eph.diffie_hellman(&recipient); + + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key( + shared.raw_secret_bytes().as_slice(), + SECP256K1_SEAL_INFO, + key_len, + )?; + let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; + + let eph_pub_bytes = eph_pub.to_sec1_point(true).as_bytes().to_vec(); + let ephemeral_mk = Builder::new(Codec::Secp256K1Pub) + .with_key_bytes(&eph_pub_bytes) + .try_build()?; + + Ok(( + encode_sealed(aead_codec, &nonce, &ct_tag), + Some(ephemeral_mk), + )) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let ephemeral_mk = ephemeral.ok_or_else(|| { + SealError::InvalidFormat("secp256k1 open requires an ephemeral public key".into()) + })?; + + let (aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + if !is_secp_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let eph_bytes = ephemeral_mk.data_view()?.key_bytes()?; + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let secret = k256::SecretKey::from_slice(&secret_bytes[..SECRET_KEY_LENGTH]) + .map_err(|e| SealError::DecapsulationFailed(e.to_string()))?; + let eph_pub = k256::PublicKey::from_sec1_bytes(&eph_bytes) + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let shared = k256::ecdh::diffie_hellman(secret.to_nonzero_scalar(), eph_pub.as_affine()); + + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key( + shared.raw_secret_bytes().as_slice(), + SECP256K1_SEAL_INFO, + key_len, + )?; + + Ok(aead::aead_open( + aead_codec, &aead_key, &nonce, &ct_tag, aad, + )?) + } +} + +#[cfg(test)] +mod ecies_tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_secp256k1_seal_open_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Secp256K1Priv, &mut rng) + .unwrap() + .with_comment("secp256k1 ecies test") + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let plaintext = b"the quick brown fox jumps over the lazy dog"; + for aead_codec in [ + Codec::Chacha20Poly1305, + Codec::Xchacha20Poly1305, + Codec::AesGcm128, + Codec::AesGcm256, + ] { + let (sealed, ephemeral) = pk + .seal_view() + .unwrap() + .seal(plaintext, aead_codec, b"") + .unwrap(); + let opened = sk + .open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + } + + #[test] + fn test_secp256k1_wrong_key_fails_to_open() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::Secp256K1Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let sk2 = Builder::new_from_random_bytes(Codec::Secp256K1Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, ephemeral) = pk1 + .seal_view() + .unwrap() + .seal(b"secret", Codec::Chacha20Poly1305, b"") + .unwrap(); + assert!(sk2 + .open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err()); + } +} diff --git a/src/views/slh_dsa.rs b/src/views/slh_dsa.rs new file mode 100644 index 0000000..66f549a --- /dev/null +++ b/src/views/slh_dsa.rs @@ -0,0 +1,661 @@ +// SPDX-License-Identifier: Apache-2.0 +//! SLH-DSA multikey view; FIPS 205. Supports all 12 parameter sets (Sha2_128f/s through Shake256f/s). + +use crate::{ + error::{AttributesError, ConversionsError, SignError, VerifyError}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, + VerifyView, Views, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::ms; +use multi_sig::Views as _; +use slh_dsa::signature::{Keypair, Signer, Verifier}; +use slh_dsa::ParameterSet; +use slh_dsa::{ + Sha2_128f, Sha2_128s, Sha2_192f, Sha2_192s, Sha2_256f, Sha2_256s, Shake128f, Shake128s, + Shake192f, Shake192s, Shake256f, Shake256s, Signature, SigningKey, VerifyingKey, +}; +use ssh_encoding::{Decode, Encode}; +use zeroize::Zeroizing; + +/// SSH algorithm name for SLH-DSA-SHA2-128f +pub const ALGORITHM_NAME_SHA2_128F: &str = "slh-dsa-sha2-128f@multikey"; +/// SSH algorithm name for SLH-DSA-SHA2-128s +pub const ALGORITHM_NAME_SHA2_128S: &str = "slh-dsa-sha2-128s@multikey"; +/// SSH algorithm name for SLH-DSA-SHA2-192f +pub const ALGORITHM_NAME_SHA2_192F: &str = "slh-dsa-sha2-192f@multikey"; +/// SSH algorithm name for SLH-DSA-SHA2-192s +pub const ALGORITHM_NAME_SHA2_192S: &str = "slh-dsa-sha2-192s@multikey"; +/// SSH algorithm name for SLH-DSA-SHA2-256f +pub const ALGORITHM_NAME_SHA2_256F: &str = "slh-dsa-sha2-256f@multikey"; +/// SSH algorithm name for SLH-DSA-SHA2-256s +pub const ALGORITHM_NAME_SHA2_256S: &str = "slh-dsa-sha2-256s@multikey"; +/// SSH algorithm name for SLH-DSA-SHAKE-128f +pub const ALGORITHM_NAME_SHAKE_128F: &str = "slh-dsa-shake-128f@multikey"; +/// SSH algorithm name for SLH-DSA-SHAKE-128s +pub const ALGORITHM_NAME_SHAKE_128S: &str = "slh-dsa-shake-128s@multikey"; +/// SSH algorithm name for SLH-DSA-SHAKE-192f +pub const ALGORITHM_NAME_SHAKE_192F: &str = "slh-dsa-shake-192f@multikey"; +/// SSH algorithm name for SLH-DSA-SHAKE-192s +pub const ALGORITHM_NAME_SHAKE_192S: &str = "slh-dsa-shake-192s@multikey"; +/// SSH algorithm name for SLH-DSA-SHAKE-256f +pub const ALGORITHM_NAME_SHAKE_256F: &str = "slh-dsa-shake-256f@multikey"; +/// SSH algorithm name for SLH-DSA-SHAKE-256s +pub const ALGORITHM_NAME_SHAKE_256S: &str = "slh-dsa-shake-256s@multikey"; + +/// Generate SLH-DSA key bytes for the given parameter set. Used by Builder::new_from_random_bytes. +pub(crate) fn gen_slh_dsa_key

() -> Vec +where + P: ParameterSet, +{ + // slh-dsa 0.2.0-rc.5 uses rand_core 0.10 CryptoRng; use getrandom 0.4 SysRng. + use getrandom::rand_core::UnwrapErr; + let mut rng = UnwrapErr(getrandom::SysRng); + SigningKey::

::new(&mut rng).to_vec() +} + +fn is_slh_dsa_priv(codec: Codec) -> bool { + matches!( + codec, + Codec::SlhDsaSha2128FPriv + | Codec::SlhDsaSha2128SPriv + | Codec::SlhDsaSha2192FPriv + | Codec::SlhDsaSha2192SPriv + | Codec::SlhDsaSha2256FPriv + | Codec::SlhDsaSha2256SPriv + | Codec::SlhDsaShake128FPriv + | Codec::SlhDsaShake128SPriv + | Codec::SlhDsaShake192FPriv + | Codec::SlhDsaShake192SPriv + | Codec::SlhDsaShake256FPriv + | Codec::SlhDsaShake256SPriv + ) +} + +fn is_slh_dsa_pub(codec: Codec) -> bool { + matches!( + codec, + Codec::SlhDsaSha2128FPub + | Codec::SlhDsaSha2128SPub + | Codec::SlhDsaSha2192FPub + | Codec::SlhDsaSha2192SPub + | Codec::SlhDsaSha2256FPub + | Codec::SlhDsaSha2256SPub + | Codec::SlhDsaShake128FPub + | Codec::SlhDsaShake128SPub + | Codec::SlhDsaShake192FPub + | Codec::SlhDsaShake192SPub + | Codec::SlhDsaShake256FPub + | Codec::SlhDsaShake256SPub + ) +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_slh_dsa_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_slh_dsa_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let (public_key, codec) = match self.mk.codec { + Codec::SlhDsaSha2128FPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaSha2128FPub, + ), + Codec::SlhDsaSha2128SPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaSha2128SPub, + ), + Codec::SlhDsaSha2192FPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaSha2192FPub, + ), + Codec::SlhDsaSha2192SPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaSha2192SPub, + ), + Codec::SlhDsaSha2256FPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaSha2256FPub, + ), + Codec::SlhDsaSha2256SPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaSha2256SPub, + ), + Codec::SlhDsaShake128FPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaShake128FPub, + ), + Codec::SlhDsaShake128SPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaShake128SPub, + ), + Codec::SlhDsaShake192FPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaShake192FPub, + ), + Codec::SlhDsaShake192SPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaShake192SPub, + ), + Codec::SlhDsaShake256FPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaShake256FPub, + ), + Codec::SlhDsaShake256SPriv => ( + SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })? + .verifying_key() + .to_vec(), + Codec::SlhDsaShake256SPub, + ), + _ => { + return Err( + ConversionsError::SecretKeyFailure("invalid SLH-DSA codec".into()).into(), + ); + } + }; + Builder::new(codec) + .with_comment(&self.mk.comment) + .with_key_bytes(&public_key) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + let mut pk = self.mk.clone(); + if self.is_secret_key() { + pk = self.to_public_key()?; + } + + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + // Determine algorithm name based on codec + let algorithm_name = match pk.codec { + Codec::SlhDsaSha2128FPub => ALGORITHM_NAME_SHA2_128F, + Codec::SlhDsaSha2128SPub => ALGORITHM_NAME_SHA2_128S, + Codec::SlhDsaSha2192FPub => ALGORITHM_NAME_SHA2_192F, + Codec::SlhDsaSha2192SPub => ALGORITHM_NAME_SHA2_192S, + Codec::SlhDsaSha2256FPub => ALGORITHM_NAME_SHA2_256F, + Codec::SlhDsaSha2256SPub => ALGORITHM_NAME_SHA2_256S, + Codec::SlhDsaShake128FPub => ALGORITHM_NAME_SHAKE_128F, + Codec::SlhDsaShake128SPub => ALGORITHM_NAME_SHAKE_128S, + Codec::SlhDsaShake192FPub => ALGORITHM_NAME_SHAKE_192F, + Codec::SlhDsaShake192SPub => ALGORITHM_NAME_SHAKE_192S, + Codec::SlhDsaShake256FPub => ALGORITHM_NAME_SHAKE_256F, + Codec::SlhDsaShake256SPub => ALGORITHM_NAME_SHAKE_256S, + _ => return Err(ConversionsError::UnsupportedCodec(pk.codec).into()), + }; + + let mut buff: Vec = Vec::new(); + key_bytes + .encode(&mut buff) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_key_bytes = ssh_key::public::OpaquePublicKeyBytes::decode(&mut buff.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PublicKey::new( + ssh_key::public::KeyData::Other(ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_key_bytes, + }), + pk.comment, + )) + } + + fn to_ssh_private_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + // Determine algorithm name based on codec + let algorithm_name = match self.mk.codec { + Codec::SlhDsaSha2128FPriv => ALGORITHM_NAME_SHA2_128F, + Codec::SlhDsaSha2128SPriv => ALGORITHM_NAME_SHA2_128S, + Codec::SlhDsaSha2192FPriv => ALGORITHM_NAME_SHA2_192F, + Codec::SlhDsaSha2192SPriv => ALGORITHM_NAME_SHA2_192S, + Codec::SlhDsaSha2256FPriv => ALGORITHM_NAME_SHA2_256F, + Codec::SlhDsaSha2256SPriv => ALGORITHM_NAME_SHA2_256S, + Codec::SlhDsaShake128FPriv => ALGORITHM_NAME_SHAKE_128F, + Codec::SlhDsaShake128SPriv => ALGORITHM_NAME_SHAKE_128S, + Codec::SlhDsaShake192FPriv => ALGORITHM_NAME_SHAKE_192F, + Codec::SlhDsaShake192SPriv => ALGORITHM_NAME_SHAKE_192S, + Codec::SlhDsaShake256FPriv => ALGORITHM_NAME_SHAKE_256F, + Codec::SlhDsaShake256SPriv => ALGORITHM_NAME_SHAKE_256S, + _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), + }; + + let mut buf: Vec = Vec::new(); + secret_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_private_key_bytes = + ssh_key::private::OpaquePrivateKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + let pk = self.to_public_key()?; + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + buf.clear(); + key_bytes + .encode(&mut buf) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + let opaque_public_key_bytes = + ssh_key::public::OpaquePublicKeyBytes::decode(&mut buf.as_slice()) + .map_err(|e| ConversionsError::Ssh(e.into()))?; + + Ok(ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Other(ssh_key::private::OpaqueKeypair { + public: ssh_key::public::OpaquePublicKey { + algorithm: ssh_key::Algorithm::Other( + ssh_key::AlgorithmName::new(algorithm_name) + .map_err(|e| ConversionsError::Ssh(e.into()))?, + ), + key: opaque_public_key_bytes, + }, + private: opaque_private_key_bytes, + }), + self.mk.comment.clone(), + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let attr = self.mk.attr_view()?; + if attr.is_secret_key() { + let pk = self.mk.conv_view()?.to_public_key()?; + return pk.fingerprint_view()?.fingerprint(codec); + } + let bytes = self.key_bytes()?; + Ok(mh::Builder::new_from_bytes(codec, bytes.as_slice())?.try_build()?) + } +} + +impl<'a> SignView for View<'a> { + fn sign( + &self, + msg: &[u8], + combined: bool, + _scheme: Option, + ) -> Result { + let attr = self.mk.attr_view()?; + if !attr.is_secret_key() { + return Err(SignError::NotSigningKey.into()); + } + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + let (signature, codec) = match self.mk.codec { + Codec::SlhDsaSha2128FPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2128FMsig) + } + Codec::SlhDsaSha2128SPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2128SMsig) + } + Codec::SlhDsaSha2192FPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2192FMsig) + } + Codec::SlhDsaSha2192SPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2192SMsig) + } + Codec::SlhDsaSha2256FPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2256FMsig) + } + Codec::SlhDsaSha2256SPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2256SMsig) + } + Codec::SlhDsaShake128FPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake128FMsig) + } + Codec::SlhDsaShake128SPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake128SMsig) + } + Codec::SlhDsaShake192FPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake192FMsig) + } + Codec::SlhDsaShake192SPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake192SMsig) + } + Codec::SlhDsaShake256FPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake256FMsig) + } + Codec::SlhDsaShake256SPriv => { + let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) + .map_err(|_| { + ConversionsError::SecretKeyFailure("invalid secret key length".into()) + })?; + (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake256SMsig) + } + _ => { + return Err( + ConversionsError::SecretKeyFailure("invalid secret key length".into()).into(), + ); + } + }; + let mut ms = ms::Builder::new(codec).with_signature_bytes(&signature); + if combined { + ms = ms.with_message_bytes(&msg); + } + Ok(ms.try_build()?) + } +} + +impl<'a> VerifyView for View<'a> { + fn verify(&self, sig: &multi_sig::Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let msg_bytes = if let Some(m) = msg { + m + } else if !sig.message.is_empty() { + sig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + let sv = sig.data_view()?; + let sig_bytes = sv.sig_bytes()?; + + match self.mk.codec { + Codec::SlhDsaSha2128FPriv | Codec::SlhDsaSha2128FPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaSha2128SPriv | Codec::SlhDsaSha2128SPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaSha2192FPriv | Codec::SlhDsaSha2192FPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaSha2192SPriv | Codec::SlhDsaSha2192SPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaSha2256FPriv | Codec::SlhDsaSha2256FPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaSha2256SPriv | Codec::SlhDsaSha2256SPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaShake128FPriv | Codec::SlhDsaShake128FPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaShake128SPriv | Codec::SlhDsaShake128SPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaShake192FPriv | Codec::SlhDsaShake192FPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaShake192SPriv | Codec::SlhDsaShake192SPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaShake256FPriv | Codec::SlhDsaShake256FPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + Codec::SlhDsaShake256SPriv | Codec::SlhDsaShake256SPub => { + let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) + .map_err(|_| { + ConversionsError::PublicKeyFailure("invalid public key length".into()) + })?; + let signature = Signature::try_from(sig_bytes.as_slice()) + .map_err(|_| VerifyError::BadSignature("invalid signature length".into()))?; + verifying_key + .verify(msg_bytes, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) + } + _ => Err(ConversionsError::PublicKeyFailure("invalid public key length".into()).into()), + } + } +} diff --git a/src/views/sntrup.rs b/src/views/sntrup.rs new file mode 100644 index 0000000..fa8cfb1 --- /dev/null +++ b/src/views/sntrup.rs @@ -0,0 +1,580 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Streamlined NTRU Prime multikey view; post-quantum KEM for all supported sizes. +//! +//! Supports sntrup761, sntrup857, sntrup953, sntrup1013, and sntrup1277. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use zeroize::Zeroizing; + +const SNTRUP_SEED_LENGTH: usize = 32; + +/// Compatibility wrapper: allows `rand 0.8` OsRng to satisfy `rand_core 0.10` +/// `CryptoRng` required by the `sntrup` crate. +struct OsRng010; + +impl rand_core::TryRng for OsRng010 { + type Error = core::convert::Infallible; + + fn try_next_u32(&mut self) -> Result { + Ok(rand_core::Rng::next_u32(&mut rand::rng())) + } + fn try_next_u64(&mut self) -> Result { + Ok(rand_core::Rng::next_u64(&mut rand::rng())) + } + fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> { + rand_core::Rng::fill_bytes(&mut rand::rng(), dst); + Ok(()) + } +} +impl rand_core::TryCryptoRng for OsRng010 {} + +/// Decoded sealed message: (kem_ct, aead_codec, nonce, ciphertext+tag) +type SealedParts = (Vec, Codec, Vec, Vec); + +fn is_sntrup_priv(codec: Codec) -> bool { + matches!( + codec, + Codec::Sntrup761Priv + | Codec::Sntrup857Priv + | Codec::Sntrup953Priv + | Codec::Sntrup1013Priv + | Codec::Sntrup1277Priv + ) +} + +fn is_sntrup_pub(codec: Codec) -> bool { + matches!( + codec, + Codec::Sntrup761Pub + | Codec::Sntrup857Pub + | Codec::Sntrup953Pub + | Codec::Sntrup1013Pub + | Codec::Sntrup1277Pub + ) +} + +/// SNTRUP AEAD codecs allowed (256-bit symmetric for PQ safety) +fn is_sntrup_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::Xchacha20Poly1305 | Codec::Chacha20Poly1305 | Codec::AesGcm256 + ) +} + +/// Return the corresponding public codec for a private codec. +fn pub_codec(priv_codec: Codec) -> Result { + match priv_codec { + Codec::Sntrup761Priv => Ok(Codec::Sntrup761Pub), + Codec::Sntrup857Priv => Ok(Codec::Sntrup857Pub), + Codec::Sntrup953Priv => Ok(Codec::Sntrup953Pub), + Codec::Sntrup1013Priv => Ok(Codec::Sntrup1013Pub), + Codec::Sntrup1277Priv => Ok(Codec::Sntrup1277Pub), + _ => Err(ConversionsError::SecretKeyFailure("not an sntrup private key".into()).into()), + } +} + +/// Return the HKDF info label for domain separation per size. +fn seal_info(codec: Codec) -> &'static [u8] { + match codec { + Codec::Sntrup761Pub | Codec::Sntrup761Priv => b"sntrup761-seal", + Codec::Sntrup857Pub | Codec::Sntrup857Priv => b"sntrup857-seal", + Codec::Sntrup953Pub | Codec::Sntrup953Priv => b"sntrup953-seal", + Codec::Sntrup1013Pub | Codec::Sntrup1013Priv => b"sntrup1013-seal", + Codec::Sntrup1277Pub | Codec::Sntrup1277Priv => b"sntrup1277-seal", + _ => b"sntrup-seal", + } +} + +/// Generate a public key from a seed for the given private codec. +fn generate_public_key(priv_codec: Codec, seed: &[u8; 32]) -> Result, Error> { + match priv_codec { + Codec::Sntrup761Priv => { + let (ek, _dk) = sntrup::sntrup761::generate_key_deterministic(seed); + Ok(ek.as_ref().to_vec()) + } + Codec::Sntrup857Priv => { + let (ek, _dk) = sntrup::sntrup857::generate_key_deterministic(seed); + Ok(ek.as_ref().to_vec()) + } + Codec::Sntrup953Priv => { + let (ek, _dk) = sntrup::sntrup953::generate_key_deterministic(seed); + Ok(ek.as_ref().to_vec()) + } + Codec::Sntrup1013Priv => { + let (ek, _dk) = sntrup::sntrup1013::generate_key_deterministic(seed); + Ok(ek.as_ref().to_vec()) + } + Codec::Sntrup1277Priv => { + let (ek, _dk) = sntrup::sntrup1277::generate_key_deterministic(seed); + Ok(ek.as_ref().to_vec()) + } + _ => Err(ConversionsError::SecretKeyFailure("not an sntrup private key".into()).into()), + } +} + +/// Encapsulate with a public key, returning (ciphertext_bytes, shared_secret_bytes). +fn kem_encapsulate(pub_codec: Codec, pub_bytes: &[u8]) -> Result<(Vec, Vec), Error> { + let mut rng = OsRng010; + match pub_codec { + Codec::Sntrup761Pub => { + let ek = sntrup::sntrup761::EncapsulationKey::try_from(pub_bytes) + .map_err(|_| SealError::EncapsulationFailed("invalid key size".into()))?; + let (ct, ss) = ek.encapsulate(&mut rng); + Ok((ct.as_ref().to_vec(), ss.as_ref().to_vec())) + } + Codec::Sntrup857Pub => { + let ek = sntrup::sntrup857::EncapsulationKey::try_from(pub_bytes) + .map_err(|_| SealError::EncapsulationFailed("invalid key size".into()))?; + let (ct, ss) = ek.encapsulate(&mut rng); + Ok((ct.as_ref().to_vec(), ss.as_ref().to_vec())) + } + Codec::Sntrup953Pub => { + let ek = sntrup::sntrup953::EncapsulationKey::try_from(pub_bytes) + .map_err(|_| SealError::EncapsulationFailed("invalid key size".into()))?; + let (ct, ss) = ek.encapsulate(&mut rng); + Ok((ct.as_ref().to_vec(), ss.as_ref().to_vec())) + } + Codec::Sntrup1013Pub => { + let ek = sntrup::sntrup1013::EncapsulationKey::try_from(pub_bytes) + .map_err(|_| SealError::EncapsulationFailed("invalid key size".into()))?; + let (ct, ss) = ek.encapsulate(&mut rng); + Ok((ct.as_ref().to_vec(), ss.as_ref().to_vec())) + } + Codec::Sntrup1277Pub => { + let ek = sntrup::sntrup1277::EncapsulationKey::try_from(pub_bytes) + .map_err(|_| SealError::EncapsulationFailed("invalid key size".into()))?; + let (ct, ss) = ek.encapsulate(&mut rng); + Ok((ct.as_ref().to_vec(), ss.as_ref().to_vec())) + } + _ => Err(SealError::NotEncapsulationKey.into()), + } +} + +/// Decapsulate with a private seed, returning shared_secret_bytes. +fn kem_decapsulate(priv_codec: Codec, seed: &[u8; 32], ct_bytes: &[u8]) -> Result, Error> { + match priv_codec { + Codec::Sntrup761Priv => { + let (_ek, dk) = sntrup::sntrup761::generate_key_deterministic(seed); + let ct = sntrup::sntrup761::Ciphertext::try_from(ct_bytes) + .map_err(|_| SealError::DecapsulationFailed("invalid ciphertext size".into()))?; + let ss = dk.decapsulate(&ct); + Ok(ss.as_ref().to_vec()) + } + Codec::Sntrup857Priv => { + let (_ek, dk) = sntrup::sntrup857::generate_key_deterministic(seed); + let ct = sntrup::sntrup857::Ciphertext::try_from(ct_bytes) + .map_err(|_| SealError::DecapsulationFailed("invalid ciphertext size".into()))?; + let ss = dk.decapsulate(&ct); + Ok(ss.as_ref().to_vec()) + } + Codec::Sntrup953Priv => { + let (_ek, dk) = sntrup::sntrup953::generate_key_deterministic(seed); + let ct = sntrup::sntrup953::Ciphertext::try_from(ct_bytes) + .map_err(|_| SealError::DecapsulationFailed("invalid ciphertext size".into()))?; + let ss = dk.decapsulate(&ct); + Ok(ss.as_ref().to_vec()) + } + Codec::Sntrup1013Priv => { + let (_ek, dk) = sntrup::sntrup1013::generate_key_deterministic(seed); + let ct = sntrup::sntrup1013::Ciphertext::try_from(ct_bytes) + .map_err(|_| SealError::DecapsulationFailed("invalid ciphertext size".into()))?; + let ss = dk.decapsulate(&ct); + Ok(ss.as_ref().to_vec()) + } + Codec::Sntrup1277Priv => { + let (_ek, dk) = sntrup::sntrup1277::generate_key_deterministic(seed); + let ct = sntrup::sntrup1277::Ciphertext::try_from(ct_bytes) + .map_err(|_| SealError::DecapsulationFailed("invalid ciphertext size".into()))?; + let ss = dk.decapsulate(&ct); + Ok(ss.as_ref().to_vec()) + } + _ => Err(SealError::NotDecapsulationKey.into()), + } +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_sntrup_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_sntrup_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != SNTRUP_SEED_LENGTH { + return Err(ConversionsError::SecretKeyFailure("invalid seed length".into()).into()); + } + + let seed: [u8; 32] = secret_bytes[..32] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; + + let pub_bytes = generate_public_key(self.mk.codec, &seed)?; + let codec = pub_codec(self.mk.codec)?; + + Builder::new(codec) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err( + ConversionsError::UnsupportedAlgorithm("sntrup not supported in SSH key format".into()) + .into(), + ) + } + fn to_ssh_private_key(&self) -> Result { + Err( + ConversionsError::UnsupportedAlgorithm("sntrup not supported in SSH key format".into()) + .into(), + ) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// 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) + .map_err(|_| SealError::InvalidFormat("missing kem ciphertext".into()))?; + let (aead_codec, ptr) = Codec::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing aead codec".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + kem_ct.to_inner(), + aead_codec, + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_sntrup_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + + let (kem_ct, shared_secret) = kem_encapsulate(self.mk.codec, &pub_bytes)?; + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let info = seal_info(self.mk.codec); + let aead_key = aead::derive_aead_key(&shared_secret, info, key_len)?; + + // 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)) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (kem_ct, aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if !is_sntrup_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != SNTRUP_SEED_LENGTH { + return Err(ConversionsError::SecretKeyFailure("invalid seed length".into()).into()); + } + + let seed: [u8; 32] = secret_bytes[..32] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid seed".into()))?; + + let shared_secret = kem_decapsulate(self.mk.codec, &seed, &kem_ct)?; + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let info = seal_info(self.mk.codec); + let aead_key = aead::derive_aead_key(&shared_secret, info, key_len)?; + + // Decrypt ciphertext + Ok(aead::aead_open( + aead_codec, &aead_key, &nonce, &ct_tag, aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk::SNTRUP_KEY_CODECS; + use crate::views::Views; + + #[test] + fn test_sntrup_key_gen_roundtrip() { + for codec in SNTRUP_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test kem key") + .try_build() + .unwrap(); + + let attr = mk.attr_view().unwrap(); + assert!(attr.is_secret_key()); + assert!(!attr.is_public_key()); + + let kd = mk.data_view().unwrap(); + assert!(kd.key_bytes().is_ok()); + assert!(kd.secret_bytes().is_ok()); + + // serialize/deserialize roundtrip + let bytes: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } + } + + #[test] + fn test_sntrup_public_key_derivation() { + for codec in SNTRUP_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let conv = mk.conv_view().unwrap(); + let pk = conv.to_public_key().unwrap(); + + let attr = pk.attr_view().unwrap(); + assert!(attr.is_public_key()); + assert!(!attr.is_secret_key()); + + // derive again and check same result + let pk2 = conv.to_public_key().unwrap(); + assert_eq!(pk, pk2); + } + } + + #[test] + fn test_sntrup_fingerprint() { + for codec in SNTRUP_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let fp = pk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + let fp_bytes: Vec = fp.into(); + assert!(!fp_bytes.is_empty()); + } + } + + #[test] + fn test_sntrup_seal_open_roundtrip() { + let aead_codecs = [ + Codec::Xchacha20Poly1305, + Codec::Chacha20Poly1305, + Codec::AesGcm256, + ]; + + for key_codec in SNTRUP_KEY_CODECS { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(key_codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + for aead_codec in &aead_codecs { + let plaintext = b"hello sntrup world!"; + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(plaintext, *aead_codec, b"") + .unwrap(); + + let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + } + } + + #[test] + fn test_sntrup_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let sk2 = Builder::new_from_random_bytes(Codec::Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let (sealed, _) = pk1 + .seal_view() + .unwrap() + .seal(b"secret data", Codec::Xchacha20Poly1305, b"") + .unwrap(); + + // Opening with wrong key should fail (decapsulation will produce different shared secret + // and AEAD open will fail) + assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_sntrup_seal_requires_public_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + // seal with private key should fail + assert!(sk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err()); + } + + #[test] + fn test_sntrup_open_requires_private_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .unwrap(); + + // open with public key should fail + assert!(pk.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_sntrup_unsupported_aead_codec() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + // AES-128-GCM is not allowed for sntrup (not PQ-safe) + assert!(pk + .seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err()); + } +} diff --git a/src/views/threshold_marker.rs b/src/views/threshold_marker.rs new file mode 100644 index 0000000..e39fd04 --- /dev/null +++ b/src/views/threshold_marker.rs @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Split (Shamir) threshold marker attributes and classifier. +//! +//! This module is **additive**: it does not change any existing signing, +//! combine, or verify logic. It introduces a way to stamp a *controller* +//! [`Multikey`] with the metadata needed to drive a BLS threshold-signing +//! ceremony for a Shamir-split key: +//! +//! - [`AttrId::ThresholdGroupPublicKey`] — the group/combined BLS public key +//! bytes (the split analogue of [`AttrId::DkgGroupPublicKey`]). +//! - [`AttrId::ThresholdParticipants`] — a CBOR map keyed by share-identifier +//! bytes (the Shamir x-coordinate, exactly as stored in +//! [`AttrId::ShareIdentifier`]/`SigShare`) to a [`ThresholdParticipant`]. +//! +//! It also provides a [`ThresholdScheme`] classifier ([`threshold_kind`]) and a +//! parameter reader ([`threshold_params`]) that work across both the Shamir +//! split codecs (`Bls12381G1PrivShare`/`PubShare`, `Bls12381G2PrivShare`/ +//! `PubShare`) and the DKG codecs (`*ThreshPrivShare`/`*ThreshPubShare`). + +use crate::{error::AttributesError, AttrId, Error, Multikey}; +use multi_codec::Codec; +use multi_util::Varuint; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +/// A single participant in a split (Shamir) threshold key. +/// +/// `public_share` is the participant's **BLS public key share** `v_i` bytes — +/// i.e. the compressed group-element bytes obtained by converting the +/// participant's private share Multikey to its public share (see +/// `ConvView::to_public_key` for `Bls12381G1PrivShare`/`Bls12381G2PrivShare`). +/// This is what a controller uses to verify a participant's partial signature +/// before accumulating it. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ThresholdParticipant { + /// The participant's VLAD bytes (verifiable long-lived address / identity). + pub vlad: Vec, + /// The participant's BLS public key share `v_i` bytes. + pub public_share: Vec, +} + +/// The kind of threshold key a [`Multikey`] participates in. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ThresholdScheme { + /// Shamir-split key (trusted-dealer split of an existing BLS secret key). + ShamirSplit, + /// Distributed key generation (frost-dkg) key. + Dkg, +} + +/// Classify the threshold scheme of a [`Multikey`]. +/// +/// Returns: +/// - [`ThresholdScheme::ShamirSplit`] for the BLS split share codecs +/// (`Bls12381G1PrivShare`, `Bls12381G1PubShare`, `Bls12381G2PrivShare`, +/// `Bls12381G2PubShare`), +/// - [`ThresholdScheme::Dkg`] for the DKG threshold share codecs +/// (`*ThreshPrivShare` / `*ThreshPubShare`), +/// - `None` for anything else (ordinary keys, non-threshold BLS keys, etc.). +pub fn threshold_kind(mk: &Multikey) -> Option { + match mk.codec { + Codec::Bls12381G1PrivShare + | Codec::Bls12381G1PubShare + | Codec::Bls12381G2PrivShare + | Codec::Bls12381G2PubShare => Some(ThresholdScheme::ShamirSplit), + Codec::Ed25519ThreshPrivShare + | Codec::Ed25519ThreshPubShare + | Codec::P256ThreshPrivShare + | Codec::P256ThreshPubShare + | Codec::P384ThreshPrivShare + | Codec::P384ThreshPubShare + | Codec::Secp256K1ThreshPrivShare + | Codec::Secp256K1ThreshPubShare + | Codec::Bls12381ThreshPrivShare + | Codec::Bls12381ThreshPubShare + | Codec::Ed448ThreshPrivShare + | Codec::Ed448ThreshPubShare => Some(ThresholdScheme::Dkg), + _ => None, + } +} + +/// Read the `(threshold, limit)` params off a threshold [`Multikey`]. +/// +/// For Shamir split keys this reads [`AttrId::Threshold`]/[`AttrId::Limit`] +/// (varuint-encoded). For DKG keys it reads [`AttrId::DkgThreshold`]/ +/// [`AttrId::DkgLimit`] (u16 little-endian). Returns `None` for non-threshold +/// keys or when the params are absent/malformed. +pub fn threshold_params(mk: &Multikey) -> Option<(u16, u16)> { + match threshold_kind(mk)? { + ThresholdScheme::ShamirSplit => { + let t = mk.attributes.get(&AttrId::Threshold)?; + let n = mk.attributes.get(&AttrId::Limit)?; + let t = *Varuint::::try_from(t.as_slice()).ok()?; + let n = *Varuint::::try_from(n.as_slice()).ok()?; + Some((u16::try_from(t).ok()?, u16::try_from(n).ok()?)) + } + ThresholdScheme::Dkg => { + let t = mk.attributes.get(&AttrId::DkgThreshold)?; + let n = mk.attributes.get(&AttrId::DkgLimit)?; + if t.len() < 2 || n.len() < 2 { + return None; + } + Some(( + u16::from_le_bytes([t[0], t[1]]), + u16::from_le_bytes([n[0], n[1]]), + )) + } + } +} + +/// Read-only / mutating helper view over a [`Multikey`]'s split-threshold +/// marker attributes. +/// +/// This is intentionally a thin helper rather than a trait impl so it can both +/// read and (additively) mutate marker attributes without touching the +/// existing view traits or their codec dispatch. +pub struct MarkerView<'a> { + mk: &'a Multikey, +} + +impl<'a> MarkerView<'a> { + /// Create a marker view over a [`Multikey`]. + pub fn new(mk: &'a Multikey) -> Self { + Self { mk } + } + + /// Get the group/combined BLS public key bytes stamped on this key, if any. + pub fn group_public_key(&self) -> Result, Error> { + let v = self + .mk + .attributes + .get(&AttrId::ThresholdGroupPublicKey) + .ok_or(AttributesError::MissingGroupPublicKey)?; + Ok(v.to_vec()) + } + + /// Get the participant map stamped on this key, if any. + pub fn participants(&self) -> Result, ThresholdParticipant>, Error> { + let v = self + .mk + .attributes + .get(&AttrId::ThresholdParticipants) + .ok_or(AttributesError::MissingThresholdParticipants)?; + let map: BTreeMap, ThresholdParticipant> = serde_cbor::from_slice(v.as_slice()) + .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 serde_cbor's generic map cap. + const MAX_THRESHOLD_PARTICIPANTS: usize = 1024; + if map.len() > MAX_THRESHOLD_PARTICIPANTS { + return Err(AttributesError::ThresholdMarkerCbor( + "threshold participant registry exceeds maximum size".to_string(), + ) + .into()); + } + Ok(map) + } + + /// Split-key answer for the group public key. + pub fn group_pubkey(&self) -> Result, Error> { + self.group_public_key() + } + + /// Split-key answer for the threshold `t`, read from [`AttrId::Threshold`]. + pub fn threshold(&self) -> Result { + let v = self + .mk + .attributes + .get(&AttrId::Threshold) + .ok_or(AttributesError::MissingThreshold)?; + let t = *Varuint::::try_from(v.as_slice())?; + u16::try_from(t).map_err(|_| AttributesError::MissingThreshold.into()) + } + + /// Split-key answer for the participant count `n`, read from + /// [`AttrId::Limit`]. + pub fn participant_count(&self) -> Result { + let v = self + .mk + .attributes + .get(&AttrId::Limit) + .ok_or(AttributesError::MissingLimit)?; + let n = *Varuint::::try_from(v.as_slice())?; + u16::try_from(n).map_err(|_| AttributesError::MissingLimit.into()) + } +} + +/// Stamp (set) the group/combined BLS public key bytes on a [`Multikey`]. +/// +/// Additive: only inserts/overwrites [`AttrId::ThresholdGroupPublicKey`]. +pub fn set_group_public_key(mk: &mut Multikey, group_pubkey: &[u8]) { + mk.attributes.insert( + AttrId::ThresholdGroupPublicKey, + group_pubkey.to_vec().into(), + ); +} + +/// Stamp (set) the participant map on a [`Multikey`]. +/// +/// Serializes a `BTreeMap, ThresholdParticipant>` to CBOR and inserts +/// it under [`AttrId::ThresholdParticipants`]. Additive only. +pub fn set_participants( + mk: &mut Multikey, + participants: &BTreeMap, ThresholdParticipant>, +) -> Result<(), Error> { + let bytes = serde_cbor::to_vec(participants) + .map_err(|e| AttributesError::ThresholdMarkerCbor(e.to_string()))?; + mk.attributes + .insert(AttrId::ThresholdParticipants, bytes.into()); + Ok(()) +} + +/// Read the group public key bytes from a [`Multikey`]'s marker attribute. +pub fn group_public_key(mk: &Multikey) -> Result, Error> { + MarkerView::new(mk).group_public_key() +} + +/// Read the participant map from a [`Multikey`]'s marker attribute. +pub fn participants(mk: &Multikey) -> Result, ThresholdParticipant>, Error> { + MarkerView::new(mk).participants() +} + +/// Canonical, deterministic bytes of the marker *bundle* — the tuple +/// `(group_public_key, participants, threshold, limit)` — that is signed to +/// authenticate the marker (TSIG-1). The signature attribute itself is excluded +/// (the getters read only the bundle attrs), so signing and verifying recompute +/// the same bytes. `BTreeMap` serializes in deterministic key order. +pub fn canonical_marker_bytes(mk: &Multikey) -> Result, Error> { + let mv = MarkerView::new(mk); + let group = mv.group_public_key()?; + let participants = mv.participants()?; + let threshold = mv.threshold()?; + let limit = mv.participant_count()?; + let payload = (group, participants, threshold, limit); + serde_cbor::to_vec(&payload) + .map_err(|e| AttributesError::ThresholdMarkerCbor(e.to_string()).into()) +} + +/// Authenticate the marker bundle: sign `canonical_marker_bytes(mk)` with +/// `signer` and stamp the resulting `Multisig` under +/// [`AttrId::ThresholdMarkerSig`]. Call AFTER the group key + participants + +/// threshold/limit are set. `signer` is normally the controller's own signing +/// key; the verifier must hold an independently-trusted copy of its public key. +pub fn sign_marker(mk: &mut Multikey, signer: &Multikey, scheme: Option) -> Result<(), Error> { + use crate::Views; + let bytes = canonical_marker_bytes(mk)?; + let sig = signer.sign_view()?.sign(&bytes, false, scheme)?; + let sig_bytes: Vec = sig.into(); + mk.attributes + .insert(AttrId::ThresholdMarkerSig, sig_bytes.into()); + Ok(()) +} + +/// Verify the marker bundle's authenticating signature against +/// `verifier_pubkey` (which must be an independently-trusted controller key — +/// passing the same secret key works because its `verify_view` derives the +/// public key). Returns an error if the signature is absent or does not verify +/// over the recomputed canonical bytes — defeating TSIG-1 marker tampering. +pub fn verify_marker(mk: &Multikey, verifier_pubkey: &Multikey) -> Result<(), Error> { + use crate::Views; + let sig_bytes = mk + .attributes + .get(&AttrId::ThresholdMarkerSig) + .ok_or(AttributesError::MissingThresholdMarkerSig)?; + let sig = multi_sig::Multisig::try_from(sig_bytes.as_slice()) + .map_err(|_| AttributesError::ThresholdMarkerSigInvalid)?; + let bytes = canonical_marker_bytes(mk)?; + verifier_pubkey + .verify_view()? + .verify(&sig, Some(&bytes)) + .map_err(|_| AttributesError::ThresholdMarkerSigInvalid.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Builder, Views}; + fn g1_priv() -> Multikey { + Builder::new_from_random_bytes(Codec::Bls12381G1Priv, &mut rand::rng()) + .unwrap() + .try_build() + .unwrap() + } + + #[test] + fn classify_split_share_g1() { + let mk = g1_priv(); + let shares = mk.threshold_view().unwrap().split(3, 5).unwrap(); + let share = &shares[0]; + assert_eq!(threshold_kind(share), Some(ThresholdScheme::ShamirSplit)); + assert_eq!(threshold_params(share), Some((3, 5))); + } + + #[test] + fn classify_split_pub_share_g1() { + let mk = g1_priv(); + let shares = mk.threshold_view().unwrap().split(2, 4).unwrap(); + let pub_share = shares[0].conv_view().unwrap().to_public_key().unwrap(); + assert_eq!(pub_share.codec, Codec::Bls12381G1PubShare); + assert_eq!( + threshold_kind(&pub_share), + Some(ThresholdScheme::ShamirSplit) + ); + assert_eq!(threshold_params(&pub_share), Some((2, 4))); + } + + #[test] + fn classify_dkg_share() { + // Build a synthetic DKG private-share Multikey carrying the Dkg attrs. + let mut mk = Builder::new(Codec::Bls12381ThreshPrivShare) + .try_build() + .unwrap(); + mk.attributes + .insert(AttrId::DkgThreshold, 2u16.to_le_bytes().to_vec().into()); + mk.attributes + .insert(AttrId::DkgLimit, 3u16.to_le_bytes().to_vec().into()); + assert_eq!(threshold_kind(&mk), Some(ThresholdScheme::Dkg)); + assert_eq!(threshold_params(&mk), Some((2, 3))); + } + + #[test] + fn classify_non_threshold_none() { + // Ed25519 ordinary key + let ed = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rand::rng()) + .unwrap() + .try_build() + .unwrap(); + assert_eq!(threshold_kind(&ed), None); + assert_eq!(threshold_params(&ed), None); + + // Plain (non-share) BLS G1 secret key + let bls = g1_priv(); + assert_eq!(threshold_kind(&bls), None); + assert_eq!(threshold_params(&bls), None); + } + + #[test] + fn marker_roundtrip() { + let mut mk = g1_priv(); + set_group_public_key(&mut mk, &[1, 2, 3, 4]); + assert_eq!(group_public_key(&mk).unwrap(), vec![1, 2, 3, 4]); + + let mut map = BTreeMap::new(); + map.insert( + vec![0u8; 32], + ThresholdParticipant { + vlad: vec![9, 9, 9], + public_share: vec![7; 48], + }, + ); + set_participants(&mut mk, &map).unwrap(); + let got = participants(&mk).unwrap(); + assert_eq!(got, map); + } +} diff --git a/src/views/x25519.rs b/src/views/x25519.rs new file mode 100644 index 0000000..0cbb988 --- /dev/null +++ b/src/views/x25519.rs @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: Apache-2.0 +//! X25519 ECIES multikey view; Curve25519 Diffie-Hellman key agreement + AEAD. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroizing; + +const X25519_SECRET_LENGTH: usize = 32; +const X25519_PUBLIC_LENGTH: usize = 32; + +/// Decoded sealed message: (aead_codec, nonce, ciphertext+tag) +/// The ephemeral public key is carried externally (in the BsMessage outer format). +type SealedParts = (Codec, Vec, Vec); + +/// X25519 AEAD codecs allowed +fn is_x25519_aead_allowed(codec: Codec) -> bool { + matches!( + codec, + Codec::AesGcm128 | Codec::Chacha20Poly1305 | Codec::Xchacha20Poly1305 + ) +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::X25519Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::X25519Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != X25519_SECRET_LENGTH { + return Err(ConversionsError::SecretKeyFailure( + "invalid X25519 secret key length".into(), + ) + .into()); + } + + let secret_arr: [u8; 32] = secret_bytes + .as_slice() + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid X25519 secret key".into()))?; + + let secret = StaticSecret::from(secret_arr); + let public = PublicKey::from(&secret); + + Builder::new(Codec::X25519Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&public.as_bytes().to_vec()) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err( + ConversionsError::UnsupportedAlgorithm("X25519 not supported in SSH key format".into()) + .into(), + ) + } + fn to_ssh_private_key(&self) -> Result { + Err( + ConversionsError::UnsupportedAlgorithm("X25519 not supported in SSH key format".into()) + .into(), + ) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// 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) + .map_err(|_| SealError::InvalidFormat("missing aead codec".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok((aead_codec, nonce.to_inner(), ct_tag.to_inner())) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_x25519_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + if pub_bytes.len() != X25519_PUBLIC_LENGTH { + return Err(SealError::EncapsulationFailed("invalid public key length".into()).into()); + } + + let recipient_pub: [u8; 32] = pub_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::EncapsulationFailed("invalid public key".into()))?; + let recipient_pub = PublicKey::from(recipient_pub); + + // Generate ephemeral keypair + let ephemeral_secret = StaticSecret::random_from_rng(&mut rand::rng()); + let ephemeral_pub = PublicKey::from(&ephemeral_secret); + + // ECDH + let shared_secret = ephemeral_secret.diffie_hellman(&recipient_pub); + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(shared_secret.as_bytes(), b"x25519-seal", key_len)?; + + // Encrypt plaintext + let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; + + // Build the ephemeral public key as a Multikey so callers can transmit it + let ephemeral_mk = Builder::new(Codec::X25519Pub) + .with_key_bytes(&ephemeral_pub.as_bytes().to_vec()) + .try_build()?; + + Ok(( + encode_sealed(aead_codec, &nonce, &ct_tag), + Some(ephemeral_mk), + )) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let ephemeral_mk = ephemeral.ok_or_else(|| { + SealError::InvalidFormat("X25519 open requires an ephemeral public key".into()) + })?; + + let (aead_codec, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if !is_x25519_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let ephemeral_pub_bytes = ephemeral_mk.data_view()?.key_bytes()?; + if ephemeral_pub_bytes.len() != X25519_PUBLIC_LENGTH { + return Err( + SealError::InvalidFormat("invalid ephemeral public key length".into()).into(), + ); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let secret_arr: [u8; 32] = secret_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid secret key".into()))?; + let static_secret = StaticSecret::from(secret_arr); + + let ephemeral_pub_arr: [u8; 32] = ephemeral_pub_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let ephemeral_pub = PublicKey::from(ephemeral_pub_arr); + + // ECDH + let shared_secret = static_secret.diffie_hellman(&ephemeral_pub); + + // Derive AEAD key from shared secret via HKDF + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(shared_secret.as_bytes(), b"x25519-seal", key_len)?; + + // Decrypt ciphertext + Ok(aead::aead_open( + aead_codec, &aead_key, &nonce, &ct_tag, aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk::X25519_KEY_CODECS; + use crate::views::Views; + + #[test] + fn test_x25519_key_gen_roundtrip() { + for codec in X25519_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test x25519 key") + .try_build() + .unwrap(); + + let attr = mk.attr_view().unwrap(); + assert!(attr.is_secret_key()); + assert!(!attr.is_public_key()); + + // serialize/deserialize roundtrip + let bytes: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } + } + + #[test] + fn test_x25519_public_key_derivation() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::X25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let conv = mk.conv_view().unwrap(); + let pk = conv.to_public_key().unwrap(); + + let attr = pk.attr_view().unwrap(); + assert!(attr.is_public_key()); + assert!(!attr.is_secret_key()); + + // derive again => same result + let pk2 = conv.to_public_key().unwrap(); + assert_eq!(pk, pk2); + } + + #[test] + fn test_x25519_fingerprint() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::X25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let fp = pk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + let fp_bytes: Vec = fp.into(); + assert!(!fp_bytes.is_empty()); + } + + #[test] + fn test_x25519_seal_open_roundtrip() { + let aead_codecs = [ + Codec::AesGcm128, + Codec::Chacha20Poly1305, + Codec::Xchacha20Poly1305, + ]; + + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + for aead_codec in &aead_codecs { + let plaintext = b"hello X25519 ECIES world!"; + let (sealed, ephemeral) = pk + .seal_view() + .unwrap() + .seal(plaintext, *aead_codec, b"") + .unwrap(); + + let opened = sk + .open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + } + + #[test] + fn test_x25519_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::X25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let sk2 = Builder::new_from_random_bytes(Codec::X25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let (sealed, ephemeral) = pk1 + .seal_view() + .unwrap() + .seal(b"secret data", Codec::Chacha20Poly1305, b"") + .unwrap(); + + // Opening with wrong key should fail + assert!(sk2 + .open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err()); + } + + #[test] + fn test_x25519_seal_requires_public_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + assert!(sk + .seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .is_err()); + } + + #[test] + fn test_x25519_open_requires_private_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, ephemeral) = pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .unwrap(); + + assert!(pk + .open_view() + .unwrap() + .open(&sealed, ephemeral.as_ref(), b"") + .is_err()); + } +} diff --git a/src/views/x25519_frodokem640.rs b/src/views/x25519_frodokem640.rs new file mode 100644 index 0000000..b0efcc6 --- /dev/null +++ b/src/views/x25519_frodokem640.rs @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: Apache-2.0 +//! X25519-FrodoKEM-640 hybrid KEM multikey view (X-Wing-like); combines X25519 ECDH +//! with FrodoKEM-640 (AES or SHAKE), ChaCha20-Poly1305 AEAD, and BLAKE3 KDF. +//! +//! Private key layout: `x25519_seed (32) || frodokem_secret_key`. +//! Public key layout (classical-first): `x25519_pub (32) || frodokem_public_key`. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroizing; + +const X25519_SEED_LEN: usize = 32; +const X25519_PUB_LEN: usize = 32; + +/// Decoded sealed message: (ephemeral_x25519_pub, frodo_ct, nonce, ciphertext+tag) +type SealedParts = (Vec, Vec, Vec, Vec); + +/// Only ChaCha20-Poly1305 allowed (matches the other X-Wing-like hybrid KEMs). +fn is_aead_allowed(codec: Codec) -> bool { + codec == Codec::Chacha20Poly1305 +} + +fn is_priv(codec: Codec) -> bool { + matches!( + codec, + Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640ShakePriv + ) +} + +fn is_pub(codec: Codec) -> bool { + matches!( + codec, + Codec::X25519Frodokem640AesPub | Codec::X25519Frodokem640ShakePub + ) +} + +fn pub_codec(codec: Codec) -> Codec { + match codec { + Codec::X25519Frodokem640AesPriv | Codec::X25519Frodokem640AesPub => { + Codec::X25519Frodokem640AesPub + } + _ => Codec::X25519Frodokem640ShakePub, + } +} + +fn is_shake(codec: Codec) -> bool { + matches!( + codec, + Codec::X25519Frodokem640ShakePriv | Codec::X25519Frodokem640ShakePub + ) +} + +/// FrodoKEM-640 keypair for the given hybrid variant. Returns `(pub, secret)`. +fn frodo_keypair(codec: Codec) -> (Vec, Zeroizing>) { + if is_shake(codec) { + super::frodokem_helper::keypair_640shake() + } else { + super::frodokem_helper::keypair_640aes() + } +} + +fn frodo_public_from_private(codec: Codec, secret: &[u8]) -> Result, Error> { + if is_shake(codec) { + super::frodokem_helper::public_from_private_640shake(secret) + } else { + super::frodokem_helper::public_from_private_640aes(secret) + } + .map_err(|e| ConversionsError::SecretKeyFailure(e).into()) +} + +fn frodo_encap(codec: Codec, pub_bytes: &[u8]) -> Result<(Vec, Zeroizing>), Error> { + if is_shake(codec) { + super::frodokem_helper::encap_640shake(pub_bytes) + } else { + super::frodokem_helper::encap_640aes(pub_bytes) + } + .map_err(|e| SealError::EncapsulationFailed(e).into()) +} + +fn frodo_decap(codec: Codec, secret: &[u8], ct: &[u8]) -> Result>, Error> { + if is_shake(codec) { + super::frodokem_helper::decap_640shake(secret, ct) + } else { + super::frodokem_helper::decap_640aes(secret, ct) + } + .map_err(|e| SealError::DecapsulationFailed(e).into()) +} + +/// Generate a hybrid private key for the builder: `x25519_seed (32) || frodo_secret`. +pub(crate) fn generate_private_key(codec: Codec) -> Vec { + let mut x25519_seed = [0u8; X25519_SEED_LEN]; + rand_core::Rng::fill_bytes(&mut rand::rng(), &mut x25519_seed); + let (_pub, frodo_secret) = frodo_keypair(codec); + let mut out = Vec::with_capacity(X25519_SEED_LEN + frodo_secret.len()); + out.extend_from_slice(&x25519_seed); + out.extend_from_slice(&frodo_secret); + out +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + is_priv(self.mk.codec) + } + fn is_public_key(&self) -> bool { + is_pub(self.mk.codec) + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() <= X25519_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid secret length".into()).into(), + ); + } + + // X25519 public key + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let x25519_pub = PublicKey::from(&x25519_secret); + + // FrodoKEM public key from the stored full secret key + let frodo_pub = frodo_public_from_private(self.mk.codec, &secret_bytes[X25519_SEED_LEN..])?; + + // Concatenate (classical-first): x25519_pub (32) || frodokem_pub + let mut pub_bytes = Vec::with_capacity(X25519_PUB_LEN + frodo_pub.len()); + pub_bytes.extend_from_slice(x25519_pub.as_bytes()); + pub_bytes.extend_from_slice(&frodo_pub); + + Builder::new(pub_codec(self.mk.codec)) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-FrodoKEM-640 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-FrodoKEM-640 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// Combine shared secrets via BLAKE3: +/// ss = BLAKE3(label || ss_frodo || ss_x25519 || frodo_ct || ephemeral_x25519_pub) +fn combine_shared_secrets( + ss_frodo: &[u8], + ss_x25519: &[u8], + frodo_ct: &[u8], + ephemeral_x25519_pub: &[u8], +) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"x25519-frodokem640-hpke"); + hasher.update(ss_frodo); + hasher.update(ss_x25519); + hasher.update(frodo_ct); + hasher.update(ephemeral_x25519_pub); + *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()))?; + let (frodo_ct, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing frodokem ciphertext".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + ephemeral_pub.to_inner(), + frodo_ct.to_inner(), + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + if pub_bytes.len() <= X25519_PUB_LEN { + return Err( + SealError::EncapsulationFailed("invalid hybrid public key length".into()).into(), + ); + } + + let x25519_pub_arr: [u8; 32] = pub_bytes[..X25519_PUB_LEN] + .try_into() + .map_err(|_| SealError::EncapsulationFailed("invalid x25519 public key".into()))?; + let recipient_x25519_pub = PublicKey::from(x25519_pub_arr); + + // X25519: ephemeral ECDH + let ephemeral_secret = StaticSecret::random_from_rng(&mut rand::rng()); + let ephemeral_pub = PublicKey::from(&ephemeral_secret); + let ss_x25519 = ephemeral_secret.diffie_hellman(&recipient_x25519_pub); + + // FrodoKEM encapsulate + let (frodo_ct, ss_frodo) = frodo_encap(self.mk.codec, &pub_bytes[X25519_PUB_LEN..])?; + + let combined_ss = combine_shared_secrets( + ss_frodo.as_ref(), + ss_x25519.as_bytes(), + frodo_ct.as_ref(), + ephemeral_pub.as_bytes(), + ); + + let (nonce, ct_tag) = aead::aead_seal(aead_codec, &combined_ss, plaintext, aad)?; + + Ok(( + encode_sealed(ephemeral_pub.as_bytes(), frodo_ct.as_ref(), &nonce, &ct_tag), + None, + )) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (ephemeral_pub_bytes, frodo_ct_bytes, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if ephemeral_pub_bytes.len() != X25519_PUB_LEN { + return Err( + SealError::InvalidFormat("invalid ephemeral public key length".into()).into(), + ); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() <= X25519_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid secret length".into()).into(), + ); + } + + // X25519 ECDH + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let ephemeral_pub_arr: [u8; 32] = ephemeral_pub_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let ephemeral_pub = PublicKey::from(ephemeral_pub_arr); + let ss_x25519 = x25519_secret.diffie_hellman(&ephemeral_pub); + + // FrodoKEM decapsulate + let ss_frodo = frodo_decap( + self.mk.codec, + &secret_bytes[X25519_SEED_LEN..], + &frodo_ct_bytes, + )?; + + let combined_ss = combine_shared_secrets( + ss_frodo.as_ref(), + ss_x25519.as_bytes(), + frodo_ct_bytes.as_slice(), + ephemeral_pub_bytes.as_slice(), + ); + + Ok(aead::aead_open( + Codec::Chacha20Poly1305, + &combined_ss, + &nonce, + &ct_tag, + aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + fn roundtrip(priv_codec: Codec) { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(priv_codec, &mut rng) + .unwrap() + .with_comment("x25519-frodokem hybrid test") + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let plaintext = b"hello X25519-FrodoKEM-640 hybrid KEM!"; + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(plaintext, Codec::Chacha20Poly1305, b"") + .unwrap(); + let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + + // wrong key fails + let sk2 = Builder::new_from_random_bytes(priv_codec, &mut rng) + .unwrap() + .try_build() + .unwrap(); + assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_640aes_roundtrip() { + roundtrip(Codec::X25519Frodokem640AesPriv); + } + + #[test] + fn test_640shake_roundtrip() { + roundtrip(Codec::X25519Frodokem640ShakePriv); + } +} diff --git a/src/views/x25519_mceliece348864.rs b/src/views/x25519_mceliece348864.rs new file mode 100644 index 0000000..8102bda --- /dev/null +++ b/src/views/x25519_mceliece348864.rs @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: Apache-2.0 +//! X25519-Classic-McEliece-348864 hybrid KEM multikey view (X-Wing-like); combines +//! X25519 ECDH with Classic McEliece 348864, ChaCha20-Poly1305 AEAD, and BLAKE3 KDF. +//! +//! Private key layout: `x25519_seed (32) || mceliece_seed (32)` = 64 bytes. +//! Public key layout (classical-first): `x25519_pub (32) || mceliece_public_key`. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +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; + +const X25519_SEED_LEN: usize = 32; +const MCELIECE_SEED_LEN: usize = 32; +const PRIV_SEED_LEN: usize = X25519_SEED_LEN + MCELIECE_SEED_LEN; // 64 +const X25519_PUB_LEN: usize = 32; + +/// Decoded sealed message: (ephemeral_x25519_pub, mceliece_ct, nonce, ciphertext+tag) +type SealedParts = (Vec, Vec, Vec, Vec); + +/// Only ChaCha20-Poly1305 allowed (matches the other X-Wing-like hybrid KEMs). +fn is_aead_allowed(codec: Codec) -> bool { + codec == Codec::Chacha20Poly1305 +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::X25519Mceliece348864Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::X25519Mceliece348864Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // X25519 public key + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let x25519_pub = PublicKey::from(&x25519_secret); + + // Classic McEliece public key from the (deterministic) seed + 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); + + // Concatenate (classical-first): x25519_pub (32) || mceliece_pub + let mut pub_bytes = Vec::with_capacity(X25519_PUB_LEN + pk.as_ref().len()); + pub_bytes.extend_from_slice(x25519_pub.as_bytes()); + pub_bytes.extend_from_slice(pk.as_ref()); + + Builder::new(Codec::X25519Mceliece348864Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-Classic-McEliece-348864 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-Classic-McEliece-348864 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// Combine shared secrets via BLAKE3: +/// ss = BLAKE3(label || ss_mceliece || ss_x25519 || mceliece_ct || ephemeral_x25519_pub) +fn combine_shared_secrets( + ss_mceliece: &[u8], + ss_x25519: &[u8], + mceliece_ct: &[u8], + ephemeral_x25519_pub: &[u8], +) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"x25519-mceliece348864-hpke"); + hasher.update(ss_mceliece); + hasher.update(ss_x25519); + hasher.update(mceliece_ct); + hasher.update(ephemeral_x25519_pub); + *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()))?; + let (mceliece_ct, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing mceliece ciphertext".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + ephemeral_pub.to_inner(), + mceliece_ct.to_inner(), + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + if pub_bytes.len() != X25519_PUB_LEN + mceliece348864::CRYPTO_PUBLICKEYBYTES { + return Err( + SealError::EncapsulationFailed("invalid hybrid public key length".into()).into(), + ); + } + + let x25519_pub_arr: [u8; 32] = pub_bytes[..X25519_PUB_LEN] + .try_into() + .map_err(|_| SealError::EncapsulationFailed("invalid x25519 public key".into()))?; + let recipient_x25519_pub = PublicKey::from(x25519_pub_arr); + + // X25519: ephemeral ECDH + let ephemeral_secret = StaticSecret::random_from_rng(&mut rand::rng()); + let ephemeral_pub = PublicKey::from(&ephemeral_secret); + 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 combined_ss = combine_shared_secrets( + ss_mceliece.as_ref(), + ss_x25519.as_bytes(), + mceliece_ct, + ephemeral_pub.as_bytes(), + ); + + let (nonce, ct_tag) = aead::aead_seal(aead_codec, &combined_ss, plaintext, aad)?; + + Ok(( + encode_sealed(ephemeral_pub.as_bytes(), mceliece_ct, &nonce, &ct_tag), + None, + )) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (ephemeral_pub_bytes, mceliece_ct_bytes, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if ephemeral_pub_bytes.len() != X25519_PUB_LEN { + return Err( + SealError::InvalidFormat("invalid ephemeral public key length".into()).into(), + ); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // X25519 ECDH + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let ephemeral_pub_arr: [u8; 32] = ephemeral_pub_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let ephemeral_pub = PublicKey::from(ephemeral_pub_arr); + let ss_x25519 = x25519_secret.diffie_hellman(&ephemeral_pub); + + // McEliece decapsulate + 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 combined_ss = combine_shared_secrets( + ss_mceliece.as_ref(), + ss_x25519.as_bytes(), + mceliece_ct_bytes.as_slice(), + ephemeral_pub_bytes.as_slice(), + ); + + Ok(aead::aead_open( + Codec::Chacha20Poly1305, + &combined_ss, + &nonce, + &ct_tag, + aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::views::Views; + + #[test] + fn test_seal_open_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Mceliece348864Priv, &mut rng) + .unwrap() + .with_comment("x25519-mceliece hybrid test") + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let plaintext = b"hello X25519-Classic-McEliece-348864 hybrid KEM!"; + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(plaintext, Codec::Chacha20Poly1305, b"") + .unwrap(); + let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + + #[test] + fn test_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::X25519Mceliece348864Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + let sk2 = Builder::new_from_random_bytes(Codec::X25519Mceliece348864Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let (sealed, _) = pk1 + .seal_view() + .unwrap() + .seal(b"secret", Codec::Chacha20Poly1305, b"") + .unwrap(); + assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err()); + } +} diff --git a/src/views/x25519_mlkem768.rs b/src/views/x25519_mlkem768.rs new file mode 100644 index 0000000..a175e18 --- /dev/null +++ b/src/views/x25519_mlkem768.rs @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: Apache-2.0 +//! X25519-ML-KEM-768 hybrid KEM multikey view; combines X25519 ECDH with ML-KEM-768 KEM, +//! ChaCha20-Poly1305 AEAD, and SHA-512 KDF. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use ml_kem::{ + kem::{Decapsulate, Encapsulate}, + EncodedSizeUser, KemCore, MlKem768, +}; +use sha2::{Digest, Sha512}; +use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroizing; + +const X25519_SEED_LEN: usize = 32; +const MLKEM_D_LEN: usize = 32; +const MLKEM_Z_LEN: usize = 32; +const PRIV_SEED_LEN: usize = X25519_SEED_LEN + MLKEM_D_LEN + MLKEM_Z_LEN; // 96 +const X25519_PUB_LEN: usize = 32; +const MLKEM768_PUB_LEN: usize = 1184; +const PUB_KEY_LEN: usize = X25519_PUB_LEN + MLKEM768_PUB_LEN; // 1216 + +/// Decoded sealed message: (ephemeral_x25519_pub, mlkem_ct, nonce, ciphertext+tag) +type SealedParts = (Vec, Vec, Vec, Vec); + +/// Only ChaCha20-Poly1305 allowed per spec +fn is_aead_allowed(codec: Codec) -> bool { + codec == Codec::Chacha20Poly1305 +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::X25519Mlkem768Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::X25519Mlkem768Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // X25519 public key + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let x25519_pub = PublicKey::from(&x25519_secret); + + // ML-KEM-768 public key + let d: [u8; 32] = secret_bytes[X25519_SEED_LEN..X25519_SEED_LEN + MLKEM_D_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-kem d".into()))?; + let z: [u8; 32] = secret_bytes[X25519_SEED_LEN + MLKEM_D_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-kem z".into()))?; + let (_dk, ek) = MlKem768::generate_deterministic(&d.into(), &z.into()); + + // Concatenate: x25519_pub (32) || mlkem_pub (1184) + let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); + pub_bytes.extend_from_slice(x25519_pub.as_bytes()); + pub_bytes.extend_from_slice(&ek.as_bytes()); + + Builder::new(Codec::X25519Mlkem768Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-ML-KEM-768 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-ML-KEM-768 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// Combine shared secrets using SHA-512: +/// ss = SHA-512(label || ss_mlkem || ss_x25519 || mlkem_ct || ephemeral_x25519_pub) +fn combine_shared_secrets( + ss_mlkem: &[u8], + ss_x25519: &[u8], + mlkem_ct: &[u8], + ephemeral_x25519_pub: &[u8], +) -> [u8; 64] { + let digest = Sha512::new() + .chain_update(b"x25519-mlkem768-hpke") + .chain_update(ss_mlkem) + .chain_update(ss_x25519) + .chain_update(mlkem_ct) + .chain_update(ephemeral_x25519_pub) + .finalize(); + let mut out = [0u8; 64]; + out.copy_from_slice(&digest); + 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) + .map_err(|_| SealError::InvalidFormat("missing ephemeral public key".into()))?; + let (mlkem_ct, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ML-KEM ciphertext".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + ephemeral_pub.to_inner(), + mlkem_ct.to_inner(), + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + if pub_bytes.len() != PUB_KEY_LEN { + return Err( + SealError::EncapsulationFailed("invalid hybrid public key length".into()).into(), + ); + } + + // Split public key + let x25519_pub_arr: [u8; 32] = pub_bytes[..X25519_PUB_LEN] + .try_into() + .map_err(|_| SealError::EncapsulationFailed("invalid x25519 public key".into()))?; + let recipient_x25519_pub = PublicKey::from(x25519_pub_arr); + + let mlkem_ek = ::EncapsulationKey::from_bytes( + pub_bytes[X25519_PUB_LEN..].try_into().map_err(|_| { + SealError::EncapsulationFailed("invalid ML-KEM-768 public key".into()) + })?, + ); + + // X25519: generate ephemeral keypair and ECDH + let ephemeral_secret = StaticSecret::random_from_rng(&mut rand::rng()); + let ephemeral_pub = PublicKey::from(&ephemeral_secret); + let ss_x25519 = ephemeral_secret.diffie_hellman(&recipient_x25519_pub); + + // ML-KEM-768: encapsulate + let mut rng = rand_core_06::OsRng; + let (mlkem_ct, ss_mlkem) = mlkem_ek + .encapsulate(&mut rng) + .map_err(|_| SealError::EncapsulationFailed("ML-KEM encapsulation failed".into()))?; + + // Combine shared secrets via SHA-512 + let combined_ss = combine_shared_secrets( + ss_mlkem.as_slice(), + ss_x25519.as_bytes(), + mlkem_ct.as_slice(), + ephemeral_pub.as_bytes(), + ); + + // Derive AEAD key via HKDF-SHA512 + let key_len = aead::key_size(aead_codec)?; + let aead_key = aead::derive_aead_key(&combined_ss, b"x25519-mlkem768-seal", key_len)?; + + // AEAD encrypt + let (nonce, ct_tag) = aead::aead_seal(aead_codec, &aead_key, plaintext, aad)?; + + Ok(( + encode_sealed( + ephemeral_pub.as_bytes(), + mlkem_ct.as_slice(), + &nonce, + &ct_tag, + ), + None, + )) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (ephemeral_pub_bytes, mlkem_ct_bytes, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if ephemeral_pub_bytes.len() != X25519_PUB_LEN { + return Err( + SealError::InvalidFormat("invalid ephemeral public key length".into()).into(), + ); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // X25519 ECDH + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let ephemeral_pub_arr: [u8; 32] = ephemeral_pub_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let ephemeral_pub = PublicKey::from(ephemeral_pub_arr); + let ss_x25519 = x25519_secret.diffie_hellman(&ephemeral_pub); + + // ML-KEM-768 decapsulate + let d: [u8; 32] = secret_bytes[X25519_SEED_LEN..X25519_SEED_LEN + MLKEM_D_LEN] + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid ml-kem d".into()))?; + let z: [u8; 32] = secret_bytes[X25519_SEED_LEN + MLKEM_D_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid ml-kem z".into()))?; + let (dk, _ek) = MlKem768::generate_deterministic(&d.into(), &z.into()); + + let mlkem_ct = mlkem_ct_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid ML-KEM ciphertext size".into()))?; + let ss_mlkem = dk + .decapsulate(&mlkem_ct) + .map_err(|_| SealError::DecapsulationFailed("ML-KEM decapsulation failed".into()))?; + + // Combine shared secrets via SHA-512 + let combined_ss = combine_shared_secrets( + ss_mlkem.as_slice(), + ss_x25519.as_bytes(), + mlkem_ct_bytes.as_slice(), + ephemeral_pub_bytes.as_slice(), + ); + + // Derive AEAD key via HKDF-SHA512 + let key_len = aead::key_size(Codec::Chacha20Poly1305)?; + let aead_key = aead::derive_aead_key(&combined_ss, b"x25519-mlkem768-seal", key_len)?; + + // AEAD decrypt + Ok(aead::aead_open( + Codec::Chacha20Poly1305, + &aead_key, + &nonce, + &ct_tag, + aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk::X25519_MLKEM768_KEY_CODECS; + use crate::views::Views; + + #[test] + fn test_key_gen_roundtrip() { + for codec in X25519_MLKEM768_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test hybrid kem key") + .try_build() + .unwrap(); + + let attr = mk.attr_view().unwrap(); + assert!(attr.is_secret_key()); + assert!(!attr.is_public_key()); + + // serialize/deserialize roundtrip + let bytes: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } + } + + #[test] + fn test_public_key_derivation() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let conv = mk.conv_view().unwrap(); + let pk = conv.to_public_key().unwrap(); + + let attr = pk.attr_view().unwrap(); + assert!(attr.is_public_key()); + assert!(!attr.is_secret_key()); + + // derive again => same result + let pk2 = conv.to_public_key().unwrap(); + assert_eq!(pk, pk2); + + // check public key length + let dv = pk.data_view().unwrap(); + assert_eq!(dv.key_bytes().unwrap().len(), PUB_KEY_LEN); + } + + #[test] + fn test_fingerprint() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let fp = pk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + let fp_bytes: Vec = fp.into(); + assert!(!fp_bytes.is_empty()); + } + + #[test] + fn test_seal_open_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let plaintext = b"hello X25519-ML-KEM-768 hybrid KEM!"; + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(plaintext, Codec::Chacha20Poly1305, b"") + .unwrap(); + + let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + + #[test] + fn test_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let sk2 = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let (sealed, _) = pk1 + .seal_view() + .unwrap() + .seal(b"secret data", Codec::Chacha20Poly1305, b"") + .unwrap(); + + assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_seal_requires_public_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + assert!(sk + .seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .is_err()); + } + + #[test] + fn test_open_requires_private_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .unwrap(); + + assert!(pk.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_unsupported_aead_codec() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Mlkem768Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + assert!(pk + .seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err()); + assert!(pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err()); + } +} diff --git a/src/views/x25519_sntrup761.rs b/src/views/x25519_sntrup761.rs new file mode 100644 index 0000000..0f051ec --- /dev/null +++ b/src/views/x25519_sntrup761.rs @@ -0,0 +1,505 @@ +// SPDX-License-Identifier: Apache-2.0 +//! X25519-sntrup761 hybrid KEM multikey view; combines X25519 ECDH with sntrup761 KEM, +//! ChaCha20-Poly1305 AEAD, and BLAKE3 KDF. + +use crate::{ + error::{AttributesError, ConversionsError, SealError}, + views::{aead, Views}, + AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, + SealView, +}; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; +use sntrup::sntrup761::{self, EncapsulationKey}; +use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroizing; + +/// Compatibility wrapper: allows `rand 0.8` OsRng to satisfy `rand_core 0.10` +/// `CryptoRng` required by the `sntrup` crate. +struct OsRng010; + +impl rand_core::TryRng for OsRng010 { + type Error = core::convert::Infallible; + + fn try_next_u32(&mut self) -> Result { + Ok(rand_core::Rng::next_u32(&mut rand::rng())) + } + fn try_next_u64(&mut self) -> Result { + Ok(rand_core::Rng::next_u64(&mut rand::rng())) + } + fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> { + rand_core::Rng::fill_bytes(&mut rand::rng(), dst); + Ok(()) + } +} +impl rand_core::TryCryptoRng for OsRng010 {} + +const X25519_SEED_LEN: usize = 32; +const SNTRUP761_SEED_LEN: usize = 32; +const PRIV_SEED_LEN: usize = X25519_SEED_LEN + SNTRUP761_SEED_LEN; // 64 +const X25519_PUB_LEN: usize = 32; +const SNTRUP761_PUB_LEN: usize = 1158; +const PUB_KEY_LEN: usize = X25519_PUB_LEN + SNTRUP761_PUB_LEN; // 1190 + +/// Decoded sealed message: (ephemeral_x25519_pub, sntrup761_ct, nonce, ciphertext+tag) +type SealedParts = (Vec, Vec, Vec, Vec); + +/// Only ChaCha20-Poly1305 allowed per spec +fn is_aead_allowed(codec: Codec) -> bool { + codec == Codec::Chacha20Poly1305 +} + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl<'a> AttrView for View<'a> { + fn is_encrypted(&self) -> bool { + false + } + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::X25519Sntrup761Priv + } + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::X25519Sntrup761Pub + } + fn is_secret_key_share(&self) -> bool { + false + } +} + +impl<'a> DataView for View<'a> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + self.key_bytes() + } +} + +impl<'a> ConvView for View<'a> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // X25519 public key + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let x25519_pub = PublicKey::from(&x25519_secret); + + // Sntrup761 public key + let sntrup_seed: [u8; 32] = secret_bytes[X25519_SEED_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| ConversionsError::SecretKeyFailure("invalid sntrup761 seed".into()))?; + let (ek, _dk) = sntrup761::generate_key_deterministic(&sntrup_seed); + + // Concatenate: x25519_pub (32) || sntrup761_pub (1158) + let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); + pub_bytes.extend_from_slice(x25519_pub.as_bytes()); + pub_bytes.extend_from_slice(ek.as_ref()); + + Builder::new(Codec::X25519Sntrup761Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&pub_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-sntrup761 not supported in SSH key format".into(), + ) + .into()) + } + fn to_ssh_private_key(&self) -> Result { + Err(ConversionsError::UnsupportedAlgorithm( + "X25519-sntrup761 not supported in SSH key format".into(), + ) + .into()) + } +} + +impl<'a> FingerprintView for View<'a> { + fn fingerprint(&self, codec: Codec) -> Result { + let pub_bytes = if self.is_secret_key() { + let pk = self.to_public_key()?; + let dv = pk.data_view()?; + dv.key_bytes()? + } else { + self.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, pub_bytes.as_slice())?.try_build()?) + } +} + +/// Combine shared secrets using BLAKE3: +/// ss = BLAKE3(label || ss_sntrup || ss_x25519 || sntrup_ct || ephemeral_x25519_pub) +fn combine_shared_secrets( + ss_sntrup: &[u8], + ss_x25519: &[u8], + sntrup_ct: &[u8], + ephemeral_x25519_pub: &[u8], +) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"x25519-sntrup761-hpke"); + hasher.update(ss_sntrup); + hasher.update(ss_x25519); + hasher.update(sntrup_ct); + hasher.update(ephemeral_x25519_pub); + *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) + .map_err(|_| SealError::InvalidFormat("missing ephemeral public key".into()))?; + let (sntrup_ct, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing sntrup761 ciphertext".into()))?; + let (nonce, ptr) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing nonce".into()))?; + let (ct_tag, _) = Varbytes::try_decode_from(ptr) + .map_err(|_| SealError::InvalidFormat("missing ciphertext".into()))?; + Ok(( + ephemeral_pub.to_inner(), + sntrup_ct.to_inner(), + nonce.to_inner(), + ct_tag.to_inner(), + )) +} + +impl<'a> SealView for View<'a> { + fn seal( + &self, + plaintext: &[u8], + aead_codec: Codec, + aad: &[u8], + ) -> Result<(Vec, Option), Error> { + if !self.is_public_key() { + return Err(SealError::NotEncapsulationKey.into()); + } + if !is_aead_allowed(aead_codec) { + return Err(SealError::UnsupportedAeadCodec(aead_codec).into()); + } + + let pub_bytes = self.key_bytes()?; + if pub_bytes.len() != PUB_KEY_LEN { + return Err( + SealError::EncapsulationFailed("invalid hybrid public key length".into()).into(), + ); + } + + // Split public key + let x25519_pub_arr: [u8; 32] = pub_bytes[..X25519_PUB_LEN] + .try_into() + .map_err(|_| SealError::EncapsulationFailed("invalid x25519 public key".into()))?; + let recipient_x25519_pub = PublicKey::from(x25519_pub_arr); + + let sntrup_ek = EncapsulationKey::try_from(&pub_bytes[X25519_PUB_LEN..]) + .map_err(|_| SealError::EncapsulationFailed("invalid sntrup761 public key".into()))?; + + // X25519: generate ephemeral keypair and ECDH + let ephemeral_secret = StaticSecret::random_from_rng(&mut rand::rng()); + let ephemeral_pub = PublicKey::from(&ephemeral_secret); + let ss_x25519 = ephemeral_secret.diffie_hellman(&recipient_x25519_pub); + + // Sntrup761: encapsulate + let (sntrup_ct, ss_sntrup) = sntrup_ek.encapsulate(&mut OsRng010); + + // Combine shared secrets via BLAKE3 + let combined_ss = combine_shared_secrets( + ss_sntrup.as_ref(), + ss_x25519.as_bytes(), + sntrup_ct.as_ref(), + ephemeral_pub.as_bytes(), + ); + + // AEAD encrypt + let (nonce, ct_tag) = aead::aead_seal(aead_codec, &combined_ss, plaintext, aad)?; + + Ok(( + encode_sealed( + ephemeral_pub.as_bytes(), + sntrup_ct.as_ref(), + &nonce, + &ct_tag, + ), + None, + )) + } +} + +impl<'a> OpenView for View<'a> { + fn open( + &self, + sealed_msg: &[u8], + _ephemeral: Option<&Multikey>, + aad: &[u8], + ) -> Result>, Error> { + if !self.is_secret_key() { + return Err(SealError::NotDecapsulationKey.into()); + } + + let (ephemeral_pub_bytes, sntrup_ct_bytes, nonce, ct_tag) = decode_sealed(sealed_msg)?; + + if ephemeral_pub_bytes.len() != X25519_PUB_LEN { + return Err( + SealError::InvalidFormat("invalid ephemeral public key length".into()).into(), + ); + } + + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + if secret_bytes.len() != PRIV_SEED_LEN { + return Err( + ConversionsError::SecretKeyFailure("invalid hybrid seed length".into()).into(), + ); + } + + // X25519 ECDH + let x25519_seed: [u8; 32] = secret_bytes[..X25519_SEED_LEN] + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid x25519 seed".into()))?; + let x25519_secret = StaticSecret::from(x25519_seed); + let ephemeral_pub_arr: [u8; 32] = ephemeral_pub_bytes + .as_slice() + .try_into() + .map_err(|_| SealError::InvalidFormat("invalid ephemeral public key".into()))?; + let ephemeral_pub = PublicKey::from(ephemeral_pub_arr); + let ss_x25519 = x25519_secret.diffie_hellman(&ephemeral_pub); + + // Sntrup761 decapsulate + let sntrup_seed: [u8; 32] = secret_bytes[X25519_SEED_LEN..PRIV_SEED_LEN] + .try_into() + .map_err(|_| SealError::DecapsulationFailed("invalid sntrup761 seed".into()))?; + let (_ek, dk) = sntrup761::generate_key_deterministic(&sntrup_seed); + + let sntrup_ct = sntrup761::Ciphertext::try_from(sntrup_ct_bytes.as_slice()) + .map_err(|_| SealError::DecapsulationFailed("invalid sntrup761 ciphertext".into()))?; + let ss_sntrup = dk.decapsulate(&sntrup_ct); + + // Combine shared secrets via BLAKE3 + let combined_ss = combine_shared_secrets( + ss_sntrup.as_ref(), + ss_x25519.as_bytes(), + sntrup_ct_bytes.as_slice(), + ephemeral_pub_bytes.as_slice(), + ); + + // AEAD decrypt + Ok(aead::aead_open( + Codec::Chacha20Poly1305, + &combined_ss, + &nonce, + &ct_tag, + aad, + )?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mk::X25519_SNTRUP761_KEY_CODECS; + use crate::views::Views; + + #[test] + fn test_key_gen_roundtrip() { + for codec in X25519_SNTRUP761_KEY_CODECS { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(codec, &mut rng) + .unwrap() + .with_comment("test hybrid kem key") + .try_build() + .unwrap(); + + let attr = mk.attr_view().unwrap(); + assert!(attr.is_secret_key()); + assert!(!attr.is_public_key()); + + // serialize/deserialize roundtrip + let bytes: Vec = mk.clone().into(); + let mk2 = Multikey::try_from(bytes.as_slice()).unwrap(); + assert_eq!(mk, mk2); + } + } + + #[test] + fn test_public_key_derivation() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let conv = mk.conv_view().unwrap(); + let pk = conv.to_public_key().unwrap(); + + let attr = pk.attr_view().unwrap(); + assert!(attr.is_public_key()); + assert!(!attr.is_secret_key()); + + // derive again => same result + let pk2 = conv.to_public_key().unwrap(); + assert_eq!(pk, pk2); + + // check public key length + let dv = pk.data_view().unwrap(); + assert_eq!(dv.key_bytes().unwrap().len(), PUB_KEY_LEN); + } + + #[test] + fn test_fingerprint() { + let mut rng = rand::rng(); + let mk = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let fp = pk + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha3256) + .unwrap(); + let fp_bytes: Vec = fp.into(); + assert!(!fp_bytes.is_empty()); + } + + #[test] + fn test_seal_open_roundtrip() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let plaintext = b"hello X25519-sntrup761 hybrid KEM!"; + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(plaintext, Codec::Chacha20Poly1305, b"") + .unwrap(); + + let opened = sk.open_view().unwrap().open(&sealed, None, b"").unwrap(); + assert_eq!(plaintext.as_slice(), opened.as_slice()); + } + + #[test] + fn test_wrong_key_fails() { + let mut rng = rand::rng(); + let sk1 = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk1 = sk1.conv_view().unwrap().to_public_key().unwrap(); + + let sk2 = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let (sealed, _) = pk1 + .seal_view() + .unwrap() + .seal(b"secret data", Codec::Chacha20Poly1305, b"") + .unwrap(); + + assert!(sk2.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_seal_requires_public_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + assert!(sk + .seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .is_err()); + } + + #[test] + fn test_open_requires_private_key() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + let (sealed, _) = pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Chacha20Poly1305, b"") + .unwrap(); + + assert!(pk.open_view().unwrap().open(&sealed, None, b"").is_err()); + } + + #[test] + fn test_unsupported_aead_codec() { + let mut rng = rand::rng(); + let sk = Builder::new_from_random_bytes(Codec::X25519Sntrup761Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + let pk = sk.conv_view().unwrap().to_public_key().unwrap(); + + // Only ChaCha20-Poly1305 is allowed + assert!(pk + .seal_view() + .unwrap() + .seal(b"data", Codec::AesGcm128, b"") + .is_err()); + assert!(pk + .seal_view() + .unwrap() + .seal(b"data", Codec::Xchacha20Poly1305, b"") + .is_err()); + } +} diff --git a/tests/edge_case_tests.rs b/tests/edge_case_tests.rs new file mode 100644 index 0000000..390feae --- /dev/null +++ b/tests/edge_case_tests.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Edge case tests for multi-key + +use multi_codec::Codec; +use multi_key::{Builder, Multikey, KEY_CODECS}; +use multi_trait::Null; + +/// Test null multikey +#[test] +fn test_null_multikey() { + let null_mk = Multikey::null(); + assert!(null_mk.is_null()); + assert_eq!(null_mk, Multikey::default()); +} + +/// Test all supported key codecs +#[test] +fn test_all_key_codecs() { + let mut rng = rand::rng(); + for &codec in KEY_CODECS.iter() { + let result = Builder::new_from_random_bytes(codec, &mut rng); + // Should be able to create builder for all supported codecs + assert!(result.is_ok(), "Failed for {:?}", codec); + } +} + +/// Test Clone trait +#[test] +fn test_clone() { + let mut rng = rand::rng(); + if let Ok(builder) = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) { + if let Ok(mk1) = builder.try_build() { + let mk2 = mk1.clone(); + assert_eq!(mk1, mk2); + } + } +} + +/// Test Send and Sync +#[test] +fn test_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); + assert_send::(); +} diff --git a/tests/proptest_tests.rs b/tests/proptest_tests.rs new file mode 100644 index 0000000..6313deb --- /dev/null +++ b/tests/proptest_tests.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Property-based tests for multi-key + +use multi_codec::Codec; +use multi_key::{Builder, Multikey, KEY_CODECS}; +use multi_trait::TryDecodeFrom; +use proptest::prelude::*; + +/// Property: Multikey encoding and decoding should roundtrip +#[test] +fn test_multikey_roundtrip() { + 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) { + if let Ok(mk1) = mk1.try_build() { + let bytes: Vec = mk1.clone().into(); + let (mk2, remaining) = Multikey::try_decode_from(&bytes).unwrap(); + + prop_assert_eq!(&mk1, &mk2); + prop_assert!(remaining.is_empty()); + } + } + } + }); +} + +/// Property: Clone produces equal value +#[test] +fn test_clone_equality() { + proptest!(|(_unit in 0..1u8)| { + let mut rng = rand::rng(); + if let Ok(builder) = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rng) { + if let Ok(mk1) = builder.try_build() { + let mk2 = mk1.clone(); + prop_assert_eq!(&mk1, &mk2); + } + } + }); +} diff --git a/tests/security_tests.rs b/tests/security_tests.rs new file mode 100644 index 0000000..ffac17b --- /dev/null +++ b/tests/security_tests.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +//! Security-focused tests for multi-key + +use multi_codec::Codec; +use multi_key::{Builder, Error, Multikey}; + +/// Test malformed multikey data +#[test] +fn test_malformed_data() { + let invalid = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]; + let result = Multikey::try_from(invalid.as_ref()); + assert!(result.is_err()); +} + +/// Test truncated multikey +#[test] +fn test_truncated_data() { + let truncated = vec![0x39]; // Just sigil + let result = Multikey::try_from(truncated.as_ref()); + assert!(result.is_err()); +} + +/// Test empty bytes +#[test] +fn test_empty_bytes() { + let result = Multikey::try_from(&[] as &[u8]); + assert!(result.is_err()); +} + +/// Test concurrent key generation +#[test] +fn test_concurrent_generation() { + use rand::SeedableRng; + use std::sync::Arc; + use std::sync::Mutex; + use std::thread; + + // ThreadRng is !Send; use the Send StdRng (still a rand_core 0.10 CryptoRng). + let rng = Arc::new(Mutex::new(rand::rngs::StdRng::from_rng(&mut rand::rng()))); + let mut handles = vec![]; + + for _ in 0..4 { + let rng_clone = Arc::clone(&rng); + let handle = thread::spawn(move || { + for _ in 0..2 { + let mut rng = rng_clone.lock().unwrap(); + if let Ok(builder) = Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut *rng) { + let _ = builder.try_build(); + } + } + }); + handles.push(handle); + } + + for handle in handles { + handle.join().unwrap(); + } +} + +/// Test error types are Send + Sync +#[test] +fn test_error_send_sync() { + fn assert_send() {} + fn assert_sync() {} + + assert_send::(); + assert_sync::(); +} + +/// Test zeroization of private keys +#[test] +fn test_private_key_zeroization() { + use multi_key::types::PrivateKeyBytes; + + { + let _privkey = PrivateKeyBytes::new(vec![0x42; 32]); + // Should be zeroized when dropped + } + // Memory should be cleared +} From df4cab81716eedc2dd1adf0bf264d57596382e5c Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 14:05:38 -0600 Subject: [PATCH 2/5] bump p521 Signed-off-by: Dave Grantham --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3297396..92bc6fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ ml-dsa = "0.1.1" ml-kem = { version = "0.2", features = ["deterministic"] } p256 = { version = "0.14", features = ["ecdsa", "ecdh"] } p384 = { version = "0.14", features = ["ecdsa", "ecdh"] } -p521 = { version = "0.14.0-rc.15", features = ["ecdsa", "ecdh"] } +p521 = { version = "0.14", features = ["ecdsa", "ecdh"] } poly1305 = "0.8" pq-mayo = "0.5.0" # RustCrypto 0.14 stack (vsss-rs 6, elliptic-curve 0.14, ed25519-dalek 3) is on From 04c58a170fc5aa3e5766356287f1cfd316df460e Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 15:54:33 -0600 Subject: [PATCH 3/5] fix codecs Signed-off-by: Dave Grantham --- Cargo.toml | 4 +- benches/multikey_bench.rs | 2 +- src/keysplit.rs | 4 +- src/mk.rs | 412 +++++++++++++++--------------- src/nonce.rs | 2 +- src/serde/de.rs | 2 +- src/types.rs | 2 +- src/views/aead.rs | 2 +- src/views/bls12381.rs | 2 +- src/views/bls12381_g1_fndsa512.rs | 6 +- src/views/bls12381_g1_mayo1.rs | 2 +- src/views/bls12381_g1_mayo2.rs | 2 +- src/views/bls12381_g1_mldsa65.rs | 24 +- src/views/chacha20.rs | 4 +- src/views/ed25519.rs | 6 +- src/views/ed25519_fndsa512.rs | 6 +- src/views/ed25519_mayo2.rs | 2 +- src/views/ed25519_mldsa65.rs | 26 +- src/views/fn_dsa.rs | 6 +- src/views/frodokem.rs | 40 ++- src/views/frodokem_helper.rs | 2 +- src/views/ml_dsa.rs | 34 +-- src/views/ml_kem.rs | 8 +- src/views/nist_p.rs | 4 +- src/views/secp256k1.rs | 10 +- src/views/slh_dsa.rs | 216 ++++++++-------- src/views/x25519_mlkem768.rs | 8 +- 27 files changed, 429 insertions(+), 409 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 92bc6fb..9d1ea65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-key" -version = "1.0.0" +version = "1.0.1" edition = "2021" authors = ["Dave Grantham "] description = "Multikey self-describing cryptographic key data" @@ -90,4 +90,4 @@ serde_test = "1.0" [[bench]] name = "multikey_bench" harness = false -path = "benches/multikey_bench.rs" \ No newline at end of file +path = "benches/multikey_bench.rs" diff --git a/benches/multikey_bench.rs b/benches/multikey_bench.rs index 0017685..9dc2212 100644 --- a/benches/multikey_bench.rs +++ b/benches/multikey_bench.rs @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 //! Performance benchmarks for multi-key +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use multi_codec::Codec; use multi_key::{Builder, Multikey}; -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; use std::hint::black_box; /// Benchmark key generation diff --git a/src/keysplit.rs b/src/keysplit.rs index ef77448..56fd793 100644 --- a/src/keysplit.rs +++ b/src/keysplit.rs @@ -22,11 +22,11 @@ use crate::mk::Attributes; use crate::{AttrId, Builder, Error, Multikey, Views}; use blsful::inner_types::{G1Projective, G2Projective, Scalar as BlsScalar}; -use multi_codec::Codec; -use multi_util::CodecInfo; 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::{ diff --git a/src/mk.rs b/src/mk.rs index f664eff..70fad31 100644 --- a/src/mk.rs +++ b/src/mk.rs @@ -17,12 +17,12 @@ use ::fn_dsa::{ sign_key_size, vrfy_key_size, KeyPairGenerator, KeyPairGeneratorStandard, FN_DSA_LOGN_1024, FN_DSA_LOGN_512, }; +use elliptic_curve::sec1::ToSec1Point; +use elliptic_curve::Generate; use multi_base::Base; use multi_codec::Codec; use multi_trait::{Null, TryDecodeFrom}; use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, Varuint}; -use elliptic_curve::sec1::ToSec1Point; -use elliptic_curve::Generate; use rand_core::CryptoRng; use ssh_key::{ private::{EcdsaKeypair, KeypairData}, @@ -50,7 +50,7 @@ pub const RSA_KEY_CODECS: [Codec; 3] = [Codec::Rsa2048Priv, Codec::Rsa3072Priv, pub const FN_DSA_KEY_CODECS: [Codec; 2] = [Codec::FnDsa512Priv, Codec::FnDsa1024Priv]; /// the list of ML-DSA key codecs supported for key generation -pub const ML_DSA_KEY_CODECS: [Codec; 2] = [Codec::MlDsa65Priv, Codec::MlDsa87Priv]; +pub const ML_DSA_KEY_CODECS: [Codec; 2] = [Codec::Mldsa65Priv, Codec::Mldsa87Priv]; /// the list of MAYO key codecs supported for key generation pub const MAYO_KEY_CODECS: [Codec; 4] = [ @@ -62,18 +62,18 @@ pub const MAYO_KEY_CODECS: [Codec; 4] = [ /// the list of SLH-DSA key codecs (all 12 parameter sets) pub const SLH_DSA_KEY_CODECS: [Codec; 12] = [ - Codec::SlhDsaSha2128FPriv, - Codec::SlhDsaSha2128SPriv, - Codec::SlhDsaSha2192FPriv, - Codec::SlhDsaSha2192SPriv, - Codec::SlhDsaSha2256FPriv, - Codec::SlhDsaSha2256SPriv, - Codec::SlhDsaShake128FPriv, - Codec::SlhDsaShake128SPriv, - Codec::SlhDsaShake192FPriv, - Codec::SlhDsaShake192SPriv, - Codec::SlhDsaShake256FPriv, - Codec::SlhDsaShake256SPriv, + Codec::SlhdsaSha2128FPriv, + Codec::SlhdsaSha2128SPriv, + Codec::SlhdsaSha2192FPriv, + Codec::SlhdsaSha2192SPriv, + Codec::SlhdsaSha2256FPriv, + Codec::SlhdsaSha2256SPriv, + Codec::SlhdsaShake128FPriv, + Codec::SlhdsaShake128SPriv, + Codec::SlhdsaShake192FPriv, + Codec::SlhdsaShake192SPriv, + Codec::SlhdsaShake256FPriv, + Codec::SlhdsaShake256SPriv, ]; /// the list of ML-KEM key codecs supported for key generation @@ -284,31 +284,31 @@ impl Views for Multikey { Ok(Box::new(secp256k1::View::try_from(self)?)) } Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(self)?)), - Codec::SlhDsaSha2128FPub - | Codec::SlhDsaSha2128SPub - | Codec::SlhDsaSha2192FPub - | Codec::SlhDsaSha2192SPub - | Codec::SlhDsaSha2256FPub - | Codec::SlhDsaSha2256SPub - | Codec::SlhDsaShake128FPub - | Codec::SlhDsaShake128SPub - | Codec::SlhDsaShake192FPub - | Codec::SlhDsaShake192SPub - | Codec::SlhDsaShake256FPub - | Codec::SlhDsaShake256SPub - | Codec::SlhDsaSha2128FPriv - | Codec::SlhDsaSha2128SPriv - | Codec::SlhDsaSha2192FPriv - | Codec::SlhDsaSha2192SPriv - | Codec::SlhDsaSha2256FPriv - | Codec::SlhDsaSha2256SPriv - | Codec::SlhDsaShake128FPriv - | Codec::SlhDsaShake128SPriv - | Codec::SlhDsaShake192FPriv - | Codec::SlhDsaShake192SPriv - | Codec::SlhDsaShake256FPriv - | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Codec::SlhdsaSha2128FPub + | Codec::SlhdsaSha2128SPub + | Codec::SlhdsaSha2192FPub + | Codec::SlhdsaSha2192SPub + | Codec::SlhdsaSha2256FPub + | Codec::SlhdsaSha2256SPub + | Codec::SlhdsaShake128FPub + | Codec::SlhdsaShake128SPub + | Codec::SlhdsaShake192FPub + | Codec::SlhdsaShake192SPub + | Codec::SlhdsaShake256FPub + | Codec::SlhdsaShake256SPub + | Codec::SlhdsaSha2128FPriv + | Codec::SlhdsaSha2128SPriv + | Codec::SlhdsaSha2192FPriv + | Codec::SlhdsaSha2192SPriv + | Codec::SlhdsaSha2256FPriv + | Codec::SlhdsaSha2256SPriv + | Codec::SlhdsaShake128FPriv + | Codec::SlhdsaShake128SPriv + | Codec::SlhdsaShake192FPriv + | Codec::SlhdsaShake192SPriv + | Codec::SlhdsaShake256FPriv + | Codec::SlhdsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Pub | Codec::Mldsa65Priv | Codec::Mldsa87Pub | Codec::Mldsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(self)?)) } Codec::Mayo1Pub @@ -434,31 +434,31 @@ impl Views for Multikey { Ok(Box::new(secp256k1::View::try_from(self)?)) } Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(self)?)), - Codec::SlhDsaSha2128FPub - | Codec::SlhDsaSha2128SPub - | Codec::SlhDsaSha2192FPub - | Codec::SlhDsaSha2192SPub - | Codec::SlhDsaSha2256FPub - | Codec::SlhDsaSha2256SPub - | Codec::SlhDsaShake128FPub - | Codec::SlhDsaShake128SPub - | Codec::SlhDsaShake192FPub - | Codec::SlhDsaShake192SPub - | Codec::SlhDsaShake256FPub - | Codec::SlhDsaShake256SPub - | Codec::SlhDsaSha2128FPriv - | Codec::SlhDsaSha2128SPriv - | Codec::SlhDsaSha2192FPriv - | Codec::SlhDsaSha2192SPriv - | Codec::SlhDsaSha2256FPriv - | Codec::SlhDsaSha2256SPriv - | Codec::SlhDsaShake128FPriv - | Codec::SlhDsaShake128SPriv - | Codec::SlhDsaShake192FPriv - | Codec::SlhDsaShake192SPriv - | Codec::SlhDsaShake256FPriv - | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Codec::SlhdsaSha2128FPub + | Codec::SlhdsaSha2128SPub + | Codec::SlhdsaSha2192FPub + | Codec::SlhdsaSha2192SPub + | Codec::SlhdsaSha2256FPub + | Codec::SlhdsaSha2256SPub + | Codec::SlhdsaShake128FPub + | Codec::SlhdsaShake128SPub + | Codec::SlhdsaShake192FPub + | Codec::SlhdsaShake192SPub + | Codec::SlhdsaShake256FPub + | Codec::SlhdsaShake256SPub + | Codec::SlhdsaSha2128FPriv + | Codec::SlhdsaSha2128SPriv + | Codec::SlhdsaSha2192FPriv + | Codec::SlhdsaSha2192SPriv + | Codec::SlhdsaSha2256FPriv + | Codec::SlhdsaSha2256SPriv + | Codec::SlhdsaShake128FPriv + | Codec::SlhdsaShake128SPriv + | Codec::SlhdsaShake192FPriv + | Codec::SlhdsaShake192SPriv + | Codec::SlhdsaShake256FPriv + | Codec::SlhdsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Pub | Codec::Mldsa65Priv | Codec::Mldsa87Pub | Codec::Mldsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(self)?)) } Codec::Mayo1Pub @@ -629,31 +629,31 @@ impl Views for Multikey { Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } - Codec::SlhDsaSha2128FPub - | Codec::SlhDsaSha2128SPub - | Codec::SlhDsaSha2192FPub - | Codec::SlhDsaSha2192SPub - | Codec::SlhDsaSha2256FPub - | Codec::SlhDsaSha2256SPub - | Codec::SlhDsaShake128FPub - | Codec::SlhDsaShake128SPub - | Codec::SlhDsaShake192FPub - | Codec::SlhDsaShake192SPub - | Codec::SlhDsaShake256FPub - | Codec::SlhDsaShake256SPub - | Codec::SlhDsaSha2128FPriv - | Codec::SlhDsaSha2128SPriv - | Codec::SlhDsaSha2192FPriv - | Codec::SlhDsaSha2192SPriv - | Codec::SlhDsaSha2256FPriv - | Codec::SlhDsaSha2256SPriv - | Codec::SlhDsaShake128FPriv - | Codec::SlhDsaShake128SPriv - | Codec::SlhDsaShake192FPriv - | Codec::SlhDsaShake192SPriv - | Codec::SlhDsaShake256FPriv - | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Codec::SlhdsaSha2128FPub + | Codec::SlhdsaSha2128SPub + | Codec::SlhdsaSha2192FPub + | Codec::SlhdsaSha2192SPub + | Codec::SlhdsaSha2256FPub + | Codec::SlhdsaSha2256SPub + | Codec::SlhdsaShake128FPub + | Codec::SlhdsaShake128SPub + | Codec::SlhdsaShake192FPub + | Codec::SlhdsaShake192SPub + | Codec::SlhdsaShake256FPub + | Codec::SlhdsaShake256SPub + | Codec::SlhdsaSha2128FPriv + | Codec::SlhdsaSha2128SPriv + | Codec::SlhdsaSha2192FPriv + | Codec::SlhdsaSha2192SPriv + | Codec::SlhdsaSha2256FPriv + | Codec::SlhdsaSha2256SPriv + | Codec::SlhdsaShake128FPriv + | Codec::SlhdsaShake128SPriv + | Codec::SlhdsaShake192FPriv + | Codec::SlhdsaShake192SPriv + | Codec::SlhdsaShake256FPriv + | Codec::SlhdsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Pub | Codec::Mldsa65Priv | Codec::Mldsa87Pub | Codec::Mldsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(self)?)) } Codec::Mayo1Pub @@ -766,31 +766,31 @@ impl Views for Multikey { Ok(Box::new(secp256k1::View::try_from(self)?)) } Codec::Chacha20Poly1305 => Ok(Box::new(chacha20::View::try_from(self)?)), - Codec::SlhDsaSha2128FPub - | Codec::SlhDsaSha2128SPub - | Codec::SlhDsaSha2192FPub - | Codec::SlhDsaSha2192SPub - | Codec::SlhDsaSha2256FPub - | Codec::SlhDsaSha2256SPub - | Codec::SlhDsaShake128FPub - | Codec::SlhDsaShake128SPub - | Codec::SlhDsaShake192FPub - | Codec::SlhDsaShake192SPub - | Codec::SlhDsaShake256FPub - | Codec::SlhDsaShake256SPub - | Codec::SlhDsaSha2128FPriv - | Codec::SlhDsaSha2128SPriv - | Codec::SlhDsaSha2192FPriv - | Codec::SlhDsaSha2192SPriv - | Codec::SlhDsaSha2256FPriv - | Codec::SlhDsaSha2256SPriv - | Codec::SlhDsaShake128FPriv - | Codec::SlhDsaShake128SPriv - | Codec::SlhDsaShake192FPriv - | Codec::SlhDsaShake192SPriv - | Codec::SlhDsaShake256FPriv - | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Codec::SlhdsaSha2128FPub + | Codec::SlhdsaSha2128SPub + | Codec::SlhdsaSha2192FPub + | Codec::SlhdsaSha2192SPub + | Codec::SlhdsaSha2256FPub + | Codec::SlhdsaSha2256SPub + | Codec::SlhdsaShake128FPub + | Codec::SlhdsaShake128SPub + | Codec::SlhdsaShake192FPub + | Codec::SlhdsaShake192SPub + | Codec::SlhdsaShake256FPub + | Codec::SlhdsaShake256SPub + | Codec::SlhdsaSha2128FPriv + | Codec::SlhdsaSha2128SPriv + | Codec::SlhdsaSha2192FPriv + | Codec::SlhdsaSha2192SPriv + | Codec::SlhdsaSha2256FPriv + | Codec::SlhdsaSha2256SPriv + | Codec::SlhdsaShake128FPriv + | Codec::SlhdsaShake128SPriv + | Codec::SlhdsaShake192FPriv + | Codec::SlhdsaShake192SPriv + | Codec::SlhdsaShake256FPriv + | Codec::SlhdsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Pub | Codec::Mldsa65Priv | Codec::Mldsa87Pub | Codec::Mldsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(self)?)) } Codec::Mayo1Pub @@ -1052,31 +1052,31 @@ impl Views for Multikey { Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } - Codec::SlhDsaSha2128FPub - | Codec::SlhDsaSha2128SPub - | Codec::SlhDsaSha2192FPub - | Codec::SlhDsaSha2192SPub - | Codec::SlhDsaSha2256FPub - | Codec::SlhDsaSha2256SPub - | Codec::SlhDsaShake128FPub - | Codec::SlhDsaShake128SPub - | Codec::SlhDsaShake192FPub - | Codec::SlhDsaShake192SPub - | Codec::SlhDsaShake256FPub - | Codec::SlhDsaShake256SPub - | Codec::SlhDsaSha2128FPriv - | Codec::SlhDsaSha2128SPriv - | Codec::SlhDsaSha2192FPriv - | Codec::SlhDsaSha2192SPriv - | Codec::SlhDsaSha2256FPriv - | Codec::SlhDsaSha2256SPriv - | Codec::SlhDsaShake128FPriv - | Codec::SlhDsaShake128SPriv - | Codec::SlhDsaShake192FPriv - | Codec::SlhDsaShake192SPriv - | Codec::SlhDsaShake256FPriv - | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Codec::SlhdsaSha2128FPub + | Codec::SlhdsaSha2128SPub + | Codec::SlhdsaSha2192FPub + | Codec::SlhdsaSha2192SPub + | Codec::SlhdsaSha2256FPub + | Codec::SlhdsaSha2256SPub + | Codec::SlhdsaShake128FPub + | Codec::SlhdsaShake128SPub + | Codec::SlhdsaShake192FPub + | Codec::SlhdsaShake192SPub + | Codec::SlhdsaShake256FPub + | Codec::SlhdsaShake256SPub + | Codec::SlhdsaSha2128FPriv + | Codec::SlhdsaSha2128SPriv + | Codec::SlhdsaSha2192FPriv + | Codec::SlhdsaSha2192SPriv + | Codec::SlhdsaSha2256FPriv + | Codec::SlhdsaSha2256SPriv + | Codec::SlhdsaShake128FPriv + | Codec::SlhdsaShake128SPriv + | Codec::SlhdsaShake192FPriv + | Codec::SlhdsaShake192SPriv + | Codec::SlhdsaShake256FPriv + | Codec::SlhdsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Pub | Codec::Mldsa65Priv | Codec::Mldsa87Pub | Codec::Mldsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(self)?)) } Codec::Mayo1Pub @@ -1153,31 +1153,31 @@ impl Views for Multikey { Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } - Codec::SlhDsaSha2128FPub - | Codec::SlhDsaSha2128SPub - | Codec::SlhDsaSha2192FPub - | Codec::SlhDsaSha2192SPub - | Codec::SlhDsaSha2256FPub - | Codec::SlhDsaSha2256SPub - | Codec::SlhDsaShake128FPub - | Codec::SlhDsaShake128SPub - | Codec::SlhDsaShake192FPub - | Codec::SlhDsaShake192SPub - | Codec::SlhDsaShake256FPub - | Codec::SlhDsaShake256SPub - | Codec::SlhDsaSha2128FPriv - | Codec::SlhDsaSha2128SPriv - | Codec::SlhDsaSha2192FPriv - | Codec::SlhDsaSha2192SPriv - | Codec::SlhDsaSha2256FPriv - | Codec::SlhDsaSha2256SPriv - | Codec::SlhDsaShake128FPriv - | Codec::SlhDsaShake128SPriv - | Codec::SlhDsaShake192FPriv - | Codec::SlhDsaShake192SPriv - | Codec::SlhDsaShake256FPriv - | Codec::SlhDsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), - Codec::MlDsa65Pub | Codec::MlDsa65Priv | Codec::MlDsa87Pub | Codec::MlDsa87Priv => { + Codec::SlhdsaSha2128FPub + | Codec::SlhdsaSha2128SPub + | Codec::SlhdsaSha2192FPub + | Codec::SlhdsaSha2192SPub + | Codec::SlhdsaSha2256FPub + | Codec::SlhdsaSha2256SPub + | Codec::SlhdsaShake128FPub + | Codec::SlhdsaShake128SPub + | Codec::SlhdsaShake192FPub + | Codec::SlhdsaShake192SPub + | Codec::SlhdsaShake256FPub + | Codec::SlhdsaShake256SPub + | Codec::SlhdsaSha2128FPriv + | Codec::SlhdsaSha2128SPriv + | Codec::SlhdsaSha2192FPriv + | Codec::SlhdsaSha2192SPriv + | Codec::SlhdsaSha2256FPriv + | Codec::SlhdsaSha2256SPriv + | Codec::SlhdsaShake128FPriv + | Codec::SlhdsaShake128SPriv + | Codec::SlhdsaShake192FPriv + | Codec::SlhdsaShake192SPriv + | Codec::SlhdsaShake256FPriv + | Codec::SlhdsaShake256SPriv => Ok(Box::new(slh_dsa::View::try_from(self)?)), + Codec::Mldsa65Pub | Codec::Mldsa65Priv | Codec::Mldsa87Pub | Codec::Mldsa87Priv => { Ok(Box::new(ml_dsa::View::try_from(self)?)) } Codec::Mayo1Pub @@ -1299,7 +1299,7 @@ impl Builder { ); sign_key } - Codec::MlDsa65Priv | Codec::MlDsa87Priv => { + Codec::Mldsa65Priv | Codec::Mldsa87Priv => { let mut seed = [0u8; 32]; rng.fill_bytes(&mut seed); seed.to_vec() @@ -1460,18 +1460,18 @@ 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>(), + 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>(), _ => return Err(ConversionsError::UnsupportedCodec(codec).into()), }; let mut attributes = Attributes::new(); @@ -1668,7 +1668,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::MlDsa65Pub, + codec: Codec::Mldsa65Pub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1687,7 +1687,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::MlDsa87Pub, + codec: Codec::Mldsa87Pub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1746,7 +1746,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2128FPub, + codec: Codec::SlhdsaSha2128FPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1765,7 +1765,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2128SPub, + codec: Codec::SlhdsaSha2128SPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1784,7 +1784,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2192FPub, + codec: Codec::SlhdsaSha2192FPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1803,7 +1803,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2192SPub, + codec: Codec::SlhdsaSha2192SPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1822,7 +1822,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2256FPub, + codec: Codec::SlhdsaSha2256FPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1841,7 +1841,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2256SPub, + codec: Codec::SlhdsaSha2256SPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1860,7 +1860,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake128FPub, + codec: Codec::SlhdsaShake128FPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1879,7 +1879,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake128SPub, + codec: Codec::SlhdsaShake128SPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1898,7 +1898,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake192FPub, + codec: Codec::SlhdsaShake192FPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1917,7 +1917,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake192SPub, + codec: Codec::SlhdsaShake192SPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1936,7 +1936,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake256FPub, + codec: Codec::SlhdsaShake256FPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -1955,7 +1955,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake256SPub, + codec: Codec::SlhdsaShake256SPub, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2163,7 +2163,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::MlDsa65Priv, + codec: Codec::Mldsa65Priv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2182,7 +2182,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::MlDsa87Priv, + codec: Codec::Mldsa87Priv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2241,7 +2241,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2128FPriv, + codec: Codec::SlhdsaSha2128FPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2260,7 +2260,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2128SPriv, + codec: Codec::SlhdsaSha2128SPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2279,7 +2279,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2192FPriv, + codec: Codec::SlhdsaSha2192FPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2298,7 +2298,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2192SPriv, + codec: Codec::SlhdsaSha2192SPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2317,7 +2317,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2256FPriv, + codec: Codec::SlhdsaSha2256FPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2336,7 +2336,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaSha2256SPriv, + codec: Codec::SlhdsaSha2256SPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2355,7 +2355,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake128FPriv, + codec: Codec::SlhdsaShake128FPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2374,7 +2374,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake128SPriv, + codec: Codec::SlhdsaShake128SPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2393,7 +2393,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake192FPriv, + codec: Codec::SlhdsaShake192FPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2412,7 +2412,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake192SPriv, + codec: Codec::SlhdsaShake192SPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2431,7 +2431,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake256FPriv, + codec: Codec::SlhdsaShake256FPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() @@ -2450,7 +2450,7 @@ impl Builder { let mut attributes = Attributes::new(); attributes.insert(AttrId::KeyData, key_bytes.into()); Ok(Builder { - codec: Codec::SlhDsaShake256SPriv, + codec: Codec::SlhdsaShake256SPriv, comment: Some(sshkey.comment().to_string()), attributes: Some(attributes), ..Default::default() diff --git a/src/nonce.rs b/src/nonce.rs index 29a4e87..6a96bfa 100644 --- a/src/nonce.rs +++ b/src/nonce.rs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{error::NonceError, Error}; +use core::fmt; use multi_base::Base; use multi_codec::Codec; use multi_trait::{Null, TryDecodeFrom}; use multi_util::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes}; -use core::fmt; use rand_core::CryptoRng; /// the Nonce multicodec sigil diff --git a/src/serde/de.rs b/src/serde/de.rs index 408efcf..ea0858a 100644 --- a/src/serde/de.rs +++ b/src/serde/de.rs @@ -3,9 +3,9 @@ use crate::{ mk::{self, Attributes}, nonce, AttrId, Multikey, Nonce, }; +use core::fmt; use multi_codec::Codec; use multi_util::EncodedVarbytes; -use core::fmt; use serde::{ de::{Error, MapAccess, Visitor}, Deserialize, Deserializer, diff --git a/src/types.rs b/src/types.rs index 87972f1..8d2f64c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 //! Type-safe wrappers for cryptographic key components -use multi_codec::Codec; use core::fmt; +use multi_codec::Codec; use zeroize::{Zeroize, ZeroizeOnDrop}; /// Public key bytes diff --git a/src/views/aead.rs b/src/views/aead.rs index d9cf9a2..b6a9419 100644 --- a/src/views/aead.rs +++ b/src/views/aead.rs @@ -6,9 +6,9 @@ use aes_gcm::{ aead::{Aead, KeyInit, Payload}, Aes128Gcm, Aes256Gcm, }; -use multi_codec::Codec; use chacha20poly1305::{ChaCha20Poly1305, XChaCha20Poly1305}; use hkdf::Hkdf; +use multi_codec::Codec; use sha2::Sha512; use zeroize::Zeroizing; diff --git a/src/views/bls12381.rs b/src/views/bls12381.rs index d1ad75d..c4e8c81 100644 --- a/src/views/bls12381.rs +++ b/src/views/bls12381.rs @@ -14,12 +14,12 @@ use blsful::{ Bls12381G1Impl, Bls12381G2Impl, PublicKey, PublicKeyShare, SecretKey, SecretKeyShare, Signature, SignatureSchemes, SignatureShare, TimeCryptCiphertext, SECRET_KEY_BYTES, }; +use elliptic_curve::group::GroupEncoding; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_sig::{ms, views::bls12381::SchemeTypeId, Multisig, Views as SigViews}; use multi_trait::TryDecodeFrom; use multi_util::{Varbytes, Varuint}; -use elliptic_curve::group::GroupEncoding; use rand_core::Rng; use ssh_encoding::{Decode, Encode}; use std::{array::TryFromSliceError, collections::BTreeMap}; diff --git a/src/views/bls12381_g1_fndsa512.rs b/src/views/bls12381_g1_fndsa512.rs index 4f727e0..8f3fea1 100644 --- a/src/views/bls12381_g1_fndsa512.rs +++ b/src/views/bls12381_g1_fndsa512.rs @@ -13,13 +13,13 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; use fn_dsa::{ sign_key_size, signature_size, vrfy_key_size, SigningKey as _, SigningKeyStandard, VerifyingKey as _, VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; use zeroize::Zeroizing; const LOGN: u32 = FN_DSA_LOGN_512; diff --git a/src/views/bls12381_g1_mayo1.rs b/src/views/bls12381_g1_mayo1.rs index 50aca57..9cc9241 100644 --- a/src/views/bls12381_g1_mayo1.rs +++ b/src/views/bls12381_g1_mayo1.rs @@ -13,10 +13,10 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; +use ml_dsa::signature::{Signer, Verifier}; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_sig::{ms, Multisig, Views as SigViews}; -use ml_dsa::signature::{Signer, Verifier}; use pq_mayo::{KeyPair, Mayo1, Signature, VerifyingKey}; use zeroize::Zeroizing; diff --git a/src/views/bls12381_g1_mayo2.rs b/src/views/bls12381_g1_mayo2.rs index b1a985f..e5f293b 100644 --- a/src/views/bls12381_g1_mayo2.rs +++ b/src/views/bls12381_g1_mayo2.rs @@ -13,10 +13,10 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; +use ml_dsa::signature::{Signer, Verifier}; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_sig::{ms, Multisig, Views as SigViews}; -use ml_dsa::signature::{Signer, Verifier}; use pq_mayo::{KeyPair, Mayo2, Signature, VerifyingKey}; use zeroize::Zeroizing; diff --git a/src/views/bls12381_g1_mldsa65.rs b/src/views/bls12381_g1_mldsa65.rs index 8598553..05a20c4 100644 --- a/src/views/bls12381_g1_mldsa65.rs +++ b/src/views/bls12381_g1_mldsa65.rs @@ -12,14 +12,14 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; +use ml_dsa::{ + signature::{Keypair, Signer as MldsaSigner, Verifier as MldsaVerifier}, + EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MldsaSeed, Signature as MldsaSig, + SigningKey as MldsaSigningKey, VerifyingKey as MldsaVerifyingKey, +}; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_sig::{ms, Multisig, Views as SigViews}; -use ml_dsa::{ - signature::{Keypair, Signer as MlDsaSigner, Verifier as MlDsaVerifier}, - EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MlDsaSeed, Signature as MlDsaSig, - SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, -}; use zeroize::Zeroizing; const MLDSA65_SEED_LEN: usize = 32; @@ -95,8 +95,8 @@ impl<'a> ConvView for View<'a> { let mldsa_seed: [u8; 32] = secret_bytes[bls::BLS_G1_SECRET_LEN..PRIV_SEED_LEN] .try_into() .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-dsa-65 seed".into()))?; - let mldsa_seed = MlDsaSeed::from(mldsa_seed); - let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + let mldsa_seed = MldsaSeed::from(mldsa_seed); + let kp = MldsaSigningKey::::from_seed(&mldsa_seed); // Concatenate (classical-first): bls_g1_pub (96) || mldsa65_pub (1952) let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); @@ -160,13 +160,13 @@ impl<'a> SignView for View<'a> { let mldsa_seed: [u8; 32] = secret_bytes[bls::BLS_G1_SECRET_LEN..PRIV_SEED_LEN] .try_into() .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-dsa-65 seed".into()))?; - let mldsa_seed = MlDsaSeed::from(mldsa_seed); - let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + let mldsa_seed = MldsaSeed::from(mldsa_seed); + let kp = MldsaSigningKey::::from_seed(&mldsa_seed); let mut m2 = Vec::with_capacity(msg.len() + s1.len()); m2.extend_from_slice(msg); m2.extend_from_slice(&s1); - let s2 = MlDsaSigner::sign(&kp, &m2); + let s2 = MldsaSigner::sign(&kp, &m2); // Step 3: sig = s1 || s2 let mut sig_bytes = Vec::with_capacity(HYBRID_SIG_LEN); @@ -233,10 +233,10 @@ impl<'a> VerifyView for View<'a> { EncodedVerifyingKey::::try_from(mldsa_pub_bytes).map_err(|_| { ConversionsError::PublicKeyFailure("invalid ML-DSA-65 public key".into()) })?; - let mldsa_vk = MlDsaVerifyingKey::::decode(&encoded_vk); + let mldsa_vk = MldsaVerifyingKey::::decode(&encoded_vk); let encoded_sig = EncodedSignature::::try_from(s2_bytes) .map_err(|_| VerifyError::BadSignature("invalid ML-DSA-65 signature".into()))?; - let s2 = MlDsaSig::decode(&encoded_sig).ok_or(VerifyError::BadSignature( + let s2 = MldsaSig::decode(&encoded_sig).ok_or(VerifyError::BadSignature( "invalid ML-DSA-65 signature".into(), ))?; diff --git a/src/views/chacha20.rs b/src/views/chacha20.rs index 66c8a6a..c24ae45 100644 --- a/src/views/chacha20.rs +++ b/src/views/chacha20.rs @@ -4,12 +4,12 @@ use crate::{ AttrId, AttrView, CipherAttrView, CipherView, DataView, Error, FingerprintView, KdfAttrView, Multikey, Views, }; +use chacha20::cipher::{KeyIvInit, StreamCipher}; +use chacha20::{ChaCha20, Nonce}; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_trait::TryDecodeFrom; use multi_util::Varuint; -use chacha20::cipher::{KeyIvInit, StreamCipher}; -use chacha20::{ChaCha20, Nonce}; use zeroize::Zeroizing; use super::bcrypt::SALT_LENGTH; diff --git a/src/views/ed25519.rs b/src/views/ed25519.rs index 5fbdc64..12b5528 100644 --- a/src/views/ed25519.rs +++ b/src/views/ed25519.rs @@ -4,14 +4,14 @@ use crate::{ AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, KdfAttrView, Multikey, SignView, VerifyView, Views, }; +use ed25519_dalek::{ + Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, +}; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_sig::{ms, Multisig, Views as SigViews}; use multi_trait::TryDecodeFrom; use multi_util::Varuint; -use ed25519_dalek::{ - Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, -}; use zeroize::Zeroizing; pub(crate) struct View<'a> { diff --git a/src/views/ed25519_fndsa512.rs b/src/views/ed25519_fndsa512.rs index ef4a36f..fbe79d4 100644 --- a/src/views/ed25519_fndsa512.rs +++ b/src/views/ed25519_fndsa512.rs @@ -11,14 +11,14 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; use fn_dsa::{ sign_key_size, signature_size, vrfy_key_size, SigningKey as _, SigningKeyStandard, VerifyingKey as _, VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_512, HASH_ID_RAW, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; use zeroize::Zeroizing; const LOGN: u32 = FN_DSA_LOGN_512; diff --git a/src/views/ed25519_mayo2.rs b/src/views/ed25519_mayo2.rs index 8007eea..d216ecc 100644 --- a/src/views/ed25519_mayo2.rs +++ b/src/views/ed25519_mayo2.rs @@ -9,10 +9,10 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; +use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_sig::{ms, Multisig, Views as SigViews}; -use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; use pq_mayo::{KeyPair, Mayo2}; use zeroize::Zeroizing; diff --git a/src/views/ed25519_mldsa65.rs b/src/views/ed25519_mldsa65.rs index e1335a8..75d3908 100644 --- a/src/views/ed25519_mldsa65.rs +++ b/src/views/ed25519_mldsa65.rs @@ -10,15 +10,15 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; use ed25519_dalek::{Signature as Ed25519Sig, SigningKey, VerifyingKey}; use ml_dsa::{ - signature::{Keypair, Signer as MlDsaSigner, Verifier as MlDsaVerifier}, - EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MlDsaSeed, Signature as MlDsaSig, - SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, + signature::{Keypair, Signer as MldsaSigner, Verifier as MldsaVerifier}, + EncodedSignature, EncodedVerifyingKey, MlDsa65, Seed as MldsaSeed, Signature as MldsaSig, + SigningKey as MldsaSigningKey, VerifyingKey as MldsaVerifyingKey, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; use zeroize::Zeroizing; const ED25519_SEED_LEN: usize = 32; @@ -101,8 +101,8 @@ impl<'a> ConvView for View<'a> { let mldsa_seed: [u8; 32] = secret_bytes[ED25519_SEED_LEN..PRIV_SEED_LEN] .try_into() .map_err(|_| ConversionsError::SecretKeyFailure("invalid ml-dsa-65 seed".into()))?; - let mldsa_seed = MlDsaSeed::from(mldsa_seed); - let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + let mldsa_seed = MldsaSeed::from(mldsa_seed); + let kp = MldsaSigningKey::::from_seed(&mldsa_seed); // Concatenate (classical-first): ed25519_pub (32) || mldsa65_pub (1952) let mut pub_bytes = Vec::with_capacity(PUB_KEY_LEN); @@ -173,14 +173,14 @@ impl<'a> SignView for View<'a> { .map_err(|_| { ConversionsError::SecretKeyFailure("failed to get ml-dsa-65 seed".into()) })?; - let mldsa_seed = MlDsaSeed::from(mldsa_seed); - let kp = MlDsaSigningKey::::from_seed(&mldsa_seed); + let mldsa_seed = MldsaSeed::from(mldsa_seed); + let kp = MldsaSigningKey::::from_seed(&mldsa_seed); let mut m2 = Vec::with_capacity(msg.len() + ED25519_SIG_LEN); m2.extend_from_slice(msg); m2.extend_from_slice(&s1.to_bytes()); - let s2 = MlDsaSigner::sign(&kp, &m2); + let s2 = MldsaSigner::sign(&kp, &m2); // Step 3: sig = s1 || s2 let mut sig_bytes = Vec::with_capacity(HYBRID_SIG_LEN); @@ -257,10 +257,10 @@ impl<'a> VerifyView for View<'a> { EncodedVerifyingKey::::try_from(mldsa_pub_bytes).map_err(|_| { ConversionsError::PublicKeyFailure("invalid ML-DSA-65 public key".into()) })?; - let mldsa_vk = MlDsaVerifyingKey::::decode(&encoded_vk); + let mldsa_vk = MldsaVerifyingKey::::decode(&encoded_vk); let encoded_sig = EncodedSignature::::try_from(s2_bytes) .map_err(|_| VerifyError::BadSignature("invalid ML-DSA-65 signature".into()))?; - let s2 = MlDsaSig::decode(&encoded_sig).ok_or(VerifyError::BadSignature( + let s2 = MldsaSig::decode(&encoded_sig).ok_or(VerifyError::BadSignature( "invalid ML-DSA-65 signature".into(), ))?; diff --git a/src/views/fn_dsa.rs b/src/views/fn_dsa.rs index ab84136..cf61454 100644 --- a/src/views/fn_dsa.rs +++ b/src/views/fn_dsa.rs @@ -7,13 +7,13 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Views as _}; use fn_dsa::{ signature_size, vrfy_key_size, SigningKey, SigningKeyStandard, VerifyingKey, VerifyingKeyStandard, DOMAIN_NONE, FN_DSA_LOGN_1024, FN_DSA_LOGN_512, HASH_ID_RAW, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Views as _}; use ssh_encoding::{Decode, Encode}; use zeroize::Zeroizing; diff --git a/src/views/frodokem.rs b/src/views/frodokem.rs index 37dc742..6fb3f6a 100644 --- a/src/views/frodokem.rs +++ b/src/views/frodokem.rs @@ -60,12 +60,24 @@ fn public_codec(codec: Codec) -> Result { fn public_from_private(codec: Codec, secret_bytes: &[u8]) -> Result, Error> { match codec { - Codec::FrodoKem640AesPriv => super::frodokem_helper::public_from_private_640aes(secret_bytes), - Codec::FrodoKem976AesPriv => super::frodokem_helper::public_from_private_976aes(secret_bytes), - Codec::FrodoKem1344AesPriv => super::frodokem_helper::public_from_private_1344aes(secret_bytes), - Codec::FrodoKem640ShakePriv => super::frodokem_helper::public_from_private_640shake(secret_bytes), - Codec::FrodoKem976ShakePriv => super::frodokem_helper::public_from_private_976shake(secret_bytes), - Codec::FrodoKem1344ShakePriv => super::frodokem_helper::public_from_private_1344shake(secret_bytes), + Codec::FrodoKem640AesPriv => { + super::frodokem_helper::public_from_private_640aes(secret_bytes) + } + Codec::FrodoKem976AesPriv => { + super::frodokem_helper::public_from_private_976aes(secret_bytes) + } + Codec::FrodoKem1344AesPriv => { + super::frodokem_helper::public_from_private_1344aes(secret_bytes) + } + Codec::FrodoKem640ShakePriv => { + super::frodokem_helper::public_from_private_640shake(secret_bytes) + } + Codec::FrodoKem976ShakePriv => { + super::frodokem_helper::public_from_private_976shake(secret_bytes) + } + Codec::FrodoKem1344ShakePriv => { + super::frodokem_helper::public_from_private_1344shake(secret_bytes) + } _ => { return Err( ConversionsError::SecretKeyFailure("not a FrodoKEM private key".into()).into(), @@ -108,10 +120,18 @@ fn decap( match codec { Codec::FrodoKem640AesPriv => super::frodokem_helper::decap_640aes(secret_bytes, ciphertext), Codec::FrodoKem976AesPriv => super::frodokem_helper::decap_976aes(secret_bytes, ciphertext), - Codec::FrodoKem1344AesPriv => super::frodokem_helper::decap_1344aes(secret_bytes, ciphertext), - Codec::FrodoKem640ShakePriv => super::frodokem_helper::decap_640shake(secret_bytes, ciphertext), - Codec::FrodoKem976ShakePriv => super::frodokem_helper::decap_976shake(secret_bytes, ciphertext), - Codec::FrodoKem1344ShakePriv => super::frodokem_helper::decap_1344shake(secret_bytes, ciphertext), + Codec::FrodoKem1344AesPriv => { + super::frodokem_helper::decap_1344aes(secret_bytes, ciphertext) + } + Codec::FrodoKem640ShakePriv => { + super::frodokem_helper::decap_640shake(secret_bytes, ciphertext) + } + Codec::FrodoKem976ShakePriv => { + super::frodokem_helper::decap_976shake(secret_bytes, ciphertext) + } + Codec::FrodoKem1344ShakePriv => { + super::frodokem_helper::decap_1344shake(secret_bytes, ciphertext) + } _ => return Err(SealError::NotDecapsulationKey.into()), } .map_err(|e| SealError::DecapsulationFailed(e).into()) diff --git a/src/views/frodokem_helper.rs b/src/views/frodokem_helper.rs index 6a19162..59219d2 100644 --- a/src/views/frodokem_helper.rs +++ b/src/views/frodokem_helper.rs @@ -200,4 +200,4 @@ pub fn decap_1344shake( pub fn public_from_private_1344shake(secret_key_bytes: &[u8]) -> Result, String> { public_from_private(Algorithm::FrodoKem1344Shake, secret_key_bytes) -} \ No newline at end of file +} diff --git a/src/views/ml_dsa.rs b/src/views/ml_dsa.rs index 5d61864..dcea5e4 100644 --- a/src/views/ml_dsa.rs +++ b/src/views/ml_dsa.rs @@ -7,14 +7,14 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, SignView, VerifyView, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Views as _}; use ml_dsa::{ signature::{Keypair, Signer, Verifier}, EncodedSignature, EncodedVerifyingKey, MlDsa65, MlDsa87, Seed, Signature, SigningKey, VerifyingKey, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Views as _}; use ssh_encoding::{Decode, Encode}; use zeroize::Zeroizing; @@ -24,11 +24,11 @@ pub const ALGORITHM_NAME_65: &str = "ml-dsa-65@multikey"; pub const ALGORITHM_NAME_87: &str = "ml-dsa-87@multikey"; fn is_ml_dsa_priv(codec: Codec) -> bool { - codec == Codec::MlDsa65Priv || codec == Codec::MlDsa87Priv + codec == Codec::Mldsa65Priv || codec == Codec::Mldsa87Priv } fn is_ml_dsa_pub(codec: Codec) -> bool { - codec == Codec::MlDsa65Pub || codec == Codec::MlDsa87Pub + codec == Codec::Mldsa65Pub || codec == Codec::Mldsa87Pub } const ML_DSA_SEED_LENGTH: usize = 32; @@ -91,7 +91,7 @@ impl<'a> ConvView for View<'a> { }; let (public_key, codec) = match (self.mk.codec, secret_bytes.len()) { - (Codec::MlDsa65Priv, ML_DSA_SEED_LENGTH) => { + (Codec::Mldsa65Priv, ML_DSA_SEED_LENGTH) => { let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = secret_bytes.as_slice().try_into().map_err(|_| { ConversionsError::SecretKeyFailure("invalid seed length".into()) @@ -100,10 +100,10 @@ impl<'a> ConvView for View<'a> { let kp = SigningKey::::from_seed(&seed); ( kp.verifying_key().encode().as_slice().to_vec(), - Codec::MlDsa65Pub, + Codec::Mldsa65Pub, ) } - (Codec::MlDsa87Priv, ML_DSA_SEED_LENGTH) => { + (Codec::Mldsa87Priv, ML_DSA_SEED_LENGTH) => { let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = secret_bytes.as_slice().try_into().map_err(|_| { ConversionsError::SecretKeyFailure("invalid seed length".into()) @@ -112,7 +112,7 @@ impl<'a> ConvView for View<'a> { let kp = SigningKey::::from_seed(&seed); ( kp.verifying_key().encode().as_slice().to_vec(), - Codec::MlDsa87Pub, + Codec::Mldsa87Pub, ) } _ => { @@ -142,8 +142,8 @@ impl<'a> ConvView for View<'a> { // Determine algorithm name based on codec let algorithm_name = match pk.codec { - Codec::MlDsa65Pub => ALGORITHM_NAME_65, - Codec::MlDsa87Pub => ALGORITHM_NAME_87, + Codec::Mldsa65Pub => ALGORITHM_NAME_65, + Codec::Mldsa87Pub => ALGORITHM_NAME_87, _ => return Err(ConversionsError::UnsupportedCodec(pk.codec).into()), }; @@ -174,8 +174,8 @@ impl<'a> ConvView for View<'a> { // Determine algorithm name based on codec let algorithm_name = match self.mk.codec { - Codec::MlDsa65Priv => ALGORITHM_NAME_65, - Codec::MlDsa87Priv => ALGORITHM_NAME_87, + Codec::Mldsa65Priv => ALGORITHM_NAME_65, + Codec::Mldsa87Priv => ALGORITHM_NAME_87, _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), }; @@ -248,7 +248,7 @@ impl<'a> SignView for View<'a> { kd.secret_bytes()? }; let (signature, codec) = match (self.mk.codec, secret_bytes.len()) { - (Codec::MlDsa65Priv, ML_DSA_SEED_LENGTH) => { + (Codec::Mldsa65Priv, ML_DSA_SEED_LENGTH) => { let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = secret_bytes.as_slice().try_into().map_err(|_| { ConversionsError::SecretKeyFailure("invalid seed length".into()) @@ -257,10 +257,10 @@ impl<'a> SignView for View<'a> { let kp = SigningKey::::from_seed(&seed); ( kp.sign(msg).encode().as_slice().to_vec(), - Codec::MlDsa65Msig, + Codec::Mldsa65Msig, ) } - (Codec::MlDsa87Priv, ML_DSA_SEED_LENGTH) => { + (Codec::Mldsa87Priv, ML_DSA_SEED_LENGTH) => { let seed_bytes: [u8; ML_DSA_SEED_LENGTH] = secret_bytes.as_slice().try_into().map_err(|_| { ConversionsError::SecretKeyFailure("invalid seed length".into()) @@ -269,7 +269,7 @@ impl<'a> SignView for View<'a> { let kp = SigningKey::::from_seed(&seed); ( kp.sign(msg).encode().as_slice().to_vec(), - Codec::MlDsa87Msig, + Codec::Mldsa87Msig, ) } _ => { diff --git a/src/views/ml_kem.rs b/src/views/ml_kem.rs index ce0381a..ac7e2e0 100644 --- a/src/views/ml_kem.rs +++ b/src/views/ml_kem.rs @@ -7,14 +7,14 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_trait::TryDecodeFrom; -use multi_util::Varbytes; use ml_kem::{ kem::{Decapsulate, Encapsulate}, EncodedSizeUser, KemCore, MlKem1024, MlKem768, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; use zeroize::Zeroizing; const ML_KEM_SEED_LENGTH: usize = 64; // d (32) || z (32) diff --git a/src/views/nist_p.rs b/src/views/nist_p.rs index 0f78c52..3d6f2d7 100644 --- a/src/views/nist_p.rs +++ b/src/views/nist_p.rs @@ -10,14 +10,14 @@ use crate::{ KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, }; +use elliptic_curve::sec1::ToSec1Point; +use elliptic_curve::Generate; use multi_codec::Codec; use multi_hash::{mh, Multihash}; use multi_sig::{ms, Multisig, Views as SigViews}; use multi_trait::TryDecodeFrom; use multi_util::Varbytes; use multi_util::Varuint; -use elliptic_curve::sec1::ToSec1Point; -use elliptic_curve::Generate; use zeroize::Zeroizing; /// P-256: 32-byte secret diff --git a/src/views/secp256k1.rs b/src/views/secp256k1.rs index 27df66b..4dfff3c 100644 --- a/src/views/secp256k1.rs +++ b/src/views/secp256k1.rs @@ -8,17 +8,17 @@ use crate::{ KdfAttrView, Multikey, OpenView, SealView, SignView, VerifyView, Views, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_sig::{ms, Multisig, Views as SigViews}; -use multi_trait::TryDecodeFrom; -use multi_util::{Varbytes, Varuint}; use elliptic_curve::sec1::ToSec1Point; use elliptic_curve::Generate; use k256::ecdsa::{ signature::{Signer, Verifier}, Signature, SigningKey, VerifyingKey, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_sig::{ms, Multisig, Views as SigViews}; +use multi_trait::TryDecodeFrom; +use multi_util::{Varbytes, Varuint}; use ssh_encoding::{Decode, Encode}; use zeroize::Zeroizing; diff --git a/src/views/slh_dsa.rs b/src/views/slh_dsa.rs index 66f549a..dcfe2da 100644 --- a/src/views/slh_dsa.rs +++ b/src/views/slh_dsa.rs @@ -58,36 +58,36 @@ where fn is_slh_dsa_priv(codec: Codec) -> bool { matches!( codec, - Codec::SlhDsaSha2128FPriv - | Codec::SlhDsaSha2128SPriv - | Codec::SlhDsaSha2192FPriv - | Codec::SlhDsaSha2192SPriv - | Codec::SlhDsaSha2256FPriv - | Codec::SlhDsaSha2256SPriv - | Codec::SlhDsaShake128FPriv - | Codec::SlhDsaShake128SPriv - | Codec::SlhDsaShake192FPriv - | Codec::SlhDsaShake192SPriv - | Codec::SlhDsaShake256FPriv - | Codec::SlhDsaShake256SPriv + Codec::SlhdsaSha2128FPriv + | Codec::SlhdsaSha2128SPriv + | Codec::SlhdsaSha2192FPriv + | Codec::SlhdsaSha2192SPriv + | Codec::SlhdsaSha2256FPriv + | Codec::SlhdsaSha2256SPriv + | Codec::SlhdsaShake128FPriv + | Codec::SlhdsaShake128SPriv + | Codec::SlhdsaShake192FPriv + | Codec::SlhdsaShake192SPriv + | Codec::SlhdsaShake256FPriv + | Codec::SlhdsaShake256SPriv ) } fn is_slh_dsa_pub(codec: Codec) -> bool { matches!( codec, - Codec::SlhDsaSha2128FPub - | Codec::SlhDsaSha2128SPub - | Codec::SlhDsaSha2192FPub - | Codec::SlhDsaSha2192SPub - | Codec::SlhDsaSha2256FPub - | Codec::SlhDsaSha2256SPub - | Codec::SlhDsaShake128FPub - | Codec::SlhDsaShake128SPub - | Codec::SlhDsaShake192FPub - | Codec::SlhDsaShake192SPub - | Codec::SlhDsaShake256FPub - | Codec::SlhDsaShake256SPub + Codec::SlhdsaSha2128FPub + | Codec::SlhdsaSha2128SPub + | Codec::SlhdsaSha2192FPub + | Codec::SlhdsaSha2192SPub + | Codec::SlhdsaSha2256FPub + | Codec::SlhdsaSha2256SPub + | Codec::SlhdsaShake128FPub + | Codec::SlhdsaShake128SPub + | Codec::SlhdsaShake192FPub + | Codec::SlhdsaShake192SPub + | Codec::SlhdsaShake256FPub + | Codec::SlhdsaShake256SPub ) } @@ -143,113 +143,113 @@ impl<'a> ConvView for View<'a> { }; let (public_key, codec) = match self.mk.codec { - Codec::SlhDsaSha2128FPriv => ( + Codec::SlhdsaSha2128FPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaSha2128FPub, + Codec::SlhdsaSha2128FPub, ), - Codec::SlhDsaSha2128SPriv => ( + Codec::SlhdsaSha2128SPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaSha2128SPub, + Codec::SlhdsaSha2128SPub, ), - Codec::SlhDsaSha2192FPriv => ( + Codec::SlhdsaSha2192FPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaSha2192FPub, + Codec::SlhdsaSha2192FPub, ), - Codec::SlhDsaSha2192SPriv => ( + Codec::SlhdsaSha2192SPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaSha2192SPub, + Codec::SlhdsaSha2192SPub, ), - Codec::SlhDsaSha2256FPriv => ( + Codec::SlhdsaSha2256FPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaSha2256FPub, + Codec::SlhdsaSha2256FPub, ), - Codec::SlhDsaSha2256SPriv => ( + Codec::SlhdsaSha2256SPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaSha2256SPub, + Codec::SlhdsaSha2256SPub, ), - Codec::SlhDsaShake128FPriv => ( + Codec::SlhdsaShake128FPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaShake128FPub, + Codec::SlhdsaShake128FPub, ), - Codec::SlhDsaShake128SPriv => ( + Codec::SlhdsaShake128SPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaShake128SPub, + Codec::SlhdsaShake128SPub, ), - Codec::SlhDsaShake192FPriv => ( + Codec::SlhdsaShake192FPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaShake192FPub, + Codec::SlhdsaShake192FPub, ), - Codec::SlhDsaShake192SPriv => ( + Codec::SlhdsaShake192SPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaShake192SPub, + Codec::SlhdsaShake192SPub, ), - Codec::SlhDsaShake256FPriv => ( + Codec::SlhdsaShake256FPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaShake256FPub, + Codec::SlhdsaShake256FPub, ), - Codec::SlhDsaShake256SPriv => ( + Codec::SlhdsaShake256SPriv => ( SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })? .verifying_key() .to_vec(), - Codec::SlhDsaShake256SPub, + Codec::SlhdsaShake256SPub, ), _ => { return Err( @@ -276,18 +276,18 @@ impl<'a> ConvView for View<'a> { // Determine algorithm name based on codec let algorithm_name = match pk.codec { - Codec::SlhDsaSha2128FPub => ALGORITHM_NAME_SHA2_128F, - Codec::SlhDsaSha2128SPub => ALGORITHM_NAME_SHA2_128S, - Codec::SlhDsaSha2192FPub => ALGORITHM_NAME_SHA2_192F, - Codec::SlhDsaSha2192SPub => ALGORITHM_NAME_SHA2_192S, - Codec::SlhDsaSha2256FPub => ALGORITHM_NAME_SHA2_256F, - Codec::SlhDsaSha2256SPub => ALGORITHM_NAME_SHA2_256S, - Codec::SlhDsaShake128FPub => ALGORITHM_NAME_SHAKE_128F, - Codec::SlhDsaShake128SPub => ALGORITHM_NAME_SHAKE_128S, - Codec::SlhDsaShake192FPub => ALGORITHM_NAME_SHAKE_192F, - Codec::SlhDsaShake192SPub => ALGORITHM_NAME_SHAKE_192S, - Codec::SlhDsaShake256FPub => ALGORITHM_NAME_SHAKE_256F, - Codec::SlhDsaShake256SPub => ALGORITHM_NAME_SHAKE_256S, + Codec::SlhdsaSha2128FPub => ALGORITHM_NAME_SHA2_128F, + Codec::SlhdsaSha2128SPub => ALGORITHM_NAME_SHA2_128S, + Codec::SlhdsaSha2192FPub => ALGORITHM_NAME_SHA2_192F, + Codec::SlhdsaSha2192SPub => ALGORITHM_NAME_SHA2_192S, + Codec::SlhdsaSha2256FPub => ALGORITHM_NAME_SHA2_256F, + Codec::SlhdsaSha2256SPub => ALGORITHM_NAME_SHA2_256S, + Codec::SlhdsaShake128FPub => ALGORITHM_NAME_SHAKE_128F, + Codec::SlhdsaShake128SPub => ALGORITHM_NAME_SHAKE_128S, + Codec::SlhdsaShake192FPub => ALGORITHM_NAME_SHAKE_192F, + Codec::SlhdsaShake192SPub => ALGORITHM_NAME_SHAKE_192S, + Codec::SlhdsaShake256FPub => ALGORITHM_NAME_SHAKE_256F, + Codec::SlhdsaShake256SPub => ALGORITHM_NAME_SHAKE_256S, _ => return Err(ConversionsError::UnsupportedCodec(pk.codec).into()), }; @@ -318,18 +318,18 @@ impl<'a> ConvView for View<'a> { // Determine algorithm name based on codec let algorithm_name = match self.mk.codec { - Codec::SlhDsaSha2128FPriv => ALGORITHM_NAME_SHA2_128F, - Codec::SlhDsaSha2128SPriv => ALGORITHM_NAME_SHA2_128S, - Codec::SlhDsaSha2192FPriv => ALGORITHM_NAME_SHA2_192F, - Codec::SlhDsaSha2192SPriv => ALGORITHM_NAME_SHA2_192S, - Codec::SlhDsaSha2256FPriv => ALGORITHM_NAME_SHA2_256F, - Codec::SlhDsaSha2256SPriv => ALGORITHM_NAME_SHA2_256S, - Codec::SlhDsaShake128FPriv => ALGORITHM_NAME_SHAKE_128F, - Codec::SlhDsaShake128SPriv => ALGORITHM_NAME_SHAKE_128S, - Codec::SlhDsaShake192FPriv => ALGORITHM_NAME_SHAKE_192F, - Codec::SlhDsaShake192SPriv => ALGORITHM_NAME_SHAKE_192S, - Codec::SlhDsaShake256FPriv => ALGORITHM_NAME_SHAKE_256F, - Codec::SlhDsaShake256SPriv => ALGORITHM_NAME_SHAKE_256S, + Codec::SlhdsaSha2128FPriv => ALGORITHM_NAME_SHA2_128F, + Codec::SlhdsaSha2128SPriv => ALGORITHM_NAME_SHA2_128S, + Codec::SlhdsaSha2192FPriv => ALGORITHM_NAME_SHA2_192F, + Codec::SlhdsaSha2192SPriv => ALGORITHM_NAME_SHA2_192S, + Codec::SlhdsaSha2256FPriv => ALGORITHM_NAME_SHA2_256F, + Codec::SlhdsaSha2256SPriv => ALGORITHM_NAME_SHA2_256S, + Codec::SlhdsaShake128FPriv => ALGORITHM_NAME_SHAKE_128F, + Codec::SlhdsaShake128SPriv => ALGORITHM_NAME_SHAKE_128S, + Codec::SlhdsaShake192FPriv => ALGORITHM_NAME_SHAKE_192F, + Codec::SlhdsaShake192SPriv => ALGORITHM_NAME_SHAKE_192S, + Codec::SlhdsaShake256FPriv => ALGORITHM_NAME_SHAKE_256F, + Codec::SlhdsaShake256SPriv => ALGORITHM_NAME_SHAKE_256S, _ => return Err(ConversionsError::UnsupportedCodec(self.mk.codec).into()), }; @@ -400,89 +400,89 @@ impl<'a> SignView for View<'a> { kd.secret_bytes()? }; let (signature, codec) = match self.mk.codec { - Codec::SlhDsaSha2128FPriv => { + Codec::SlhdsaSha2128FPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2128FMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaSha2128FMsig) } - Codec::SlhDsaSha2128SPriv => { + Codec::SlhdsaSha2128SPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2128SMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaSha2128SMsig) } - Codec::SlhDsaSha2192FPriv => { + Codec::SlhdsaSha2192FPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2192FMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaSha2192FMsig) } - Codec::SlhDsaSha2192SPriv => { + Codec::SlhdsaSha2192SPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2192SMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaSha2192SMsig) } - Codec::SlhDsaSha2256FPriv => { + Codec::SlhdsaSha2256FPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2256FMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaSha2256FMsig) } - Codec::SlhDsaSha2256SPriv => { + Codec::SlhdsaSha2256SPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaSha2256SMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaSha2256SMsig) } - Codec::SlhDsaShake128FPriv => { + Codec::SlhdsaShake128FPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake128FMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaShake128FMsig) } - Codec::SlhDsaShake128SPriv => { + Codec::SlhdsaShake128SPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake128SMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaShake128SMsig) } - Codec::SlhDsaShake192FPriv => { + Codec::SlhdsaShake192FPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake192FMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaShake192FMsig) } - Codec::SlhDsaShake192SPriv => { + Codec::SlhdsaShake192SPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake192SMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaShake192SMsig) } - Codec::SlhDsaShake256FPriv => { + Codec::SlhdsaShake256FPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake256FMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaShake256FMsig) } - Codec::SlhDsaShake256SPriv => { + Codec::SlhdsaShake256SPriv => { let signing_key = SigningKey::::try_from(secret_bytes.as_slice()) .map_err(|_| { ConversionsError::SecretKeyFailure("invalid secret key length".into()) })?; - (signing_key.sign(msg).to_vec(), Codec::SlhDsaShake256SMsig) + (signing_key.sign(msg).to_vec(), Codec::SlhdsaShake256SMsig) } _ => { return Err( @@ -523,7 +523,7 @@ impl<'a> VerifyView for View<'a> { let sig_bytes = sv.sig_bytes()?; match self.mk.codec { - Codec::SlhDsaSha2128FPriv | Codec::SlhDsaSha2128FPub => { + Codec::SlhdsaSha2128FPriv | Codec::SlhdsaSha2128FPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -534,7 +534,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaSha2128SPriv | Codec::SlhDsaSha2128SPub => { + Codec::SlhdsaSha2128SPriv | Codec::SlhdsaSha2128SPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -545,7 +545,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaSha2192FPriv | Codec::SlhDsaSha2192FPub => { + Codec::SlhdsaSha2192FPriv | Codec::SlhdsaSha2192FPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -556,7 +556,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaSha2192SPriv | Codec::SlhDsaSha2192SPub => { + Codec::SlhdsaSha2192SPriv | Codec::SlhdsaSha2192SPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -567,7 +567,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaSha2256FPriv | Codec::SlhDsaSha2256FPub => { + Codec::SlhdsaSha2256FPriv | Codec::SlhdsaSha2256FPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -578,7 +578,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaSha2256SPriv | Codec::SlhDsaSha2256SPub => { + Codec::SlhdsaSha2256SPriv | Codec::SlhdsaSha2256SPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -589,7 +589,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaShake128FPriv | Codec::SlhDsaShake128FPub => { + Codec::SlhdsaShake128FPriv | Codec::SlhdsaShake128FPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -600,7 +600,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaShake128SPriv | Codec::SlhDsaShake128SPub => { + Codec::SlhdsaShake128SPriv | Codec::SlhdsaShake128SPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -611,7 +611,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaShake192FPriv | Codec::SlhDsaShake192FPub => { + Codec::SlhdsaShake192FPriv | Codec::SlhdsaShake192FPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -622,7 +622,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaShake192SPriv | Codec::SlhDsaShake192SPub => { + Codec::SlhdsaShake192SPriv | Codec::SlhdsaShake192SPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -633,7 +633,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaShake256FPriv | Codec::SlhDsaShake256FPub => { + Codec::SlhdsaShake256FPriv | Codec::SlhdsaShake256FPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) @@ -644,7 +644,7 @@ impl<'a> VerifyView for View<'a> { .verify(msg_bytes, &signature) .map_err(|e| VerifyError::BadSignature(e.to_string()).into()) } - Codec::SlhDsaShake256SPriv | Codec::SlhDsaShake256SPub => { + Codec::SlhdsaShake256SPriv | Codec::SlhdsaShake256SPub => { let verifying_key = VerifyingKey::::try_from(key_bytes.as_slice()) .map_err(|_| { ConversionsError::PublicKeyFailure("invalid public key length".into()) diff --git a/src/views/x25519_mlkem768.rs b/src/views/x25519_mlkem768.rs index a175e18..b296023 100644 --- a/src/views/x25519_mlkem768.rs +++ b/src/views/x25519_mlkem768.rs @@ -8,14 +8,14 @@ use crate::{ AttrId, AttrView, Builder, ConvView, DataView, Error, FingerprintView, Multikey, OpenView, SealView, }; -use multi_codec::Codec; -use multi_hash::{mh, Multihash}; -use multi_trait::TryDecodeFrom; -use multi_util::Varbytes; use ml_kem::{ kem::{Decapsulate, Encapsulate}, EncodedSizeUser, KemCore, MlKem768, }; +use multi_codec::Codec; +use multi_hash::{mh, Multihash}; +use multi_trait::TryDecodeFrom; +use multi_util::Varbytes; use sha2::{Digest, Sha512}; use x25519_dalek::{PublicKey, StaticSecret}; use zeroize::Zeroizing; From 1b641748b2f927fbdef118ae84d791d7e9a04842 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Mon, 13 Jul 2026 16:12:32 -0600 Subject: [PATCH 4/5] update deps Signed-off-by: Dave Grantham --- Cargo.toml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9d1ea65..42b831a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "multi-key" -version = "1.0.1" +version = "1.0.2" edition = "2021" authors = ["Dave Grantham "] description = "Multikey self-describing cryptographic key data" @@ -18,15 +18,9 @@ wasm = ["getrandom/wasm_js"] aes-gcm = "0.10" bcrypt-pbkdf = "0.10" blake3 = { version = "1.5.1", features = ["traits-preview", "zeroize"] } +curve25519-dalek = { version = "5.0.0", features = ["group"] } # blsful configured per-target below (blst for native, rust for wasm) -mceliece348864 = { version = "1.0" } frodo-kem-rs = { version = "0.7", default-features = false, features = ["frodo640aes", "frodo976aes", "frodo1344aes", "frodo640shake", "frodo976shake", "frodo1344shake"] } -multi-base = { version = "1.0", path = "../multi-base" } -multi-codec = { version = "1.0", path = "../multi-codec" } -multi-hash = { version = "1.0", path = "../multi-hash" } -multi-sig = { version = "1.0", path = "../multi-sig" } -multi-trait = { version = "1.0", path = "../multi-trait" } -multi-util = { version = "1.0", path = "../multi-util" } chacha20 = "0.9" chacha20poly1305 = "0.10" ed25519-dalek = { version = "3.0", features = ["pkcs8", "rand_core", "alloc"] } @@ -36,8 +30,15 @@ getrandom = { version = "0.4", features = ["sys_rng"] } hex = "0.4" hkdf = "0.12" k256 = { version = "0.14", features = ["ecdh"] } +mceliece348864 = { version = "1.0" } ml-dsa = "0.1.1" ml-kem = { version = "0.2", features = ["deterministic"] } +multi-base = "1.0" +multi-codec = "1.0" +multi-hash = "1.0" +multi-sig = "1.0" +multi-trait = "1.0" +multi-util = "1.0" p256 = { version = "0.14", features = ["ecdsa", "ecdh"] } p384 = { version = "0.14", features = ["ecdsa", "ecdh"] } p521 = { version = "0.14", features = ["ecdsa", "ecdh"] } @@ -58,6 +59,7 @@ rand_core_06 = { package = "rand_core", version = "0.6", features = ["getrandom" rsa = { version = "0.10.0-rc.18", features = ["sha2"] } sec1 = "0.8" serde = { version = "1.0", default-features = false, features = ["alloc", "derive"], optional = true } +serde_cbor = "0.11" sha2 = "0.10" signature = "3" slh-dsa = "0.2.0-rc.5" @@ -66,9 +68,7 @@ ssh-encoding = "0.3" thiserror = { version = "2.0" } typenum = "1.17" unsigned-varint = { version = "0.8", features = ["std"] } -serde_cbor = "0.11" vsss-rs = "6.0.0-rc2" -curve25519-dalek = { version = "5.0.0", features = ["group"] } x25519-dalek = { version = "3.0", features = ["static_secrets", "getrandom"] } zeroize = "1" From 839448dc0ed03640c96bd59e71d61782b03fe8b7 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Tue, 14 Jul 2026 09:50:04 -0600 Subject: [PATCH 5/5] update README.md Signed-off-by: Dave Grantham --- README.md | 373 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 311 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index dc855e7..6529a6a 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,336 @@ [![](https://img.shields.io/badge/made%20by-Cryptid%20Technologies-gold.svg?style=flat-square)][CRYPTID] [![](https://img.shields.io/badge/project-provenance-purple.svg?style=flat-square)][PROVENANCE] [![](https://img.shields.io/badge/project-multiformats-blue.svg?style=flat-square)][MULTIFORMATS] -![](https://github.com/cryptidtech/multikey/actions/workflows/rust.yml/badge.svg) +![](https://github.com/cryptidtech/multi-key/actions/workflows/rust.yml/badge.svg) -# Multikey +# Multi-Key A Rust implementation of the [multiformats][MULTIFORMATS] [multikey specification][MULTIKEY] and -[nonce specification][NONCE]. +[nonce specification][NONCE]. The published crate is **`multi-key`** (depend on it as +`multi-key = "1.0"` in `Cargo.toml` and import it as `multi_key` in Rust, e.g. +`use multi_key::Builder;`). ## Current Status -This implementation of the Multikey specification supports both public key and -secret key cryptography keys. For public key cryptography, it supports Ed25519, -Nist P256, P384, and P521, as well as BLS12-381 G1/G2 curves. For secret key -cryptography it supports ChaCha-256 keys. +This implementation of the multikey specification supports an extensive set of public key +and secret key cryptography keys spanning classical, post-quantum, and hybrid schemes: -This implementation supports encrypting/decrypting keys using ChaCha20-Poly1305 -AEAD with keys derived with Bcrypt KDF from a preimage. +- **Classical signing** — Ed25519, secp256k1, NIST P-256/P-384/P-521, RSA-2048/3072/4096, + and BLS12-381 G1/G2. +- **Post-quantum signing** — FN-DSA, ML-DSA, MAYO, and SLH-DSA (all parameter sets). +- **Key encapsulation / key agreement** — X25519, ML-KEM, sntrup, Classic McEliece, + FrodoKEM, and the BLS12-381 TimeCrypt pairing-based KEM. +- **Hybrid signing** — combinations of Ed25519 or BLS12-381 G1 with a PQ signing scheme. +- **Hybrid KEMs** — combinations of X25519 with a PQ KEM. +- **Secret-key / symmetric** — ChaCha20-Poly1305 keys. -When using BLS12-381 keys, this implementations supports threshold key -splitting and combining as well as threshold signing and verifying. +See the [Supported Key Formats](#supported-key-formats) section below for the exhaustive +list of codecs. -This crate also supports converting to/from SSH format keys using the -[`ssh-key`][SSHKEY] crate. This gives full OpenSSH compatibility for reading in -OpenSSH serialized keys and converting them to Multikey format. This even -includes non-standard SSH key protocols such as secp256k1 and BLS12-381 G1/G2 -keys through the use of [RFC 4251][RFC4251] standard for "additional algorithms" -names using the "@multikey" domain suffix. For instance, using this crate, an -secp256k1 Multikey converted to an SSH format key has the algorithm name -"secp256k1@multikey". A BLS12-381 G1 key share converted to SSH format has the -algorithm name "bls12_381-g1-share@multkey". +This implementation supports encrypting and decrypting keys at rest using +ChaCha20-Poly1305 AEAD with keys 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. -For the technical details of the design of Multikey or Nonce, please refer to +KEM-based message encryption uses `SealView`/`OpenView` with a choice of four AEAD codecs +(ChaCha20-Poly1305, XChaCha20-Poly1305, AES-GCM-128, AES-GCM-256) and HKDF-SHA512 to derive +the AEAD key from the KEM shared secret. + +For threshold cryptography, this implementation supports three mechanisms: + +1. **BLS12-381 Shamir splitting** of G1/G2 keys, including threshold signing and verifying. +2. **Distributed Key Generation (DKG)** threshold shares for Ed25519, P-256, P-384, + secp256k1, BLS12-381, and Ed448, with an authenticated threshold marker bundle (TSIG-1). +3. A generic **`keysplit`** module providing verifiable Feldman VSS for ECC keys, gf256 + byte-sharing for RSA and all PQ/hybrid keys, and a dual mode (gf256 + Feldman) for + Ed25519/X25519. + +This crate also supports converting to and from SSH format keys using the +[`ssh-key`][SSHKEY] crate, giving full OpenSSH compatibility for reading OpenSSH serialized +keys and converting them to Multi-Key format. This includes non-standard SSH key protocols +such as secp256k1 and BLS12-381 G1/G2 keys through the [RFC 4251][RFC4251] standard for +"additional algorithms" names using the `@multikey` domain suffix. See the +[SSH Key Conversions](#ssh-key-conversions) section for the full table. + +For the technical details of the design of the multikey or nonce format, please refer to the specifications linked above. ## Introduction -This is a Rust implementation of a multicodec format for encryption keys. The -design of the format is intentionally abstract to support any kind of -encryption key in any state (e.g. encrypted or unencrypted). This format should -be best thought of as a container of key material with abstract, algorithm- -specific views and a generic and self-describing data storage format. - -Every piece of data in the serialized Multikey object either has a known-fixed -size or a self-describing variable size such that software processing these -objects do not need to support all encryption algorithms to be able to -accurately calculate the size of the serialized object and skip over it if -needed. - -## Views on the Multikey Data - -To provide an abstract interface to encryption keys for all algorithms, this -Multikey crate provides "views" on the Multikey data. These are read-only -abstract interfaces to the Multikey attributes with implementations for -different supporting encryption algorithms. - -Currently the set of views provide generic access to the general attributes -(`multikey::AttrView`) of the Multikey, the key data (`multikey::DataView`), -as well as views on the KDF attributes (`multikey::KdfAttrView`), and cipher -attributes (`multikey::CipherAttrView`) for encrypted Multikeys. For algorithms -that support threshold operations there is a threshold attributes view -(`multikey::ThresholdAttrView`). - -For operations you can do with a Multikey, there is a cipher view -(`multikey::CipherView`) for encrypting/decrypting a Multikey, a conversion -view (`multikey::ConvView`) for converting the Multikey to other formats (e.g. -to/from ssh key format and secret keys to public keys), a fingerprint view -(`multikey::FingerprintView`) for getting a key fingerprint using a given -hashing codec, a KDF view (`multikey::KdfView`) for generating cipher keys for -use by a cipher view to encrypt/decrypt the Multikey, a threshold view -(`multikey::ThresholdView`) for key splitting and combining keys, and lastly a -sign view (`multikey::SignView`) and verify view (`multikey::VerifyView`) for -creating and verifying [`Multisig`][MULTISIG] digital signatures. - -It is important to note that the operations that seem to mutate the Multikey -(e.g. encrypt, decrypt, convert, etc) in fact do a copy-on-write (CoW) -operation and return a new Multikey with the mutation applied. +This is a Rust implementation of a multicodec format for cryptographic keys. The design of +the format is intentionally abstract to support any kind of cryptographic key in any state +(e.g. encrypted or unencrypted). This format is best thought of as a container of key +material with abstract, algorithm-specific views and a generic, self-describing data storage +format. + +Every piece of data in the serialized Multi-Key object either has a known fixed size or a +self-describing variable size, such that software processing these objects does not need to +support all encryption algorithms to accurately calculate the size of the serialized object +and skip over it if needed. + +## Supported Key Formats + +The tables below enumerate every key codec supported by this crate. Each algorithm has +`Pub` (public key) and `Priv` (private key) variants unless otherwise noted. The codec +identifiers come from the [multicodec][MULTICODEC] registry and are surfaced as +`multi_codec::Codec` variants. + +### Classical Signing + +| Algorithm | Codecs | Notes | +|---|---|---| +| Ed25519 | `Ed25519Pub` / `Ed25519Priv` | Ed25519 signatures | +| secp256k1 | `Secp256K1Pub` / `Secp256K1Priv` | ECDSA over secp256k1 | +| NIST P-256 | `P256Pub` / `P256Priv` | ECDSA + ECDH | +| NIST P-384 | `P384Pub` / `P384Priv` | ECDSA + ECDH | +| NIST P-521 | `P521Pub` / `P521Priv` | ECDSA + ECDH | +| RSA-2048 | `Rsa2048Pub` / `Rsa2048Priv` | RSA-SHA256 signatures | +| RSA-3072 | `Rsa3072Pub` / `Rsa3072Priv` | RSA-SHA256 signatures | +| RSA-4096 | `Rsa4096Pub` / `Rsa4096Priv` | RSA-SHA256 signatures | +| BLS12-381 G1 | `Bls12381G1Pub` / `Bls12381G1Priv` | BLS signatures on G1; also a TimeCrypt KEM | +| BLS12-381 G2 | `Bls12381G2Pub` / `Bls12381G2Priv` | BLS signatures on G2; also a TimeCrypt KEM | + +### Post-Quantum Signing + +| Algorithm | Codecs | Parameter sets | +|---|---|---| +| FN-DSA | `FnDsa512Pub`/`Priv`, `FnDsa1024Pub`/`Priv` | 512, 1024 | +| ML-DSA | `Mldsa65Pub`/`Priv`, `Mldsa87Pub`/`Priv` | 65, 87 | +| MAYO | `Mayo1Pub`/`Priv`, `Mayo2Pub`/`Priv`, `Mayo3Pub`/`Priv`, `Mayo5Pub`/`Priv` | 1, 2, 3, 5 | +| SLH-DSA | `SlhdsaSha2128FPub`/`Priv`, `SlhdsaSha2128SPub`/`Priv`, `SlhdsaSha2192FPub`/`Priv`, `SlhdsaSha2192SPub`/`Priv`, `SlhdsaSha2256FPub`/`Priv`, `SlhdsaSha2256SPub`/`Priv`, `SlhdsaShake128FPub`/`Priv`, `SlhdsaShake128SPub`/`Priv`, `SlhdsaShake192FPub`/`Priv`, `SlhdsaShake192SPub`/`Priv`, `SlhdsaShake256FPub`/`Priv`, `SlhdsaShake256SPub`/`Priv` | 12 sets: SHA-2/SHAKE × 128/192/256 × F/S | + +### KEMs / Key Agreement + +| Algorithm | Codecs | Notes | +|---|---|---| +| X25519 | `X25519Pub` / `X25519Priv` | ECDH; returns ephemeral public key from `seal` | +| ML-KEM | `Mlkem768Pub`/`Priv`, `Mlkem1024Pub`/`Priv` | 768, 1024 | +| sntrup | `Sntrup761Pub`/`Priv`, `Sntrup857Pub`/`Priv`, `Sntrup953Pub`/`Priv`, `Sntrup1013Pub`/`Priv`, `Sntrup1277Pub`/`Priv` | 761, 857, 953, 1013, 1277 | +| Classic McEliece | `Mceliece348864Pub` / `Mceliece348864Priv` | 348864 | +| FrodoKEM | `FrodoKem640AesPub`/`Priv`, `FrodoKem976AesPub`/`Priv`, `FrodoKem1344AesPub`/`Priv`, `FrodoKem640ShakePub`/`Priv`, `FrodoKem976ShakePub`/`Priv`, `FrodoKem1344ShakePub`/`Priv` | 640/976/1344 × AES/SHAKE | +| BLS12-381 TimeCrypt | (uses the G1/G2 codecs above) | Pairing-based KEM built into the BLS views | + +### Hybrid Signing (Classical + Post-Quantum) + +| Hybrid | Codecs | Components | +|---|---|---| +| Ed25519-MAYO2 | `Ed25519Mayo2Pub` / `Ed25519Mayo2Priv` | Ed25519 + MAYO-2 | +| Ed25519-ML-DSA-65 | `Ed25519Mldsa65Pub` / `Ed25519Mldsa65Priv` | Ed25519 + ML-DSA-65 | +| Ed25519-FN-DSA-512 | `Ed25519Fndsa512Pub` / `Ed25519Fndsa512Priv` | Ed25519 + FN-DSA-512 | +| BLS12-381-G1-ML-DSA-65 | `Bls12381G1Mldsa65Pub` / `Bls12381G1Mldsa65Priv` | BLS G1 + ML-DSA-65 | +| BLS12-381-G1-FN-DSA-512 | `Bls12381G1Fndsa512Pub` / `Bls12381G1Fndsa512Priv` | BLS G1 + FN-DSA-512 | +| BLS12-381-G1-MAYO-1 | `Bls12381G1Mayo1Pub` / `Bls12381G1Mayo1Priv` | BLS G1 + MAYO-1 | +| BLS12-381-G1-MAYO-2 | `Bls12381G1Mayo2Pub` / `Bls12381G1Mayo2Priv` | BLS G1 + MAYO-2 | + +### Hybrid KEMs (Classical + Post-Quantum) + +| Hybrid | Codecs | Components | +|---|---|---| +| X25519-sntrup761 | `X25519Sntrup761Pub` / `X25519Sntrup761Priv` | X25519 + sntrup761 | +| X25519-ML-KEM-768 | `X25519Mlkem768Pub` / `X25519Mlkem768Priv` | X25519 + ML-KEM-768 | +| X25519-FrodoKEM-640 | `X25519Frodokem640AesPub`/`Priv`, `X25519Frodokem640ShakePub`/`Priv` | X25519 + FrodoKEM-640 (AES/SHAKE) | +| X25519-McEliece-348864 | `X25519Mceliece348864Pub` / `X25519Mceliece348864Priv` | X25519 + Classic McEliece 348864 | + +### Threshold Key Shares + +| Mechanism | Codecs | Notes | +|---|---|---| +| BLS12-381 Shamir shares | `Bls12381G1PubShare`/`PrivShare`, `Bls12381G2PubShare`/`PrivShare` | Split/combine via `ThresholdView`; threshold sign/verify | +| DKG threshold shares | `Ed25519ThreshPubShare`/`PrivShare`, `P256ThreshPubShare`/`PrivShare`, `P384ThreshPubShare`/`PrivShare`, `Secp256K1ThreshPubShare`/`PrivShare`, `Bls12381ThreshPubShare`/`PrivShare`, `Ed448ThreshPubShare`/`PrivShare` | DKG metadata via `ThresholdKeyView`; authenticated marker (TSIG-1) | +| Generic `keysplit` shares | `KeySplitShare` | Feldman VSS (ECC), gf256 byte-sharing (RSA + PQ + hybrids), dual mode (Ed25519/X25519) | + +### Symmetric + +| Algorithm | Codec | Notes | +|---|---|---| +| ChaCha20-Poly1305 | `Chacha20Poly1305` | Used both for at-rest Multi-Key encryption and as a symmetric key codec | + +## Views on the Multi-Key Data + +To provide an abstract interface to cryptographic keys for all algorithms, this crate +provides "views" on the Multi-Key data. These are read-only abstract interfaces to the +Multi-Key attributes with implementations for different supporting algorithms. + +Currently the set of views provides generic access to the general attributes +(`multi_key::AttrView`) of the Multi-Key, the key data (`multi_key::DataView`), as well as +views on the KDF attributes (`multi_key::KdfAttrView`) and cipher attributes +(`multi_key::CipherAttrView`) for encrypted Multi-Keys. For algorithms that support +threshold operations there is a threshold attributes view +(`multi_key::ThresholdAttrView`) and a higher-level DKG metadata view +(`multi_key::ThresholdKeyView`). + +For operations you can do with a Multi-Key, there is: + +- a cipher view (`multi_key::CipherView`) for encrypting/decrypting a Multi-Key at rest, +- a conversion view (`multi_key::ConvView`) for converting the Multi-Key to other formats + (e.g. to/from SSH key format, and secret keys to public keys), +- a fingerprint view (`multi_key::FingerprintView`) for getting a key fingerprint using a + given hashing codec, +- a KDF view (`multi_key::KdfView`) for generating cipher keys for use by a cipher view to + encrypt/decrypt the Multi-Key, +- a seal view (`multi_key::SealView`) and open view (`multi_key::OpenView`) for KEM-based + message encryption/decryption, +- a threshold view (`multi_key::ThresholdView`) for key splitting and combining keys, +- a sign view (`multi_key::SignView`) and verify view (`multi_key::VerifyView`) for + creating and verifying [`Multisig`][MULTISIG] digital signatures. + +Two additional modules provide threshold functionality outside the view traits: + +- `multi_key::keysplit` — generic verifiable threshold key splitting (Feldman VSS, gf256, + dual) exposed as free `split`/`combine`/`verify_share` functions. +- `multi_key::threshold_marker` — DKG marker stamping/reading and TSIG-1 marker + authentication, including the `MarkerView` trait and `threshold_kind`/`threshold_params` + helpers. + +It is important to note that the operations that seem to mutate the Multi-Key (e.g. encrypt, +decrypt, convert, etc.) in fact do a copy-on-write (CoW) operation and return a new +Multi-Key with the mutation applied. + +## SSH Key Conversions + +This crate converts to and from the SSH key format using the [`ssh-key`][SSHKEY] crate. +Standard SSH algorithms are handled natively; non-standard algorithms use the [RFC 4251][RFC4251] +"additional algorithms" mechanism with an `ssh_key::Algorithm::Other` opaque key and an +algorithm name ending in the literal `@multikey` suffix (this is a wire-format identifier, +distinct from the crate name). + +### Native SSH algorithms (no `@multikey` suffix) + +| Algorithm | SSH algorithm name | +|---|---| +| Ed25519 | `ssh-ed25519` | +| ECDSA P-256 | `ecdsa-sha2-nistp256` | +| ECDSA P-384 | `ecdsa-sha2-nistp384` | +| ECDSA P-521 | `ecdsa-sha2-nistp521` | + +### Custom `@multikey` algorithms (opaque SSH keys) + +| Algorithm | SSH algorithm name | +|---|---| +| secp256k1 | `secp256k1@multikey` | +| BLS12-381 G1 | `bls12_381-g1@multikey` | +| BLS12-381 G1 share | `bls12_381-g1-share@multikey` | +| BLS12-381 G2 | `bls12_381-g2@multikey` | +| BLS12-381 G2 share | `bls12_381-g2-share@multikey` | +| RSA-2048/3072/4096 | `rsa-sha256@multikey` | +| ML-DSA-65 | `ml-dsa-65@multikey` | +| ML-DSA-87 | `ml-dsa-87@multikey` | +| FN-DSA-512 | `fn-dsa-512@multikey` | +| FN-DSA-1024 | `fn-dsa-1024@multikey` | +| MAYO-1 | `mayo-1@multikey` | +| MAYO-2 | `mayo-2@multikey` | +| MAYO-3 | `mayo-3@multikey` | +| MAYO-5 | `mayo-5@multikey` | +| SLH-DSA SHA-2 128f | `slh-dsa-sha2-128f@multikey` | +| SLH-DSA SHA-2 128s | `slh-dsa-sha2-128s@multikey` | +| SLH-DSA SHA-2 192f | `slh-dsa-sha2-192f@multikey` | +| SLH-DSA SHA-2 192s | `slh-dsa-sha2-192s@multikey` | +| SLH-DSA SHA-2 256f | `slh-dsa-sha2-256f@multikey` | +| SLH-DSA SHA-2 256s | `slh-dsa-sha2-256s@multikey` | +| SLH-DSA SHAKE 128f | `slh-dsa-shake-128f@multikey` | +| SLH-DSA SHAKE 128s | `slh-dsa-shake-128s@multikey` | +| SLH-DSA SHAKE 192f | `slh-dsa-shake-192f@multikey` | +| SLH-DSA SHAKE 192s | `slh-dsa-shake-192s@multikey` | +| SLH-DSA SHAKE 256f | `slh-dsa-shake-256f@multikey` | +| SLH-DSA SHAKE 256s | `slh-dsa-shake-256s@multikey` | + +The import direction (`Builder::new_from_ssh_public_key` / +`Builder::new_from_ssh_private_key`) supports all of the algorithms above. + +### Key types that do not support SSH conversion + +All KEM-only and hybrid key types explicitly reject SSH conversion and return +`UnsupportedAlgorithm`. These include: X25519, ML-KEM, all sntrup sizes, Classic McEliece, +all FrodoKEM variants, the BLS12-381 TimeCrypt KEM, and all hybrid signing and hybrid KEM +schemes. + +## Threshold Operations + +### BLS12-381 Shamir Splitting + +`ThresholdView::split(threshold, limit)` splits a `Bls12381G1Priv` or `Bls12381G2Priv` into +`Bls12381G1PrivShare` / `Bls12381G2PrivShare` shares using `blsful`'s `SecretKey::split`. +Shares are recombined with `combine`, and threshold signing/verifying is supported on the +share codecs. Requires `2 <= threshold <= limit <= 255`. + +### DKG Threshold Shares + +The DKG share codecs (`Ed25519Thresh*`, `P256Thresh*`, `P384Thresh*`, `Secp256K1Thresh*`, +`Bls12381Thresh*`, `Ed448Thresh*`) carry DKG metadata attributes (`DkgThreshold`, `DkgLimit`, +`DkgIdentifier`, `DkgGroupPublicKey`, `DkgOwnerId`). The `ThresholdKeyView` trait exposes +`group_pubkey()`, `is_threshold_key()`, `participant_count()`, `threshold()`, and +`owner_vlad()`. The `threshold_marker` module stamps and authenticates a marker bundle +(TSIG-1) with a controller signing key via `sign_marker` / `verify_marker`. + +### Generic `keysplit` Module + +`multi_key::keysplit` provides scheme-aware verifiable threshold splitting for any key type +as free functions (`split`, `combine`, `verify_share`) producing `KeySplitShare` Multi-Keys: + +- **Feldman VSS** — secp256k1, P-256/P-384/P-521, BLS12-381 G1/G2 (verifiable, with + commitments). +- **gf256 byte-sharing** — RSA and all PQ families (ML-DSA, ML-KEM, SLH-DSA, FN-DSA, MAYO, + sntrup, FrodoKEM, Classic McEliece) and all hybrids. +- **Dual mode** — Ed25519 and X25519: a gf256 share of the 32-byte seed (exact restore) + plus a Feldman scalar share (threshold-signing-ready). + +## Encryption + +### At-Rest Multi-Key Encryption + +Multi-Keys can be encrypted at rest using **ChaCha20-Poly1305** AEAD (`CipherView`) with the +cipher key derived from a preimage via the **bcrypt PBKDF** (`KdfView`, 32-byte salt, +configurable rounds). A legacy bare-ChaCha20 fallback is honored on decrypt so keystores +encrypted before AEAD was added continue to work; re-encrypting upgrades them to the +authenticated format. + +### KEM Seal / Open + +KEM-based message encryption uses `SealView` / `OpenView`. The KEM shared secret is +expanded into an AEAD key via **HKDF-SHA512**, then one of four AEAD codecs may be used: + +| AEAD codec | Key size | Nonce size | +|---|---|---| +| `Chacha20Poly1305` | 32 bytes | 12 bytes | +| `Xchacha20Poly1305` | 32 bytes | 24 bytes | +| `AesGcm128` | 16 bytes | 12 bytes | +| `AesGcm256` | 32 bytes | 12 bytes | + +Individual KEM views may restrict the allowed AEAD codec (e.g. X25519-ML-KEM-768 only +permits `Chacha20Poly1305` per its specification). + +## Cargo Features + +| Feature | Default | Description | +|---|---|---| +| `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` | + +## Security + +- Private keys are wrapped in `Zeroizing` buffers and automatically zeroized on drop. +- `Debug` output for private key material is redacted. +- All views are thread-safe (`Send` + `Sync`) for concurrent operations. +- Mutation operations use copy-on-write semantics, returning a new `Multi-Key` rather than + mutating in place. + +## Links + +- [Cryptid Technologies][CRYPTID] +- [Provenance Specifications][PROVENANCE] +- [Multiformats][MULTIFORMATS] +- [Multikey Specification][MULTIKEY] +- [Nonce Specification][NONCE] +- [Multisig][MULTISIG] +- [`ssh-key` crate][SSHKEY] +- [RFC 4251][RFC4251] [CRYPTID]: https://cryptid.tech [PROVENANCE]: https://github.com/cryptidtech/provenance-specifications/ [MULTIFORMATS]: https://github.com/multiformats/multiformats [MULTIKEY]: https://github.com/cryptidtech/provenance-specifications/blob/main/specifications/multikey.md [NONCE]: https://github.com/cryptidtech/provenance-specifications/blob/main/specifications/nonce.md +[MULTICODEC]: https://github.com/multiformats/multicodec [SSHKEY]: https://crates.io/crates/ssh-key [RFC4251]: https://www.rfc-editor.org/rfc/rfc4251.html#page-11 -[MULTISIG]: https://github.com/cryptidtech/multisig +[MULTISIG]: https://github.com/cryptidtech/multisig \ No newline at end of file