Skip to main content

native_ossl/
mac.rs

1//! `MacAlg` — `EVP_MAC` algorithm descriptor.
2//!
3//! Phase 3.3 delivers `MacAlg`; Phase 4.3 extends this module with
4//! `MacCtx`, `HmacCtx`, and `CmacCtx`.
5
6use crate::error::ErrorStack;
7use native_ossl_sys as sys;
8use std::ffi::CStr;
9use std::sync::Arc;
10
11// ── MacAlg — algorithm descriptor ─────────────────────────────────────────────
12
13/// An OpenSSL MAC algorithm descriptor (`EVP_MAC*`).
14///
15/// Fetched once and reused.  Implements `Clone` via `EVP_MAC_up_ref`.
16pub struct MacAlg {
17    ptr: *mut sys::EVP_MAC,
18    /// Keeps the library context alive while this descriptor is in use.
19    lib_ctx: Option<Arc<crate::lib_ctx::LibCtx>>,
20}
21
22impl MacAlg {
23    /// Fetch a MAC algorithm from the global default library context.
24    ///
25    /// # Errors
26    ///
27    /// Returns `Err` if the algorithm is not available.
28    pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack> {
29        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
30        let ptr = unsafe { sys::EVP_MAC_fetch(std::ptr::null_mut(), name.as_ptr(), props_ptr) };
31        if ptr.is_null() {
32            return Err(ErrorStack::drain());
33        }
34        Ok(MacAlg { ptr, lib_ctx: None })
35    }
36
37    /// Fetch a MAC algorithm from an explicit library context.
38    ///
39    /// # Errors
40    pub fn fetch_in(
41        ctx: &Arc<crate::lib_ctx::LibCtx>,
42        name: &CStr,
43        props: Option<&CStr>,
44    ) -> Result<Self, ErrorStack> {
45        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
46        let ptr = unsafe { sys::EVP_MAC_fetch(ctx.as_ptr(), name.as_ptr(), props_ptr) };
47        if ptr.is_null() {
48            return Err(ErrorStack::drain());
49        }
50        Ok(MacAlg {
51            ptr,
52            lib_ctx: Some(Arc::clone(ctx)),
53        })
54    }
55
56    /// Return the raw `EVP_MAC*` pointer.  Valid for the lifetime of `self`.
57    #[must_use]
58    pub fn as_ptr(&self) -> *const sys::EVP_MAC {
59        self.ptr
60    }
61
62    /// Return the canonical name of this MAC algorithm (e.g. `"HMAC"`, `"CMAC"`).
63    ///
64    /// The returned reference is valid for the lifetime of `self`.
65    #[must_use]
66    pub fn name(&self) -> &CStr {
67        // SAFETY: EVP_MAC_get0_name returns a pointer into the EVP_MAC object's
68        // internal storage.  It is valid for the lifetime of the EVP_MAC*, which
69        // is at least as long as &self.
70        unsafe { CStr::from_ptr(sys::EVP_MAC_get0_name(self.ptr)) }
71    }
72}
73
74impl Clone for MacAlg {
75    fn clone(&self) -> Self {
76        unsafe { sys::EVP_MAC_up_ref(self.ptr) };
77        MacAlg {
78            ptr: self.ptr,
79            lib_ctx: self.lib_ctx.clone(),
80        }
81    }
82}
83
84impl Drop for MacAlg {
85    fn drop(&mut self) {
86        unsafe { sys::EVP_MAC_free(self.ptr) };
87    }
88}
89
90// SAFETY: `EVP_MAC*` is reference-counted and immutable after fetch.
91unsafe impl Send for MacAlg {}
92unsafe impl Sync for MacAlg {}
93
94// ── MacCtx — stateful context (Phase 4.3) ────────────────────────────────────
95
96/// Stateful MAC context (`EVP_MAC_CTX*`).
97///
98/// `!Clone` — use `fork()` to duplicate mid-stream state.
99/// All stateful operations require `&mut self`.
100pub struct MacCtx {
101    ptr: *mut sys::EVP_MAC_CTX,
102}
103
104impl MacCtx {
105    /// Create a new (uninitialised) MAC context from an algorithm descriptor.
106    ///
107    /// Call [`MacCtx::init`] with a key before feeding data.
108    ///
109    /// # Errors
110    pub fn new(alg: &MacAlg) -> Result<Self, ErrorStack> {
111        let ptr = unsafe { sys::EVP_MAC_CTX_new(alg.ptr) };
112        if ptr.is_null() {
113            return Err(ErrorStack::drain());
114        }
115        Ok(MacCtx { ptr })
116    }
117
118    /// Initialise (or re-initialise) with a key and optional parameters.
119    ///
120    /// **`key` is always copied** into the MAC context by `EVP_MAC_init`.
121    /// HMAC additionally pads the key into IPAD/OPAD.  The copy is unavoidable.
122    ///
123    /// # Errors
124    pub fn init(
125        &mut self,
126        key: &[u8],
127        params: Option<&crate::params::Params<'_>>,
128    ) -> Result<(), ErrorStack> {
129        let params_ptr = params.map_or(crate::params::null_params(), crate::params::Params::as_ptr);
130        crate::ossl_call!(sys::EVP_MAC_init(
131            self.ptr,
132            key.as_ptr(),
133            key.len(),
134            params_ptr
135        ))
136    }
137
138    /// Feed data into the ongoing MAC computation.
139    ///
140    /// # Errors
141    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
142        crate::ossl_call!(sys::EVP_MAC_update(self.ptr, data.as_ptr(), data.len()))
143    }
144
145    /// Finalise the MAC and write into `out`.
146    ///
147    /// `out` must be at least `self.mac_size()` bytes.
148    /// Returns the number of bytes written.
149    ///
150    /// # Errors
151    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
152        let mut outl: usize = 0;
153        crate::ossl_call!(sys::EVP_MAC_final(
154            self.ptr,
155            out.as_mut_ptr(),
156            std::ptr::addr_of_mut!(outl),
157            out.len()
158        ))?;
159        Ok(outl)
160    }
161
162    /// Finalise with variable-length XOF output (KMAC-128, KMAC-256).
163    ///
164    /// # Errors
165    pub fn finish_xof(&mut self, out: &mut [u8]) -> Result<(), ErrorStack> {
166        crate::ossl_call!(sys::EVP_MAC_finalXOF(self.ptr, out.as_mut_ptr(), out.len()))
167    }
168
169    /// Expected MAC output length in bytes.  Available after `init`.
170    #[must_use]
171    pub fn mac_size(&self) -> usize {
172        unsafe { sys::EVP_MAC_CTX_get_mac_size(self.ptr) }
173    }
174
175    /// Block size of the underlying MAC algorithm in bytes.
176    ///
177    /// For HMAC, this is the block size of the underlying hash (e.g. 64 for
178    /// SHA-256, 128 for SHA-384/SHA-512). Required by protocols that derive
179    /// IPAD/OPAD or align buffers to the hash block size.
180    ///
181    /// Returns `0` before [`MacCtx::init`] is called.
182    #[must_use]
183    pub fn block_size(&self) -> usize {
184        // SAFETY:
185        // - self.ptr is non-null (constructor invariant)
186        // - EVP_MAC_CTX_get_block_size reads algorithm metadata; no mutation
187        // - &self ensures no concurrent mutable access
188        unsafe { sys::EVP_MAC_CTX_get_block_size(self.ptr) }
189    }
190
191    /// Return the algorithm descriptor associated with this context.
192    ///
193    /// Bumps the algorithm's reference count so the returned [`MacAlg`] is
194    /// independently owned and may outlive the context.
195    ///
196    /// Returns `None` if no algorithm is associated (e.g. context was just
197    /// allocated but not yet bound to an algorithm).
198    #[must_use]
199    pub fn alg(&self) -> Option<MacAlg> {
200        // SAFETY:
201        // - self.ptr is non-null (constructor invariant)
202        // - EVP_MAC_CTX_get0_mac returns a borrowed EVP_MAC* valid for the
203        //   lifetime of the context; we immediately bump its refcount so the
204        //   returned MacAlg owns an independent reference
205        // - &self ensures the context is not mutated while we read it
206        let ptr = unsafe { sys::EVP_MAC_CTX_get0_mac(self.ptr) };
207        if ptr.is_null() {
208            return None;
209        }
210        // SAFETY: ptr is non-null (checked above); EVP_MAC_up_ref only
211        // increments an atomic refcount — it does not mutate algorithm data
212        unsafe { sys::EVP_MAC_up_ref(ptr) };
213        Some(MacAlg { ptr, lib_ctx: None })
214    }
215
216    /// Fork the current mid-stream state into a new context (`EVP_MAC_CTX_dup`).
217    ///
218    /// Named `fork` (not `clone`) to signal this is a potentially expensive deep copy.
219    ///
220    /// # Errors
221    pub fn fork(&self) -> Result<MacCtx, ErrorStack> {
222        let ptr = unsafe { sys::EVP_MAC_CTX_dup(self.ptr) };
223        if ptr.is_null() {
224            return Err(ErrorStack::drain());
225        }
226        Ok(MacCtx { ptr })
227    }
228}
229
230impl Drop for MacCtx {
231    fn drop(&mut self) {
232        unsafe { sys::EVP_MAC_CTX_free(self.ptr) };
233    }
234}
235
236unsafe impl Send for MacCtx {}
237
238// ── HmacCtx — typed HMAC wrapper ─────────────────────────────────────────────
239
240/// HMAC context bound to a specific digest algorithm.
241pub struct HmacCtx(MacCtx);
242
243impl HmacCtx {
244    /// Create an HMAC context.
245    ///
246    /// The digest name is passed to `EVP_MAC_init` as an `OSSL_PARAM`.
247    ///
248    /// # Errors
249    pub fn new(digest: &crate::digest::DigestAlg, key: &[u8]) -> Result<Self, ErrorStack> {
250        // HMAC algorithm name — must pass the digest name as a parameter.
251        let alg = MacAlg::fetch(c"HMAC", None)?;
252        let mut ctx = MacCtx::new(&alg)?;
253
254        // Build params: { digest = "<name>" }
255        // We use push_utf8_string (copies) since the digest NID→name is dynamic.
256        let nid = digest.nid();
257        let name_cstr: std::ffi::CString = {
258            let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
259            if name_ptr.is_null() {
260                return Err(crate::error::ErrorStack::drain());
261            }
262            unsafe { std::ffi::CStr::from_ptr(name_ptr) }.to_owned()
263        };
264
265        let params = crate::params::ParamBuilder::new()?
266            .push_utf8_string(c"digest", &name_cstr)?
267            .build()?;
268
269        ctx.init(key, Some(&params))?;
270        Ok(HmacCtx(ctx))
271    }
272
273    /// Feed data into the HMAC computation.
274    ///
275    /// # Errors
276    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
277        self.0.update(data)
278    }
279
280    /// Finalise the HMAC.
281    ///
282    /// # Errors
283    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
284        self.0.finish(out)
285    }
286
287    /// Finalise and return the HMAC in a freshly allocated `Vec<u8>`.
288    ///
289    /// # Errors
290    pub fn finish_to_vec(&mut self) -> Result<Vec<u8>, ErrorStack> {
291        let size = self.0.mac_size();
292        let mut out = vec![0u8; size];
293        let n = self.0.finish(&mut out)?;
294        out.truncate(n);
295        Ok(out)
296    }
297
298    /// Expected MAC size in bytes.
299    #[must_use]
300    pub fn mac_size(&self) -> usize {
301        self.0.mac_size()
302    }
303
304    /// One-shot HMAC computation.
305    ///
306    /// # Errors
307    pub fn oneshot(
308        digest: &crate::digest::DigestAlg,
309        key: &[u8],
310        data: &[u8],
311        out: &mut [u8],
312    ) -> Result<usize, ErrorStack> {
313        let mut ctx = HmacCtx::new(digest, key)?;
314        ctx.update(data)?;
315        ctx.finish(out)
316    }
317}
318
319// ── CmacCtx — typed CMAC wrapper ─────────────────────────────────────────────
320
321/// CMAC context bound to a specific block cipher.
322pub struct CmacCtx(MacCtx);
323
324impl CmacCtx {
325    /// Create a CMAC context.
326    ///
327    /// # Errors
328    pub fn new(cipher: &crate::cipher::CipherAlg, key: &[u8]) -> Result<Self, ErrorStack> {
329        let alg = MacAlg::fetch(c"CMAC", None)?;
330        let mut ctx = MacCtx::new(&alg)?;
331
332        // For CMAC we need the cipher name. Use OBJ_nid2sn to get it.
333        // Actually, CMAC needs the cipher name as a string parameter.
334        // Use a simple string: AES-256-CBC etc. (caller chose the cipher).
335        // We'll use push_octet_slice since we don't have a name accessor yet.
336        // For now: use a known cipher name based on key_len and block_size.
337        // Better: expose CipherAlg::name() when we have it.
338        // Workaround: use OSSL_CIPHER_PARAM_NAME if available.
339        // For the test, just hardcode based on the cipher's key_len.
340        let key_len = cipher.key_len();
341        // Map key length to the corresponding AES-CBC cipher name.
342        // Return Err for any length that does not correspond to a known AES variant
343        // rather than silently using the wrong algorithm.
344        // TODO: replace with CipherAlg::name() via EVP_CIPHER_get0_name once available.
345        let cipher_name: &std::ffi::CStr = match key_len {
346            16 => c"AES-128-CBC",
347            24 => c"AES-192-CBC",
348            32 => c"AES-256-CBC",
349            _ => return Err(ErrorStack::drain()),
350        };
351
352        let params = crate::params::ParamBuilder::new()?
353            .push_utf8_string(c"cipher", cipher_name)?
354            .build()?;
355
356        ctx.init(key, Some(&params))?;
357        Ok(CmacCtx(ctx))
358    }
359
360    /// Feed data into the CMAC computation.
361    ///
362    /// # Errors
363    pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
364        self.0.update(data)
365    }
366
367    /// Finalise the CMAC.
368    ///
369    /// # Errors
370    pub fn finish(&mut self, out: &mut [u8]) -> Result<usize, ErrorStack> {
371        self.0.finish(out)
372    }
373}
374
375// ── Tests ─────────────────────────────────────────────────────────────────────
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use crate::digest::DigestAlg;
381
382    #[test]
383    fn fetch_hmac_succeeds() {
384        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
385        drop(alg);
386    }
387
388    #[test]
389    fn fetch_nonexistent_fails() {
390        assert!(MacAlg::fetch(c"NONEXISTENT_MAC_XYZ", None).is_err());
391    }
392
393    #[test]
394    fn clone_then_drop_both() {
395        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
396        let alg2 = alg.clone();
397        drop(alg);
398        drop(alg2);
399    }
400
401    /// HMAC-SHA256 RFC 4231 test vector #1.
402    /// Key  = 0b0b...0b (20 bytes)
403    /// Data = "Hi There"
404    /// MAC  = b0344c61...
405    #[test]
406    fn hmac_sha256_rfc4231_tv1() {
407        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
408        let key = [0x0b_u8; 20];
409        let data = b"Hi There";
410
411        let mut ctx = HmacCtx::new(&digest, &key).unwrap();
412        ctx.update(data).unwrap();
413        let mac = ctx.finish_to_vec().unwrap();
414
415        assert_eq!(
416            hex::encode(&mac),
417            "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
418        );
419    }
420
421    /// Oneshot HMAC — same vector.
422    #[test]
423    fn hmac_sha256_oneshot() {
424        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
425        let key = [0x0b_u8; 20];
426        let mut out = [0u8; 32];
427        let n = HmacCtx::oneshot(&digest, &key, b"Hi There", &mut out).unwrap();
428        assert_eq!(n, 32);
429        assert_eq!(
430            hex::encode(out),
431            "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
432        );
433    }
434
435    #[test]
436    fn mac_ctx_block_size_before_init_is_zero() {
437        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
438        let ctx = MacCtx::new(&alg).unwrap();
439        // EVP_MAC_CTX_get_block_size returns 0 when the sub-algorithm (digest)
440        // has not been set yet via init.
441        assert_eq!(ctx.block_size(), 0);
442    }
443
444    #[test]
445    fn mac_ctx_block_size_hmac_sha256() {
446        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
447        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
448        let mut ctx = MacCtx::new(&alg).unwrap();
449        let nid = digest.nid();
450        let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
451        let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
452        let params = crate::params::ParamBuilder::new()
453            .unwrap()
454            .push_utf8_string(c"digest", name)
455            .unwrap()
456            .build()
457            .unwrap();
458        ctx.init(&[0u8; 32], Some(&params)).unwrap();
459        // SHA-256 uses 512-bit (64-byte) blocks.
460        assert_eq!(ctx.block_size(), 64);
461    }
462
463    #[test]
464    fn mac_ctx_block_size_hmac_sha512() {
465        let digest = DigestAlg::fetch(c"SHA2-512", None).unwrap();
466        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
467        let mut ctx = MacCtx::new(&alg).unwrap();
468        let nid = digest.nid();
469        let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
470        let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
471        let params = crate::params::ParamBuilder::new()
472            .unwrap()
473            .push_utf8_string(c"digest", name)
474            .unwrap()
475            .build()
476            .unwrap();
477        ctx.init(&[0u8; 64], Some(&params)).unwrap();
478        // SHA-512 uses 1024-bit (128-byte) blocks.
479        assert_eq!(ctx.block_size(), 128);
480    }
481
482    #[test]
483    fn mac_ctx_alg_returns_hmac_name() {
484        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
485        let ctx = MacCtx::new(&alg).unwrap();
486        let retrieved = ctx.alg().expect("alg() should return Some after new()");
487        assert_eq!(retrieved.name().to_bytes(), b"HMAC");
488    }
489
490    #[test]
491    fn mac_ctx_alg_outlives_context() {
492        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
493        let ctx = MacCtx::new(&alg).unwrap();
494        let retrieved = ctx.alg().unwrap();
495        // Drop ctx; retrieved should still be valid (independent refcount).
496        drop(ctx);
497        assert_eq!(retrieved.name().to_bytes(), b"HMAC");
498        drop(retrieved);
499    }
500
501    /// `MacCtx::fork` mid-stream — two different suffixes produce different MACs.
502    #[test]
503    fn mac_ctx_fork_mid_stream() {
504        let alg = MacAlg::fetch(c"HMAC", None).unwrap();
505        let digest = DigestAlg::fetch(c"SHA2-256", None).unwrap();
506        let nid = digest.nid();
507        let name_ptr = unsafe { native_ossl_sys::OBJ_nid2sn(nid) };
508        let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) };
509        let params = crate::params::ParamBuilder::new()
510            .unwrap()
511            .push_utf8_string(c"digest", name)
512            .unwrap()
513            .build()
514            .unwrap();
515
516        let mut ctx = MacCtx::new(&alg).unwrap();
517        ctx.init(&[0u8; 32], Some(&params)).unwrap();
518        ctx.update(b"common").unwrap();
519
520        let mut fork = ctx.fork().unwrap();
521        ctx.update(b" A").unwrap();
522        fork.update(b" B").unwrap();
523
524        let mut out_a = [0u8; 32];
525        let mut out_b = [0u8; 32];
526        ctx.finish(&mut out_a).unwrap();
527        fork.finish(&mut out_b).unwrap();
528
529        assert_ne!(out_a, out_b);
530    }
531}