Skip to main content

uselesskey_core_negative_der/
lib.rs

1#![forbid(unsafe_code)]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4//! DER corruption helpers for negative test fixtures.
5//!
6//! Provides deterministic truncation, byte-flipping, and combined corruption
7//! strategies for DER-encoded blobs. Used by higher-level negative fixture
8//! crates (`uselesskey-core-negative`) to generate invalid DER artifacts
9//! that exercise parser error paths in tests.
10
11extern crate alloc;
12
13use alloc::vec::Vec;
14
15use uselesskey_core_hash::hash32;
16
17/// Truncate `der` to at most `len` bytes, returning the original if already shorter.
18pub fn truncate_der(der: &[u8], len: usize) -> Vec<u8> {
19    if len >= der.len() {
20        return der.to_vec();
21    }
22    der[..len].to_vec()
23}
24
25/// XOR the byte at `offset` with `0x01`, returning the original if `offset` is out of range.
26pub fn flip_byte(der: &[u8], offset: usize) -> Vec<u8> {
27    if offset >= der.len() {
28        return der.to_vec();
29    }
30
31    let mut out = der.to_vec();
32    out[offset] ^= 0x01;
33    out
34}
35
36/// Choose a corruption strategy deterministically from `variant` and apply it to `der`.
37///
38/// The same `(der, variant)` pair always produces the same corrupted output.
39pub fn corrupt_der_deterministic(der: &[u8], variant: &str) -> Vec<u8> {
40    let digest = hash32(variant.as_bytes());
41    let bytes = digest.as_bytes();
42
43    match bytes[0] % 3 {
44        0 => {
45            let len = derived_truncate_len_bytes(der.len(), bytes);
46            truncate_der(der, len)
47        }
48        1 => {
49            let offset = derived_offset(der.len(), bytes[1]);
50            flip_byte(der, offset)
51        }
52        _ => {
53            let offset = derived_offset(der.len(), bytes[1]);
54            let flipped = flip_byte(der, offset);
55            let len = derived_truncate_len_bytes(flipped.len(), bytes);
56            truncate_der(&flipped, len)
57        }
58    }
59}
60
61fn derived_offset(len: usize, selector: u8) -> usize {
62    if len == 0 {
63        return 0;
64    }
65    selector as usize % len
66}
67
68fn derived_truncate_len_bytes(len: usize, digest: &[u8; 32]) -> usize {
69    if len <= 1 {
70        return 0;
71    }
72    let span = len - 1;
73    digest[2] as usize % span
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn flip_byte_changes_only_target_offset() {
82        let der = vec![0x30, 0x82, 0x01, 0x22];
83        let flipped = flip_byte(&der, 0);
84
85        assert_eq!(flipped[0], 0x31);
86        assert_eq!(&flipped[1..], &der[1..]);
87    }
88
89    #[test]
90    fn truncate_der_shortens_when_len_smaller() {
91        let der = vec![0x30, 0x82, 0x01, 0x22];
92        let truncated = truncate_der(&der, 2);
93        assert_eq!(truncated, vec![0x30, 0x82]);
94    }
95
96    #[test]
97    fn deterministic_der_corruption_is_stable_for_same_variant() {
98        let der = vec![0x30, 0x82, 0x01, 0x22, 0x10, 0x20];
99        let first = corrupt_der_deterministic(&der, "corrupt:variant-a");
100        let second = corrupt_der_deterministic(&der, "corrupt:variant-a");
101        assert_eq!(first, second);
102        assert_ne!(first, der);
103    }
104
105    #[test]
106    fn derived_truncate_len_bytes_exact_arithmetic() {
107        let mut digest = [0u8; 32];
108        digest[2] = 0x0B; // 11 % 4 = 3
109        assert_eq!(derived_truncate_len_bytes(5, &digest), 3);
110    }
111
112    #[test]
113    fn derived_truncate_len_bytes_single_returns_zero() {
114        let digest = [0u8; 32];
115        assert_eq!(derived_truncate_len_bytes(1, &digest), 0);
116    }
117
118    #[test]
119    fn derived_offset_exact_arithmetic() {
120        assert_eq!(derived_offset(5, 7), 2); // 7 % 5 = 2
121    }
122
123    #[test]
124    fn derived_offset_zero_len_returns_zero() {
125        assert_eq!(derived_offset(0, 7), 0);
126    }
127
128    #[test]
129    fn flip_byte_xor_vs_or_on_set_bit() {
130        // XOR: 0x01 ^ 0x01 = 0x00; OR mutation would give 0x01 | 0x01 = 0x01.
131        let data = vec![0x01];
132        let result = flip_byte(&data, 0);
133        assert_eq!(result[0], 0x00);
134    }
135
136    #[test]
137    fn deterministic_der_arm0_truncation() {
138        let der = vec![0x30, 0x82, 0x01, 0x22, 0x10, 0x20];
139        let variant = find_der_variant(0);
140        let out = corrupt_der_deterministic(&der, &variant);
141        assert!(out.len() < der.len());
142        assert_eq!(&out[..], &der[..out.len()]);
143    }
144
145    #[test]
146    fn deterministic_der_arm1_flip() {
147        let der = vec![0x30, 0x82, 0x01, 0x22, 0x10, 0x20];
148        let variant = find_der_variant(1);
149        let out = corrupt_der_deterministic(&der, &variant);
150        assert_eq!(out.len(), der.len());
151        let diffs = out.iter().zip(der.iter()).filter(|(a, b)| a != b).count();
152        assert_eq!(diffs, 1);
153    }
154
155    #[test]
156    fn deterministic_der_arm2_flip_truncate() {
157        let der = vec![0x30, 0x82, 0x01, 0x22, 0x10, 0x20];
158        let variant = find_der_variant(2);
159        let out = corrupt_der_deterministic(&der, &variant);
160        assert!(out.len() < der.len());
161    }
162
163    #[test]
164    fn deterministic_der_not_constant() {
165        let der1 = vec![0x30, 0x82, 0x01, 0x22, 0x10, 0x20];
166        let der2 = vec![0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA];
167        let out1 = corrupt_der_deterministic(&der1, "same-variant");
168        let out2 = corrupt_der_deterministic(&der2, "same-variant");
169        assert_ne!(out1, out2);
170    }
171
172    #[test]
173    fn truncate_der_noop_when_len_exceeds_input() {
174        let der = vec![0x30, 0x82];
175        let out = truncate_der(&der, 100);
176        assert_eq!(
177            out, der,
178            "truncate with len >= der.len() must return original"
179        );
180    }
181
182    #[test]
183    fn flip_byte_noop_when_offset_exceeds_input() {
184        let der = vec![0x30, 0x82];
185        let out = flip_byte(&der, 100);
186        assert_eq!(
187            out, der,
188            "flip with offset >= der.len() must return original"
189        );
190    }
191
192    #[test]
193    fn derived_truncate_len_bytes_nonzero_for_large_input() {
194        // Catches `return 0` and `return 1` mutations when tested
195        // through corrupt_der_deterministic arm 0.
196        let mut digest = [0u8; 32];
197        digest[2] = 50;
198        // len=10, span=9, 50%9=5
199        assert_eq!(derived_truncate_len_bytes(10, &digest), 5);
200    }
201
202    #[test]
203    fn derived_truncate_len_bytes_zero_len_returns_zero() {
204        let digest = [0xFF; 32];
205        assert_eq!(derived_truncate_len_bytes(0, &digest), 0);
206    }
207
208    fn find_der_variant(target: u8) -> String {
209        use uselesskey_core_hash::hash32;
210
211        for i in 0u64.. {
212            let variant = format!("v{i}");
213            if hash32(variant.as_bytes()).as_bytes()[0] % 3 == target {
214                return variant;
215            }
216        }
217        unreachable!()
218    }
219}