diff --git a/.github/workflows/dhkem.yml b/.github/workflows/dhkem.yml new file mode 100644 index 0000000..3c8a58d --- /dev/null +++ b/.github/workflows/dhkem.yml @@ -0,0 +1,68 @@ +name: dhkem + +on: + pull_request: + paths: + - ".github/workflows/dhkem.yml" + - "dhkem/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: dhkem + +env: + RUSTFLAGS: "-Dwarnings" + CARGO_INCREMENTAL: 0 + +jobs: + set-msrv: + uses: RustCrypto/actions/.github/workflows/set-msrv.yml@master + with: + msrv: 1.74.0 + + minimal-versions: + # temporarily disabled as requested by Tony (https://github.com/RustCrypto/KEMs/pull/15#pullrequestreview-2006378802) + if: false + uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master + with: + working-directory: ${{ github.workflow }} + + test: + needs: set-msrv + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - ${{needs.set-msrv.outputs.msrv}} + - stable + steps: + - uses: actions/checkout@v4 + - uses: RustCrypto/actions/cargo-cache@master + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - run: cargo test --no-default-features + - run: cargo test + - run: cargo test --all-features + + cross: + needs: set-msrv + strategy: + matrix: + include: + - target: powerpc-unknown-linux-gnu + rust: ${{needs.set-msrv.outputs.msrv}} + - target: powerpc-unknown-linux-gnu + rust: stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + targets: ${{ matrix.target }} + - uses: RustCrypto/actions/cross-install@master + - run: cross test --release --target ${{ matrix.target }} --all-features diff --git a/Cargo.lock b/Cargo.lock index 8d9f1f5..5b9d018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,48 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "belt-block" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9aa1eef3994e2ccd304a78fe3fea4a73e5792007f85f09b79bb82143ca5f82b" + +[[package]] +name = "belt-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc405b3b8472f6e019aedf942fdee9516a0546d12e053d3744416e8f21ddb8a" +dependencies = [ + "belt-block", + "digest", +] + +[[package]] +name = "bign256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a060e09443574e5518c7eced1ef6ff33496be5aee6dc101f99102039d3922eff" +dependencies = [ + "belt-hash", + "crypto-bigint", + "elliptic-curve", + "primeorder", + "rfc6979", + "signature", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -108,6 +150,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -184,6 +232,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -194,6 +254,67 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "dhkem" +version = "0.1.0" +dependencies = [ + "bign256", + "elliptic-curve", + "hex-literal", + "hkdf", + "k256", + "kem", + "p192", + "p224", + "p256", + "p384", + "p521", + "rand", + "rand_core", + "sha2", + "sm2", + "x25519-dalek", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -201,7 +322,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] @@ -210,6 +347,43 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" + [[package]] name = "generic-array" version = "0.14.7" @@ -218,6 +392,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -231,6 +406,17 @@ dependencies = [ "wasi", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "half" version = "2.4.0" @@ -259,6 +445,24 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "hybrid-array" version = "0.2.0-rc.8" @@ -303,6 +507,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "keccak" version = "0.1.5" @@ -375,6 +593,93 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "p192" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b0533bc6c238f2669aab8db75ae52879dc74e88d6bd3685bd4022a00fa85cd2" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sec1", +] + +[[package]] +name = "p224" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c06436d66652bc2f01ade021592c80a2aad401570a18aa18b82e440d2b9aa1" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "platforms" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" + [[package]] name = "plotters" version = "0.3.5" @@ -409,6 +714,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -506,6 +820,25 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.17" @@ -521,6 +854,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" version = "1.0.197" @@ -552,6 +905,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha3" version = "0.10.8" @@ -562,6 +926,54 @@ dependencies = [ "keccak", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "sm2" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b22092ef242a118f03ee41dc46b2720c0ca076f544116dbc915cacf532cfaa" +dependencies = [ + "elliptic-curve", + "primeorder", + "rfc6979", + "signature", + "sm3", +] + +[[package]] +name = "sm3" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +dependencies = [ + "digest", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "2.0.52" @@ -778,8 +1190,34 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index c6c8220..bd8b8df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "dhkem", "ml-kem", ] diff --git a/dhkem/Cargo.toml b/dhkem/Cargo.toml new file mode 100644 index 0000000..c040cc0 --- /dev/null +++ b/dhkem/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "dhkem" +description = """ +Key Encapsulation Mechanism (KEM) adapters for Elliptic Curve Diffie Hellman (ECDH) protocols +""" +version = "0.1.0" +edition = "2021" +rust-version = "1.74" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/RustCrypto/KEMs/tree/master/dhkem" +categories = ["cryptography"] +keywords = ["crypto", "ecdh", "ecc"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +kem = "0.3.0-pre.0" +rand_core = "0.6.4" +x25519 = { version = "2.0.1", package = "x25519-dalek", optional = true } +elliptic-curve = { version = "0.13.8", optional = true } +bign256 = { version = "0.13.1", optional = true } +k256 = { version = "0.13.3", optional = true } +p192 = { version = "0.13.0", optional = true } +p224 = { version = "0.13.2", optional = true } +p256 = { version = "0.13.2", optional = true } +p384 = { version = "0.13.0", optional = true } +p521 = { version = "0.13.3", optional = true } +sm2 = { version = "0.13.3", optional = true } +zeroize = { version = "1.7.0", optional = true } + +[features] +default = ["zeroize"] +arithmetic = ["dep:elliptic-curve", "elliptic-curve/ecdh"] +x25519 = ["dep:x25519", "x25519/reusable_secrets"] +bign256 = ["dep:bign256", "arithmetic"] +k256 = ["dep:k256", "arithmetic"] +p192 = ["dep:p192", "arithmetic"] +p224 = ["dep:p224", "arithmetic"] +p256 = ["dep:p256", "arithmetic"] +p384 = ["dep:p384", "arithmetic"] +p521 = ["dep:p521", "arithmetic"] +sm2 = ["dep:sm2", "arithmetic"] +zeroize = ["dep:zeroize"] + +[dev-dependencies] +rand = "0.8.5" +hex-literal = "0.4.1" +hkdf = "0.12.4" +sha2 = "0.10.8" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/dhkem/src/arithmetic.rs b/dhkem/src/arithmetic.rs new file mode 100644 index 0000000..c76aaa0 --- /dev/null +++ b/dhkem/src/arithmetic.rs @@ -0,0 +1,59 @@ +use crate::{DhDecapsulator, DhEncapsulator, DhKem}; +use elliptic_curve::ecdh::{EphemeralSecret, SharedSecret}; +use elliptic_curve::{CurveArithmetic, PublicKey}; +use kem::{Decapsulate, Encapsulate}; +use rand_core::CryptoRngCore; +use std::marker::PhantomData; + +pub struct ArithmeticKem(PhantomData); + +impl Encapsulate, SharedSecret> for DhEncapsulator> +where + C: CurveArithmetic, +{ + type Error = (); + + fn encapsulate( + &self, + rng: &mut impl CryptoRngCore, + ) -> Result<(PublicKey, SharedSecret), Self::Error> { + // ECDH encapsulation involves creating a new ephemeral key pair and then doing DH + let sk = EphemeralSecret::random(rng); + let pk = sk.public_key(); + let ss = sk.diffie_hellman(&self.0); + + Ok((pk, ss)) + } +} + +impl Decapsulate, SharedSecret> for DhDecapsulator> +where + C: CurveArithmetic, +{ + type Error = (); + + fn decapsulate(&self, encapsulated_key: &PublicKey) -> Result, Self::Error> { + let ss = self.0.diffie_hellman(encapsulated_key); + + Ok(ss) + } +} + +impl DhKem for ArithmeticKem +where + C: CurveArithmetic, +{ + type DecapsulatingKey = DhDecapsulator>; + type EncapsulatingKey = DhEncapsulator>; + type EncapsulatedKey = PublicKey; + type SharedSecret = SharedSecret; + + fn random_keypair( + rng: &mut impl CryptoRngCore, + ) -> (Self::DecapsulatingKey, Self::EncapsulatingKey) { + let sk = EphemeralSecret::random(rng); + let pk = PublicKey::from(&sk); + + (DhDecapsulator(sk), DhEncapsulator(pk)) + } +} diff --git a/dhkem/src/hpke_p256_test.rs b/dhkem/src/hpke_p256_test.rs new file mode 100644 index 0000000..a1cb151 --- /dev/null +++ b/dhkem/src/hpke_p256_test.rs @@ -0,0 +1,107 @@ +use crate::{DhKem, NistP256}; +use elliptic_curve::sec1::ToEncodedPoint; +use hex_literal::hex; +use hkdf::Hkdf; +use kem::{Decapsulate, Encapsulate}; +use rand_core::{CryptoRng, RngCore}; +use sha2::Sha256; + +/// Constant RNG for testing purposes only. +struct ConstantRng<'a>(pub &'a [u8]); + +impl<'a> RngCore for ConstantRng<'a> { + fn next_u32(&mut self) -> u32 { + let (head, tail) = self.0.split_at(4); + self.0 = tail; + u32::from_be_bytes(head.try_into().unwrap()) + } + + fn next_u64(&mut self) -> u64 { + let (head, tail) = self.0.split_at(8); + self.0 = tail; + u64::from_be_bytes(head.try_into().unwrap()) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let (hd, tl) = self.0.split_at(dest.len()); + dest.copy_from_slice(hd); + self.0 = tl; + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + if dest.len() > self.0.len() { + return Err(rand_core::Error::new("not enough bytes")); + } + let (hd, tl) = self.0.split_at(dest.len()); + dest.copy_from_slice(hd); + self.0 = tl; + Ok(()) + } +} + +// this is only ever ok for testing +impl CryptoRng for ConstantRng<'_> {} + +fn labeled_extract(salt: &[u8], label: &[u8], ikm: &[u8]) -> Vec { + let labeled_ikm = [b"HPKE-v1".as_slice(), b"KEM\x00\x10".as_slice(), label, ikm].concat(); + Hkdf::::extract(Some(salt), &labeled_ikm).0.to_vec() +} + +fn labeled_expand(prk: &[u8], label: &[u8], info: &[u8], l: u16) -> Vec { + let labeled_info = [ + &l.to_be_bytes(), + b"HPKE-v1".as_slice(), + b"KEM\x00\x10".as_slice(), + label, + info, + ] + .concat(); + let mut out = Vec::with_capacity(l as usize); + out.resize(l as usize, 0); + Hkdf::::from_prk(prk) + .unwrap() + .expand(&labeled_info, &mut out) + .expect("ok"); + out +} + +fn extract_and_expand(dh: ::SharedSecret, kem_context: &[u8]) -> Vec { + let eae_prk = labeled_extract(b"", b"eae_prk", dh.raw_secret_bytes()); + labeled_expand(&eae_prk, b"shared_secret", kem_context, 32) +} + +#[test] +// section A.3.1 https://datatracker.ietf.org/doc/html/rfc9180#appendix-A.3.1 +fn test_dhkem_p256_hkdf_sha256() { + let pke_hex = hex!( + "04a92719c6195d5085104f469a8b9814d5838ff72b60501e2c4466e5e67b32\ + 5ac98536d7b61a1af4b78e5b7f951c0900be863c403ce65c9bfcb9382657222d18c4" + ); + let pkr_hex = hex!( + "04fe8c19ce0905191ebc298a9245792531f26f0cece2460639e8bc39cb7f70\ + 6a826a779b4cf969b8a0e539c7f62fb3d30ad6aa8f80e30f1d128aafd68a2ce72ea0" + ); + let shared_secret_hex = + hex!("c0d26aeab536609a572b07695d933b589dcf363ff9d93c93adea537aeabb8cb8"); + + let (skr, pkr) = NistP256::random_keypair(&mut ConstantRng(&hex!( + "f3ce7fdae57e1a310d87f1ebbde6f328be0a99cdbcadf4d6589cf29de4b8ffd2" + ))); + assert_eq!(pkr.0.to_encoded_point(false).as_bytes(), &pkr_hex); + + let (pke, ss1) = pkr + .encapsulate(&mut ConstantRng(&hex!( + "4995788ef4b9d6132b249ce59a77281493eb39af373d236a1fe415cb0c2d7beb" + ))) + .expect("never fails"); + assert_eq!(pke.to_encoded_point(false).as_bytes(), &pke_hex); + + let ss2 = skr.decapsulate(&pke).expect("never fails"); + + assert_eq!(ss1.raw_secret_bytes(), ss2.raw_secret_bytes()); + + let kem_context = [pke_hex, pkr_hex].concat(); + let shared_secret = extract_and_expand(ss1, &kem_context); + + assert_eq!(&shared_secret, &shared_secret_hex); +} diff --git a/dhkem/src/lib.rs b/dhkem/src/lib.rs new file mode 100644 index 0000000..058374f --- /dev/null +++ b/dhkem/src/lib.rs @@ -0,0 +1,141 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +//! # Diffie-Hellman (DH) based Key Encapsulation Mechanisms (KEM) +//! +//! This crate provides a KEM interface for DH protocols as specified in +//! [RFC9180](https://datatracker.ietf.org/doc/html/rfc9180#name-dh-based-kem-dhkem) +//! without the shared secret extraction process. In particular, `Encaps(pk)` in the +//! RFC returns the encapsulated key and an extracted shared secret, while our +//! implementation leaves the extraction process up to the user. This type of KEM +//! construction is currently being used in HPKE, as per the RFC, and in the current +//! draft of the [TLS KEM +//! combiner](https://datatracker.ietf.org/doc/html/draft-ietf-tls-hybrid-design-10). + +use kem::{Decapsulate, Encapsulate}; +use rand_core::CryptoRngCore; +#[cfg(feature = "zeroize")] +use zeroize::{Zeroize, ZeroizeOnDrop}; + +/// Newtype for a piece of data that may be encapsulated +#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Default)] +pub struct DhEncapsulator(X); +/// Newtype for a piece of data that may be decapsulated +#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Default)] +pub struct DhDecapsulator(X); + +impl AsRef for DhEncapsulator { + fn as_ref(&self) -> &X { + &self.0 + } +} + +impl From for DhEncapsulator { + fn from(value: X) -> Self { + Self(value) + } +} + +impl AsRef for DhDecapsulator { + fn as_ref(&self) -> &X { + &self.0 + } +} + +impl From for DhDecapsulator { + fn from(value: X) -> Self { + Self(value) + } +} + +impl DhEncapsulator { + /// Consumes `self` and returns the wrapped value + pub fn into_inner(self) -> X { + self.0 + } +} + +impl DhDecapsulator { + /// Consumes `self` and returns the wrapped value + pub fn into_inner(self) -> X { + self.0 + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for DhEncapsulator { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for DhDecapsulator { + fn zeroize(&mut self) { + self.0.zeroize() + } +} + +#[cfg(feature = "zeroize")] +impl ZeroizeOnDrop for DhEncapsulator {} + +#[cfg(feature = "zeroize")] +impl ZeroizeOnDrop for DhDecapsulator {} + +/// This is a trait that all KEM models should implement, and should probably be +/// promoted to the kem crate itself. It specifies the types of encapsulating and +/// decapsulating keys created by key generation, the shared secret type, and the +/// encapsulated key type +pub trait DhKem { + /// The type that will implement [`Decapsulate`] + type DecapsulatingKey: Decapsulate; + + /// The type that will implement [`Encapsulate`] + type EncapsulatingKey: Encapsulate; + + /// The type of the encapsulated key + type EncapsulatedKey; + + /// The type of the shared secret + type SharedSecret; + + /// Generates a new (decapsulating key, encapsulating key) keypair for the KEM + /// model + fn random_keypair( + rng: &mut impl CryptoRngCore, + ) -> (Self::DecapsulatingKey, Self::EncapsulatingKey); +} + +#[cfg(feature = "arithmetic")] +pub mod arithmetic; + +#[cfg(feature = "x25519")] +mod x25519_kem; +#[cfg(feature = "x25519")] +pub use x25519_kem::X25519; + +#[cfg(feature = "bign256")] +pub type BignP256 = arithmetic::ArithmeticKem; +#[cfg(feature = "k256")] +pub type Secp256k1 = arithmetic::ArithmeticKem; +#[cfg(feature = "p192")] +pub type NistP192 = arithmetic::ArithmeticKem; +#[cfg(feature = "p224")] +pub type NistP224 = arithmetic::ArithmeticKem; +#[cfg(feature = "p256")] +pub type NistP256 = arithmetic::ArithmeticKem; +// include an additional alias Secp256r1 = NistP256 +#[cfg(feature = "p256")] +pub type Secp256r1 = arithmetic::ArithmeticKem; +#[cfg(feature = "p384")] +pub type NistP384 = arithmetic::ArithmeticKem; +#[cfg(feature = "p521")] +pub type NistP521 = arithmetic::ArithmeticKem; +#[cfg(feature = "sm2")] +pub type Sm2 = arithmetic::ArithmeticKem; + +#[cfg(test)] +mod tests; + +#[cfg(test)] +#[cfg(feature = "p256")] +mod hpke_p256_test; diff --git a/dhkem/src/tests.rs b/dhkem/src/tests.rs new file mode 100644 index 0000000..659d8e4 --- /dev/null +++ b/dhkem/src/tests.rs @@ -0,0 +1,93 @@ +use crate::DhKem; +use kem::{Decapsulate, Encapsulate}; +use rand::thread_rng; + +trait SecretBytes { + fn as_slice(&self) -> &[u8]; +} + +#[cfg(feature = "x25519")] +impl SecretBytes for x25519::SharedSecret { + fn as_slice(&self) -> &[u8] { + self.as_bytes().as_slice() + } +} + +#[cfg(feature = "arithmetic")] +impl SecretBytes for elliptic_curve::ecdh::SharedSecret +where + C: elliptic_curve::CurveArithmetic, +{ + fn as_slice(&self) -> &[u8] { + self.raw_secret_bytes().as_slice() + } +} + +// we need this because if the crate is compiled with no features this function never +// gets used +#[allow(dead_code)] +fn test_kem() +where + ::SharedSecret: SecretBytes, +{ + let mut rng = thread_rng(); + let (sk, pk) = K::random_keypair(&mut rng); + let (ek, ss1) = pk.encapsulate(&mut rng).expect("never fails"); + let ss2 = sk.decapsulate(&ek).expect("never fails"); + + assert_eq!(ss1.as_slice(), ss2.as_slice()); +} + +#[cfg(feature = "x25519")] +#[test] +fn test_x25519() { + test_kem::(); +} + +#[cfg(feature = "bign256")] +#[test] +fn test_bign256() { + test_kem::(); +} + +#[cfg(feature = "k256")] +#[test] +fn test_k256() { + test_kem::(); +} + +#[cfg(feature = "p192")] +#[test] +fn test_p192() { + test_kem::(); +} + +#[cfg(feature = "p224")] +#[test] +fn test_p224() { + test_kem::(); +} + +#[cfg(feature = "p256")] +#[test] +fn test_p256() { + test_kem::(); +} + +#[cfg(feature = "p384")] +#[test] +fn test_p384() { + test_kem::(); +} + +#[cfg(feature = "p521")] +#[test] +fn test_p521() { + test_kem::(); +} + +#[cfg(feature = "sm2")] +#[test] +fn test_sm2() { + test_kem::(); +} diff --git a/dhkem/src/x25519_kem.rs b/dhkem/src/x25519_kem.rs new file mode 100644 index 0000000..daaf7c6 --- /dev/null +++ b/dhkem/src/x25519_kem.rs @@ -0,0 +1,48 @@ +use crate::{DhDecapsulator, DhEncapsulator, DhKem}; +use kem::{Decapsulate, Encapsulate}; +use rand_core::CryptoRngCore; +use x25519::{PublicKey, ReusableSecret, SharedSecret}; + +pub struct X25519; + +impl Encapsulate for DhEncapsulator { + type Error = (); + + fn encapsulate( + &self, + rng: &mut impl CryptoRngCore, + ) -> Result<(PublicKey, SharedSecret), Self::Error> { + // ECDH encapsulation involves creating a new ephemeral key pair and then doing DH + let sk = ReusableSecret::random_from_rng(rng); + let pk = PublicKey::from(&sk); + let ss = sk.diffie_hellman(&self.0); + + Ok((pk, ss)) + } +} + +impl Decapsulate for DhDecapsulator { + type Error = (); + + fn decapsulate(&self, encapsulated_key: &PublicKey) -> Result { + let ss = self.0.diffie_hellman(encapsulated_key); + + Ok(ss) + } +} + +impl DhKem for X25519 { + type DecapsulatingKey = DhDecapsulator; + type EncapsulatingKey = DhEncapsulator; + type EncapsulatedKey = PublicKey; + type SharedSecret = SharedSecret; + + fn random_keypair( + rng: &mut impl CryptoRngCore, + ) -> (Self::DecapsulatingKey, Self::EncapsulatingKey) { + let sk = ReusableSecret::random_from_rng(rng); + let pk = PublicKey::from(&sk); + + (DhDecapsulator(sk), DhEncapsulator(pk)) + } +}