Skip to main content

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}