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 pre-computed `outer` keyed-state (`SM3` after absorbing
133/// `K' XOR opad`) holds key-derived material. [`HmacSm3::finalize`]
134/// and [`HmacSm3::verify`] consume `self` and zeroize it before
135/// returning. If the caller drops the `HmacSm3` without calling
136/// either method, the [`Drop`] impl wipes the state.
137pub struct HmacSm3 {
138 /// Inner-hash state, currently absorbing `K' XOR ipad || message-so-far`.
139 inner: Sm3,
140 /// Outer-hash state, currently holding the absorbed `K' XOR opad`
141 /// (will be finalized with the inner digest at `finalize` time).
142 outer: Sm3,
143}
144
145impl HmacSm3 {
146 /// Construct a new keyed HMAC-SM3 instance.
147 ///
148 /// `key` may be any length; the standard RFC 2104 hash-first
149 /// reduction applies for `key.len() > 64`. Both intermediate
150 /// `K'` / `K' XOR ipad` / `K' XOR opad` buffers are zeroized
151 /// after the inner/outer SM3 instances absorb them.
152 #[must_use]
153 pub fn new(key: &[u8]) -> Self {
154 let mut k_prime = [0u8; BLOCK_SIZE];
155 if key.len() > BLOCK_SIZE {
156 let mut hashed = hash(key);
157 k_prime[..DIGEST_SIZE].copy_from_slice(&hashed);
158 hashed.zeroize();
159 } else {
160 k_prime[..key.len()].copy_from_slice(key);
161 }
162
163 let mut ipad_key = [0x36u8; BLOCK_SIZE];
164 let mut opad_key = [0x5cu8; BLOCK_SIZE];
165 for i in 0..BLOCK_SIZE {
166 ipad_key[i] ^= k_prime[i];
167 opad_key[i] ^= k_prime[i];
168 }
169
170 // Pre-load the inner SM3 with `K' XOR ipad`. The streaming
171 // update path then absorbs message bytes directly.
172 let mut inner = Sm3::new();
173 inner.update(&ipad_key);
174
175 // Pre-load the outer SM3 with `K' XOR opad`. The finalize
176 // path will then feed the inner-finalized digest.
177 let mut outer = Sm3::new();
178 outer.update(&opad_key);
179
180 // Wipe key-derived buffers. The keyed states inside `inner`
181 // and `outer` carry the same information but are now folded
182 // into the SM3 compression state, not stored in plaintext.
183 k_prime.zeroize();
184 ipad_key.zeroize();
185 opad_key.zeroize();
186
187 Self { inner, outer }
188 }
189
190 /// Absorb message bytes into the inner hash.
191 pub fn update(&mut self, data: &[u8]) {
192 self.inner.update(data);
193 }
194
195 /// Consume the instance and produce the 32-byte MAC tag.
196 ///
197 /// The `outer` keyed-state and the `inner` final state are both
198 /// dropped after consuming `self`; `Sm3`'s `Drop` impl is the
199 /// one we rely on here. To be defensive against a future change
200 /// where `Sm3` is no longer `ZeroizeOnDrop`, both fields are
201 /// explicitly wiped via `clone-then-drop` would be safer — but
202 /// `Sm3` does not currently implement `Zeroize` directly. The
203 /// state is consumed by `outer.finalize()` which produces the
204 /// public output and discards the rest.
205 #[must_use]
206 pub fn finalize(self) -> [u8; DIGEST_SIZE] {
207 let inner_digest = self.inner.finalize();
208 let mut outer = self.outer;
209 outer.update(&inner_digest);
210 outer.finalize()
211 }
212
213 /// Constant-time verify a candidate tag against the finalized
214 /// HMAC. Returns `true` on match.
215 #[must_use]
216 pub fn verify(self, expected: &[u8; DIGEST_SIZE]) -> bool {
217 let computed = self.finalize();
218 bool::from(computed.ct_eq(expected))
219 }
220}
221
222impl HashTrait for Sm3 {
223 type Output = [u8; DIGEST_SIZE];
224
225 fn new() -> Self {
226 Self::new()
227 }
228
229 fn update(&mut self, data: &[u8]) {
230 Self::update(self, data);
231 }
232
233 fn finalize(self) -> Self::Output {
234 Self::finalize(self)
235 }
236}
237
238impl MacTrait for HmacSm3 {
239 type Output = [u8; DIGEST_SIZE];
240
241 fn new(key: &[u8]) -> Self {
242 Self::new(key)
243 }
244
245 fn update(&mut self, data: &[u8]) {
246 Self::update(self, data);
247 }
248
249 fn finalize(self) -> Self::Output {
250 Self::finalize(self)
251 }
252
253 fn verify(self, expected: &Self::Output) -> bool {
254 Self::verify(self, expected)
255 }
256}
257
258#[cfg(feature = "digest-traits")]
259mod digest_impl {
260 //! `digest::Mac`-compatible impl for [`HmacSm3`] (v0.4 W2; Q4.3).
261 //!
262 //! Behind the `digest-traits` feature flag. The HMAC key is
263 //! variable-length per RFC 2104; we set [`KeySize`] to the SM3
264 //! block size (64 bytes) as the canonical fixed-length entry point
265 //! and override [`KeyInit::new_from_slice`] to accept any length
266 //! (matching the `RustCrypto` `hmac` crate's posture).
267
268 use super::{BLOCK_SIZE, DIGEST_SIZE, HmacSm3};
269 use digest::{
270 FixedOutput, MacMarker, Output, OutputSizeUser, Reset, Update,
271 consts::{U32, U64},
272 crypto_common::{InvalidLength, Key, KeyInit, KeySizeUser},
273 };
274
275 const _: () = assert!(BLOCK_SIZE == 64, "U64 KeySize matches SM3 BLOCK_SIZE");
276 const _: () = assert!(DIGEST_SIZE == 32, "U32 OutputSize matches DIGEST_SIZE");
277
278 impl MacMarker for HmacSm3 {}
279
280 impl KeySizeUser for HmacSm3 {
281 type KeySize = U64;
282 }
283
284 impl KeyInit for HmacSm3 {
285 fn new(key: &Key<Self>) -> Self {
286 Self::new(key.as_slice())
287 }
288
289 fn new_from_slice(key: &[u8]) -> Result<Self, InvalidLength> {
290 // Variable-length keys are accepted per RFC 2104 §2.
291 Ok(Self::new(key))
292 }
293 }
294
295 impl OutputSizeUser for HmacSm3 {
296 type OutputSize = U32;
297 }
298
299 impl Update for HmacSm3 {
300 fn update(&mut self, data: &[u8]) {
301 Self::update(self, data);
302 }
303 }
304
305 impl FixedOutput for HmacSm3 {
306 fn finalize_into(self, out: &mut Output<Self>) {
307 let tag: [u8; DIGEST_SIZE] = Self::finalize(self);
308 out.copy_from_slice(&tag);
309 }
310 }
311
312 impl Reset for HmacSm3 {
313 fn reset(&mut self) {
314 // `HmacSm3` doesn't retain the key after construction, so a
315 // post-finalize reset isn't well-defined. The `digest::Mac`
316 // documentation notes `Reset` is rarely useful on MACs;
317 // implementations may panic or no-op. We panic with a clear
318 // message: callers wanting a fresh MAC should call
319 // `HmacSm3::new(key)` directly.
320 panic!(
321 "HmacSm3::reset is not supported; construct a fresh instance via HmacSm3::new(key)"
322 );
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 /// Helper: hex-format a byte slice as a lowercase string.
332 fn to_hex(bytes: &[u8]) -> alloc::string::String {
333 use alloc::string::String;
334 use core::fmt::Write;
335 let mut s = String::with_capacity(bytes.len() * 2);
336 for b in bytes {
337 // Infallible: writing to a `String` only fails on
338 // `write_str` for an exhausted-capacity `String` — which
339 // is unreachable for `String` (always grows).
340 let _ = write!(s, "{b:02x}");
341 }
342 s
343 }
344
345 /// "Test 1"-style HMAC-SM3 KAT. Key: 20 bytes of `0x0b`. Message:
346 /// ASCII "Hi There". Expected MAC cross-validated against
347 /// `gmssl sm3hmac -key '0b0b...0b'` v3.1.1.
348 #[test]
349 fn test1_hi_there() {
350 let key = [0x0bu8; 20];
351 let message = b"Hi There";
352 let mac = hmac_sm3(&key, message);
353 assert_eq!(
354 to_hex(&mac),
355 "51b00d1fb49832bfb01c3ce27848e59f871d9ba938dc563b338ca964755cce70"
356 );
357 }
358
359 /// "Test 2"-style HMAC-SM3 KAT. Short ASCII key + sentence message.
360 /// Cross-validated against `gmssl sm3hmac -key '4a656665'` v3.1.1.
361 #[test]
362 fn test2_jefe_what_do_ya_want() {
363 let key = b"Jefe";
364 let message = b"what do ya want for nothing?";
365 let mac = hmac_sm3(key, message);
366 assert_eq!(
367 to_hex(&mac),
368 "2e87f1d16862e6d964b50a5200bf2b10b764faa9680a296a2405f24bec39f882"
369 );
370 }
371
372 /// "Test 6"-style HMAC-SM3 KAT exercising the **hash-first** path
373 /// (key longer than the 64-byte block size). Cross-validated by
374 /// computing `gmssl sm3` over the 131-byte key, then
375 /// `gmssl sm3hmac -key <sm3_of_key>` over the message — i.e.
376 /// reducing through RFC 2104's hash-first equivalence (gmssl 3.1.1's
377 /// `sm3hmac` CLI rejects keys > 32 bytes, so we exercise the
378 /// equivalence by hand).
379 #[test]
380 fn test6_long_key_hash_first() {
381 let key = [0xaau8; 131];
382 let message = b"Test Using Larger Than Block-Size Key - Hash Key First";
383 let mac = hmac_sm3(&key, message);
384 assert_eq!(
385 to_hex(&mac),
386 "b4fd844e13342002f0b2e0690ea7741f1497d993a70494cea601e657bedf67a0"
387 );
388 }
389
390 /// Empty key + empty message — exercises the zero-pad path.
391 /// Cross-validated against `gmssl sm3hmac -key ''` v3.1.1.
392 #[test]
393 fn empty_key_empty_message() {
394 let mac = hmac_sm3(&[], &[]);
395 assert_eq!(
396 to_hex(&mac),
397 "0d23f72ba15e9c189a879aefc70996b06091de6e64d31b7a84004356dd915261"
398 );
399 }
400
401 /// Key longer than 64 bytes triggers the hash-first path.
402 /// Verify the result differs from key=Sm3(key)|pad's MAC over the
403 /// same message — i.e. the hash-first path is actually exercised.
404 #[test]
405 fn long_key_takes_hash_first_path() {
406 let long_key = [0xaau8; 131]; // > 64 bytes
407 let message = b"test message";
408 let mac_long = hmac_sm3(&long_key, message);
409
410 // Independently compute: pre-hash the key, then HMAC with the
411 // pre-hashed key (which is now ≤ 32 bytes ≤ 64). If the
412 // hash-first path is correctly implemented, the two outputs
413 // must agree.
414 let prehashed = hash(&long_key);
415 let mac_short = hmac_sm3(&prehashed, message);
416
417 assert_eq!(
418 mac_long, mac_short,
419 "hash-first path on long key must match HMAC over pre-hashed key"
420 );
421 }
422
423 /// Key exactly the block size (64 bytes) takes the no-hash path.
424 /// Boundary condition test — the spec says `len(K) ≤ B` uses the
425 /// pad path (not the hash path).
426 #[test]
427 fn key_exactly_block_size() {
428 let key = [0xccu8; BLOCK_SIZE];
429 let mac = hmac_sm3(&key, b"x");
430 // Verify it's a 32-byte output (i.e. produced output, didn't panic).
431 assert_eq!(mac.len(), DIGEST_SIZE);
432 }
433
434 /// Different messages under the same key must produce different MACs.
435 #[test]
436 fn different_messages_different_macs() {
437 let key = b"key123";
438 let mac_a = hmac_sm3(key, b"message a");
439 let mac_b = hmac_sm3(key, b"message b");
440 assert_ne!(mac_a, mac_b);
441 }
442
443 /// Different keys over the same message must produce different MACs.
444 #[test]
445 fn different_keys_different_macs() {
446 let mac_a = hmac_sm3(b"key1", b"the message");
447 let mac_b = hmac_sm3(b"key2", b"the message");
448 assert_ne!(mac_a, mac_b);
449 }
450
451 // ---------- v0.3 W5: streaming HmacSm3 ----------
452
453 /// Streaming `HmacSm3::new`/`update`/`finalize` produces the same
454 /// tag as single-shot `hmac_sm3` on KAT vector "Hi There".
455 #[test]
456 fn streaming_test1_matches_oneshot() {
457 let key = [0x0bu8; 20];
458 let message = b"Hi There";
459 let mut mac = HmacSm3::new(&key);
460 mac.update(message);
461 let tag = mac.finalize();
462 assert_eq!(
463 to_hex(&tag),
464 "51b00d1fb49832bfb01c3ce27848e59f871d9ba938dc563b338ca964755cce70"
465 );
466 }
467
468 /// Chunking-equivalence on KAT 2: a streaming `HmacSm3` fed any
469 /// partition of the message produces the same tag as the
470 /// single-shot path.
471 #[test]
472 fn streaming_chunking_equivalence_test2() {
473 let key = b"Jefe";
474 let message: &[u8] = b"what do ya want for nothing?";
475 let oneshot = hmac_sm3(key, message);
476 for chunk_size in [1usize, 3, 7, 14, message.len()] {
477 let mut mac = HmacSm3::new(key);
478 for chunk in message.chunks(chunk_size) {
479 mac.update(chunk);
480 }
481 let streamed = mac.finalize();
482 assert_eq!(streamed, oneshot, "chunk_size={chunk_size}");
483 }
484 }
485
486 /// Long-key path round-trips through streaming.
487 #[test]
488 fn streaming_long_key() {
489 let key = [0xaau8; 131];
490 let message: &[u8] = b"Test Using Larger Than Block-Size Key - Hash Key First";
491 let mut mac = HmacSm3::new(&key);
492 for chunk in message.chunks(7) {
493 mac.update(chunk);
494 }
495 let tag = mac.finalize();
496 assert_eq!(
497 to_hex(&tag),
498 "b4fd844e13342002f0b2e0690ea7741f1497d993a70494cea601e657bedf67a0"
499 );
500 }
501
502 /// `verify` accepts the correct tag.
503 #[test]
504 fn verify_accepts_correct_tag() {
505 let key = b"vkey";
506 let message = b"verify me";
507 let expected = hmac_sm3(key, message);
508 let mut mac = HmacSm3::new(key);
509 mac.update(message);
510 assert!(mac.verify(&expected));
511 }
512
513 /// `verify` rejects a wrong tag.
514 #[test]
515 fn verify_rejects_wrong_tag() {
516 let key = b"vkey";
517 let message = b"verify me";
518 let mut bogus = hmac_sm3(key, message);
519 bogus[0] ^= 0x01;
520 let mut mac = HmacSm3::new(key);
521 mac.update(message);
522 assert!(!mac.verify(&bogus));
523 }
524
525 /// Empty key + empty message via streaming.
526 #[test]
527 fn streaming_empty_key_empty_message() {
528 let mac = HmacSm3::new(&[]);
529 let tag = mac.finalize();
530 assert_eq!(
531 to_hex(&tag),
532 "0d23f72ba15e9c189a879aefc70996b06091de6e64d31b7a84004356dd915261"
533 );
534 }
535}