lexe-crypto 0.1.21

Lexe cryptography library (ring wrapper)
Documentation
//! HMAC-SHA256 message authentication.
//!
//! A light wrapper around [`ring::hmac`]. Can be extended as needed.

use std::fmt;

use ring;

use crate::constant_time;

/// The length in bytes of an HMAC-SHA256 tag.
pub const TAG_LEN: usize = 32;

/// A key for computing and verifying HMAC-SHA256 tags.
pub struct Key(ring::hmac::Key);

impl Key {
    /// Create a new `ring::hmac::Key` from a random 32-byte seed.
    ///
    /// Use this when deriving a key from a KDF like `RootSeed`.
    pub fn from_seed(seed: &[u8; 32]) -> Self {
        Self(ring::hmac::Key::new(ring::hmac::HMAC_SHA256, seed))
    }

    /// Sign a given plaintext (`write_data_cb`) and `aad`, and return
    /// `plaintext || tag`.
    pub fn sign_and_append(
        &self,
        domain_separator: &[u8; 32],
        aad: &[&[u8]],
        // An optional hint indicating the size of the plaintext written
        // by `write_data_cb`; used to avoid `Vec` reallocation.
        data_size_hint: Option<usize>,
        // A closure which appends the plaintext to the provided `Vec`.
        write_data_cb: &dyn Fn(&mut Vec<u8>),
    ) -> Vec<u8> {
        let capacity = data_size_hint.unwrap_or_default() + TAG_LEN;
        let mut out = Vec::with_capacity(capacity);

        // out: `plaintext`
        write_data_cb(&mut out);

        let tag = self.sign(domain_separator, aad, &out);

        // out: `plaintext || tag`
        out.extend_from_slice(tag.as_ref());

        out
    }

    /// Verify a `plaintext || tag` blob produced by [`Self::sign_and_append`],
    /// returning `plaintext` on success. Constant-time.
    pub fn verify<'a>(
        &self,
        domain_separator: &[u8; 32],
        aad: &[&[u8]],
        signed: &'a [u8],
    ) -> Result<&'a [u8], &'static str> {
        let (plaintext, tag) =
            signed.split_last_chunk::<TAG_LEN>().ok_or("Tag mismatch")?;

        let expected_tag = self.sign(domain_separator, aad, plaintext);
        let expected_tag =
            <&[u8; TAG_LEN]>::try_from(expected_tag.as_ref()).unwrap();

        if constant_time::u8x32_eq(tag, expected_tag) {
            Ok(plaintext)
        } else {
            Err("Tag mismatch")
        }
    }

    /// `sign(domain_sep (|| aad_len || aad_text)* || text_len || plaintext)`
    fn sign(
        &self,
        domain_separator: &[u8; 32],
        aad: &[&[u8]],
        plaintext: &[u8],
    ) -> ring::hmac::Tag {
        let mut ctx = ring::hmac::Context::with_key(&self.0);
        ctx.update(domain_separator);
        for &datum in aad {
            let datum_len: u64 = u64::try_from(datum.len()).unwrap();
            ctx.update(&datum_len.to_le_bytes());
            ctx.update(datum);
        }
        let plaintext_len: u64 = u64::try_from(plaintext.len()).unwrap();
        ctx.update(&plaintext_len.to_le_bytes());
        ctx.update(plaintext);
        ctx.sign()
    }
}

impl fmt::Debug for Key {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("hmac::Key(..)")
    }
}

#[cfg(test)]
mod test {
    use std::{assert_eq, println};

    use proptest::{
        collection::vec,
        prelude::*,
        proptest,
        strategy::ValueTree,
        test_runner::{self, TestRng, TestRunner},
    };

    use super::*;
    use crate::rng::{FastRng, RngExt};

    fn sign(
        key: &Key,
        domain: &[u8; 32],
        aad: &[&[u8]],
        plaintext: &str,
    ) -> Vec<u8> {
        key.sign_and_append(domain, aad, Some(plaintext.len()), &|out| {
            out.extend_from_slice(plaintext.as_bytes())
        })
    }

    /// Test the [`Key::sign_and_append`] -> [`Key::verify`] roundtrip via
    /// proptest. Also ensures length is `plaintext.len() + TAG_LEN`.
    #[test]
    fn sign_verify_roundtrip() {
        proptest!(|(
            seed: [u8; 32],
            domain: [u8; 32],
            aads in vec(
                vec(any::<u8>(), 0..64),
                0..=5
            ),
            plaintexts in vec(
                vec(any::<u8>(), 0..64),
                0..=5
            )
        )| {
            let key = Key::from_seed(&seed);
            let aad = aads.iter().map(Vec::as_slice).collect::<Vec<&[u8]>>();

            for plaintext in plaintexts {
                // Sign
                let signed = key.sign_and_append(
                    &domain,
                    &aad,
                    Some(plaintext.len()),
                    &|out| out.extend_from_slice(&plaintext),
                );

                // Verify length
                prop_assert_eq!(signed.len(), plaintext.len() + TAG_LEN);

                // Verify roundtrip
                let verified = key.verify(&domain, &aad, &signed);
                prop_assert_eq!(verified, Ok(plaintext.as_slice()));
            }
        });
    }

    // --- Unit tests --- //

    /// Ensure that our signed output remains backwards compatible.
    /// Snapshot sourced from [`print_snapshot_data`].
    #[test]
    fn snapshot_compat() {
        // --- Test case 1 --- //
        #[rustfmt::skip]
        let snapshot: (&[u8; 32], &[u8; 32], &[&[u8]], &[u8], &[u8]) = (
            /* seed      */ &[194, 128, 19, 113, 226, 250, 32, 233, 155, 123, 134, 83, 255, 2, 82, 142, 75, 252, 132, 118, 107, 203, 153, 82, 115, 146, 100, 251, 113, 66, 0, 164],
            /* domain    */ &[205, 174, 107, 153, 218, 56, 43, 171, 131, 97, 216, 115, 245, 148, 158, 129, 94, 121, 23, 95, 106, 195, 55, 241, 87, 91, 201, 34, 195, 166, 69, 6],
            /* aad       */ &[&[174, 176, 193, 58, 9, 240, 69, 146, 14, 115, 42, 112, 120, 75, 200, 60, 90, 76, 223, 99, 120, 142, 248, 60, 152, 44, 128, 156, 129, 120, 142, 140, 156, 82, 164, 216, 173, 162, 141, 135, 248, 121, 233, 183, 248, 239]],
            /* plaintext */ &[83, 53, 240, 125, 240, 64, 242, 207, 18, 70, 169, 92, 124, 37, 237, 27, 74, 139, 179, 117, 7, 130, 0, 146, 156, 249, 151, 75, 95, 75, 63, 189, 200, 167, 18, 13, 71, 25, 233, 20, 37, 164, 37, 218, 139, 161, 150, 85, 220, 238, 118, 18, 125, 54, 15, 143, 217, 202, 150],
            /* expected  */ &[83, 53, 240, 125, 240, 64, 242, 207, 18, 70, 169, 92, 124, 37, 237, 27, 74, 139, 179, 117, 7, 130, 0, 146, 156, 249, 151, 75, 95, 75, 63, 189, 200, 167, 18, 13, 71, 25, 233, 20, 37, 164, 37, 218, 139, 161, 150, 85, 220, 238, 118, 18, 125, 54, 15, 143, 217, 202, 150, 196, 167, 246, 228, 49, 114, 137, 165, 170, 79, 139, 49, 167, 196, 122, 69, 100, 33, 29, 234, 82, 57, 56, 152, 154, 75, 5, 71, 191, 149, 235, 183],
        );
        let (seed, domain, aad, plaintext, expected) = snapshot;

        let key = Key::from_seed(seed);
        let current =
            key.sign_and_append(domain, aad, Some(plaintext.len()), &|out| {
                out.extend_from_slice(plaintext)
            });
        assert_eq!(current, expected);

        // --- Test case 2 --- //
        #[rustfmt::skip]
        let snapshot: (&[u8; 32], &[u8; 32], &[&[u8]], &[u8], &[u8]) = (
            /* seed      */ &[173, 43, 12, 81, 7, 247, 91, 248, 211, 206, 87, 20, 13, 237, 65, 121, 194, 68, 248, 183, 167, 235, 201, 126, 239, 216, 89, 221, 132, 172, 69, 226],
            /* domain    */ &[46, 15, 88, 59, 109, 35, 131, 252, 22, 35, 193, 134, 136, 203, 32, 176, 160, 171, 73, 196, 15, 72, 242, 0, 45, 159, 65, 90, 45, 136, 92, 120],
            /* aad       */ &[&[81, 236, 111, 72, 77, 218, 20, 177, 176, 57, 243, 157, 41, 66, 15, 187, 164, 173, 85, 143, 4, 64, 213, 206, 147, 249, 6, 148, 65, 126, 77, 173, 183, 143, 87], &[105, 23, 87, 33, 135, 201, 32, 41, 87, 167, 87, 13, 94, 30, 49, 167, 56, 72, 36, 202, 75, 33, 241, 107, 84, 105, 255, 237, 27], &[32, 79, 153, 80, 253, 150, 181, 151, 5, 112, 247, 90, 28, 142, 64, 136, 139, 2, 46, 41, 173, 1, 174, 29, 107, 101, 139, 140, 94, 217, 180, 96, 73, 161, 157, 240, 11, 56, 121, 96, 151, 110, 188, 67, 45, 203, 13, 236, 32, 157, 220]],
            /* plaintext */ &[110, 192, 94, 70, 121, 234, 52, 58, 15, 101, 180, 91, 21, 151, 236, 105, 62, 37, 67, 205, 90, 214, 117, 146, 201, 123, 167, 61, 7, 175, 67, 81, 80, 175, 92, 167, 250, 73, 179, 2, 207, 253, 31, 145, 122, 75],
            /* signed    */ &[110, 192, 94, 70, 121, 234, 52, 58, 15, 101, 180, 91, 21, 151, 236, 105, 62, 37, 67, 205, 90, 214, 117, 146, 201, 123, 167, 61, 7, 175, 67, 81, 80, 175, 92, 167, 250, 73, 179, 2, 207, 253, 31, 145, 122, 75, 237, 12, 73, 125, 242, 196, 242, 81, 49, 29, 160, 90, 115, 80, 173, 136, 168, 219, 49, 154, 171, 71, 205, 17, 180, 187, 62, 82, 19, 35, 81, 33],
        );
        let (seed, domain, aad, plaintext, expected) = snapshot;

        let key = Key::from_seed(seed);
        let current =
            key.sign_and_append(domain, aad, Some(plaintext.len()), &|out| {
                out.extend_from_slice(plaintext)
            });
        assert_eq!(current, expected);

        // --- Test case 3 --- //
        #[rustfmt::skip]
        let snapshot: (&[u8; 32], &[u8; 32], &[&[u8]], &[u8], &[u8]) = (
            /* seed      */ &[52, 174, 187, 80, 227, 50, 89, 135, 176, 155, 248, 40, 194, 93, 212, 236, 30, 160, 67, 46, 160, 123, 82, 81, 33, 150, 189, 122, 25, 221, 254, 139],
            /* domain    */ &[100, 169, 17, 187, 44, 171, 149, 246, 126, 214, 71, 249, 146, 242, 233, 251, 1, 116, 102, 161, 118, 195, 39, 95, 171, 208, 109, 71, 119, 22, 8, 249],
            /* aad       */ &[&[239, 141, 103, 219, 18, 254, 151, 204, 227, 159, 19, 138, 196], &[251, 82, 191, 138, 33, 129, 155, 205, 76, 19, 58, 114, 156]],
            /* plaintext */ &[134, 159, 173, 109, 127, 231, 204, 230, 202, 129, 221, 5, 107, 202, 192, 165, 193, 216, 149, 124, 16, 9, 198, 65, 100, 154, 19, 150, 1, 93, 182, 253, 104, 63, 32, 150, 11, 174, 103, 185, 145, 255, 222, 242, 212, 13, 10, 67, 81, 152, 160, 50, 169, 45, 178, 241],
            /* signed    */ &[134, 159, 173, 109, 127, 231, 204, 230, 202, 129, 221, 5, 107, 202, 192, 165, 193, 216, 149, 124, 16, 9, 198, 65, 100, 154, 19, 150, 1, 93, 182, 253, 104, 63, 32, 150, 11, 174, 103, 185, 145, 255, 222, 242, 212, 13, 10, 67, 81, 152, 160, 50, 169, 45, 178, 241, 59, 126, 52, 102, 153, 18, 146, 129, 184, 46, 203, 30, 136, 181, 142, 128, 198, 66, 61, 227, 243, 15, 246, 190, 243, 138, 19, 227, 134, 141, 56, 184],
        );
        let (seed, domain, aad, plaintext, expected) = snapshot;

        let key = Key::from_seed(seed);
        let current =
            key.sign_and_append(domain, aad, Some(plaintext.len()), &|out| {
                out.extend_from_slice(plaintext)
            });
        assert_eq!(current, expected);
    }

    #[test]
    fn verify_rejects_tampering() {
        let key = Key::from_seed(&[42u8; 32]);
        let domain = [7u8; 32];
        let aad: &[&[u8]] = &[b"aad1", b"aad2"];

        let signed = sign(&key, &domain, aad, "hello world");

        for i in 0..signed.len() {
            let mut tampered = signed.clone();
            tampered[i] ^= 1;
            key.verify(&domain, aad, &tampered).unwrap_err();
        }
    }

    #[test]
    fn verify_rejects_wrong_key_domain_or_aad() {
        let key = Key::from_seed(&[1u8; 32]);
        let wrong_key = Key::from_seed(&[2u8; 32]);

        let domain = [7u8; 32];
        let wrong_domain = [8u8; 32];

        let aad: &[&[u8]] = &[b"aad1", b"aad2"];
        let wrong_aad: &[&[u8]] = &[b"aad1", b"aad3"];

        let signed = sign(&key, &domain, aad, "hello world");

        // Test wrong key
        wrong_key.verify(&domain, aad, &signed).unwrap_err();

        // Test wrong domain
        key.verify(&wrong_domain, aad, &signed).unwrap_err();

        // Test wrong and empty aad
        key.verify(&domain, wrong_aad, &signed).unwrap_err();
        key.verify(&domain, &[], &signed).unwrap_err();
    }

    /// The length prefixes must make each `aad` field unambiguous, so that
    /// shifting bytes across a field boundary changes the tag.
    #[test]
    fn aad_serialization_is_unambiguous() {
        let key = Key::from_seed(&[42u8; 32]);
        let domain = [7u8; 32];

        let signed = sign(&key, &domain, &[b"ab", b"c"], "hello world");

        // Test shifted `aad`s.
        key.verify(&domain, &[b"a", b"bc"], &signed).unwrap_err();
        key.verify(&domain, &[b"abc"], &signed).unwrap_err();
    }

    #[test]
    fn verify_rejects_too_short() {
        let key = Key::from_seed(&[42u8; 32]);
        let domain = [7u8; 32];
        let aad: &[&[u8]] = &[b"aad1", b"aad2"];

        key.verify(&domain, aad, &[0u8; TAG_LEN - 1]).unwrap_err();
        key.verify(&domain, aad, &[]).unwrap_err();
    }

    // --- Util --- //

    /// Snapshot some signed data. Output used in [`snapshot_compat`].
    /// ```bash
    /// $ cargo test -p lexe-crypto hmac::test::print_snapshot_data -- --show-output --ignored
    /// ```
    #[ignore]
    #[test]
    fn print_snapshot_data() {
        let mut rng = FastRng::from_u64(202608041547);

        let runner_rng = TestRng::from_seed(
            test_runner::RngAlgorithm::ChaCha,
            &rng.gen_bytes::<32>(),
        );
        let mut runner = TestRunner::new_with_rng(
            test_runner::Config::default(),
            runner_rng,
        );

        const NUM_CASES: usize = 3;
        let strategy = (
            any::<[u8; 32]>(),                   // seed
            any::<[u8; 32]>(),                   // domain
            vec(vec(any::<u8>(), 0..64), 0..=5), // AAD
            vec(any::<u8>(), 0..64),             // plaintexts
        );
        for i in 0..NUM_CASES {
            let (seed, domain, aad, plaintext) =
                strategy.new_tree(&mut runner).unwrap().current();
            let key = Key::from_seed(&seed);
            let aad = aad.iter().map(Vec::as_slice).collect::<Vec<&[u8]>>();
            let signed = key.sign_and_append(
                &domain,
                &aad,
                Some(plaintext.len()),
                &|out| out.extend_from_slice(&plaintext),
            );
            const PAD: usize = "plaintext".len();
            println!("--- ({i}) ---");
            println!("{:<PAD$} : {seed:?}", "seed");
            println!("{:<PAD$} : {domain:?}", "domain");
            println!("{:<PAD$} : {aad:?}", "aad");
            println!("{:<PAD$} : {plaintext:?}", "plaintext");
            println!("{:<PAD$} : {signed:?}", "signed");
        }
    }
}