gmcrypto_core/hmac.rs
1//! HMAC-SM3 — RFC 2104 keyed MAC over GB/T 32905-2016 SM3.
2//!
3//! # Construction
4//!
5//! Standard RFC 2104 with SM3 as the underlying hash:
6//!
7//! ```text
8//! HMAC(K, m) = SM3((K' XOR opad) || SM3((K' XOR ipad) || m))
9//! ```
10//!
11//! where:
12//!
13//! - `B = 64` (SM3 block size); `L = 32` (SM3 output size).
14//! - `K'` is `K` zero-padded to `B` bytes if `len(K) ≤ B`, or `SM3(K)`
15//! zero-padded to `B` bytes if `len(K) > B`.
16//! - `ipad = 0x36` repeated `B` times; `opad = 0x5C` repeated `B` times.
17//!
18//! # Single-shot + streaming API
19//!
20//! - [`hmac_sm3`] (v0.2) is the single-shot path.
21//! - [`HmacSm3`] (v0.3 W5) is the streaming
22//! `new` / `update` / `finalize` shape, plus a constant-time
23//! `verify` helper. Both produce byte-identical output for the
24//! same `(key, message)` regardless of how the message is
25//! chunked across `update` calls.
26//!
27//! # KAT
28//!
29//! All KAT vectors below are cross-validated against `gmssl sm3hmac`
30//! v3.1.1 at commit time. RFC 4231 specifies HMAC for SHA-2 only;
31//! HMAC-SM3 vectors of identical shape are computed by gmssl and
32//! captured here as compile-time regression locks.
33//!
34//! - `K = 0x0b × 20`, `M = "Hi There"` →
35//! `51b00d1fb49832bfb01c3ce27848e59f871d9ba938dc563b338ca964755cce70`.
36//! - `K = "Jefe"`, `M = "what do ya want for nothing?"` →
37//! `2e87f1d16862e6d964b50a5200bf2b10b764faa9680a296a2405f24bec39f882`.
38//! - `K = 0xaa × 131`, `M = "Test Using Larger Than Block-Size Key - Hash Key First"` →
39//! `b4fd844e13342002f0b2e0690ea7741f1497d993a70494cea601e657bedf67a0`
40//! (exercises the hash-first long-key path; gmssl 3.1.1's CLI rejects
41//! keys > 32 bytes, so the published value is computed by feeding
42//! `gmssl sm3hmac` the SM3-hashed key — RFC 2104's hash-first
43//! reduction in action).
44//! - `K = ""`, `M = ""` →
45//! `0d23f72ba15e9c189a879aefc70996b06091de6e64d31b7a84004356dd915261`.
46//!
47//! Phase 4 chunk 4 adds gmssl `sm3hmac` invocations to
48//! `tests/interop_gmssl.rs` so the cross-validation runs in CI when
49//! `GMCRYPTO_GMSSL=1` is set.
50//!
51//! # Zeroization
52//!
53//! Intermediate `K'`, `K' XOR ipad`, and `K' XOR opad` buffers are
54//! wiped before return. The outer hash's input includes the key
55//! (XOR'd with opad), so this matters for callers reusing memory.
56
57use crate::sm3::{BLOCK_SIZE, DIGEST_SIZE, Sm3, hash};
58use crate::traits::{Hash as HashTrait, Mac as MacTrait};
59use subtle::ConstantTimeEq;
60use zeroize::Zeroize;
61
62/// Compute HMAC-SM3 over `message` keyed by `key`. Returns the 32-byte
63/// MAC tag.
64///
65/// `key` may be any length. Per RFC 2104:
66///
67/// - If `key.len() > 64`, the key is first hashed with SM3 (yielding a
68/// 32-byte intermediate) and then zero-padded to 64 bytes.
69/// - Otherwise it is used directly, zero-padded to 64 bytes.
70///
71/// Both intermediate buffers are zeroized before return.
72#[must_use]
73pub fn hmac_sm3(key: &[u8], message: &[u8]) -> [u8; DIGEST_SIZE] {
74 let mut k_prime = [0u8; BLOCK_SIZE];
75 if key.len() > BLOCK_SIZE {
76 // Per RFC 2104, when `key.len() > B` the effective HMAC key is
77 // `K' = SM3(key)` zero-padded to `B`. `hashed` is therefore the
78 // *actual key material* used by the inner and outer hashes —
79 // not merely "key-derived" — so it must be wiped in lockstep
80 // with `k_prime`, `ipad_key`, and `opad_key`. The
81 // `Zeroize::zeroize` call below is a `core::ptr::write_volatile`
82 // sequence that the optimizer is required to emit, closing the
83 // long-key zeroization gap surfaced in the v0.2 codex review.
84 let mut hashed = hash(key);
85 k_prime[..DIGEST_SIZE].copy_from_slice(&hashed);
86 hashed.zeroize();
87 } else {
88 k_prime[..key.len()].copy_from_slice(key);
89 }
90
91 let mut ipad_key = [0x36u8; BLOCK_SIZE];
92 let mut opad_key = [0x5cu8; BLOCK_SIZE];
93 for i in 0..BLOCK_SIZE {
94 ipad_key[i] ^= k_prime[i];
95 opad_key[i] ^= k_prime[i];
96 }
97
98 // Inner hash: SM3(K' XOR ipad || message).
99 let mut inner = Sm3::new();
100 inner.update(&ipad_key);
101 inner.update(message);
102 let inner_digest = inner.finalize();
103
104 // Outer hash: SM3(K' XOR opad || inner_digest).
105 let mut outer = Sm3::new();
106 outer.update(&opad_key);
107 outer.update(&inner_digest);
108 let result = outer.finalize();
109
110 // Wipe key-derived intermediates. The MAC `result` is the public
111 // output; the inner_digest is also a function of (key, message)
112 // but its information content is captured by `result` and the
113 // public outer-hash structure.
114 k_prime.zeroize();
115 ipad_key.zeroize();
116 opad_key.zeroize();
117
118 result
119}
120
121/// Streaming HMAC-SM3 (v0.3 W5).
122///
123/// Construct with `new(&key)`, feed message chunks via `update`,
124/// finalize with `finalize` (32-byte tag) or `verify` (constant-
125/// time compare against an expected tag).
126///
127/// Equivalent to [`hmac_sm3`] for the same `(key, message)` byte
128/// sequence — chunking does not affect the output.
129///
130/// # Zeroization
131///
132/// The keyed `inner` / `outer` [`Sm3`] states (the SM3 compression
133/// state after absorbing `K' XOR ipad` / `K' XOR opad`) hold
134/// key-derived material. They are wiped at the **field layer** by
135/// [`Sm3`]'s own [`Drop`] impl (v0.23): whenever an `HmacSm3` — or the
136/// `Sm3` values moved out of it by [`HmacSm3::finalize`] — is dropped,
137/// the compression state + input buffer are scrubbed. There is no
138/// `impl Drop for HmacSm3` (and there must not be: `finalize(self)`
139/// moves the `inner`/`outer` fields out of `self`, which a `Drop` impl
140/// would forbid). Construction-time intermediates (`K'`, `K' XOR ipad`,
141/// `K' XOR opad`) are zeroized inside [`HmacSm3::new`] after they are
142/// folded into the keyed states. The 32-byte tag returned by
143/// [`HmacSm3::finalize`] is the public output and is not wiped.
144pub struct HmacSm3 {
145 /// Inner-hash state, currently absorbing `K' XOR ipad || message-so-far`.
146 inner: Sm3,
147 /// Outer-hash state, currently holding the absorbed `K' XOR opad`
148 /// (will be finalized with the inner digest at `finalize` time).
149 outer: Sm3,
150}
151
152impl HmacSm3 {
153 /// Construct a new keyed HMAC-SM3 instance.
154 ///
155 /// `key` may be any length; the standard RFC 2104 hash-first
156 /// reduction applies for `key.len() > 64`. Both intermediate
157 /// `K'` / `K' XOR ipad` / `K' XOR opad` buffers are zeroized
158 /// after the inner/outer SM3 instances absorb them.
159 #[must_use]
160 pub fn new(key: &[u8]) -> Self {
161 let mut k_prime = [0u8; BLOCK_SIZE];
162 if key.len() > BLOCK_SIZE {
163 let mut hashed = hash(key);
164 k_prime[..DIGEST_SIZE].copy_from_slice(&hashed);
165 hashed.zeroize();
166 } else {
167 k_prime[..key.len()].copy_from_slice(key);
168 }
169
170 let mut ipad_key = [0x36u8; BLOCK_SIZE];
171 let mut opad_key = [0x5cu8; BLOCK_SIZE];
172 for i in 0..BLOCK_SIZE {
173 ipad_key[i] ^= k_prime[i];
174 opad_key[i] ^= k_prime[i];
175 }
176
177 // Pre-load the inner SM3 with `K' XOR ipad`. The streaming
178 // update path then absorbs message bytes directly.
179 let mut inner = Sm3::new();
180 inner.update(&ipad_key);
181
182 // Pre-load the outer SM3 with `K' XOR opad`. The finalize
183 // path will then feed the inner-finalized digest.
184 let mut outer = Sm3::new();
185 outer.update(&opad_key);
186
187 // Wipe key-derived buffers. The keyed states inside `inner`
188 // and `outer` carry the same information but are now folded
189 // into the SM3 compression state, not stored in plaintext.
190 k_prime.zeroize();
191 ipad_key.zeroize();
192 opad_key.zeroize();
193
194 Self { inner, outer }
195 }
196
197 /// Absorb message bytes into the inner hash.
198 pub fn update(&mut self, data: &[u8]) {
199 self.inner.update(data);
200 }
201
202 /// Consume the instance and produce the 32-byte MAC tag.
203 ///
204 /// `self.inner` is finalized (by value) into the inner digest and
205 /// `self.outer` is moved out, fed the inner digest, and finalized.
206 /// Both keyed `Sm3` values are scrubbed by [`Sm3`]'s [`Drop`] impl
207 /// (v0.23) when they go out of scope here; the returned tag is the
208 /// public output.
209 #[must_use]
210 pub fn finalize(self) -> [u8; DIGEST_SIZE] {
211 let inner_digest = self.inner.finalize();
212 let mut outer = self.outer;
213 outer.update(&inner_digest);
214 outer.finalize()
215 }
216
217 /// Constant-time verify a candidate tag against the finalized
218 /// HMAC. Returns `true` on match.
219 #[must_use]
220 pub fn verify(self, expected: &[u8; DIGEST_SIZE]) -> bool {
221 let computed = self.finalize();
222 bool::from(computed.ct_eq(expected))
223 }
224}
225
226impl HashTrait for Sm3 {
227 type Output = [u8; DIGEST_SIZE];
228
229 fn new() -> Self {
230 Self::new()
231 }
232
233 fn update(&mut self, data: &[u8]) {
234 Self::update(self, data);
235 }
236
237 fn finalize(self) -> Self::Output {
238 Self::finalize(self)
239 }
240}
241
242impl MacTrait for HmacSm3 {
243 type Output = [u8; DIGEST_SIZE];
244
245 fn new(key: &[u8]) -> Self {
246 Self::new(key)
247 }
248
249 fn update(&mut self, data: &[u8]) {
250 Self::update(self, data);
251 }
252
253 fn finalize(self) -> Self::Output {
254 Self::finalize(self)
255 }
256
257 fn verify(self, expected: &Self::Output) -> bool {
258 Self::verify(self, expected)
259 }
260}
261
262#[cfg(feature = "digest-traits")]
263mod digest_impl {
264 //! `digest::Mac`-compatible impl for [`HmacSm3`] (v0.4 W2; Q4.3).
265 //!
266 //! Behind the `digest-traits` feature flag. The HMAC key is
267 //! variable-length per RFC 2104; we set [`KeySize`] to the SM3
268 //! block size (64 bytes) as the canonical fixed-length entry point
269 //! and override [`KeyInit::new_from_slice`] to accept any length
270 //! (matching the `RustCrypto` `hmac` crate's posture).
271
272 use super::{BLOCK_SIZE, DIGEST_SIZE, HmacSm3};
273 use digest::{
274 FixedOutput, MacMarker, Output, OutputSizeUser, Reset, Update,
275 common::{InvalidLength, Key, KeyInit, KeySizeUser},
276 consts::{U32, U64},
277 };
278
279 const _: () = assert!(BLOCK_SIZE == 64, "U64 KeySize matches SM3 BLOCK_SIZE");
280 const _: () = assert!(DIGEST_SIZE == 32, "U32 OutputSize matches DIGEST_SIZE");
281
282 impl MacMarker for HmacSm3 {}
283
284 impl KeySizeUser for HmacSm3 {
285 type KeySize = U64;
286 }
287
288 impl KeyInit for HmacSm3 {
289 fn new(key: &Key<Self>) -> Self {
290 Self::new(key.as_slice())
291 }
292
293 fn new_from_slice(key: &[u8]) -> Result<Self, InvalidLength> {
294 // Variable-length keys are accepted per RFC 2104 §2.
295 Ok(Self::new(key))
296 }
297 }
298
299 impl OutputSizeUser for HmacSm3 {
300 type OutputSize = U32;
301 }
302
303 impl Update for HmacSm3 {
304 fn update(&mut self, data: &[u8]) {
305 Self::update(self, data);
306 }
307 }
308
309 impl FixedOutput for HmacSm3 {
310 fn finalize_into(self, out: &mut Output<Self>) {
311 let tag: [u8; DIGEST_SIZE] = Self::finalize(self);
312 out.copy_from_slice(&tag);
313 }
314 }
315
316 impl Reset for HmacSm3 {
317 fn reset(&mut self) {
318 // `HmacSm3` doesn't retain the key after construction, so a
319 // post-finalize reset isn't well-defined. The `digest::Mac`
320 // documentation notes `Reset` is rarely useful on MACs;
321 // implementations may panic or no-op. We panic with a clear
322 // message: callers wanting a fresh MAC should call
323 // `HmacSm3::new(key)` directly.
324 panic!(
325 "HmacSm3::reset is not supported; construct a fresh instance via HmacSm3::new(key)"
326 );
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 /// Helper: hex-format a byte slice as a lowercase string.
336 fn to_hex(bytes: &[u8]) -> alloc::string::String {
337 use alloc::string::String;
338 use core::fmt::Write;
339 let mut s = String::with_capacity(bytes.len() * 2);
340 for b in bytes {
341 // Infallible: writing to a `String` only fails on
342 // `write_str` for an exhausted-capacity `String` — which
343 // is unreachable for `String` (always grows).
344 let _ = write!(s, "{b:02x}");
345 }
346 s
347 }
348
349 /// "Test 1"-style HMAC-SM3 KAT. Key: 20 bytes of `0x0b`. Message:
350 /// ASCII "Hi There". Expected MAC cross-validated against
351 /// `gmssl sm3hmac -key '0b0b...0b'` v3.1.1.
352 #[test]
353 fn test1_hi_there() {
354 let key = [0x0bu8; 20];
355 let message = b"Hi There";
356 let mac = hmac_sm3(&key, message);
357 assert_eq!(
358 to_hex(&mac),
359 "51b00d1fb49832bfb01c3ce27848e59f871d9ba938dc563b338ca964755cce70"
360 );
361 }
362
363 /// "Test 2"-style HMAC-SM3 KAT. Short ASCII key + sentence message.
364 /// Cross-validated against `gmssl sm3hmac -key '4a656665'` v3.1.1.
365 #[test]
366 fn test2_jefe_what_do_ya_want() {
367 let key = b"Jefe";
368 let message = b"what do ya want for nothing?";
369 let mac = hmac_sm3(key, message);
370 assert_eq!(
371 to_hex(&mac),
372 "2e87f1d16862e6d964b50a5200bf2b10b764faa9680a296a2405f24bec39f882"
373 );
374 }
375
376 /// "Test 6"-style HMAC-SM3 KAT exercising the **hash-first** path
377 /// (key longer than the 64-byte block size). Cross-validated by
378 /// computing `gmssl sm3` over the 131-byte key, then
379 /// `gmssl sm3hmac -key <sm3_of_key>` over the message — i.e.
380 /// reducing through RFC 2104's hash-first equivalence (gmssl 3.1.1's
381 /// `sm3hmac` CLI rejects keys > 32 bytes, so we exercise the
382 /// equivalence by hand).
383 #[test]
384 fn test6_long_key_hash_first() {
385 let key = [0xaau8; 131];
386 let message = b"Test Using Larger Than Block-Size Key - Hash Key First";
387 let mac = hmac_sm3(&key, message);
388 assert_eq!(
389 to_hex(&mac),
390 "b4fd844e13342002f0b2e0690ea7741f1497d993a70494cea601e657bedf67a0"
391 );
392 }
393
394 /// Empty key + empty message — exercises the zero-pad path.
395 /// Cross-validated against `gmssl sm3hmac -key ''` v3.1.1.
396 #[test]
397 fn empty_key_empty_message() {
398 let mac = hmac_sm3(&[], &[]);
399 assert_eq!(
400 to_hex(&mac),
401 "0d23f72ba15e9c189a879aefc70996b06091de6e64d31b7a84004356dd915261"
402 );
403 }
404
405 /// Key longer than 64 bytes triggers the hash-first path.
406 /// Verify the result differs from key=Sm3(key)|pad's MAC over the
407 /// same message — i.e. the hash-first path is actually exercised.
408 #[test]
409 fn long_key_takes_hash_first_path() {
410 let long_key = [0xaau8; 131]; // > 64 bytes
411 let message = b"test message";
412 let mac_long = hmac_sm3(&long_key, message);
413
414 // Independently compute: pre-hash the key, then HMAC with the
415 // pre-hashed key (which is now ≤ 32 bytes ≤ 64). If the
416 // hash-first path is correctly implemented, the two outputs
417 // must agree.
418 let prehashed = hash(&long_key);
419 let mac_short = hmac_sm3(&prehashed, message);
420
421 assert_eq!(
422 mac_long, mac_short,
423 "hash-first path on long key must match HMAC over pre-hashed key"
424 );
425 }
426
427 /// Key exactly the block size (64 bytes) takes the no-hash path.
428 /// Boundary condition test — the spec says `len(K) ≤ B` uses the
429 /// pad path (not the hash path).
430 #[test]
431 fn key_exactly_block_size() {
432 let key = [0xccu8; BLOCK_SIZE];
433 let mac = hmac_sm3(&key, b"x");
434 // Verify it's a 32-byte output (i.e. produced output, didn't panic).
435 assert_eq!(mac.len(), DIGEST_SIZE);
436 }
437
438 /// Different messages under the same key must produce different MACs.
439 #[test]
440 fn different_messages_different_macs() {
441 let key = b"key123";
442 let mac_a = hmac_sm3(key, b"message a");
443 let mac_b = hmac_sm3(key, b"message b");
444 assert_ne!(mac_a, mac_b);
445 }
446
447 /// Different keys over the same message must produce different MACs.
448 #[test]
449 fn different_keys_different_macs() {
450 let mac_a = hmac_sm3(b"key1", b"the message");
451 let mac_b = hmac_sm3(b"key2", b"the message");
452 assert_ne!(mac_a, mac_b);
453 }
454
455 // ---------- v0.3 W5: streaming HmacSm3 ----------
456
457 /// Streaming `HmacSm3::new`/`update`/`finalize` produces the same
458 /// tag as single-shot `hmac_sm3` on KAT vector "Hi There".
459 #[test]
460 fn streaming_test1_matches_oneshot() {
461 let key = [0x0bu8; 20];
462 let message = b"Hi There";
463 let mut mac = HmacSm3::new(&key);
464 mac.update(message);
465 let tag = mac.finalize();
466 assert_eq!(
467 to_hex(&tag),
468 "51b00d1fb49832bfb01c3ce27848e59f871d9ba938dc563b338ca964755cce70"
469 );
470 }
471
472 /// Chunking-equivalence on KAT 2: a streaming `HmacSm3` fed any
473 /// partition of the message produces the same tag as the
474 /// single-shot path.
475 #[test]
476 fn streaming_chunking_equivalence_test2() {
477 let key = b"Jefe";
478 let message: &[u8] = b"what do ya want for nothing?";
479 let oneshot = hmac_sm3(key, message);
480 for chunk_size in [1usize, 3, 7, 14, message.len()] {
481 let mut mac = HmacSm3::new(key);
482 for chunk in message.chunks(chunk_size) {
483 mac.update(chunk);
484 }
485 let streamed = mac.finalize();
486 assert_eq!(streamed, oneshot, "chunk_size={chunk_size}");
487 }
488 }
489
490 /// Long-key path round-trips through streaming.
491 #[test]
492 fn streaming_long_key() {
493 let key = [0xaau8; 131];
494 let message: &[u8] = b"Test Using Larger Than Block-Size Key - Hash Key First";
495 let mut mac = HmacSm3::new(&key);
496 for chunk in message.chunks(7) {
497 mac.update(chunk);
498 }
499 let tag = mac.finalize();
500 assert_eq!(
501 to_hex(&tag),
502 "b4fd844e13342002f0b2e0690ea7741f1497d993a70494cea601e657bedf67a0"
503 );
504 }
505
506 /// `verify` accepts the correct tag.
507 #[test]
508 fn verify_accepts_correct_tag() {
509 let key = b"vkey";
510 let message = b"verify me";
511 let expected = hmac_sm3(key, message);
512 let mut mac = HmacSm3::new(key);
513 mac.update(message);
514 assert!(mac.verify(&expected));
515 }
516
517 /// `verify` rejects a wrong tag.
518 #[test]
519 fn verify_rejects_wrong_tag() {
520 let key = b"vkey";
521 let message = b"verify me";
522 let mut bogus = hmac_sm3(key, message);
523 bogus[0] ^= 0x01;
524 let mut mac = HmacSm3::new(key);
525 mac.update(message);
526 assert!(!mac.verify(&bogus));
527 }
528
529 /// Empty key + empty message via streaming.
530 #[test]
531 fn streaming_empty_key_empty_message() {
532 let mac = HmacSm3::new(&[]);
533 let tag = mac.finalize();
534 assert_eq!(
535 to_hex(&tag),
536 "0d23f72ba15e9c189a879aefc70996b06091de6e64d31b7a84004356dd915261"
537 );
538 }
539}