重构:RustCrypto 兼容接口

主要变更:
- 新增 sm2/、sm3/、sm4/ 独立 crate,实现 RustCrypto traits
  - sm2:实现 signature、elliptic-curve traits
  - sm3:实现 digest、crypto-common traits
  - sm4:实现 cipher、aead traits
- 新增 fuzz/ 模糊测试目标
- 新增 tests/sm2_proptest.rs 属性测试
- 重构 src/sm4/ 使用 RustCrypto AEAD traits
- 更新 CI 工作流支持多 crate 测试
- 更新 .gitignore 忽略 fuzz/target/
This commit is contained in:
huangxt
2026-03-11 18:32:21 +08:00
parent 21e7e65b32
commit a4ca734d0d
36 changed files with 5184 additions and 961 deletions
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "sm3"
version = "0.1.0"
edition = "2021"
rust-version = "1.83.0"
license = "Apache-2.0"
description = "SM3 (ShangMi 3) hash function — GB/T 32905-2016. Pure-Rust, no_std, implements digest::Digest."
repository = "https://github.com/kintaiW/libsmx"
documentation = "https://docs.rs/sm3"
categories = ["cryptography", "no-std"]
keywords = ["crypto", "hash", "sm3", "shangmi", "digest"]
readme = "README.md"
[dependencies]
digest = { workspace = true, features = ["block-api"] }
[dev-dependencies]
hex-literal = { workspace = true }
digest = { workspace = true, features = ["dev"] }
[features]
default = []
oid = ["digest/oid"]
+98
View File
@@ -0,0 +1,98 @@
//! SM3 block-level core (low-level internal type).
//!
//! Users should use [`Sm3`] from the crate root instead.
use core::fmt;
use digest::{
HashMarker,
block_api::{
AlgorithmName, Block, BlockSizeUser, Buffer, BufferKindUser,
Eager, FixedOutputCore, OutputSizeUser, UpdateCore,
},
typenum::{U32, U64, Unsigned},
};
use crate::compress;
// ── Sm3Core ───────────────────────────────────────────────────────────────────
/// Low-level SM3 block-processing core.
///
/// Implements the `digest::block_api` low-level traits so that the
/// [`digest::buffer_fixed!`] macro can wrap it into a fully-featured [`Sm3`].
#[derive(Clone)]
pub struct Sm3Core {
state: [u32; 8],
/// Number of **complete** 64-byte blocks already compressed.
block_len: u64,
}
impl HashMarker for Sm3Core {}
impl BlockSizeUser for Sm3Core {
/// SM3 processes 512-bit (64-byte) blocks.
type BlockSize = U64;
}
impl BufferKindUser for Sm3Core {
/// Eager: compress each full block immediately.
type BufferKind = Eager;
}
impl OutputSizeUser for Sm3Core {
/// SM3 produces a 256-bit (32-byte) digest.
type OutputSize = U32;
}
impl Default for Sm3Core {
fn default() -> Self {
Self { state: compress::IV, block_len: 0 }
}
}
impl digest::Reset for Sm3Core {
fn reset(&mut self) {
*self = Self::default();
}
}
impl UpdateCore for Sm3Core {
#[inline]
fn update_blocks(&mut self, blocks: &[Block<Self>]) {
// Reason: only complete blocks are counted here; the partial tail is
// held by the surrounding BlockBuffer and counted in finalize.
self.block_len += blocks.len() as u64;
for block in blocks {
// Reason: hybrid_array::Array<u8, U64> implements Deref<Target=[u8]>,
// so we get a &[u8] slice then cast to &[u8; 64] via try_into.
let b: &[u8; 64] = (&**block).try_into().unwrap();
compress::compress(&mut self.state, b);
}
}
}
impl FixedOutputCore for Sm3Core {
#[inline]
fn finalize_fixed_core(&mut self, buffer: &mut Buffer<Self>, out: &mut digest::Output<Self>) {
// Total bit length = (complete blocks × 64 + partial tail) × 8
let bs = U64::U64;
let bit_len = 8 * (buffer.get_pos() as u64 + bs * self.block_len);
// GB/T 32905 §5.3.1 padding: 0x80, zeros, 64-bit big-endian bit count
buffer.len64_padding_be(bit_len, |block| {
let b: &[u8; 64] = (&**block).try_into().unwrap();
compress::compress(&mut self.state, b);
});
// Serialize state as big-endian u32 words
for (chunk, &word) in out.chunks_exact_mut(4).zip(self.state.iter()) {
chunk.copy_from_slice(&word.to_be_bytes());
}
}
}
impl AlgorithmName for Sm3Core {
fn write_alg_name(f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Sm3")
}
}
+81
View File
@@ -0,0 +1,81 @@
//! SM3 compression function (GB/T 32905-2016 §5)
/// SM3 initial hash values (IV), GB/T 32905 §4.3
pub(super) const IV: [u32; 8] = [
0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600,
0xA96F30BC, 0x163138AA, 0xE38DEE4D, 0xB0FB0E4E,
];
/// Round constants T_j (GB/T 32905 §4.2), precomputed to avoid runtime branches.
///
/// Reason: Eliminates the `if j < 16` branch in each round; the compiler
/// embeds these as immediates with zero runtime rotation overhead.
const T: [u32; 64] = {
let mut t = [0u32; 64];
let mut j = 0usize;
while j < 16 {
t[j] = 0x79CC4519u32.rotate_left(j as u32);
j += 1;
}
while j < 64 {
t[j] = 0x7A879D8Au32.rotate_left((j % 32) as u32);
j += 1;
}
t
};
/// Permutation function P0 (GB/T 32905 §4.5)
#[inline(always)]
fn p0(x: u32) -> u32 {
x ^ x.rotate_left(9) ^ x.rotate_left(17)
}
/// Permutation function P1 (GB/T 32905 §4.5)
#[inline(always)]
fn p1(x: u32) -> u32 {
x ^ x.rotate_left(15) ^ x.rotate_left(23)
}
/// SM3 compression function: processes one 64-byte block, updates `state`.
///
/// Reason: Two-segment loop (j=0..15 and j=16..63) eliminates runtime `if`
/// branches inside ff/gg/T; W' is inlined as `w[j] ^ w[j+4]`.
pub(super) fn compress(state: &mut [u32; 8], block: &[u8; 64]) {
// ── Message expansion ────────────────────────────────────────────────────
let mut w = [0u32; 68];
for i in 0..16 {
w[i] = u32::from_be_bytes(block[i * 4..i * 4 + 4].try_into().unwrap());
}
for i in 16..68 {
let v = w[i - 16] ^ w[i - 9] ^ w[i - 3].rotate_left(15);
w[i] = p1(v) ^ w[i - 13].rotate_left(7) ^ w[i - 6];
}
// ── Compression: 64 rounds ───────────────────────────────────────────────
let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = *state;
// j = 0..15: FF = x^y^z, GG = x^y^z
for j in 0..16 {
let ss1 = a.rotate_left(12).wrapping_add(e).wrapping_add(T[j]).rotate_left(7);
let ss2 = ss1 ^ a.rotate_left(12);
let tt1 = (a ^ b ^ c).wrapping_add(d).wrapping_add(ss2).wrapping_add(w[j] ^ w[j + 4]);
let tt2 = (e ^ f ^ g).wrapping_add(h).wrapping_add(ss1).wrapping_add(w[j]);
d = c; c = b.rotate_left(9); b = a; a = tt1;
h = g; g = f.rotate_left(19); f = e; e = p0(tt2);
}
// j = 16..63: FF = majority(x,y,z), GG = choice(x,y,z)
for j in 16..64 {
let ss1 = a.rotate_left(12).wrapping_add(e).wrapping_add(T[j]).rotate_left(7);
let ss2 = ss1 ^ a.rotate_left(12);
let tt1 = ((a & b) | (a & c) | (b & c))
.wrapping_add(d).wrapping_add(ss2).wrapping_add(w[j] ^ w[j + 4]);
let tt2 = ((e & f) | (!e & g))
.wrapping_add(h).wrapping_add(ss1).wrapping_add(w[j]);
d = c; c = b.rotate_left(9); b = a; a = tt1;
h = g; g = f.rotate_left(19); f = e; e = p0(tt2);
}
state[0] ^= a; state[1] ^= b; state[2] ^= c; state[3] ^= d;
state[4] ^= e; state[5] ^= f; state[6] ^= g; state[7] ^= h;
}
+124
View File
@@ -0,0 +1,124 @@
//! SM3 cryptographic hash function (GB/T 32905-2016).
//!
//! This crate provides a [`Digest`]-compatible SM3 implementation suitable for
//! use anywhere in the RustCrypto ecosystem.
//!
//! ## Security
//!
//! SM3 is standardised by the Chinese National Standard (GB/T 32905-2016) and
//! provides a 256-bit (32-byte) digest. It has a similar structure to SHA-256
//! but uses different constants, mixing functions, and message scheduling.
//!
//! ## Usage
//!
//! ```rust
//! use sm3::{Sm3, Digest};
//!
//! let hash = Sm3::digest(b"abc");
//! assert_eq!(
//! hash[..],
//! hex_literal::hex!("66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0"),
//! );
//! ```
#![no_std]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
mod compress;
/// Block-level SM3 core — low-level building block, not for direct use.
///
/// Prefer the top-level [`Sm3`] type which provides the full `Digest` API.
pub mod block_api;
pub use digest::{self, Digest};
// Re-export the core type for users who need low-level access (e.g. HMAC cores).
pub use block_api::Sm3Core;
// Generate the buffered `Sm3` wrapper using the digest crate macro.
// BaseFixedTraits provides: Debug, BlockSizeUser, OutputSizeUser, CoreProxy, Update, FixedOutput.
// We also add AlgorithmName, Default, Clone, HashMarker, Reset, FixedOutputReset explicitly.
// Reason: FixedHashTraits additionally requires SerializableState and ZeroizeOnDrop which
// are non-trivial to implement safely; BaseFixedTraits is sufficient for the Digest interface.
digest::buffer_fixed!(
/// SM3 hash function (GB/T 32905-2016).
///
/// Implements [`Digest`] and is a drop-in for SHA-256 in generic protocols.
pub struct Sm3(block_api::Sm3Core);
impl: BaseFixedTraits AlgorithmName Default Clone HashMarker Reset FixedOutputReset;
);
#[cfg(test)]
mod tests {
use super::*;
use digest::Digest;
use hex_literal::hex;
/// GB/T 32905-2016 Appendix A, Example 1: SM3("abc")
#[test]
fn test_vector_abc() {
let hash = Sm3::digest(b"abc");
let expected = hex!("66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0");
assert_eq!(hash[..], expected[..]);
}
/// GB/T 32905-2016 Appendix A, Example 2: 64-byte repeated string
#[test]
fn test_vector_64bytes() {
let msg = b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
let hash = Sm3::digest(msg);
let expected = hex!("debe9ff92275b8a138604889c18e5a4d6fdb70e5387e5765293dcba39c0c5732");
assert_eq!(hash[..], expected[..]);
}
/// Empty input
#[test]
fn test_vector_empty() {
let hash = Sm3::digest(b"");
let expected = hex!("1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b");
assert_eq!(hash[..], expected[..]);
}
/// Cross-block boundary: 65 bytes (one full block + 1 byte tail)
#[test]
fn test_cross_block_boundary() {
let data = [0x61u8; 65]; // 65 x 'a'
let once = Sm3::digest(&data);
// Streaming 1 byte at a time must match one-shot
let mut h = Sm3::new();
for b in &data {
h.update(&[*b]);
}
assert_eq!(once, h.finalize());
}
/// Streaming must match one-shot for an arbitrary input
#[test]
fn test_streaming_matches_oneshot() {
let data = b"hello world, streaming SM3 test";
let once = Sm3::digest(data);
let mut h = Sm3::new();
for chunk in data.chunks(7) {
h.update(chunk);
}
assert_eq!(once, h.finalize());
}
/// Clone mid-stream must produce the same result
#[test]
fn test_clone_midstream() {
let mut h1 = Sm3::new();
h1.update(b"hello");
let h2 = h1.clone();
h1.update(b" world");
let mut h3 = h2;
h3.update(b" world");
assert_eq!(h1.finalize(), h3.finalize());
}
}