From 1b4494bc34836ab20c20619b6679cdd02aaf0216 Mon Sep 17 00:00:00 2001 From: "Khang Nguyen (ENS)" Date: Tue, 7 Jul 2026 16:18:25 -0400 Subject: [PATCH 1/3] mscrypto trait skeleton --- Cargo.lock | 12 ++++++ Cargo.toml | 5 ++- mscrypto-bcrypt/Cargo.toml | 9 +++++ mscrypto-bcrypt/src/lib.rs | 4 ++ mscrypto-symcrypt/Cargo.toml | 9 +++++ mscrypto-symcrypt/src/lib.rs | 4 ++ mscrypto/Cargo.toml | 15 ++++++++ mscrypto/src/algorithm.rs | 26 +++++++++++++ mscrypto/src/error.rs | 39 +++++++++++++++++++ mscrypto/src/hash.rs | 72 ++++++++++++++++++++++++++++++++++++ mscrypto/src/lib.rs | 14 +++++++ mscrypto/src/provider.rs | 34 +++++++++++++++++ mscrypto/src/sha3.rs | 14 +++++++ 13 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 mscrypto-bcrypt/Cargo.toml create mode 100644 mscrypto-bcrypt/src/lib.rs create mode 100644 mscrypto-symcrypt/Cargo.toml create mode 100644 mscrypto-symcrypt/src/lib.rs create mode 100644 mscrypto/Cargo.toml create mode 100644 mscrypto/src/algorithm.rs create mode 100644 mscrypto/src/error.rs create mode 100644 mscrypto/src/hash.rs create mode 100644 mscrypto/src/lib.rs create mode 100644 mscrypto/src/provider.rs create mode 100644 mscrypto/src/sha3.rs diff --git a/Cargo.lock b/Cargo.lock index 551f18b..3f799e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mscrypto" +version = "0.1.0" + +[[package]] +name = "mscrypto-bcrypt" +version = "0.1.0" + +[[package]] +name = "mscrypto-symcrypt" +version = "0.1.0" + [[package]] name = "nom" version = "7.1.3" diff --git a/Cargo.toml b/Cargo.toml index 4a75bca..da9a454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,10 @@ members = [ "rust-symcrypt", "symcrypt-bindgen", - "symcrypt-sys" + "symcrypt-sys", + "mscrypto", + "mscrypto-symcrypt", + "mscrypto-bcrypt" ] resolver = "2" diff --git a/mscrypto-bcrypt/Cargo.toml b/mscrypto-bcrypt/Cargo.toml new file mode 100644 index 0000000..2bc111b --- /dev/null +++ b/mscrypto-bcrypt/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mscrypto-bcrypt" +authors = ["Microsoft"] +version = "0.1.0" +license = "MIT OR Apache-2.0" +description = "BCrypt/CNG backend for the mscrypto contract (stub, Windows-only; not yet implemented)" +edition.workspace = true +rust-version.workspace = true +repository = "https://github.com/microsoft/rust-symcrypt" diff --git a/mscrypto-bcrypt/src/lib.rs b/mscrypto-bcrypt/src/lib.rs new file mode 100644 index 0000000..6197efc --- /dev/null +++ b/mscrypto-bcrypt/src/lib.rs @@ -0,0 +1,4 @@ +//! BCrypt/CNG backend for `mscrypto`. +//! +//! Stub: intentionally empty so the workspace resolves. The provider is +//! implemented separately (Windows-only). diff --git a/mscrypto-symcrypt/Cargo.toml b/mscrypto-symcrypt/Cargo.toml new file mode 100644 index 0000000..29712fa --- /dev/null +++ b/mscrypto-symcrypt/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mscrypto-symcrypt" +authors = ["Microsoft"] +version = "0.1.0" +license = "MIT OR Apache-2.0" +description = "SymCrypt backend for the mscrypto contract (stub, not yet implemented)" +edition.workspace = true +rust-version.workspace = true +repository = "https://github.com/microsoft/rust-symcrypt" diff --git a/mscrypto-symcrypt/src/lib.rs b/mscrypto-symcrypt/src/lib.rs new file mode 100644 index 0000000..6899b19 --- /dev/null +++ b/mscrypto-symcrypt/src/lib.rs @@ -0,0 +1,4 @@ +//! SymCrypt backend for `mscrypto`. +//! +//! Stub: intentionally empty so the workspace resolves. The provider is +//! implemented separately. diff --git a/mscrypto/Cargo.toml b/mscrypto/Cargo.toml new file mode 100644 index 0000000..e6c2034 --- /dev/null +++ b/mscrypto/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mscrypto" +authors = ["Microsoft"] +version = "0.1.0" +license = "MIT OR Apache-2.0" +description = "Backend-neutral cryptographic provider contract (traits and types) shared by the SymCrypt and BCrypt/CNG backends" +edition.workspace = true +rust-version.workspace = true +homepage = "https://github.com/microsoft/SymCrypt" +repository = "https://github.com/microsoft/rust-symcrypt" +categories = ["cryptography", "api-bindings"] +keywords = ["symcrypt", "bcrypt", "crypto", "fips", "hashing"] + +[features] +sha3 = [] diff --git a/mscrypto/src/algorithm.rs b/mscrypto/src/algorithm.rs new file mode 100644 index 0000000..66dea54 --- /dev/null +++ b/mscrypto/src/algorithm.rs @@ -0,0 +1,26 @@ +//! Algorithm identifiers used across the backend-neutral contract. + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub enum Algorithm { + Hash(BaseHashAlgorithm), + #[cfg(feature = "sha3")] + Sha3(Sha3Algorithm), +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub enum BaseHashAlgorithm { + Sha256, + Sha384, + Sha512, +} + +#[cfg(feature = "sha3")] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub enum Sha3Algorithm { + Sha3_256, + Sha3_384, + Sha3_512, +} diff --git a/mscrypto/src/error.rs b/mscrypto/src/error.rs new file mode 100644 index 0000000..3201ad9 --- /dev/null +++ b/mscrypto/src/error.rs @@ -0,0 +1,39 @@ +//! Error types for the contract. +use crate::algorithm::Algorithm; + +// Provider construction (`build()`) failures. +#[derive(Debug)] +#[non_exhaustive] +pub enum ProviderBuildError { + UnsupportedAlgorithms { + backend: &'static str, + missing: Vec, + }, + // Opaque low-level backend failure (e.g. a provider handle could not be + // opened). `operation` names the failing step; the raw status is not exposed. + Backend { + backend: &'static str, + operation: &'static str, + }, +} + +/// Runtime error for fallible crypto operations. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub enum Error { + /// The requested algorithm is not available on this backend / OS + /// (e.g. SHA-3 on older Windows). + Unavailable, +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Unavailable => { + f.write_str("requested algorithm is not available on this backend") + } + } + } +} + +impl std::error::Error for Error {} diff --git a/mscrypto/src/hash.rs b/mscrypto/src/hash.rs new file mode 100644 index 0000000..6c3aedd --- /dev/null +++ b/mscrypto/src/hash.rs @@ -0,0 +1,72 @@ +//! Base hashing +use crate::algorithm::BaseHashAlgorithm; + +pub trait Hash { + type Hasher: HashOps; + fn hash(&self, a: BaseHashAlgorithm) -> Self::Hasher; + fn digest(&self, a: BaseHashAlgorithm, data: &[u8]) -> Digest; +} + +pub trait HashOps { + fn update(&mut self, data: &[u8]); + fn finalize(self) -> Digest; +} + +#[derive(Clone)] +pub struct Digest { + bytes: [u8; Digest::MAX_LEN], + len: u8, +} + +impl Digest { + pub const MAX_LEN: usize = 64; + + pub fn as_bytes(&self) -> &[u8] { + &self.bytes[..self.len as usize] + } + + pub fn len(&self) -> usize { + self.len as usize + } +} + +impl AsRef<[u8]> for Digest { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +/// Provider-facing construction seam. Not part of the caller-facing API: provider +/// crates import `BuildDigest` to produce a `Digest` from a backend's output. It +/// lives here rather than as an inherent method so it stays off `Digest`'s public +/// surface; it is reachable cross-crate only because Rust has no friend-crate +/// visibility. Ordinary callers receive a `Digest` from `Hash::digest` / +/// `HashOps::finalize` and read it via `as_bytes` / `as_ref`. +#[doc(hidden)] +pub mod provider { + use super::Digest; + + mod sealed { + pub trait Sealed {} + impl Sealed for crate::hash::Digest {} + } + + /// Builds a `Digest` by writing its `len` output bytes directly into the buffer + /// via `fill`, with no intermediate copy. Maps onto the out-pointer C APIs + /// (SymCrypt/BCrypt write straight into `fill`'s slice). `len` must be + /// <= `Digest::MAX_LEN` or `fill` will panic. Sealed: only `Digest` implements it. + pub trait BuildDigest: sealed::Sealed { + fn from_fn(len: usize, fill: impl FnOnce(&mut [u8])) -> Self; + } + + impl BuildDigest for Digest { + fn from_fn(len: usize, fill: impl FnOnce(&mut [u8])) -> Self { + let mut bytes = [0u8; Digest::MAX_LEN]; + fill(&mut bytes[..len]); + Self { + bytes, + len: len as u8, + } + } + } +} diff --git a/mscrypto/src/lib.rs b/mscrypto/src/lib.rs new file mode 100644 index 0000000..f8c8489 --- /dev/null +++ b/mscrypto/src/lib.rs @@ -0,0 +1,14 @@ +//! `mscrypto` is the backend-neutral cryptographic provider contract. +//! +//! It defines the traits, types, and errors shared by the SymCrypt and +//! BCrypt/CNG backends. This crate has zero native dependencies and implements +//! no cryptography itself; concrete providers live in separate crates +//! (`mscrypto-symcrypt`, `mscrypto-bcrypt`). + +pub mod algorithm; +pub mod error; +pub mod hash; +pub mod provider; + +#[cfg(feature = "sha3")] +pub mod sha3; diff --git a/mscrypto/src/provider.rs b/mscrypto/src/provider.rs new file mode 100644 index 0000000..6283582 --- /dev/null +++ b/mscrypto/src/provider.rs @@ -0,0 +1,34 @@ +//! The provider surface: identity, capability queries, and backend metadata. +//! +//! Base hashing (`Hash`) is a supertrait, so every provider can hash. + +use crate::algorithm::Algorithm; +use crate::hash::Hash; + +pub trait CryptoProvider: Hash { + fn info(&self) -> &BackendInfo; + fn supports(&self, a: Algorithm) -> bool; +} + +pub struct BackendInfo { + pub name: &'static str, + pub version: BackendVersion, + pub link_mode: LinkMode, + pub fips: bool, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct BackendVersion { + pub major: u32, + pub minor: u32, + pub patch: u32, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub enum LinkMode { + DynamicSystem, + PrebuiltStatic, + BuildFromSourceStatic, + NotApplicable, +} diff --git a/mscrypto/src/sha3.rs b/mscrypto/src/sha3.rs new file mode 100644 index 0000000..1f92d10 --- /dev/null +++ b/mscrypto/src/sha3.rs @@ -0,0 +1,14 @@ +//! SHA-3 hashing: the one fallible hash surface in v1. + +use crate::algorithm::Sha3Algorithm; +use crate::error::Error; +use crate::hash::{Digest, HashOps}; + +// SHA-3 is fallible because it is version-variable: BCrypt returns STATUS_NOT_FOUND +// on older Windows. The Result gate is at construction only; update() and +// finalize() (via HashOps) are infallible. +pub trait Sha3 { + type Sha3Hasher: HashOps; + fn sha3(&self, a: Sha3Algorithm) -> Result; + fn sha3_digest(&self, a: Sha3Algorithm, data: &[u8]) -> Result; +} From 9c5fef2a8b97da2099780d2b5ff9ae549e3b99bc Mon Sep 17 00:00:00 2001 From: "Khang Nguyen (ENS)" Date: Thu, 9 Jul 2026 11:49:15 -0400 Subject: [PATCH 2/3] Hashing trait for symcrypt and bcrypt --- Cargo.lock | 19 ++ Cargo.toml | 3 +- mscrypto-bcrypt/Cargo.toml | 19 +- mscrypto-bcrypt/src/hash.rs | 338 ++++++++++++++++++++++++++++++++++ mscrypto-bcrypt/src/lib.rs | 157 +++++++++++++++- mscrypto-example/Cargo.toml | 17 ++ mscrypto-example/src/main.rs | 59 ++++++ mscrypto-symcrypt/Cargo.toml | 16 +- mscrypto-symcrypt/build.rs | 19 ++ mscrypto-symcrypt/src/hash.rs | 268 +++++++++++++++++++++++++++ mscrypto-symcrypt/src/lib.rs | 178 +++++++++++++++++- mscrypto/src/algorithm.rs | 6 +- 12 files changed, 1088 insertions(+), 11 deletions(-) create mode 100644 mscrypto-bcrypt/src/hash.rs create mode 100644 mscrypto-example/Cargo.toml create mode 100644 mscrypto-example/src/main.rs create mode 100644 mscrypto-symcrypt/build.rs create mode 100644 mscrypto-symcrypt/src/hash.rs diff --git a/Cargo.lock b/Cargo.lock index 3f799e7..682028d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,10 +228,29 @@ version = "0.1.0" [[package]] name = "mscrypto-bcrypt" version = "0.1.0" +dependencies = [ + "hex", + "mscrypto", + "windows-sys", +] + +[[package]] +name = "mscrypto-example" +version = "0.1.0" +dependencies = [ + "mscrypto-bcrypt", + "mscrypto-symcrypt", +] [[package]] name = "mscrypto-symcrypt" version = "0.1.0" +dependencies = [ + "hex", + "mscrypto", + "symcrypt", + "symcrypt-sys", +] [[package]] name = "nom" diff --git a/Cargo.toml b/Cargo.toml index da9a454..7542dd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,8 @@ members = [ "symcrypt-sys", "mscrypto", "mscrypto-symcrypt", - "mscrypto-bcrypt" + "mscrypto-bcrypt", + "mscrypto-example" ] resolver = "2" diff --git a/mscrypto-bcrypt/Cargo.toml b/mscrypto-bcrypt/Cargo.toml index 2bc111b..803028d 100644 --- a/mscrypto-bcrypt/Cargo.toml +++ b/mscrypto-bcrypt/Cargo.toml @@ -3,7 +3,24 @@ name = "mscrypto-bcrypt" authors = ["Microsoft"] version = "0.1.0" license = "MIT OR Apache-2.0" -description = "BCrypt/CNG backend for the mscrypto contract (stub, Windows-only; not yet implemented)" +description = "BCrypt backend for the mscrypto contract (Windows-only)" edition.workspace = true rust-version.workspace = true repository = "https://github.com/microsoft/rust-symcrypt" + +[dependencies] +mscrypto = { path = "../mscrypto", version = "0.1.0" } + +# BCrypt is a Windows component, so the backend and its binding are Windows-only. +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_Security_Cryptography", +] } + +[dev-dependencies] +hex = { workspace = true } + +[features] +# Forwards to the contract's SHA-3 surface; off by default. +sha3 = ["mscrypto/sha3"] diff --git a/mscrypto-bcrypt/src/hash.rs b/mscrypto-bcrypt/src/hash.rs new file mode 100644 index 0000000..e0911a2 --- /dev/null +++ b/mscrypto-bcrypt/src/hash.rs @@ -0,0 +1,338 @@ +//! Hashing for the BCrypt provider + +use std::ptr; + +use mscrypto::algorithm::BaseHashAlgorithm; +#[cfg(feature = "sha3")] +use mscrypto::algorithm::Sha3Algorithm; +use mscrypto::hash::provider::BuildDigest; +use mscrypto::hash::{Digest, Hash, HashOps}; + +use windows_sys::Win32::Foundation::NTSTATUS; +use windows_sys::Win32::Security::Cryptography::*; + +use crate::BcryptProvider; + +const SHA256_LEN: usize = 32; +const SHA384_LEN: usize = 48; +const SHA512_LEN: usize = 64; + +// windows-sys exposes the SHA-2 algorithm pseudo-handles but not the SHA-3 ones, so +// define them the same way it defines its own (a BCRYPT_ALG_HANDLE built from the value +// in the bcrypt pseudo-handle table). SHA-3 pseudo-handles exist on Windows 11 24H2 and later. +#[cfg(feature = "sha3")] +const BCRYPT_SHA3_256_ALG_HANDLE: BCRYPT_ALG_HANDLE = 0x0000_03B1_u32 as _; +#[cfg(feature = "sha3")] +const BCRYPT_SHA3_384_ALG_HANDLE: BCRYPT_ALG_HANDLE = 0x0000_03C1_u32 as _; +#[cfg(feature = "sha3")] +const BCRYPT_SHA3_512_ALG_HANDLE: BCRYPT_ALG_HANDLE = 0x0000_03D1_u32 as _; + +// Mirrors the SDK's BCRYPT_SUCCESS / NT_SUCCESS macro: a nonnegative NTSTATUS is +// success; only negative values are failures. +fn bcrypt_success(status: NTSTATUS) -> bool { + status >= 0 +} + +/// Panics on a non-success `NTSTATUS`. Used on the infallible SHA-2 path (and once +/// SHA-3 is running), where a failure is catastrophic and cannot be reported. +fn expect_success(call: &str, status: NTSTATUS) { + if !bcrypt_success(status) { + panic!( + "mscrypto-bcrypt: {} failed: NTSTATUS 0x{:08X}", + call, status as u32 + ); + } +} + +/// Owns a hash object handle, destroyed on drop. +struct HashHandle(BCRYPT_HASH_HANDLE); + +unsafe impl Send for HashHandle {} + +impl Drop for HashHandle { + fn drop(&mut self) { + // SAFETY: handle came from BCryptCreateHash and is destroyed once. + unsafe { BCryptDestroyHash(self.0) }; + } +} + +/// Streaming hasher shared by the SHA-2 and SHA-3 surfaces. Both run off process-global +/// algorithm pseudo-handles, so a hasher owns only its hash object. +pub struct BcryptHasher { + hash: HashHandle, + output_len: usize, +} + +impl BcryptHasher { + /// Builds a hasher from an algorithm pseudo-handle. + fn new(alg: BCRYPT_ALG_HANDLE, output_len: usize) -> Self { + BcryptHasher { + hash: create_hash_handle(alg), + output_len, + } + } +} + +/// Creates a hash object from an algorithm pseudo-handle. Passing NULL/0 for the object +/// buffer allocates it and free it via BCryptDestroyHash. Panics on failure. +fn create_hash_handle(alg: BCRYPT_ALG_HANDLE) -> HashHandle { + let mut handle: BCRYPT_HASH_HANDLE = ptr::null_mut(); + // SAFETY: alg is a valid algorithm pseudo-handle; the hash object is bcrypt owned. + let status = + unsafe { BCryptCreateHash(alg, &mut handle, ptr::null_mut(), 0, ptr::null_mut(), 0, 0) }; + expect_success("BCryptCreateHash", status); + HashHandle(handle) +} + +impl HashOps for BcryptHasher { + fn update(&mut self, data: &[u8]) { + // BCryptHashData's length is a u32 (ULONG). On 64-bit a &[u8] can exceed + // u32::MAX (for example a memory-mapped multi-GB file), and `len as u32` would + // silently truncate it, so feed the input in chunks of at most u32::MAX bytes. + // Successive calls hash the concatenation, so the digest covers the whole input. + for chunk in data.chunks(u32::MAX as usize) { + // SAFETY: hash handle is live; chunk is valid for its length. + let status = + unsafe { BCryptHashData(self.hash.0, chunk.as_ptr(), chunk.len() as u32, 0) }; + expect_success("BCryptHashData", status); + } + } + + fn finalize(self) -> Digest { + let len = self.output_len; + let handle = self.hash.0; + Digest::from_fn(len, |buf| { + // SAFETY: handle is live for the duration of this call (self is dropped only + // after `from_fn` returns); buf holds exactly `len` bytes. + let status = unsafe { BCryptFinishHash(handle, buf.as_mut_ptr(), len as u32, 0) }; + expect_success("BCryptFinishHash", status); + }) + } +} + +impl Hash for BcryptProvider { + type Hasher = BcryptHasher; + + fn hash(&self, algorithm: BaseHashAlgorithm) -> BcryptHasher { + let (alg, len) = match algorithm { + BaseHashAlgorithm::Sha256 => (BCRYPT_SHA256_ALG_HANDLE, SHA256_LEN), + BaseHashAlgorithm::Sha384 => (BCRYPT_SHA384_ALG_HANDLE, SHA384_LEN), + BaseHashAlgorithm::Sha512 => (BCRYPT_SHA512_ALG_HANDLE, SHA512_LEN), + }; + BcryptHasher::new(alg, len) + } + + fn digest(&self, algorithm: BaseHashAlgorithm, data: &[u8]) -> Digest { + let mut hasher = self.hash(algorithm); + hasher.update(data); + hasher.finalize() + } +} + +/// Maps a SHA-3 variant to its algorithm pseudo-handle. +#[cfg(feature = "sha3")] +fn sha3_alg_handle(algorithm: Sha3Algorithm) -> BCRYPT_ALG_HANDLE { + match algorithm { + Sha3Algorithm::Sha3_256 => BCRYPT_SHA3_256_ALG_HANDLE, + Sha3Algorithm::Sha3_384 => BCRYPT_SHA3_384_ALG_HANDLE, + Sha3Algorithm::Sha3_512 => BCRYPT_SHA3_512_ALG_HANDLE, + } +} + +/// Reports whether this Windows build implements a SHA-3 variant by trying to create a +/// throwaway hash from its pseudo-handle. SHA-3 landed in Windows 11 24H2, so the create +/// fails on older builds and the variant is reported as unavailable. +#[cfg(feature = "sha3")] +pub(crate) fn sha3_available(algorithm: Sha3Algorithm) -> bool { + let mut handle: BCRYPT_HASH_HANDLE = ptr::null_mut(); + // SAFETY: a throwaway availability probe; the object, if created, is destroyed on drop. + let status = unsafe { + BCryptCreateHash( + sha3_alg_handle(algorithm), + &mut handle, + ptr::null_mut(), + 0, + ptr::null_mut(), + 0, + 0, + ) + }; + if bcrypt_success(status) { + drop(HashHandle(handle)); + true + } else { + false + } +} + +#[cfg(feature = "sha3")] +mod sha3_impl { + use super::{sha3_alg_handle, BcryptHasher, SHA256_LEN, SHA384_LEN, SHA512_LEN}; + use crate::BcryptProvider; + use mscrypto::algorithm::Sha3Algorithm; + use mscrypto::error::Error; + use mscrypto::hash::{Digest, HashOps}; + use mscrypto::sha3::Sha3; + + impl Sha3 for BcryptProvider { + type Sha3Hasher = BcryptHasher; + + fn sha3(&self, algorithm: Sha3Algorithm) -> Result { + let (len, available) = match algorithm { + Sha3Algorithm::Sha3_256 => (SHA256_LEN, self.sha3_256), + Sha3Algorithm::Sha3_384 => (SHA384_LEN, self.sha3_384), + Sha3Algorithm::Sha3_512 => (SHA512_LEN, self.sha3_512), + }; + if !available { + return Err(Error::Unavailable); + } + Ok(BcryptHasher::new(sha3_alg_handle(algorithm), len)) + } + + fn sha3_digest(&self, algorithm: Sha3Algorithm, data: &[u8]) -> Result { + let mut hasher = self.sha3(algorithm)?; + hasher.update(data); + Ok(hasher.finalize()) + } + } +} + +#[cfg(test)] +mod test { + use crate::BcryptProvider; + use mscrypto::algorithm::BaseHashAlgorithm; + use mscrypto::hash::{Digest, Hash, HashOps}; + + #[cfg(feature = "sha3")] + use mscrypto::algorithm::Sha3Algorithm; + + struct ShaVector { + algorithm: BaseHashAlgorithm, + msg: &'static str, + md: &'static str, + } + + const SHA2_VECTORS: &[ShaVector] = &[ + ShaVector { + algorithm: BaseHashAlgorithm::Sha256, + msg: "", + md: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha256, + msg: "3ec009", + md: "579badde3d29ecbdcbd56dacaf3f7fcfd40b1aac60dbc5b17e3902613864e470", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha384, + msg: "", + md: "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha384, + msg: "ac8a50", + md: "1bdcb04240b4b43a110407baa08b404f042ea05c517ce2d9cc2be38cdfd916ce0db81615f869449e26416430cd5eb120", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha512, + msg: "", + md: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha512, + msg: "d4fc1f", + md: "e668327148443a73d6ec4a9db28aac4280bd11e8a652175b1757de4fc03ebed5ea85e8945dae67b394be3065c84c15261f2f05bd071c13bc77fadeb6786911a1", + }, + ]; + + #[cfg(feature = "sha3")] + struct Sha3Vector { + algorithm: Sha3Algorithm, + msg: &'static str, + md: &'static str, + } + + #[cfg(feature = "sha3")] + const SHA3_VECTORS: &[Sha3Vector] = &[ + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_256, + msg: "", + md: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_256, + msg: "b053fa", + md: "9d0ff086cd0ec06a682c51c094dc73abdc492004292344bd41b82a60498ccfdb", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_384, + msg: "", + md: "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_384, + msg: "6ab7d6", + md: "ea12d6d32d69ad2154a57e0e1be481a45add739ee7dd6e2a27e544b6c8b5ad122654bbf95134d567987156295d5e57db", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_512, + msg: "", + md: "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_512, + msg: "37d518", + md: "4aa96b1547e6402c0eee781acaa660797efe26ec00b4f2e0aec4a6d10688dd64cbd7f12b3b6c7f802e2096c041208b9289aec380d1a748fdfcd4128553d781e3", + }, + ]; + + fn hex_of(digest: &Digest) -> String { + hex::encode(digest.as_bytes()) + } + + #[test] + fn sha2_matches_kat() { + let provider = BcryptProvider::new().expect("SHA-2 providers open"); + for vector in SHA2_VECTORS { + let input = hex::decode(vector.msg).expect("valid hex vector"); + let digest = provider.digest(vector.algorithm, &input); + assert_eq!(hex_of(&digest), vector.md, "{:?}", vector.algorithm); + } + } + + #[test] + fn streaming_matches_kat() { + let provider = BcryptProvider::new().expect("SHA-2 providers open"); + let mut hasher = provider.hash(BaseHashAlgorithm::Sha256); + hasher.update(&[0x3e]); + hasher.update(&[0xc0, 0x09]); + assert_eq!( + hex_of(&hasher.finalize()), + "579badde3d29ecbdcbd56dacaf3f7fcfd40b1aac60dbc5b17e3902613864e470" + ); + } + + #[cfg(feature = "sha3")] + #[test] + fn sha3_matches_kat_when_available() { + use mscrypto::algorithm::Algorithm; + use mscrypto::error::Error; + use mscrypto::provider::CryptoProvider; + use mscrypto::sha3::Sha3; + + let provider = BcryptProvider::new().expect("SHA-2 providers open"); + for vector in SHA3_VECTORS { + let input = hex::decode(vector.msg).expect("valid hex vector"); + match provider.sha3_digest(vector.algorithm, &input) { + Ok(digest) => { + assert!(provider.supports(Algorithm::Sha3(vector.algorithm))); + assert_eq!(hex_of(&digest), vector.md, "{:?}", vector.algorithm); + } + Err(Error::Unavailable) => { + assert!(!provider.supports(Algorithm::Sha3(vector.algorithm))); + } + Err(other) => panic!("unexpected error for {:?}: {other:?}", vector.algorithm), + } + } + } +} diff --git a/mscrypto-bcrypt/src/lib.rs b/mscrypto-bcrypt/src/lib.rs index 6197efc..daf98fc 100644 --- a/mscrypto-bcrypt/src/lib.rs +++ b/mscrypto-bcrypt/src/lib.rs @@ -1,4 +1,155 @@ -//! BCrypt/CNG backend for `mscrypto`. +//! BCrypt backend for the `mscrypto` contract. //! -//! Stub: intentionally empty so the workspace resolves. The provider is -//! implemented separately (Windows-only). +//! Provides [`BcryptProvider`], a concrete [`CryptoProvider`] backed by Windows +//! (`bcryptprimitives.dll`). + +#![cfg(windows)] + +mod hash; + +pub use hash::BcryptHasher; + +/// Everything needed to use this provider in one glob import: +/// `use mscrypto_bcrypt::prelude::*;`. It re-exports the provider and traits +/// (whose methods are otherwise not in scope), and the shared +/// algorithm, error, and metadata types from the contract, so a consumer +/// does not need a separate dependency on `mscrypto` for the common path. +pub mod prelude { + pub use crate::{BcryptHasher, BcryptProvider, BcryptProviderBuilder}; + + pub use mscrypto::algorithm::{Algorithm, BaseHashAlgorithm}; + pub use mscrypto::error::{Error, ProviderBuildError}; + pub use mscrypto::hash::{Digest, Hash, HashOps}; + pub use mscrypto::provider::{BackendInfo, BackendVersion, CryptoProvider, LinkMode}; + + #[cfg(feature = "sha3")] + pub use mscrypto::algorithm::Sha3Algorithm; + #[cfg(feature = "sha3")] + pub use mscrypto::sha3::Sha3; +} + +use mscrypto::algorithm::{Algorithm, BaseHashAlgorithm}; +use mscrypto::error::ProviderBuildError; +use mscrypto::provider::{BackendInfo, BackendVersion, CryptoProvider, LinkMode}; + +#[cfg(feature = "sha3")] +use mscrypto::algorithm::Sha3Algorithm; + +const BACKEND_NAME: &str = "bcrypt"; + +/// BCrypt cryptographic provider. +pub struct BcryptProvider { + #[cfg(feature = "sha3")] + sha3_256: bool, + #[cfg(feature = "sha3")] + sha3_384: bool, + #[cfg(feature = "sha3")] + sha3_512: bool, + info: BackendInfo, +} + +impl BcryptProvider { + /// Builds a provider (probing SHA-3 availability under the `sha3` feature), + /// requiring no specific algorithms. + pub fn new() -> Result { + BcryptProvider::builder().build() + } + + /// Starts a builder used to declare algorithms that must be present. + pub fn builder() -> BcryptProviderBuilder { + BcryptProviderBuilder::default() + } +} + +/// Builder for [`BcryptProvider`]. Collects a require-list checked at `build()`. +#[derive(Default)] +pub struct BcryptProviderBuilder { + required: Vec, +} + +impl BcryptProviderBuilder { + /// Records an algorithm that `build()` must confirm is supported. + pub fn require(mut self, algorithm: Algorithm) -> Self { + self.required.push(algorithm); + self + } + + /// Probes SHA-3 availability, then verifies the require-list. SHA-2 needs no + /// setup (pseudo-handles), so it never fails here. + pub fn build(self) -> Result { + let provider = BcryptProvider { + #[cfg(feature = "sha3")] + sha3_256: hash::sha3_available(Sha3Algorithm::Sha3_256), + #[cfg(feature = "sha3")] + sha3_384: hash::sha3_available(Sha3Algorithm::Sha3_384), + #[cfg(feature = "sha3")] + sha3_512: hash::sha3_available(Sha3Algorithm::Sha3_512), + info: backend_info(), + }; + + let missing: Vec = self + .required + .into_iter() + .filter(|algorithm| !provider.supports(*algorithm)) + .collect(); + if !missing.is_empty() { + return Err(ProviderBuildError::UnsupportedAlgorithms { + backend: BACKEND_NAME, + missing, + }); + } + Ok(provider) + } +} + +impl CryptoProvider for BcryptProvider { + fn info(&self) -> &BackendInfo { + &self.info + } + + fn supports(&self, algorithm: Algorithm) -> bool { + match algorithm { + Algorithm::Hash( + BaseHashAlgorithm::Sha256 | BaseHashAlgorithm::Sha384 | BaseHashAlgorithm::Sha512, + ) => true, + #[cfg(feature = "sha3")] + Algorithm::Sha3(variant) => match variant { + Sha3Algorithm::Sha3_256 => self.sha3_256, + Sha3Algorithm::Sha3_384 => self.sha3_384, + Sha3Algorithm::Sha3_512 => self.sha3_512, + }, + _ => false, + } + } +} + +fn backend_info() -> BackendInfo { + BackendInfo { + name: BACKEND_NAME, + // bcrypt ships with the OS and carries no independent crypto-library version. + version: BackendVersion { + major: 0, + minor: 0, + patch: 0, + }, + link_mode: LinkMode::NotApplicable, + fips: false, + } +} + +#[cfg(test)] +mod test { + use super::BcryptProvider; + use mscrypto::algorithm::{Algorithm, BaseHashAlgorithm}; + use mscrypto::provider::{CryptoProvider, LinkMode}; + + #[test] + fn provider_metadata() { + let provider = BcryptProvider::new().expect("SHA-2 providers open"); + let info = provider.info(); + assert_eq!(info.name, "bcrypt"); + assert!(!info.fips); + assert!(matches!(info.link_mode, LinkMode::NotApplicable)); + assert!(provider.supports(Algorithm::Hash(BaseHashAlgorithm::Sha256))); + } +} diff --git a/mscrypto-example/Cargo.toml b/mscrypto-example/Cargo.toml new file mode 100644 index 0000000..c38a9e9 --- /dev/null +++ b/mscrypto-example/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mscrypto-example" +authors = ["Microsoft"] +version = "0.1.0" +license = "MIT OR Apache-2.0" +description = "Minimal example of choosing an mscrypto backend and hashing" +edition.workspace = true +rust-version.workspace = true +repository = "https://github.com/microsoft/rust-symcrypt" +publish = false + +[dependencies] +mscrypto-symcrypt = { path = "../mscrypto-symcrypt", version = "0.1.0" } + +# BCrypt is Windows-only; on other targets the example uses SymCrypt alone. +[target.'cfg(windows)'.dependencies] +mscrypto-bcrypt = { path = "../mscrypto-bcrypt", version = "0.1.0" } diff --git a/mscrypto-example/src/main.rs b/mscrypto-example/src/main.rs new file mode 100644 index 0000000..c1aa3dc --- /dev/null +++ b/mscrypto-example/src/main.rs @@ -0,0 +1,59 @@ +//! Minimal example: pick a backend at startup, then SHA-256 "hello world". +//! +//! ```text +//! cargo run -p mscrypto-example # SymCrypt (default) +//! $env:MSCRYPTO_BACKEND = "bcrypt"; cargo run -p mscrypto-example # BCrypt/CNG (Windows) +//! ``` + +use mscrypto_symcrypt::prelude::*; + +#[cfg(windows)] +use mscrypto_bcrypt::BcryptProvider; + +/// Holds whichever backend was chosen. Providers are concrete types (the contract +/// has no `dyn`), so a small enum is how a consumer keeps "either backend" in one +/// value and dispatches at runtime. +enum Backend { + SymCrypt(SymCryptProvider), + #[cfg(windows)] + Bcrypt(BcryptProvider), +} + +impl Backend { + fn name(&self) -> &'static str { + match self { + Backend::SymCrypt(provider) => provider.info().name, + #[cfg(windows)] + Backend::Bcrypt(provider) => provider.info().name, + } + } + + fn sha256(&self, data: &[u8]) -> Digest { + match self { + Backend::SymCrypt(provider) => provider.digest(BaseHashAlgorithm::Sha256, data), + #[cfg(windows)] + Backend::Bcrypt(provider) => provider.digest(BaseHashAlgorithm::Sha256, data), + } + } +} + +/// The decision point: choose a backend at startup. Here it is driven by an +/// environment variable and defaults to SymCrypt; BCrypt is Windows-only. +fn select_backend() -> Backend { + match std::env::var("MSCRYPTO_BACKEND").as_deref() { + #[cfg(windows)] + Ok("bcrypt") => Backend::Bcrypt(BcryptProvider::new().expect("open CNG SHA-2 providers")), + _ => Backend::SymCrypt(SymCryptProvider::new().expect("initialize SymCrypt module")), + } +} + +fn main() { + let backend = select_backend(); + let digest = backend.sha256(b"hello world"); + println!("backend: {}", backend.name()); + println!("sha256(\"hello world\") = {}", to_hex(&digest)); +} + +fn to_hex(digest: &Digest) -> String { + digest.as_bytes().iter().map(|byte| format!("{byte:02x}")).collect() +} diff --git a/mscrypto-symcrypt/Cargo.toml b/mscrypto-symcrypt/Cargo.toml index 29712fa..be3ce6f 100644 --- a/mscrypto-symcrypt/Cargo.toml +++ b/mscrypto-symcrypt/Cargo.toml @@ -3,7 +3,21 @@ name = "mscrypto-symcrypt" authors = ["Microsoft"] version = "0.1.0" license = "MIT OR Apache-2.0" -description = "SymCrypt backend for the mscrypto contract (stub, not yet implemented)" +description = "SymCrypt backend for the mscrypto contract" edition.workspace = true rust-version.workspace = true repository = "https://github.com/microsoft/rust-symcrypt" + +[dependencies] +mscrypto = { path = "../mscrypto", version = "0.1.0" } +symcrypt = { path = "../rust-symcrypt", version = "0.6.0" } +# Used only to read the SYMCRYPT_CODE_VERSION_* constants reported through +# BackendInfo::version. symcrypt already pulls this in, so it adds nothing new. +symcrypt-sys = { workspace = true } + +[dev-dependencies] +hex = { workspace = true } + +[features] +# Forwards to the contract's SHA-3 surface; off by default. +sha3 = ["mscrypto/sha3"] diff --git a/mscrypto-symcrypt/build.rs b/mscrypto-symcrypt/build.rs new file mode 100644 index 0000000..fc9ea2b --- /dev/null +++ b/mscrypto-symcrypt/build.rs @@ -0,0 +1,19 @@ +// Records how symcrypt-sys is linked so the provider can report it through +// BackendInfo::link_mode. There is no runtime way to learn this, so it is read +// from the same env vars symcrypt-sys uses and exposed as a cfg. +fn main() { + // A target-prefixed variable takes precedence, mirroring symcrypt-sys. + let target = std::env::var("TARGET").unwrap_or_default(); + let prefix = target.to_uppercase().replace('-', "_"); + println!("cargo::rerun-if-env-changed=SYMCRYPT_STATIC"); + println!("cargo::rerun-if-env-changed={prefix}_SYMCRYPT_STATIC"); + + let read = |name: &str| std::env::var(format!("{prefix}_{name}")).or_else(|_| std::env::var(name)); + let is_static = read("SYMCRYPT_STATIC").map(|v| v != "0").unwrap_or(false); + + // v1 distinguishes dynamic vs prebuilt-static only. "from_source" is reserved + // for a later linking change and is never emitted here. + let mode = if is_static { "prebuilt" } else { "dynamic" }; + println!("cargo::rustc-cfg=mscrypto_link=\"{mode}\""); + println!("cargo::rustc-check-cfg=cfg(mscrypto_link, values(\"dynamic\", \"prebuilt\", \"from_source\"))"); +} diff --git a/mscrypto-symcrypt/src/hash.rs b/mscrypto-symcrypt/src/hash.rs new file mode 100644 index 0000000..52c2c71 --- /dev/null +++ b/mscrypto-symcrypt/src/hash.rs @@ -0,0 +1,268 @@ +//! Hashing for the SymCrypt provider. + +use mscrypto::algorithm::BaseHashAlgorithm; +use mscrypto::hash::provider::BuildDigest; +use mscrypto::hash::{Digest, Hash, HashOps}; + +use symcrypt::hash::{HashState, Sha256State, Sha384State, Sha512State}; + +use crate::SymCryptProvider; + +/// Streaming SHA hasher. One concrete type covers every base algorithm, as the +/// contract's `Hash::Hasher` is a single associated type. +pub enum SymCryptHasher { + Sha256(Sha256State), + Sha384(Sha384State), + Sha512(Sha512State), +} + +impl HashOps for SymCryptHasher { + fn update(&mut self, data: &[u8]) { + match self { + SymCryptHasher::Sha256(state) => state.append(data), + SymCryptHasher::Sha384(state) => state.append(data), + SymCryptHasher::Sha512(state) => state.append(data), + } + } + + fn finalize(self) -> Digest { + match self { + SymCryptHasher::Sha256(mut state) => digest_from(state.result()), + SymCryptHasher::Sha384(mut state) => digest_from(state.result()), + SymCryptHasher::Sha512(mut state) => digest_from(state.result()), + } + } +} + +/// Copies a fixed-size SymCrypt result into a `Digest`. SymCrypt's safe API hands +/// back an owned array, so the digest bytes are moved through a single stack copy +/// of at most `Digest::MAX_LEN` bytes. +fn digest_from(out: [u8; N]) -> Digest { + Digest::from_fn(N, |buf| buf.copy_from_slice(&out)) +} + +impl Hash for SymCryptProvider { + type Hasher = SymCryptHasher; + + fn hash(&self, algorithm: BaseHashAlgorithm) -> SymCryptHasher { + match algorithm { + BaseHashAlgorithm::Sha256 => SymCryptHasher::Sha256(Sha256State::new()), + BaseHashAlgorithm::Sha384 => SymCryptHasher::Sha384(Sha384State::new()), + BaseHashAlgorithm::Sha512 => SymCryptHasher::Sha512(Sha512State::new()), + } + } + + fn digest(&self, algorithm: BaseHashAlgorithm, data: &[u8]) -> Digest { + match algorithm { + BaseHashAlgorithm::Sha256 => digest_from(symcrypt::hash::sha256(data)), + BaseHashAlgorithm::Sha384 => digest_from(symcrypt::hash::sha384(data)), + BaseHashAlgorithm::Sha512 => digest_from(symcrypt::hash::sha512(data)), + } + } +} + +#[cfg(feature = "sha3")] +mod sha3_impl { + use super::{digest_from, Digest, HashOps, SymCryptProvider}; + use mscrypto::algorithm::Sha3Algorithm; + use mscrypto::error::Error; + use mscrypto::sha3::Sha3; + use symcrypt::hash::{HashState, Sha3_256State, Sha3_384State, Sha3_512State}; + + /// Streaming SHA-3 hasher. One concrete type covers every SHA-3 algorithm. + pub enum SymCryptSha3Hasher { + Sha3_256(Sha3_256State), + Sha3_384(Sha3_384State), + Sha3_512(Sha3_512State), + } + + impl HashOps for SymCryptSha3Hasher { + fn update(&mut self, data: &[u8]) { + match self { + SymCryptSha3Hasher::Sha3_256(state) => state.append(data), + SymCryptSha3Hasher::Sha3_384(state) => state.append(data), + SymCryptSha3Hasher::Sha3_512(state) => state.append(data), + } + } + + fn finalize(self) -> Digest { + match self { + SymCryptSha3Hasher::Sha3_256(mut state) => digest_from(state.result()), + SymCryptSha3Hasher::Sha3_384(mut state) => digest_from(state.result()), + SymCryptSha3Hasher::Sha3_512(mut state) => digest_from(state.result()), + } + } + } + + impl Sha3 for SymCryptProvider { + type Sha3Hasher = SymCryptSha3Hasher; + + // SymCrypt always provides SHA-3, so construction never reports Unavailable. + fn sha3(&self, algorithm: Sha3Algorithm) -> Result { + Ok(match algorithm { + Sha3Algorithm::Sha3_256 => SymCryptSha3Hasher::Sha3_256(Sha3_256State::new()), + Sha3Algorithm::Sha3_384 => SymCryptSha3Hasher::Sha3_384(Sha3_384State::new()), + Sha3Algorithm::Sha3_512 => SymCryptSha3Hasher::Sha3_512(Sha3_512State::new()), + }) + } + + fn sha3_digest(&self, algorithm: Sha3Algorithm, data: &[u8]) -> Result { + Ok(match algorithm { + Sha3Algorithm::Sha3_256 => digest_from(symcrypt::hash::sha3_256(data)), + Sha3Algorithm::Sha3_384 => digest_from(symcrypt::hash::sha3_384(data)), + Sha3Algorithm::Sha3_512 => digest_from(symcrypt::hash::sha3_512(data)), + }) + } + } +} + +#[cfg(feature = "sha3")] +pub use sha3_impl::SymCryptSha3Hasher; + +#[cfg(test)] +mod test { + use crate::SymCryptProvider; + use mscrypto::algorithm::BaseHashAlgorithm; + use mscrypto::hash::{Digest, Hash, HashOps}; + + #[cfg(feature = "sha3")] + use mscrypto::algorithm::Sha3Algorithm; + + struct ShaVector { + algorithm: BaseHashAlgorithm, + msg: &'static str, + md: &'static str, + } + + const SHA2_VECTORS: &[ShaVector] = &[ + ShaVector { + algorithm: BaseHashAlgorithm::Sha256, + msg: "", + md: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha256, + msg: "3ec009", + md: "579badde3d29ecbdcbd56dacaf3f7fcfd40b1aac60dbc5b17e3902613864e470", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha384, + msg: "", + md: "38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha384, + msg: "ac8a50", + md: "1bdcb04240b4b43a110407baa08b404f042ea05c517ce2d9cc2be38cdfd916ce0db81615f869449e26416430cd5eb120", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha512, + msg: "", + md: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + }, + ShaVector { + algorithm: BaseHashAlgorithm::Sha512, + msg: "d4fc1f", + md: "e668327148443a73d6ec4a9db28aac4280bd11e8a652175b1757de4fc03ebed5ea85e8945dae67b394be3065c84c15261f2f05bd071c13bc77fadeb6786911a1", + }, + ]; + + #[cfg(feature = "sha3")] + struct Sha3Vector { + algorithm: Sha3Algorithm, + msg: &'static str, + md: &'static str, + } + + #[cfg(feature = "sha3")] + const SHA3_VECTORS: &[Sha3Vector] = &[ + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_256, + msg: "", + md: "a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_256, + msg: "b053fa", + md: "9d0ff086cd0ec06a682c51c094dc73abdc492004292344bd41b82a60498ccfdb", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_384, + msg: "", + md: "0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_384, + msg: "6ab7d6", + md: "ea12d6d32d69ad2154a57e0e1be481a45add739ee7dd6e2a27e544b6c8b5ad122654bbf95134d567987156295d5e57db", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_512, + msg: "", + md: "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26", + }, + Sha3Vector { + algorithm: Sha3Algorithm::Sha3_512, + msg: "37d518", + md: "4aa96b1547e6402c0eee781acaa660797efe26ec00b4f2e0aec4a6d10688dd64cbd7f12b3b6c7f802e2096c041208b9289aec380d1a748fdfcd4128553d781e3", + }, + ]; + + fn hex_of(digest: &Digest) -> String { + hex::encode(digest.as_bytes()) + } + + #[test] + fn sha2_matches_kat() { + let provider = SymCryptProvider::new().expect("SymCrypt module initializes"); + for vector in SHA2_VECTORS { + let input = hex::decode(vector.msg).expect("valid hex vector"); + let digest = provider.digest(vector.algorithm, &input); + assert_eq!(hex_of(&digest), vector.md, "{:?}", vector.algorithm); + } + } + + #[cfg(feature = "sha3")] + #[test] + fn sha3_matches_kat() { + use mscrypto::sha3::Sha3; + + let provider = SymCryptProvider::new().expect("SymCrypt module initializes"); + for vector in SHA3_VECTORS { + let input = hex::decode(vector.msg).expect("valid hex vector"); + let digest = provider + .sha3_digest(vector.algorithm, &input) + .expect("SymCrypt always provides SHA-3"); + assert_eq!(hex_of(&digest), vector.md, "{:?}", vector.algorithm); + } + } + + #[test] + fn streaming_matches_kat() { + let provider = SymCryptProvider::new().expect("SymCrypt module initializes"); + let mut hasher = provider.hash(BaseHashAlgorithm::Sha256); + hasher.update(&[0x3e]); + hasher.update(&[0xc0, 0x09]); + assert_eq!( + hex_of(&hasher.finalize()), + "579badde3d29ecbdcbd56dacaf3f7fcfd40b1aac60dbc5b17e3902613864e470" + ); + } + + #[cfg(feature = "sha3")] + #[test] + fn sha3_streaming_matches_kat() { + use mscrypto::sha3::Sha3; + + let provider = SymCryptProvider::new().expect("SymCrypt module initializes"); + let mut hasher = provider + .sha3(Sha3Algorithm::Sha3_256) + .expect("SymCrypt always provides SHA-3"); + hasher.update(&[0xb0]); + hasher.update(&[0x53, 0xfa]); + assert_eq!( + hex_of(&hasher.finalize()), + "9d0ff086cd0ec06a682c51c094dc73abdc492004292344bd41b82a60498ccfdb" + ); + } +} diff --git a/mscrypto-symcrypt/src/lib.rs b/mscrypto-symcrypt/src/lib.rs index 6899b19..39733a9 100644 --- a/mscrypto-symcrypt/src/lib.rs +++ b/mscrypto-symcrypt/src/lib.rs @@ -1,4 +1,176 @@ -//! SymCrypt backend for `mscrypto`. +//! SymCrypt backend for the `mscrypto` contract. //! -//! Stub: intentionally empty so the workspace resolves. The provider is -//! implemented separately. +//! Provides [`SymCryptProvider`], a concrete [`CryptoProvider`] backed by the +//! `symcrypt` crate + +mod hash; + +pub use hash::SymCryptHasher; +#[cfg(feature = "sha3")] +pub use hash::SymCryptSha3Hasher; + +/// Everything needed to use this provider in one glob import: +/// `use mscrypto_symcrypt::prelude::*;`. It re-exports the provider. builder, and traits +/// (whose methods are otherwise not in scope), and the shared +/// algorithm, error, and metadata types from the contract, so a consumer +/// does not need a separate dependency on `mscrypto` for the common path. +pub mod prelude { + pub use crate::{SymCryptHasher, SymCryptProvider, SymCryptProviderBuilder}; + + pub use mscrypto::algorithm::{Algorithm, BaseHashAlgorithm}; + pub use mscrypto::error::{Error, ProviderBuildError}; + pub use mscrypto::hash::{Digest, Hash, HashOps}; + pub use mscrypto::provider::{BackendInfo, BackendVersion, CryptoProvider, LinkMode}; + + #[cfg(feature = "sha3")] + pub use crate::SymCryptSha3Hasher; + #[cfg(feature = "sha3")] + pub use mscrypto::algorithm::Sha3Algorithm; + #[cfg(feature = "sha3")] + pub use mscrypto::sha3::Sha3; +} + +use mscrypto::algorithm::{Algorithm, BaseHashAlgorithm}; +use mscrypto::error::ProviderBuildError; +use mscrypto::provider::{BackendInfo, BackendVersion, CryptoProvider, LinkMode}; + +#[cfg(feature = "sha3")] +use mscrypto::algorithm::Sha3Algorithm; + +const BACKEND_NAME: &str = "symcrypt"; + +/// SymCrypt-backed cryptographic provider. +pub struct SymCryptProvider { + info: BackendInfo, +} + +impl SymCryptProvider { + /// Builds a provider with no required algorithms. + pub fn new() -> Result { + SymCryptProvider::builder().build() + } + + /// Starts a builder used to declare algorithms that must be present. + pub fn builder() -> SymCryptProviderBuilder { + SymCryptProviderBuilder::default() + } +} + +/// Builder for [`SymCryptProvider`]. Collects a require-list checked at `build()`. +#[derive(Default)] +pub struct SymCryptProviderBuilder { + required: Vec, +} + +impl SymCryptProviderBuilder { + /// Records an algorithm that `build()` must confirm is supported. + pub fn require(mut self, algorithm: Algorithm) -> Self { + self.required.push(algorithm); + self + } + + /// Builds the provider, failing if any required algorithm is unsupported. + pub fn build(self) -> Result { + initialize_module()?; + let provider = SymCryptProvider { info: backend_info() }; + let missing: Vec = self + .required + .into_iter() + .filter(|algorithm| !provider.supports(*algorithm)) + .collect(); + if !missing.is_empty() { + return Err(ProviderBuildError::UnsupportedAlgorithms { + backend: BACKEND_NAME, + missing, + }); + } + Ok(provider) + } +} + +impl CryptoProvider for SymCryptProvider { + fn info(&self) -> &BackendInfo { + &self.info + } + + fn supports(&self, algorithm: Algorithm) -> bool { + match algorithm { + Algorithm::Hash( + BaseHashAlgorithm::Sha256 | BaseHashAlgorithm::Sha384 | BaseHashAlgorithm::Sha512, + ) => true, + #[cfg(feature = "sha3")] + Algorithm::Sha3( + Sha3Algorithm::Sha3_256 | Sha3Algorithm::Sha3_384 | Sha3Algorithm::Sha3_512, + ) => true, + _ => false, + } + } +} + +// Single point where SymCrypt module usability is verified when a provider is +// built. SymCrypt checks version compatibility during its lazy initialization on +// first use, which aborts on a mismatch, so there is no recoverable failure to +// report today. A graceful module-init entry point will surface an incompatible +// module here as `ProviderBuildError::Backend { backend, operation }`. +fn initialize_module() -> Result<(), ProviderBuildError> { + // SAFETY: FFI call to a stateless version check that aborts on an incompatible + // module and is safe to call more than once (the symcrypt crate also calls it + // lazily on first use). TODO: Change to SymCryptModuleInitEX. + unsafe { + symcrypt_sys::SymCryptModuleInit( + symcrypt_sys::SYMCRYPT_CODE_VERSION_API, + symcrypt_sys::SYMCRYPT_CODE_VERSION_MINOR, + ); + } + Ok(()) +} + +fn backend_info() -> BackendInfo { + BackendInfo { + name: BACKEND_NAME, + version: BackendVersion { + major: symcrypt_sys::SYMCRYPT_CODE_VERSION_API, + minor: symcrypt_sys::SYMCRYPT_CODE_VERSION_MINOR, + patch: symcrypt_sys::SYMCRYPT_CODE_VERSION_PATCH, + }, + link_mode: link_mode(), + fips: false, + } +} + +fn link_mode() -> LinkMode { + if cfg!(mscrypto_link = "prebuilt") { + LinkMode::PrebuiltStatic + } else { + LinkMode::DynamicSystem + } +} + +#[cfg(test)] +mod test { + use super::SymCryptProvider; + use mscrypto::algorithm::{Algorithm, BaseHashAlgorithm}; + use mscrypto::provider::{CryptoProvider, LinkMode}; + + #[test] + fn provider_metadata() { + let provider = SymCryptProvider::new().expect("SymCrypt module initializes"); + let info = provider.info(); + assert_eq!(info.name, "symcrypt"); + assert_eq!(info.version.major, 103); + assert!(!info.fips); + assert!(matches!( + info.link_mode, + LinkMode::DynamicSystem | LinkMode::PrebuiltStatic + )); + } + + #[test] + fn builder_requires_supported_algorithm() { + let provider = SymCryptProvider::builder() + .require(Algorithm::Hash(BaseHashAlgorithm::Sha256)) + .build() + .expect("SHA-256 is supported by SymCrypt"); + assert!(provider.supports(Algorithm::Hash(BaseHashAlgorithm::Sha512))); + } +} diff --git a/mscrypto/src/algorithm.rs b/mscrypto/src/algorithm.rs index 66dea54..e6cd4db 100644 --- a/mscrypto/src/algorithm.rs +++ b/mscrypto/src/algorithm.rs @@ -1,5 +1,9 @@ //! Algorithm identifiers used across the backend-neutral contract. +// `Algorithm` is non_exhaustive because new algorithm families are added over +// time, so callers must handle an unknown family. The per-family enums below are +// exhaustive: their members are a fixed, known set, so adding one is a breaking +// change that forces every backend to handle it at compile time. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[non_exhaustive] pub enum Algorithm { @@ -9,7 +13,6 @@ pub enum Algorithm { } #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum BaseHashAlgorithm { Sha256, Sha384, @@ -18,7 +21,6 @@ pub enum BaseHashAlgorithm { #[cfg(feature = "sha3")] #[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[non_exhaustive] pub enum Sha3Algorithm { Sha3_256, Sha3_384, From e9572e2acb2bd022bde9fc73ce245158c2850d48 Mon Sep 17 00:00:00 2001 From: "Khang Nguyen (ENS)" Date: Thu, 9 Jul 2026 12:30:51 -0400 Subject: [PATCH 3/3] Clippy --- mscrypto/src/hash.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mscrypto/src/hash.rs b/mscrypto/src/hash.rs index 6c3aedd..a99b1b8 100644 --- a/mscrypto/src/hash.rs +++ b/mscrypto/src/hash.rs @@ -25,6 +25,9 @@ impl Digest { &self.bytes[..self.len as usize] } + // A digest has a fixed, nonzero length set by its algorithm, so an `is_empty` + // companion would be dead API. + #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { self.len as usize }