diff --git a/.github/workflows/rust_checks.yml b/.github/workflows/rust_checks.yml index df1ff68..6dd89c2 100644 --- a/.github/workflows/rust_checks.yml +++ b/.github/workflows/rust_checks.yml @@ -31,7 +31,7 @@ jobs: toolchain: stable components: clippy - name: Run clippy - run: cargo clippy -- -D warnings + run: cargo clippy --workspace --all-targets --all-features -- -D warnings test: runs-on: ubuntu-latest steps: @@ -45,7 +45,7 @@ jobs: components: cargo - name: Run tests run: | - cargo +nightly test --workspace --all-targets -- -Z unstable-options --format json --report-time | tee results.json + cargo +nightly test --workspace --all-targets --all-features -- -Z unstable-options --format json --report-time | tee results.json - name: Prepare junit report id: cargo_reporter uses: innoq/action-cargo-test-report@v1 diff --git a/Cargo.lock b/Cargo.lock index 0eb4667..22945aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + [[package]] name = "async-stream" version = "0.3.5" @@ -95,6 +101,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "binascii" version = "0.1.4" @@ -107,6 +119,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytemuck" version = "1.18.0" @@ -151,6 +172,25 @@ dependencies = [ "version_check", ] +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deranged" version = "0.3.11" @@ -193,6 +233,16 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.13.0" @@ -244,6 +294,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "fips204" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea2461567040f75a882162ffe38f6dc35bf5a193e995ca3f8c1f5d3c101be1d" +dependencies = [ + "rand_core", + "sha3", + "zeroize", +] + [[package]] name = "fnv" version = "1.0.7" @@ -328,6 +389,16 @@ dependencies = [ "windows", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -491,6 +562,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -604,6 +684,12 @@ dependencies = [ [[package]] name = "next-gen-signatures" version = "0.0.1" +dependencies = [ + "anyhow", + "base64", + "fips204", + "paste", +] [[package]] name = "next-gen-signing-service" @@ -611,6 +697,7 @@ version = "0.0.1" dependencies = [ "next-gen-signatures", "rocket", + "serde", ] [[package]] @@ -683,6 +770,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pear" version = "0.2.9" @@ -899,6 +992,7 @@ dependencies = [ "rocket_codegen", "rocket_http", "serde", + "serde_json", "state", "tempfile", "time", @@ -1038,6 +1132,16 @@ dependencies = [ "serde", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1335,6 +1439,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "ubyte" version = "0.10.4" @@ -1601,3 +1711,23 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +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/next-gen-signatures/Cargo.toml b/next-gen-signatures/Cargo.toml index ae838dc..d3a52a0 100644 --- a/next-gen-signatures/Cargo.toml +++ b/next-gen-signatures/Cargo.toml @@ -3,4 +3,12 @@ name = "next-gen-signatures" version = "0.0.1" edition = "2021" +[features] +all-algos = ["fips204"] +fips204 = ["dep:fips204"] + [dependencies] +anyhow = "1.0.89" +base64 = "0.22.1" +fips204 = { version = "0.2.2", optional = true } +paste = "1.0.15" diff --git a/next-gen-signatures/src/common.rs b/next-gen-signatures/src/common.rs new file mode 100644 index 0000000..c72d0c7 --- /dev/null +++ b/next-gen-signatures/src/common.rs @@ -0,0 +1,19 @@ +use anyhow::Result; + +pub type ByteArray = Vec; + +pub trait CryptoProvider { + type PublicKey; + type SecretKey; + + fn gen_keypair() -> Result<(Self::PublicKey, Self::SecretKey)>; + + fn pk_into_bytes(pk: Self::PublicKey) -> Result; + fn pk_from_bytes(bytes: ByteArray) -> Result; + + fn sk_into_bytes(sk: Self::SecretKey) -> Result; + fn sk_from_bytes(bytes: ByteArray) -> Result; + + fn sign(sk: &Self::SecretKey, msg: ByteArray) -> Result; + fn verify(pk: &Self::PublicKey, msg: ByteArray, sig: ByteArray) -> Result; +} diff --git a/next-gen-signatures/src/crypto.rs b/next-gen-signatures/src/crypto.rs new file mode 100644 index 0000000..511504e --- /dev/null +++ b/next-gen-signatures/src/crypto.rs @@ -0,0 +1,99 @@ +#[cfg(feature = "fips204")] +pub use fips204::*; + +#[cfg(feature = "fips204")] +pub mod fips204 { + use fips204::traits::{SerDes, Signer, Verifier}; + + use crate::common::CryptoProvider; + + macro_rules! fips_provider_impl { + ($provider_name:ident, $pkg_name:ident) => { + pub struct $provider_name; + impl CryptoProvider for $provider_name { + type PublicKey = fips204::$pkg_name::PublicKey; + + type SecretKey = fips204::$pkg_name::PrivateKey; + + fn gen_keypair() -> anyhow::Result<(Self::PublicKey, Self::SecretKey)> { + fips204::$pkg_name::try_keygen().map_err(|err| anyhow::anyhow!(err)) + } + + fn pk_into_bytes(pk: Self::PublicKey) -> anyhow::Result { + Ok(pk.into_bytes().to_vec()) + } + + fn pk_from_bytes( + bytes: crate::common::ByteArray, + ) -> anyhow::Result { + let bytes_len = bytes.len(); + Self::PublicKey::try_from_bytes(bytes.try_into().map_err(|_| { + anyhow::anyhow!( + "Expected an array of length {}, got {}!", + fips204::$pkg_name::PK_LEN, + bytes_len + ) + })?) + .map_err(|err| anyhow::anyhow!(err)) + } + + fn sk_into_bytes(sk: Self::SecretKey) -> anyhow::Result { + Ok(sk.into_bytes().to_vec()) + } + + fn sk_from_bytes( + bytes: crate::common::ByteArray, + ) -> anyhow::Result { + let bytes_len = bytes.len(); + Self::SecretKey::try_from_bytes(bytes.try_into().map_err(|_| { + anyhow::anyhow!( + "Expected an array of length {}, got {}!", + fips204::$pkg_name::SK_LEN, + bytes_len + ) + })?) + .map_err(|err| anyhow::anyhow!(err)) + } + + fn sign( + sk: &Self::SecretKey, + msg: crate::common::ByteArray, + ) -> anyhow::Result { + sk.try_sign(&msg) + .map(|res| res.to_vec()) + .map_err(|err| anyhow::anyhow!(err)) + } + + fn verify( + pk: &Self::PublicKey, + msg: crate::common::ByteArray, + sig: crate::common::ByteArray, + ) -> anyhow::Result { + let sig_len = sig.len(); + let sig = sig.try_into().map_err(|_| { + anyhow::anyhow!( + "Expected a signature length of {}, got {}!", + fips204::$pkg_name::SIG_LEN, + sig_len + ) + })?; + Ok(pk.verify(&msg, &sig)) + } + } + }; + } + + fips_provider_impl!(Fips204MlDsa44Provider, ml_dsa_44); + fips_provider_impl!(Fips204MlDsa65Provider, ml_dsa_65); + fips_provider_impl!(Fips204MlDsa87Provider, ml_dsa_87); + + #[cfg(test)] + mod tests { + use super::*; + use crate::test_provider; + + test_provider!(Fips204MlDsa44Provider); + test_provider!(Fips204MlDsa65Provider); + test_provider!(Fips204MlDsa87Provider); + } +} diff --git a/next-gen-signatures/src/lib.rs b/next-gen-signatures/src/lib.rs index 97f1788..90567c1 100644 --- a/next-gen-signatures/src/lib.rs +++ b/next-gen-signatures/src/lib.rs @@ -1,3 +1,5 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub mod common; +pub mod crypto; +pub mod macros; + +pub use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; diff --git a/next-gen-signatures/src/macros.rs b/next-gen-signatures/src/macros.rs new file mode 100644 index 0000000..cb39714 --- /dev/null +++ b/next-gen-signatures/src/macros.rs @@ -0,0 +1,95 @@ +pub use paste; + +#[macro_export] +macro_rules! test_provider { + ($provider:ident) => { + paste::item! { + #[cfg(test)] + #[allow(non_snake_case)] + mod [<$provider _tests>] { + use super::*; + use $crate::common::CryptoProvider; + + #[test] + fn test_pk_roundtrip() -> anyhow::Result<()> { + let (pk, _) = $provider::gen_keypair()?; + let bytes = $provider::pk_into_bytes(pk)?; + let pk2 = $provider::pk_from_bytes(bytes.clone())?; + let bytes2 = $provider::pk_into_bytes(pk2)?; + + assert_eq!(bytes, bytes2); + + Ok(()) + } + + #[test] + fn test_sk_roundtrip() -> anyhow::Result<()> { + let (_, sk) = $provider::gen_keypair()?; + let bytes = $provider::sk_into_bytes(sk)?; + let sk2 = $provider::sk_from_bytes(bytes.clone())?; + let bytes2 = $provider::sk_into_bytes(sk2)?; + + assert_eq!(bytes, bytes2); + + Ok(()) + } + + #[test] + fn test_sign_verify_roundtrip() -> anyhow::Result<()> { + let (pk, sk) = $provider::gen_keypair()?; + + let msg = b"Hello, World".to_vec(); + let sig = $provider::sign(&sk, msg.clone())?; + let valid = $provider::verify(&pk, msg, sig)?; + + assert!(valid); + + Ok(()) + } + + } + } + }; +} + +#[macro_export] +macro_rules! generate_crypto_routes { + ($provider:ident) => { + $crate::macros::paste::item! { + #[get("/keypair")] + #[allow(non_snake_case)] + pub(crate) fn [<$provider _gen_keypair>]() -> Json { + let (pk, sk) = $crate::crypto::$provider::gen_keypair().unwrap(); + let pk = $crate::crypto::$provider::pk_into_bytes(pk).unwrap(); + let sk = $crate::crypto::$provider::sk_into_bytes(sk).unwrap(); + let pk = BASE64_URL_SAFE_NO_PAD.encode(pk); + let sk = BASE64_URL_SAFE_NO_PAD.encode(sk); + Json(KeyPair { public_key: pk, secret_key: sk }) + } + + #[get("/sign?&")] + #[allow(non_snake_case)] + pub(crate) fn [<$provider _sign>](secret_key: &str, message: &str) -> Json { + let sk = BASE64_URL_SAFE_NO_PAD.decode(secret_key).unwrap(); + let sk = $crate::crypto::$provider::sk_from_bytes(sk).unwrap(); + let msg = BASE64_URL_SAFE_NO_PAD.decode(message).unwrap(); + + let sig = $crate::crypto::$provider::sign(&sk, msg).unwrap(); + Json(BASE64_URL_SAFE_NO_PAD.encode(sig)) + } + + #[get("/verify?&&")] + #[allow(non_snake_case)] + pub(crate) fn [<$provider _verify>](public_key: &str, message: &str, signature: &str) -> Json { + let pk = BASE64_URL_SAFE_NO_PAD.decode(public_key).unwrap(); + let pk = $crate::crypto::$provider::pk_from_bytes(pk).unwrap(); + let msg = BASE64_URL_SAFE_NO_PAD.decode(message).unwrap(); + let sig = BASE64_URL_SAFE_NO_PAD.decode(signature).unwrap(); + + let valid = $crate::crypto::$provider::verify(&pk, msg, sig).unwrap(); + + Json(valid) + } + } + }; +} diff --git a/next-gen-signatures/tests/sample-test.rs b/next-gen-signatures/tests/sample-test.rs index 3dcdc84..8b13789 100644 --- a/next-gen-signatures/tests/sample-test.rs +++ b/next-gen-signatures/tests/sample-test.rs @@ -1,4 +1 @@ -#[test] -fn test() { - assert_eq!(next_gen_signatures::add(2, 2), 4) -} + diff --git a/next-gen-signing-service/Cargo.toml b/next-gen-signing-service/Cargo.toml index 1a0ecf6..261f7b6 100644 --- a/next-gen-signing-service/Cargo.toml +++ b/next-gen-signing-service/Cargo.toml @@ -4,5 +4,8 @@ version = "0.0.1" edition = "2021" [dependencies] -next-gen-signatures = { path = "../next-gen-signatures" } -rocket = "0.5.1" +next-gen-signatures = { path = "../next-gen-signatures", features = [ + "all-algos", +] } +rocket = { version = "0.5.1", features = ["json"] } +serde = "1.0.210" diff --git a/next-gen-signing-service/src/main.rs b/next-gen-signing-service/src/main.rs index 0211e14..c6dc435 100644 --- a/next-gen-signing-service/src/main.rs +++ b/next-gen-signing-service/src/main.rs @@ -1,4 +1,22 @@ -use rocket::{get, launch, routes}; +use rocket::{get, launch, routes, serde::json::Json}; + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct KeyPair { + pub public_key: String, + pub secret_key: String, +} + +mod fips204_routes { + use super::*; + use next_gen_signatures::common::CryptoProvider; + use next_gen_signatures::generate_crypto_routes; + use next_gen_signatures::{Engine, BASE64_URL_SAFE_NO_PAD}; + + generate_crypto_routes!(Fips204MlDsa44Provider); + generate_crypto_routes!(Fips204MlDsa65Provider); + generate_crypto_routes!(Fips204MlDsa87Provider); +} #[get("/")] fn index() -> &'static str { @@ -7,7 +25,32 @@ fn index() -> &'static str { #[launch] fn rocket() -> _ { - rocket::build().mount("/", routes![index]) + rocket::build() + .mount("/", routes![index]) + .mount( + "/fips204/44", + routes![ + fips204_routes::Fips204MlDsa44Provider_gen_keypair, + fips204_routes::Fips204MlDsa44Provider_sign, + fips204_routes::Fips204MlDsa44Provider_verify + ], + ) + .mount( + "/fips204/65", + routes![ + fips204_routes::Fips204MlDsa65Provider_gen_keypair, + fips204_routes::Fips204MlDsa65Provider_sign, + fips204_routes::Fips204MlDsa65Provider_verify + ], + ) + .mount( + "/fips204/87", + routes![ + fips204_routes::Fips204MlDsa87Provider_gen_keypair, + fips204_routes::Fips204MlDsa87Provider_sign, + fips204_routes::Fips204MlDsa87Provider_verify + ], + ) } #[cfg(test)]