//! Message Authentication Codes with type-level tracking. //! //! [`Mac`] represents data with a MAC tag attached. Like [`Signed`](crate::signed::Signed), //! the payload is inaccessible until verification succeeds — but MACs use symmetric //! keys rather than public-key cryptography. //! //! # Type Parameters //! //! - `T`: The payload type that was authenticated //! - `M`: The MAC primitive (e.g., [`HmacSha256`](hmac::HmacSha256)) //! - `C`: The codec used to serialize the payload //! //! # Example //! //! ``` //! # #[cfg(feature = "hmac")] //! # { //! use evidence::{codec::Identity, mac::{Mac, hmac::HmacSha256}}; //! //! let key = hmac::digest::Key::>::from_slice(&[0u8; 64]); //! let data = b"hello world"; //! //! let mac: Mac<[u8; 11], HmacSha256, Identity> = Mac::tag(key, data); //! //! // Payload is NOT accessible here — must verify first //! let payload = mac.try_verify(key).unwrap(); //! assert_eq!(payload, *data); //! # } //! ``` use alloc::vec::Vec; use core::{fmt::Debug, marker::PhantomData}; use crate::codec::{Decode, Encode}; /// A MAC primitive that can authenticate and verify messages. /// /// This trait abstracts over different MAC schemes (HMAC, KMAC, Poly1305, etc.) /// similar to how [`SignaturePrimitive`](crate::signature::SignaturePrimitive) abstracts /// over signature algorithms — but MACs use symmetric keys. pub trait MacPrimitive { /// The secret key type (same key for tagging and verification). type Key; /// The MAC tag type. type Tag: Clone + Eq; /// Error type returned when verification fails. type Error: Debug; /// Compute a MAC tag over the message. fn mac(key: &Self::Key, message: &[u8]) -> Self::Tag; /// Verify a MAC tag against a message. /// /// # Errors /// /// Returns an error if the MAC tag does not match. fn verify(key: &Self::Key, message: &[u8], tag: &Self::Tag) -> Result<(), Self::Error>; } /// Authenticated data with a MAC tag. /// /// This type deliberately does _not_ provide access to the payload. /// You must call [`try_verify`](Self::try_verify) to obtain the payload, /// which ensures you cannot accidentally use unauthenticated data. /// /// # Construction /// /// Use [`tag`](Self::tag) to create an authenticated payload. /// /// For deserialization from untrusted sources, import the /// [`MacUnchecked`] extension trait. pub struct Mac { tag: M::Tag, encoded_payload: Vec, _marker: PhantomData (T, C)>, } impl Clone for Mac where M::Tag: Clone, { fn clone(&self) -> Self { Self { tag: self.tag.clone(), encoded_payload: self.encoded_payload.clone(), _marker: PhantomData, } } } impl PartialEq for Mac where M::Tag: PartialEq, { fn eq(&self, other: &Self) -> bool { self.tag == other.tag && self.encoded_payload == other.encoded_payload } } impl Eq for Mac where M::Tag: Eq {} impl core::fmt::Debug for Mac where M::Tag: core::fmt::Debug, { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Mac") .field("tag", &self.tag) .field("encoded_payload", &self.encoded_payload) .finish() } } impl Mac { /// Create a MAC from its components. /// /// This is `pub(crate)` — external users should use [`tag`](Self::tag) /// or the [`MacUnchecked`] extension trait. #[must_use] pub(crate) fn new(tag: M::Tag, encoded_payload: Vec) -> Self { Self { tag, encoded_payload, _marker: PhantomData, } } /// Compute a MAC tag over a payload. /// /// The payload is encoded using codec `C`, then authenticated with key. /// /// # Panics /// /// Panics if the codec fails to encode the payload. #[must_use] #[allow(clippy::expect_used)] // documented panic on encode failure pub fn tag(key: &M::Key, payload: &T) -> Self where C: Encode, { let encoded = C::encode(payload).expect("encoding failed"); let tag = M::mac(key, &encoded); Self::new(tag, encoded) } /// Verify the MAC and decode the payload. /// /// # Errors /// /// Returns [`MacError::InvalidMac`] if the tag does not verify. /// /// Returns [`MacError::DecodeError`] if the payload cannot be decoded. pub fn try_verify(&self, key: &M::Key) -> Result where C: Decode, { M::verify(key, &self.encoded_payload, &self.tag).map_err(|_| MacError::InvalidMac)?; C::decode(&self.encoded_payload).map_err(|_| MacError::DecodeError) } /// Get the MAC tag. #[must_use] pub const fn mac_tag(&self) -> &M::Tag { &self.tag } /// Get the encoded payload bytes. /// /// Note: This returns the raw encoded bytes, _not_ the decoded payload. /// The decoded payload is only accessible after verification via /// [`try_verify`](Self::try_verify). #[must_use] pub fn encoded_payload(&self) -> &[u8] { &self.encoded_payload } } /// Error returned when MAC verification fails. /// /// Details of _why_ verification failed are intentionally hidden /// to avoid leaking information. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum MacError { /// MAC tag did not verify. InvalidMac, /// Payload could not be decoded. DecodeError, } impl core::fmt::Display for MacError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::InvalidMac => write!(f, "invalid MAC"), Self::DecodeError => write!(f, "payload decode error"), } } } /// Extension trait for constructing [`Mac`] from raw components. /// /// This trait is _intentionally_ not in the prelude. Importing it is an explicit /// acknowledgment that you are bypassing the normal authentication flow. /// /// # When to use /// /// - Deserializing authenticated data from storage or network /// - Interoperating with external systems /// - Testing /// /// # Example /// /// ``` /// # #[cfg(feature = "hmac")] /// # { /// use evidence::{codec::Identity, mac::{Mac, MacUnchecked, hmac::HmacSha256}}; /// /// // Reconstruct from deserialized components /// let tag: [u8; 32] = [0u8; 32]; /// let encoded = vec![1, 2, 3, 4]; /// /// let mac: Mac, HmacSha256, Identity> = Mac::from_unchecked_parts(tag, encoded); /// # } /// ``` pub trait MacUnchecked { /// Create a MAC from raw components. /// /// # Safety (logical) /// /// This does not perform any verification. The caller must ensure /// the components represent valid authenticated data. fn from_unchecked_parts(tag: M::Tag, encoded_payload: Vec) -> Self; } impl MacUnchecked for Mac { fn from_unchecked_parts(tag: M::Tag, encoded_payload: Vec) -> Self { Self::new(tag, encoded_payload) } } #[cfg(feature = "serde")] impl serde::Serialize for Mac where M::Tag: serde::Serialize, { fn serialize(&self, serializer: S) -> Result { use serde::ser::SerializeStruct; let mut state = serializer.serialize_struct("Mac", 2)?; state.serialize_field("tag", &self.tag)?; state.serialize_field("encoded_payload", &self.encoded_payload)?; state.end() } } #[cfg(feature = "serde")] impl<'de, T, M: MacPrimitive, C> serde::Deserialize<'de> for Mac where M::Tag: serde::Deserialize<'de>, { fn deserialize>(deserializer: D) -> Result { use serde::de::{MapAccess, Visitor}; struct MacVisitor(PhantomData<(T, M, C)>); impl<'de, T, M: MacPrimitive, C> Visitor<'de> for MacVisitor where M::Tag: serde::Deserialize<'de>, { type Value = Mac; fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { formatter.write_str("struct Mac") } fn visit_map>(self, mut map: V) -> Result, V::Error> { let mut tag = None; let mut encoded_payload = None; while let Some(key) = map.next_key::<&str>()? { match key { "tag" => tag = Some(map.next_value()?), "encoded_payload" => encoded_payload = Some(map.next_value()?), _ => { let _: serde::de::IgnoredAny = map.next_value()?; } } } let tag = tag.ok_or_else(|| serde::de::Error::missing_field("tag"))?; let encoded_payload = encoded_payload .ok_or_else(|| serde::de::Error::missing_field("encoded_payload"))?; Ok(Mac::new(tag, encoded_payload)) } } const FIELDS: &[&str] = &["tag", "encoded_payload"]; deserializer.deserialize_struct("Mac", FIELDS, MacVisitor(PhantomData)) } } #[cfg(feature = "arbitrary")] impl<'a, T, M: MacPrimitive, C> arbitrary::Arbitrary<'a> for Mac where M::Tag: arbitrary::Arbitrary<'a>, { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { let tag = M::Tag::arbitrary(u)?; let encoded_payload = Vec::arbitrary(u)?; Ok(Self::new(tag, encoded_payload)) } } #[cfg(feature = "bolero")] impl bolero_generator::TypeGenerator for Mac where M::Tag: bolero_generator::TypeGenerator, { fn generate(driver: &mut D) -> Option { let tag = M::Tag::generate(driver)?; let encoded_payload = Vec::generate(driver)?; Some(Self::new(tag, encoded_payload)) } } #[cfg(feature = "proptest")] impl proptest::arbitrary::Arbitrary for Mac where M::Tag: proptest::arbitrary::Arbitrary + 'static, { type Parameters = (); type Strategy = proptest::strategy::BoxedStrategy; fn arbitrary_with((): Self::Parameters) -> Self::Strategy { use proptest::prelude::*; ( any::(), proptest::collection::vec(any::(), 0..256), ) .prop_map(|(tag, encoded_payload)| Self::new(tag, encoded_payload)) .boxed() } } #[cfg(feature = "rkyv")] /// Zero-copy [`rkyv`] serialization support for [`Mac`]. pub mod archive { use super::{Mac, MacPrimitive}; use alloc::vec::Vec; use rkyv::{Archive, Archived, Deserialize, Serialize, rancor::Fallible}; impl Archive for Mac where M::Tag: Archive, { type Archived = ArchivedMac; type Resolver = MacResolver; fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place) { let helper = MacHelper { tag: self.tag.clone(), encoded_payload: self.encoded_payload.clone(), }; helper.resolve(resolver, out); } } impl Serialize for Mac where M::Tag: Serialize, S: Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized, { fn serialize(&self, serializer: &mut S) -> Result { let helper = MacHelper { tag: self.tag.clone(), encoded_payload: self.encoded_payload.clone(), }; helper.serialize(serializer) } } impl Deserialize, D> for ArchivedMac where M::Tag: Archive, Archived: Deserialize, D: Fallible + ?Sized, D::Error: rkyv::rancor::Source, { fn deserialize(&self, deserializer: &mut D) -> Result, D::Error> { let helper: MacHelper = as Deserialize< MacHelper, D, >>::deserialize(self, deserializer)?; Ok(Mac::new(helper.tag, helper.encoded_payload)) } } /// Helper struct for rkyv serialization. /// /// The phantom type parameters from [`Mac`] are erased in the archived /// form since they only matter at compile time. #[derive(Debug, Archive, Serialize, Deserialize)] pub struct MacHelper { tag: Tag, encoded_payload: Vec, } /// Type alias for the archived form of [`Mac`]. pub type ArchivedMac = ArchivedMacHelper; /// Type alias for the resolver of [`Mac`]. pub type MacResolver = MacHelperResolver; } #[cfg(feature = "hmac")] pub mod hmac;