# auth.rs - source
Source of the Rust file `crates/tlsn/src/transcript_internal/auth.rs`.

Skip to main content

#
tlsn/transcript_internal/
auth.rs

    1use std::sync::Arc;
    2
    3use aes::Aes128;
    4use ctr::{
    5    Ctr32BE,
    6    cipher::{KeyIvInit, StreamCipher, StreamCipherSeek},
    7};
    8use mpz_circuits::{AES128, circuits::xor};
    9use mpz_core::bitvec::BitVec;
    10use mpz_memory_core::{
    11    Array, DecodeFutureTyped, MemoryExt, Vector, ViewExt,
    12    binary::{Binary, U8},
    13};
    14use mpz_vm_core::{Call, CallableExt, Vm};
    15use rangeset::{iter::RangeIterator, ops::Set, set::RangeSet};
    16use tlsn_core::transcript::Record;
    17
    18use crate::transcript_internal::ReferenceMap;
    19
    20pub(crate) fn prove_plaintext<'a>(
    21    vm: &mut dyn Vm<Binary>,
    22    key: Array<U8, 16>,
    23    iv: Array<U8, 4>,
    24    plaintext: &[u8],
    25    records: impl IntoIterator<Item = &'a Record>,
    26    reveal: &RangeSet<usize>,
    27    commit: &RangeSet<usize>,
    28) -> Result<ReferenceMap, PlaintextAuthError> {
    29    let is_reveal_all = reveal == (0..plaintext.len());
    30
    31    let alloc_ranges = if is_reveal_all {
    32        commit.clone()
    33    } else {
    34        // The plaintext is only partially revealed, so we need to authenticate in ZK.
    35        commit.union(reveal).into_set()
    36    };
    37
    38    let plaintext_refs = alloc_plaintext(vm, &alloc_ranges)?;
    39    let records = RecordParams::from_iter(records).collect::<Vec<_>>();
    40
    41    if is_reveal_all {
    42        drop(vm.decode(key).map_err(PlaintextAuthError::vm)?);
    43        drop(vm.decode(iv).map_err(PlaintextAuthError::vm)?);
    44
    45        for (range, slice) in plaintext_refs.iter() {
    46            vm.mark_public(*slice).map_err(PlaintextAuthError::vm)?;
    47            vm.assign(*slice, plaintext[range].to_vec())
    48                .map_err(PlaintextAuthError::vm)?;
    49            vm.commit(*slice).map_err(PlaintextAuthError::vm)?;
    50        }
    51    } else {
    52        let private = commit.difference(reveal).into_set();
    53        for (_, slice) in plaintext_refs
    54            .index(&private)
    55            .expect("all ranges are allocated")
    56            .iter()
    57        {
    58            vm.mark_private(*slice).map_err(PlaintextAuthError::vm)?;
    59        }
    60
    61        for (_, slice) in plaintext_refs
    62            .index(reveal)
    63            .expect("all ranges are allocated")
    64            .iter()
    65        {
    66            vm.mark_public(*slice).map_err(PlaintextAuthError::vm)?;
    67        }
    68
    69        for (range, slice) in plaintext_refs.iter() {
    70            vm.assign(*slice, plaintext[range].to_vec())
    71                .map_err(PlaintextAuthError::vm)?;
    72            vm.commit(*slice).map_err(PlaintextAuthError::vm)?;
    73        }
    74
    75        let ciphertext = alloc_ciphertext(vm, key, iv, plaintext_refs.clone(), &records)?;
    76        for (_, slice) in ciphertext.iter() {
    77            drop(vm.decode(*slice).map_err(PlaintextAuthError::vm)?);
    78        }
    79    }
    80
    81    Ok(plaintext_refs)
    82}
    83
    84#[allow(clippy::too_many_arguments)]
    85pub(crate) fn verify_plaintext<'a>(
    86    vm: &mut dyn Vm<Binary>,
    87    key: Array<U8, 16>,
    88    iv: Array<U8, 4>,
    89    plaintext: &'a [u8],
    90    ciphertext: &'a [u8],
    91    records: impl IntoIterator<Item = &'a Record>,
    92    reveal: &RangeSet<usize>,
    93    commit: &RangeSet<usize>,
    94) -> Result<(ReferenceMap, PlaintextProof<'a>), PlaintextAuthError> {
    95    let is_reveal_all = reveal == (0..plaintext.len());
    96
    97    let alloc_ranges = if is_reveal_all {
    98        commit.clone()
    99    } else {
    100        // The plaintext is only partially revealed, so we need to authenticate in ZK.
    101        commit.union(reveal).into_set()
    102    };
    103
    104    let plaintext_refs = alloc_plaintext(vm, &alloc_ranges)?;
    105    let records = RecordParams::from_iter(records).collect::<Vec<_>>();
    106
    107    let plaintext_proof = if is_reveal_all {
    108        let key = vm.decode(key).map_err(PlaintextAuthError::vm)?;
    109        let iv = vm.decode(iv).map_err(PlaintextAuthError::vm)?;
    110
    111        for (range, slice) in plaintext_refs.iter() {
    112            vm.mark_public(*slice).map_err(PlaintextAuthError::vm)?;
    113            vm.assign(*slice, plaintext[range].to_vec())
    114                .map_err(PlaintextAuthError::vm)?;
    115            vm.commit(*slice).map_err(PlaintextAuthError::vm)?;
    116        }
    117
    118        PlaintextProof(ProofInner::WithKey {
    119            key,
    120            iv,
    121            records,
    122            plaintext,
    123            ciphertext,
    124        })
    125    } else {
    126        let private = commit.difference(reveal).into_set();
    127        for (_, slice) in plaintext_refs
    128            .index(&private)
    129            .expect("all ranges are allocated")
    130            .iter()
    131        {
    132            vm.mark_blind(*slice).map_err(PlaintextAuthError::vm)?;
    133        }
    134
    135        for (range, slice) in plaintext_refs
    136            .index(reveal)
    137            .expect("all ranges are allocated")
    138            .iter()
    139        {
    140            vm.mark_public(*slice).map_err(PlaintextAuthError::vm)?;
    141            vm.assign(*slice, plaintext[range].to_vec())
    142                .map_err(PlaintextAuthError::vm)?;
    143        }
    144
    145        for (_, slice) in plaintext_refs.iter() {
    146            vm.commit(*slice).map_err(PlaintextAuthError::vm)?;
    147        }
    148
    149        let ciphertext_map = alloc_ciphertext(vm, key, iv, plaintext_refs.clone(), &records)?;
    150
    151        let mut ciphertexts = Vec::new();
    152        for (range, chunk) in ciphertext_map.iter() {
    153            ciphertexts.push((
    154                &ciphertext[range],
    155                vm.decode(*chunk).map_err(PlaintextAuthError::vm)?,
    156            ));
    157        }
    158
    159        PlaintextProof(ProofInner::WithZk { ciphertexts })
    160    };
    161
    162    Ok((plaintext_refs, plaintext_proof))
    163}
    164
    165fn alloc_plaintext(
    166    vm: &mut dyn Vm<Binary>,
    167    ranges: &RangeSet<usize>,
    168) -> Result<ReferenceMap, PlaintextAuthError> {
    169    let len = ranges.len();
    170
    171    if len == 0 {
    172        return Ok(ReferenceMap::default());
    173    }
    174
    175    let plaintext = vm.alloc_vec::<U8>(len).map_err(PlaintextAuthError::vm)?;
    176
    177    let mut pos = 0;
    178    Ok(ReferenceMap::from_iter(ranges.iter().map(move |range| {
    179        let chunk = plaintext
    180            .get(pos..pos + range.len())
    181            .expect("length was checked");
    182        pos += range.len();
    183        (range.start, chunk)
    184    })))
    185}
    186
    187fn alloc_ciphertext<'a>(
    188    vm: &mut dyn Vm<Binary>,
    189    key: Array<U8, 16>,
    190    iv: Array<U8, 4>,
    191    plaintext: ReferenceMap,
    192    records: impl IntoIterator<Item = &'a RecordParams>,
    193) -> Result<ReferenceMap, PlaintextAuthError> {
    194    if plaintext.is_empty() {
    195        return Ok(ReferenceMap::default());
    196    }
    197
    198    let ranges = RangeSet::from(plaintext.keys().collect::<Vec<_>>());
    199
    200    let keystream = alloc_keystream(vm, key, iv, &ranges, records)?;
    201    let mut builder = Call::builder(Arc::new(xor(ranges.len() * 8)));
    202    for (_, slice) in plaintext.iter() {
    203        builder = builder.arg(*slice);
    204    }
    205    for slice in keystream {
    206        builder = builder.arg(slice);
    207    }
    208    let call = builder.build().expect("call should be valid");
    209
    210    let ciphertext: Vector<U8> = vm.call(call).map_err(PlaintextAuthError::vm)?;
    211
    212    let mut pos = 0;
    213    Ok(ReferenceMap::from_iter(ranges.iter().map(move |range| {
    214        let chunk = ciphertext
    215            .get(pos..pos + range.len())
    216            .expect("length was checked");
    217        pos += range.len();
    218        (range.start, chunk)
    219    })))
    220}
    221
    222fn alloc_keystream<'a>(
    223    vm: &mut dyn Vm<Binary>,
    224    key: Array<U8, 16>,
    225    iv: Array<U8, 4>,
    226    ranges: &RangeSet<usize>,
    227    records: impl IntoIterator<Item = &'a RecordParams>,
    228) -> Result<Vec<Vector<U8>>, PlaintextAuthError> {
    229    let mut keystream = Vec::new();
    230
    231    let mut pos = 0;
    232    let mut range_iter = ranges.iter();
    233    let mut current_range = range_iter.next();
    234    for record in records {
    235        let mut explicit_nonce = None;
    236        let mut current_block = None;
    237        loop {
    238            let Some(range) = current_range.take().or_else(|| range_iter.next()) else {
    239                return Ok(keystream);
    240            };
    241
    242            let record_range = pos..pos + record.len;
    243            if range.start >= record_range.end {
    244                current_range = Some(range);
    245                break;
    246            }
    247
    248            // Range with record offset applied.
    249            let offset_range = range.start - pos..range.end - pos;
    250
    251            let explicit_nonce = if let Some(explicit_nonce) = explicit_nonce {
    252                explicit_nonce
    253            } else {
    254                let nonce = alloc_explicit_nonce(vm, record.explicit_nonce.clone())?;
    255                explicit_nonce = Some(nonce);
    256                nonce
    257            };
    258
    259            const BLOCK_SIZE: usize = 16;
    260            let block_num = offset_range.start / BLOCK_SIZE;
    261            let block = if let Some((current_block_num, block)) = current_block.take()
    262                && current_block_num == block_num
    263            {
    264                block
    265            } else {
    266                let block = alloc_block(vm, key, iv, explicit_nonce, block_num)?;
    267                current_block = Some((block_num, block));
    268                block
    269            };
    270
    271            // Range within the block.
    272            let block_range_start = offset_range.start % BLOCK_SIZE;
    273            let len =
    274                (range.end.min(record_range.end) - range.start).min(BLOCK_SIZE - block_range_start);
    275            let block_range = block_range_start..block_range_start + len;
    276
    277            keystream.push(block.get(block_range).expect("range is checked"));
    278
    279            // If the range extends past the block, process the tail.
    280            if range.start + len < range.end {
    281                current_range = Some(range.start + len..range.end);
    282            }
    283        }
    284
    285        pos += record.len;
    286    }
    287
    288    Err(ErrorRepr::OutOfBounds.into())
    289}
    290
    291fn alloc_explicit_nonce(
    292    vm: &mut dyn Vm<Binary>,
    293    explicit_nonce: Vec<u8>,
    294) -> Result<Vector<U8>, PlaintextAuthError> {
    295    const EXPLICIT_NONCE_LEN: usize = 8;
    296    let nonce = vm
    297        .alloc_vec::<U8>(EXPLICIT_NONCE_LEN)
    298        .map_err(PlaintextAuthError::vm)?;
    299    vm.mark_public(nonce).map_err(PlaintextAuthError::vm)?;
    300    vm.assign(nonce, explicit_nonce)
    301        .map_err(PlaintextAuthError::vm)?;
    302    vm.commit(nonce).map_err(PlaintextAuthError::vm)?;
    303
    304    Ok(nonce)
    305}
    306
    307fn alloc_block(
    308    vm: &mut dyn Vm<Binary>,
    309    key: Array<U8, 16>,
    310    iv: Array<U8, 4>,
    311    explicit_nonce: Vector<U8>,
    312    block: usize,
    313) -> Result<Vector<U8>, PlaintextAuthError> {
    314    let ctr: Array<U8, 4> = vm.alloc().map_err(PlaintextAuthError::vm)?;
    315    vm.mark_public(ctr).map_err(PlaintextAuthError::vm)?;
    316    const START_CTR: u32 = 2;
    317    vm.assign(ctr, (START_CTR + block as u32).to_be_bytes())
    318        .map_err(PlaintextAuthError::vm)?;
    319    vm.commit(ctr).map_err(PlaintextAuthError::vm)?;
    320
    321    let block: Array<U8, 16> = vm
    322        .call(
    323            Call::builder(AES128.clone())
    324                .arg(key)
    325                .arg(iv)
    326                .arg(explicit_nonce)
    327                .arg(ctr)
    328                .build()
    329                .expect("call should be valid"),
    330        )
    331        .map_err(PlaintextAuthError::vm)?;
    332
    333    Ok(Vector::from(block))
    334}
    335
    336struct RecordParams {
    337    explicit_nonce: Vec<u8>,
    338    len: usize,
    339}
    340
    341impl RecordParams {
    342    fn from_iter<'a>(records: impl IntoIterator<Item = &'a Record>) -> impl Iterator<Item = Self> {
    343        records.into_iter().map(|record| Self {
    344            explicit_nonce: record.explicit_nonce.clone(),
    345            len: record.ciphertext.len(),
    346        })
    347    }
    348}
    349
    350#[must_use]
    351pub(crate) struct PlaintextProof<'a>(ProofInner<'a>);
    352
    353impl<'a> PlaintextProof<'a> {
    354    pub(crate) fn verify(self) -> Result<(), PlaintextAuthError> {
    355        match self.0 {
    356            ProofInner::WithKey {
    357                mut key,
    358                mut iv,
    359                records,
    360                plaintext,
    361                ciphertext,
    362            } => {
    363                let key = key
    364                    .try_recv()
    365                    .map_err(PlaintextAuthError::vm)?
    366                    .ok_or(ErrorRepr::MissingDecoding)?;
    367                let iv = iv
    368                    .try_recv()
    369                    .map_err(PlaintextAuthError::vm)?
    370                    .ok_or(ErrorRepr::MissingDecoding)?;
    371
    372                verify_plaintext_with_key(key, iv, &records, plaintext, ciphertext)?;
    373            }
    374            ProofInner::WithZk { ciphertexts } => {
    375                for (expected, mut actual) in ciphertexts {
    376                    let actual = actual
    377                        .try_recv()
    378                        .map_err(PlaintextAuthError::vm)?
    379                        .ok_or(PlaintextAuthError(ErrorRepr::MissingDecoding))?;
    380
    381                    if actual != expected {
    382                        return Err(PlaintextAuthError(ErrorRepr::InvalidPlaintext));
    383                    }
    384                }
    385            }
    386        }
    387
    388        Ok(())
    389    }
    390}
    391
    392enum ProofInner<'a> {
    393    WithKey {
    394        key: DecodeFutureTyped<BitVec, [u8; 16]>,
    395        iv: DecodeFutureTyped<BitVec, [u8; 4]>,
    396        records: Vec<RecordParams>,
    397        plaintext: &'a [u8],
    398        ciphertext: &'a [u8],
    399    },
    400    WithZk {
    401        // (expected, actual)
    402        #[allow(clippy::type_complexity)]
    403        ciphertexts: Vec<(&'a [u8], DecodeFutureTyped<BitVec, Vec<u8>>)>,
    404    },
    405}
    406
    407fn aes_ctr_apply_keystream(key: &[u8; 16], iv: &[u8; 4], explicit_nonce: &[u8], input: &mut [u8]) {
    408    let mut full_iv = [0u8; 16];
    409    full_iv[0..4].copy_from_slice(iv);
    410    full_iv[4..12].copy_from_slice(&explicit_nonce[..8]);
    411
    412    const START_CTR: u32 = 2;
    413    let mut cipher = Ctr32BE::<Aes128>::new(key.into(), &full_iv.into());
    414    cipher
    415        .try_seek(START_CTR * 16)
    416        .expect("start counter is less than keystream length");
    417    cipher.apply_keystream(input);
    418}
    419
    420fn verify_plaintext_with_key<'a>(
    421    key: [u8; 16],
    422    iv: [u8; 4],
    423    records: impl IntoIterator<Item = &'a RecordParams>,
    424    plaintext: &[u8],
    425    ciphertext: &[u8],
    426) -> Result<(), PlaintextAuthError> {
    427    let mut pos = 0;
    428    let mut text = Vec::new();
    429    for record in records {
    430        text.clear();
    431        text.extend_from_slice(&plaintext[pos..pos + record.len]);
    432
    433        aes_ctr_apply_keystream(&key, &iv, &record.explicit_nonce, &mut text);
    434
    435        if text != ciphertext[pos..pos + record.len] {
    436            return Err(PlaintextAuthError(ErrorRepr::InvalidPlaintext));
    437        }
    438
    439        pos += record.len;
    440    }
    441
    442    Ok(())
    443}
    444
    445#[derive(Debug, thiserror::Error)]
    446#[error("plaintext authentication error: {0}")]
    447pub(crate) struct PlaintextAuthError(#[from] ErrorRepr);
    448
    449impl PlaintextAuthError {
    450    fn vm<E>(err: E) -> Self
    451    where
    452        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
    453    {
    454        Self(ErrorRepr::Vm(err.into()))
    455    }
    456}
    457
    458#[derive(Debug, thiserror::Error)]
    459enum ErrorRepr {
    460    #[error("vm error: {0}")]
    461    Vm(Box<dyn std::error::Error + Send + Sync + 'static>),
    462    #[error("plaintext out of bounds of records. This should never happen and is an internal bug.")]
    463    OutOfBounds,
    464    #[error("missing decoding")]
    465    MissingDecoding,
    466    #[error("plaintext does not match ciphertext")]
    467    InvalidPlaintext,
    468}
    469
    470#[cfg(test)]
    471#[allow(clippy::all)]
    472mod tests {
    473    use super::*;
    474    use mpz_common::context::test_st_context;
    475    use mpz_ideal_vm::IdealVm;
    476    use mpz_vm_core::prelude::*;
    477    use rand::{Rng, SeedableRng, rngs::StdRng};
    478    use rstest::*;
    479    use std::ops::Range;
    480
    481    fn build_vm(key: [u8; 16], iv: [u8; 4]) -> (IdealVm, Array<U8, 16>, Array<U8, 4>) {
    482        let mut vm = IdealVm::new();
    483        let key_ref = vm.alloc::<Array<U8, 16>>().unwrap();
    484        let iv_ref = vm.alloc::<Array<U8, 4>>().unwrap();
    485
    486        vm.mark_public(key_ref).unwrap();
    487        vm.mark_public(iv_ref).unwrap();
    488        vm.assign(key_ref, key).unwrap();
    489        vm.assign(iv_ref, iv).unwrap();
    490        vm.commit(key_ref).unwrap();
    491        vm.commit(iv_ref).unwrap();
    492
    493        (vm, key_ref, iv_ref)
    494    }
    495
    496    fn expected_aes_ctr<'a>(
    497        key: [u8; 16],
    498        iv: [u8; 4],
    499        records: impl IntoIterator<Item = &'a RecordParams>,
    500        ranges: &RangeSet<usize>,
    501    ) -> Vec<u8> {
    502        let mut keystream = Vec::new();
    503        let mut pos = 0;
    504        for record in records {
    505            let mut record_keystream = vec![0u8; record.len];
    506            aes_ctr_apply_keystream(&key, &iv, &record.explicit_nonce, &mut record_keystream);
    507            for mut range in ranges.iter() {
    508                range.start = range.start.max(pos);
    509                range.end = range.end.min(pos + record.len);
    510                if range.start < range.end {
    511                    keystream
    512                        .extend_from_slice(&record_keystream[range.start - pos..range.end - pos]);
    513                }
    514            }
    515            pos += record.len;
    516        }
    517
    518        keystream
    519    }
    520
    521    #[rstest]
    522    #[case::single_record_empty([0], [])]
    523    #[case::multiple_empty_records_empty([0, 0], [])]
    524    #[case::multiple_records_empty([128, 64], [])]
    525    #[case::single_block_full([16], [0..16])]
    526    #[case::single_block_partial([16], [2..14])]
    527    #[case::partial_block_full([15], [0..15])]
    528    #[case::out_of_bounds([16], [0..17])]
    529    #[case::multiple_records_full([128, 63, 33, 15, 4], [0..243])]
    530    #[case::multiple_records_partial([128, 63, 33, 15, 4], [1..15, 16..17, 18..19, 126..130, 224..225, 242..243])]
    531    #[tokio::test]
    532    async fn test_alloc_keystream(
    533        #[case] record_lens: impl IntoIterator<Item = usize>,
    534        #[case] ranges: impl IntoIterator<Item = Range<usize>>,
    535    ) {
    536        let mut rng = StdRng::seed_from_u64(0);
    537        let mut key = [0u8; 16];
    538        let mut iv = [0u8; 4];
    539        rng.fill(&mut key);
    540        rng.fill(&mut iv);
    541
    542        let mut total_len = 0;
    543        let records = record_lens
    544            .into_iter()
    545            .map(|len| {
    546                let mut explicit_nonce = [0u8; 8];
    547                rng.fill(&mut explicit_nonce);
    548                total_len += len;
    549                RecordParams {
    550                    explicit_nonce: explicit_nonce.to_vec(),
    551                    len,
    552                }
    553            })
    554            .collect::<Vec<_>>();
    555
    556        let ranges = RangeSet::from(ranges.into_iter().collect::<Vec<_>>());
    557        let is_out_of_bounds = ranges.end().unwrap_or(0) > total_len;
    558
    559        let (mut ctx, _) = test_st_context(1024);
    560        let (mut vm, key_ref, iv_ref) = build_vm(key, iv);
    561
    562        let keystream = match alloc_keystream(&mut vm, key_ref, iv_ref, &ranges, &records) {
    563            Ok(_) if is_out_of_bounds => panic!("should be out of bounds"),
    564            Ok(keystream) => keystream,
    565            Err(PlaintextAuthError(ErrorRepr::OutOfBounds)) if is_out_of_bounds => {
    566                return;
    567            }
    568            Err(e) => panic!("unexpected error: {:?}", e),
    569        };
    570
    571        vm.execute(&mut ctx).await.unwrap();
    572
    573        let keystream: Vec<u8> = keystream
    574            .iter()
    575            .flat_map(|slice| vm.get(*slice).unwrap().unwrap())
    576            .collect();
    577
    578        assert_eq!(keystream.len(), ranges.len());
    579
    580        let expected = expected_aes_ctr(key, iv, &records, &ranges);
    581
    582        assert_eq!(keystream, expected);
    583    }
    584
    585    #[rstest]
    586    #[case::single_record_empty([0])]
    587    #[case::single_record([32])]
    588    #[case::multiple_records([128, 63, 33, 15, 4])]
    589    #[case::multiple_records_with_empty([128, 63, 33, 0, 15, 4])]
    590    fn test_verify_plaintext_with_key(
    591        #[case] record_lens: impl IntoIterator<Item = usize>,
    592        #[values(false, true)] tamper: bool,
    593    ) {
    594        let mut rng = StdRng::seed_from_u64(0);
    595        let mut key = [0u8; 16];
    596        let mut iv = [0u8; 4];
    597        rng.fill(&mut key);
    598        rng.fill(&mut iv);
    599
    600        let mut total_len = 0;
    601        let records = record_lens
    602            .into_iter()
    603            .map(|len| {
    604                let mut explicit_nonce = [0u8; 8];
    605                rng.fill(&mut explicit_nonce);
    606                total_len += len;
    607                RecordParams {
    608                    explicit_nonce: explicit_nonce.to_vec(),
    609                    len,
    610                }
    611            })
    612            .collect::<Vec<_>>();
    613
    614        let mut plaintext = vec![0u8; total_len];
    615        rng.fill(plaintext.as_mut_slice());
    616
    617        let mut ciphertext = plaintext.clone();
    618        expected_aes_ctr(key, iv, &records, &(0..total_len).into())
    619            .iter()
    620            .zip(ciphertext.iter_mut())
    621            .for_each(|(key, pt)| {
    622                *pt ^= *key;
    623            });
    624
    625        if tamper {
    626            plaintext.first_mut().map(|pt| *pt ^= 1);
    627        }
    628
    629        match verify_plaintext_with_key(key, iv, &records, &plaintext, &ciphertext) {
    630            Ok(_) if tamper && !plaintext.is_empty() => panic!("should be invalid"),
    631            Err(e) if !tamper => panic!("unexpected error: {:?}", e),
    632            _ => {}
    633        }
    634    }
    635}