wolf_crypto/ct.rs
1//! Constant-Time Programming Utilities
2
3#[cfg(llvm_ir_check)]
4extern crate core;
5
6use core::hint::black_box;
7#[cfg(not(llvm_ir_check))]
8use crate::opaque_res::Res;
9
10#[cfg(llvm_ir_check)]
11pub struct Res(pub bool);
12#[cfg(not(llvm_ir_check))]
13use crate::buf::InvalidSize;
14#[cfg(llvm_ir_check)]
15pub struct InvalidSize;
16
17macro_rules! smear {
18 ($b:ident) => {{
19 $b |= $b >> 1;
20 $b |= $b >> 2;
21 $b |= $b >> 4;
22 $b |= $b >> 8;
23 $b |= $b >> 16;
24 }};
25}
26
27/// Performs a constant-time greater-than comparison.
28///
29/// # Arguments
30///
31/// * `left` - The left-hand side operand.
32/// * `right` - The right-hand side operand.
33///
34/// # Returns
35///
36/// Returns `1` if `left > right`, otherwise `0`.
37///
38/// # Constant Time Verification
39///
40/// To verify that this function is truly constant-time we leveraged `haybale-pitchfork` by
41/// UCSD `PLSysSec`.
42///
43/// ## Debug Build
44///
45/// This project only supports llvm-14, so the original llvm-18 bitcode required slight manual
46/// modifications. These modifications were only in removing LLVM's opaque pointer, replacing it
47/// with the associated LLVM 15 and below pointer type (i32*).
48///
49/// You can find the original LLVM and the translation which we verified in the llvm directory in
50/// this crate's manifest directory.
51///
52/// ### Results
53///
54/// ```txt
55/// Results for gt:
56///
57/// verified paths: 1
58/// constant-time violations found: 0
59///
60/// Coverage stats:
61///
62/// Block coverage of toplevel function (gt): 100.0%
63///
64///
65/// gt is constant-time
66/// ```
67///
68/// ## Optimized Build
69///
70/// In the optimized build, the LLVM IR contained no usage of opaque pointers, though the return
71/// type was unsupported. So, the returning of the result was removed. Though in a separate check
72/// this was confirmed to be constant time on its own.
73///
74/// ### Results
75///
76/// ```txt
77/// Results for gt:
78///
79/// verified paths: 1
80/// constant-time violations found: 0
81///
82/// Coverage stats:
83///
84/// Block coverage of toplevel function (gt): 100.0%
85///
86///
87/// gt is constant-time
88/// ```
89///
90/// # Functional Correctness
91///
92/// This was verified to be correct using `kani` with the assurance of completeness. You can
93/// find the associated proof at the bottom of this file.
94#[cfg_attr(llvm_ir_check, no_mangle)]
95pub const fn gt(left: u32, right: u32) -> u32 {
96 let gtb = left & !right;
97 let mut ltb = !left & right;
98
99 smear!(ltb);
100
101 let mut bit = gtb & !ltb;
102 // smear the highest set bit
103 smear!(bit);
104
105 bit & 1
106}
107
108#[inline(always)]
109const fn create_mask(overflow: u32) -> u32 {
110 !overflow.wrapping_neg()
111}
112
113#[inline(always)]
114const fn mask_add(left: u32, right: u32, mask: u32) -> u32 {
115 left.wrapping_add(right & mask)
116}
117
118/// Performs constant-time addition without wrapping on overflow.
119///
120/// # Arguments
121///
122/// * `a` - The first operand.
123/// * `b` - The second operand.
124///
125/// # Returns
126///
127/// A tuple containing the sum and a `Res` indicating if there was no overflow.
128///
129/// # Constant Time Verification
130///
131/// See the above [`gt`] functions Constant Time Verification section for more details regarding
132/// the setup, as the process is equivalent.
133///
134/// ## Results
135///
136/// ```txt
137/// Results for add_no_wrap:
138///
139/// verified paths: 1
140/// constant-time violations found: 0
141///
142/// Coverage stats:
143///
144/// Block coverage of toplevel function (add_no_wrap): 100.0%
145///
146///
147/// add_no_wrap is constant-time
148/// ```
149#[cfg_attr(not(llvm_ir_check), inline)]
150#[cfg_attr(llvm_ir_check, no_mangle)]
151#[cfg_attr(not(feature = "allow-non-fips"), allow(dead_code))]
152pub fn add_no_wrap(a: u32, b: u32) -> (u32, Res) {
153 let overflow = gt(b, u32::MAX.wrapping_sub(a));
154
155 // LLVM and rustc is ridiculous
156 //
157 // So, this:
158 // let sum = a.wrapping_add(b & (!overflow.wrapping_neg()));
159 //
160 // Got "optimized" (if you want to call pipeline bubbles optimization) into LLVM's select.
161 // LLVM's select WITHOUT optimizations (what??) was performed with the bitmask as I wrote.
162 //
163 // Now, WITH optimizations LLVM's select got "optimized" into the following instructions:
164 // - testl $65537, %ecx
165 // - cmovel %esi, %eax
166 // - sete %dl
167 //
168 // Great optimizations! Violated all constant time properties, and there isn't a chance in
169 // hell that this is faster.
170 //
171 // black_box fixed this, there is no longer any select instruction in the IR, but this is still
172 // quite annoying.
173 let sum = mask_add(a, b, black_box(create_mask(overflow)));
174 (sum, Res(overflow as u8 == 0))
175}
176
177#[inline(always)]
178fn volatile(byte: u8) -> u8 {
179 unsafe { core::ptr::read_volatile(&byte) }
180}
181
182#[inline(always)]
183fn eq_hsb(xor: u8) -> u8 {
184 volatile(xor | volatile(xor.wrapping_neg())) >> 7
185}
186
187#[cfg_attr(not(llvm_ir_check), inline(always))]
188#[cfg_attr(llvm_ir_check, no_mangle)]
189pub fn byte_eq(a: u8, b: u8) -> u8 {
190 // LLVM IR without optimizations:
191 // ; Function Attrs: nonlazybind uwtable
192 // define i8 @byte_eq(i8 %a, i8 %b) unnamed_addr #1 {
193 // start:
194 // %xor = xor i8 %a, %b
195 // %_0.i = sub i8 0, %xor
196 // %_5 = or i8 %xor, %_0.i
197 // %set_hsb = lshr i8 %_5, 7
198 // %_0 = xor i8 %set_hsb, 1
199 // ret i8 %_0
200 // }
201 //
202 // LLVM IR with optimizations:
203 // ; Function Attrs: mustprogress nofree norecurse nosync nounwind nonlazybind willreturn memory(none) uwtable
204 // define noundef i8 @byte_eq(i8 noundef %a, i8 noundef %b) unnamed_addr #0 {
205 // start:
206 // %0 = icmp eq i8 %a, %b
207 // %_0 = zext i1 %0 to i8
208 // ret i8 %_0
209 // }
210 //
211 // OK so with optimizations it seems our constant time properties are being violated, that's
212 // obviously unacceptable.
213 //
214 // Issue was in the original inlined:
215 //
216 // (xor | xor.wrapping_neg()) >> 7
217 //
218 // Using a volatile read on the result of (xor | xor.wrapping_neg()) prevented our constant
219 // time properties from being violated.
220 //
221 // Output assembly is also constant time:
222 //
223 // xorl %esi, %edi
224 // movl %edi, %eax
225 // negb %al
226 // orb %dil, %al
227 // movb %al, -1(%rsp)
228 // movzbl -1(%rsp), %eax
229 // notb %al
230 // shrb $7, %al
231 // retq
232 //
233 // Most concerning instruction to me is MOVZLB, but this is in general considered constant
234 // time.
235 //
236 // Output assembly for debug builds is constant time as well:
237 //
238 // movb %sil, %al
239 // movb %dil, %cl
240 // xorb %cl, %al
241 // xorl %ecx, %ecx
242 // subb %al, %cl
243 // orb %cl, %al
244 // movb %al, 7(%rsp)
245 // leaq 7(%rsp), %rdi
246 // callq *_ZN4core3ptr13read_volatile17h7014fc51810b013aE@GOTPCREL(%rip)
247 // shrb $7, %al
248 // xorb $1, %al
249 // popq %rcx
250 //
251 // OK, we are ready for verification of these constant time properties.
252
253 eq_hsb(b ^ a) ^ volatile(1)
254}
255
256macro_rules! unroll_ct_cmp {
257 (g2 $start:expr, $result:ident, $left:ident, $right:ident) => {{
258 // I am using unchecked operations so that I can focus on what matters in the assembly for
259 // debug builds without all of the noise of bounds checking. My usage is sound, and the
260 // caller must wrap this macro in unsafe.
261 $result &= byte_eq(*$left.get_unchecked($start), *$right.get_unchecked($start));
262 $result &= byte_eq(
263 *$left.get_unchecked($start.wrapping_add(1)),
264 *$right.get_unchecked($start.wrapping_add(1))
265 );
266 }};
267 (g4 $start:expr, $result:ident, $left:ident, $right:ident) => {
268 unroll_ct_cmp!(g2 $start, $result, $left, $right);
269 unroll_ct_cmp!(g2 $start.wrapping_add(2) as usize, $result, $left, $right);
270 };
271 (g8 $start:expr, $result:ident, $left:ident, $right:ident) => {{
272 unroll_ct_cmp!(g4 $start, $result, $left, $right);
273 unroll_ct_cmp!(g4 $start + 4, $result, $left, $right);
274 }};
275 (g16 $start:expr, $result:ident, $left:ident, $right:ident) => {{
276 unroll_ct_cmp!(g8 $start, $result, $left, $right);
277 unroll_ct_cmp!(g8 $start + 8, $result, $left, $right);
278 }}
279}
280
281#[cfg_attr(llvm_ir_check, no_mangle)]
282pub unsafe fn cmp_bytes_4_unchecked(mut res: u8, a: &[u8], b: &[u8]) -> u8 {
283 unroll_ct_cmp!(g4 0usize, res, a, b);
284 res
285}
286
287/// Compare two slices in constant-time.
288///
289/// # Note
290///
291/// If the length of slice `a` and slice `b` are not equivalent, this will exit early. In short,
292/// there is variable timing on length comparisons.
293///
294/// # Warning
295///
296/// Constant-time programming is nuanced, this implementation provides a *best-effort*
297/// constant-time equivalence check. While tools for verifying constant time properties over LLVM
298/// bitcode, a great deal of testing, paired with manual review of the output assembly,
299/// build some degree of confidence, there is still no guarantee of constant-time properties across
300/// all existing hardware.
301///
302/// # Returns
303///
304/// * `0`: `a != b`
305/// * `1`: `a == b`
306#[cfg_attr(llvm_ir_check, no_mangle)]
307#[must_use]
308pub fn cmp_slice(a: &[u8], b: &[u8]) -> u8 {
309 if a.len() != b.len() { return 0 }
310
311 let mut rem = a.len();
312 let mut res = volatile(1u8);
313
314 while rem >= 4 {
315 // loop invariant (added post ct analysis).
316 debug_assert!(rem <= a.len());
317
318 let next = rem.wrapping_sub(4);
319 // Again, bounds checking branches in debug builds are quite noisy, they get in the way
320 // of reviewing the assembly for genuine constant-time violations. My usage is sound,
321 // see the tests below if you are curious.
322 res &= unsafe {
323 cmp_bytes_4_unchecked(
324 res,
325 a.get_unchecked(next..rem),
326 b.get_unchecked(next..rem)
327 )
328 };
329
330 rem = next;
331 }
332
333 match rem {
334 3 => unsafe {
335 res &= byte_eq(*a.get_unchecked(0), *b.get_unchecked(0));
336 unroll_ct_cmp!(g2 rem.wrapping_sub(2), res, a, b);
337 }
338 2 => unsafe { unroll_ct_cmp!(g2 rem.wrapping_sub(2), res, a, b) },
339 1 => unsafe { res &= byte_eq(*a.get_unchecked(0), *b.get_unchecked(0)) },
340 _ => {}
341 }
342
343 res
344}
345
346/// Compare two slices in constant-time.
347///
348/// # Arguments
349///
350/// The two arguments being compared in constant-time, both of these arguments must implement
351/// `AsRef<[u8]>` (such as `&str`, `&[u8]` itself, etc.)
352///
353/// # Note
354///
355/// If the length of slice `a` and slice `b` are not equivalent, this will exit early. In short,
356/// there is variable timing on length comparisons.
357///
358/// # Warning
359///
360/// Constant-time programming is nuanced, this implementation provides a *best-effort*
361/// constant-time equivalence check. While tools for verifying constant time properties over LLVM
362/// bitcode, a great deal of testing, paired with manual review of the output assembly,
363/// build some degree of confidence, there is still no guarantee of constant-time properties across
364/// all existing hardware.
365///
366/// # Returns
367///
368/// `true` if `a == b`, `false` otherwise.
369#[must_use]
370pub fn ct_eq<A: AsRef<[u8]>, B: AsRef<[u8]>>(a: A, b: B) -> bool {
371 cmp_slice(a.as_ref(), b.as_ref()) != 0
372}
373
374#[must_use]
375#[inline]
376pub const fn hex_encode_len(len: usize) -> usize {
377 len << 1
378}
379
380#[inline]
381fn encode_byte(byte: u8, output: &mut [u8]) {
382 let lower = (byte & 0xf) as u32;
383 let upper = (byte >> 4) as u32;
384
385 let h =
386 87u32.wrapping_add(lower)
387 .wrapping_add(lower.wrapping_sub(10u32).wrapping_shr(8) & !38u32)
388 .wrapping_shl(8)
389 |
390 87u32.wrapping_add(upper)
391 .wrapping_add(upper.wrapping_sub(10u32).wrapping_shr(8) & !38u32);
392
393 // truncate
394 output[0] = h as u8;
395 // get highest byte
396 output[1] = h.wrapping_shr(8) as u8;
397}
398
399/*
400 Hex Encode to the str Type Considerations.
401
402 While we are certain we are always producing valid utf8, using str::from_utf8 is still risky
403 due to its data dependent branches.
404
405 So, it is best to avoid this in favor of the unsafe alternative, though this comes with risks,
406 and these risks must be addressed via verification. While we already have a property test which
407 checks that the hex_encode function always produces utf8 with 1 million inputs, this is simply
408 not enough.
409
410 The challenge with using a CBMC approach with some degree of completeness is unwinding. If we
411 were to verify for all slices that hex_encode produces utf8 this would be incredibly
412 resource intensive, and a consumer grade PC simply cannot hold up here.
413
414 But this is thinking about the problem incorrectly, as what really matters is does the encoding
415 of a single byte always produce valid utf8. If that is guaranteed, then the correctness of
416 hex_encode can be guaranteed via structural induction over the input slice.
417
418 The inductive argument would be as follows:
419 1. Base case: Prove that encoding a single byte always produces valid UTF-8.
420 2. Inductive step: Assume encoding n bytes produces valid UTF-8, then prove that
421 encoding n+1 bytes also produces valid UTF-8.
422
423 This approach allows us to reason about the correctness of hex_encode for inputs
424 of arbitrary length without needing to verify all possible inputs exhaustively.
425*/
426
427/// Constant-Time Hex Encoding
428///
429/// # Arguments
430///
431/// * `input` - A slice of bytes to be hex-encoded.
432/// * `output` - The buffer to write the hex string into.
433///
434/// # Returns
435///
436/// The number of bytes written to the `output` buffer (which will always be twice the length of
437/// the input).
438///
439/// # Errors
440///
441/// This function returns `Err(InvalidSize)` if the `output` buffer has a length less than
442/// `input.len() * 2`.
443///
444/// # Security
445///
446/// This function is designed to operate in constant-time, avoiding data-dependent branches that
447/// could lead to timing side-channel attacks. The encoding process uses arithmetic and bitwise
448/// operations to guarantee uniform execution time regardless of the input values.
449///
450/// # UTF-8 Considerations
451///
452/// Rust's `str::from_utf8` is discouraged in contexts that require constant-time execution due
453/// to the presence of data-dependent branches plus lookup tables in its implementation.
454///
455/// **It is safe to use `str::from_utf8_unchecked` on the output of this function**, however,
456/// **it is only safe over the encoded region of the output buffer** (up until the returned
457/// length). If you need a `str` representation, consider the less error-prone [`encode_str`]
458/// function.
459///
460/// If you are curious / skeptical of the safety in this recommendation, please read the
461/// verification section below.
462///
463/// # Example
464///
465/// ```
466/// use wolf_crypto::{hex, ct_eq};
467///
468/// let mut output = [0u8; 22]; // 11 * 2
469/// let len = hex::encode_into(b"hello world", &mut output).unwrap();
470///
471/// assert_eq!(len, 22);
472/// assert!(
473/// // since we are using a constant-time encoding, we probably also want
474/// // to use a constant-time comparison.
475/// ct_eq(
476/// // SAFETY: The encoded output is formally guaranteed to be valid
477/// // UTF-8. This is just for example, ct_eq can of course compare slices.
478/// unsafe { core::str::from_utf8_unchecked(&output[..len]) },
479/// "68656c6c6f20776f726c64"
480/// )
481/// );
482///
483/// // or use the `encode_str` function, which is simply a less error-prone variant of this
484/// // if `str` representation is a requirement.
485///
486/// let mut output = [0u8; 22];
487/// let str_repr = hex::encode_str(b"hello world", &mut output).unwrap();
488///
489/// // ...
490/// ```
491///
492/// # Verification
493///
494/// The correctness and safety of the `encode_into` function have been rigorously verified using a
495/// combination of formal methods and property-based testing. This multi-faceted approach ensures
496/// a high degree of confidence in the function's behavior and its adherence to the specified
497/// properties.
498///
499/// ## Formal Verification
500///
501/// We employed formal verification techniques using the Kani Rust Verifier to prove key properties
502/// of the `encode_into` function and its components.
503///
504/// ### Inductive Proof Strategy
505///
506/// The verification process follows an inductive proof strategy:
507///
508/// 1. Base Case: We prove that encoding a single byte always produces valid UTF-8.
509/// 2. Inductive Step: We prove that if encoding n bytes produces valid UTF-8, then encoding
510/// n+1 bytes also produces valid UTF-8.
511///
512/// This approach allows us to reason about the correctness of `encode_into` for inputs of arbitrary
513/// length without needing to verify all possible inputs exhaustively.
514///
515/// ### Key Proofs
516///
517/// 1. Single Byte Encoding:
518/// The `encode_byte_is_always_utf8` proof verifies that for any input byte, the output of
519/// `encode_byte` is always valid hexadecimal (and thus valid UTF-8).
520///
521/// 2. Inductive Step:
522/// - The `verify_encode_n_plus_1_bytes_symbolic` proof demonstrates that if encoding n bytes
523/// produces valid UTF-8, encoding an additional byte preserves this property.
524/// - The `verify_hex_encode_inductive_step_symbolic` proof applies this principle to the
525/// `encode_into` function itself, verifying that the UTF-8 validity is maintained for
526/// arbitrary input lengths.
527///
528/// [`encode_str`]: crate::hex::encode_str
529#[cfg_attr(llvm_ir_check, no_mangle)]
530pub fn hex_encode(input: &[u8], output: &mut [u8]) -> Result<usize, InvalidSize> {
531 let hex_len = hex_encode_len(input.len());
532
533 #[cfg(any(check, kani, test))] {
534 ensure!((hex_len == 0) <==> (input.len() == 0));
535 ensure!((hex_len != 0) <==> (input.len() != 0));
536 }
537
538 if output.len() < hex_len { return Err(InvalidSize) }
539
540 #[cfg(any(check, kani, test))]
541 let mut post_len = 0usize;
542
543 for (pos, byte) in input.iter().enumerate() {
544 let o_pos = pos.wrapping_shl(1);
545
546 #[cfg(any(check, kani, test))] {
547 ensure!((pos != 0) ==> (is_valid_hex_2(output[o_pos - 2], output[o_pos - 1])));
548 post_len = o_pos + 1;
549 }
550
551 encode_byte(*byte, &mut output[o_pos..o_pos + 2]);
552 }
553
554 #[cfg(any(check, kani, test))] {
555 ensure!((hex_len != 0) ==> (is_valid_hex_2(output[post_len - 1], output[post_len])));
556 ensure!((hex_len != 0) ==> (post_len + 1 == hex_len));
557 }
558
559 Ok(hex_len)
560}
561
562/// Constant-Time Hex Encoding to a `&str`
563///
564/// # Arguments
565///
566/// * `input` - A slice of bytes to be hex-encoded.
567/// * `output` - A mutable byte buffer where the encoded string will be written.
568///
569/// # Returns
570///
571/// The encoded string slice (`&str`).
572///
573/// # Errors
574///
575/// If the `output` buffer is less than the encoded length (`input.len() * 2`), this returns
576/// `InvalidSize`.
577///
578/// # Notes
579///
580/// This function is a convenience wrapper around [`encode_into`], which handles the
581/// encoding process, this simply returns the `str` representation in a safe manner.
582///
583/// For full details on the underlying encoding and security/safety guarantees, see [`encode_into`].
584///
585/// # Example
586///
587/// ```
588/// use wolf_crypto::{hex, ct_eq};
589///
590/// let mut output = [0u8; 22]; // 11 * 2
591/// let str_repr = hex::encode_str(b"hello world", &mut output).unwrap();
592///
593/// assert!(ct_eq(str_repr, "68656c6c6f20776f726c64"));
594/// ```
595///
596/// [`encode_into`]: crate::hex::encode_into
597#[inline]
598pub fn hex_encode_str<'o>(input: &[u8], output: &'o mut [u8]) -> Result<&'o str, InvalidSize> {
599 hex_encode(input, output)
600 // SAFETY: See the verification section of `hex_encode` and the proofs at the bottom
601 // of this module.
602 .map(move |len| unsafe { core::str::from_utf8_unchecked(&output[..len]) })
603}
604
605#[cfg(not(llvm_ir_check))]
606alloc! {
607 /// Constant-Time Hex Encoding
608 ///
609 /// # Arguments
610 ///
611 /// * `input` - A slice of bytes to be hex-encoded.
612 ///
613 /// # Returns
614 ///
615 /// The hex-encoded `String`
616 ///
617 /// # Example
618 ///
619 /// ```
620 /// use wolf_crypto::{hex, ct_eq};
621 ///
622 /// let encoded = hex::encode(b"hello world");
623 /// let decoded = hex::decode(encoded.as_bytes()).unwrap();
624 ///
625 /// assert!(ct_eq(b"hello world", decoded));
626 /// ```
627 #[allow(clippy::missing_panics_doc)] // this is infallible.
628 pub fn hex_encode_alloc(input: &[u8]) -> alloc::string::String {
629 let mut output = vec![0u8; hex_encode_len(input.len())];
630 hex_encode(input, output.as_mut_slice()).unwrap(/* Infallible */);
631 // We may ignore the returned output length, as we provided a vector with the exact size
632 // of the output
633 unsafe {
634 alloc::string::String::from_utf8_unchecked(output)
635 }
636 }
637}
638
639#[inline]
640const fn decode_nibble(first: u8) -> u16 {
641 let byte = first as i16;
642 let mut ret: i16 = -1;
643
644 ret = ret.wrapping_add(
645 (0x2fi16.wrapping_sub(byte) & byte.wrapping_sub(0x3a)).wrapping_shr(8)
646 & byte.wrapping_sub(47)
647 );
648
649 ret = ret.wrapping_add(
650 (0x60i16.wrapping_sub(byte) & byte.wrapping_sub(0x67)).wrapping_shr(8)
651 & byte.wrapping_sub(86)
652 );
653
654 ret as u16
655}
656
657#[must_use]
658const fn decode_predicate(inp_len: usize, out_len: usize) -> (bool, usize) {
659 let dec_len = inp_len >> 1;
660 (inp_len & 1 == 0 && out_len >= dec_len, dec_len)
661}
662
663/// Possible errors while decoding a hex string.
664pub enum HexError {
665 /// An invalid character was encountered or the length of the hex-encoded data was invalid.
666 Encoding,
667 /// The output size was not large enough.
668 Size
669}
670
671impl From<InvalidSize> for HexError {
672 fn from(_value: InvalidSize) -> Self {
673 Self::Size
674 }
675}
676
677#[cfg(not(llvm_ir_check))]
678impl From<HexError> for crate::Unspecified {
679 fn from(_value: HexError) -> Self {
680 Self
681 }
682}
683
684#[cfg(not(llvm_ir_check))]
685impl core::fmt::Display for HexError {
686 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
687 match self {
688 Self::Size => f.write_str("HexError::Size"),
689 Self::Encoding => f.write_str("HexError::Encoding")
690 }
691 }
692}
693
694#[cfg(not(llvm_ir_check))]
695impl core::fmt::Debug for HexError {
696 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
697 <Self as core::fmt::Display>::fmt(self, f)
698 }
699}
700
701#[cfg(not(llvm_ir_check))]
702std! { impl std::error::Error for HexError {} }
703
704/// Constant-Time Hex Decoding
705///
706/// # Arguments
707///
708/// * `input` - The hex-encoded slice to decode into `output`.
709/// * `output` - The output buffer which the `input` is decoded into. This must be at least
710/// `input.len() / 2` in size.
711///
712/// # Errors
713///
714/// - [`HexError::Size`]: The output could not fit the decoded input, or the input was of invalid
715/// length.
716/// - [`HexError::Encoding`]: An invalid character was encountered.
717///
718/// # Returns
719///
720/// The amount of data which was decoded (`input.len() / 2`).
721pub fn hex_decode(input: &[u8], output: &mut [u8]) -> Result<usize, HexError> {
722 let (valid_len, dec_len) = decode_predicate(input.len(), output.len());
723 if !valid_len { return Err(HexError::Size) }
724
725 let mut err: u16 = 0;
726
727 // `take` to guard against the length being zero
728 for (pos, o_byte) in output.iter_mut().enumerate().take(dec_len) {
729 let src_pos = pos << 1;
730
731 let byte = decode_nibble(input[src_pos]).wrapping_shl(4)
732 | decode_nibble(input[src_pos + 1]);
733
734 err |= byte >> 8;
735
736 *o_byte = byte as u8;
737 }
738
739 if err == 0 {
740 Ok(dec_len)
741 } else {
742 Err(HexError::Encoding)
743 }
744}
745
746#[cfg(not(llvm_ir_check))]
747alloc! {
748 /// Constant-Time Hex Decoding
749 ///
750 /// # Arguments
751 ///
752 /// * `input` - The hex-encoded slice to decode.
753 ///
754 /// # Errors
755 ///
756 /// - [`HexError::Size`]: The `input` length was not an even number.
757 /// - [`HexError::Encoding`]: An invalid character was encountered.
758 ///
759 /// # Returns
760 ///
761 /// The decoded bytes.
762 ///
763 /// # Example
764 ///
765 /// ```
766 /// use wolf_crypto::{hex, ct_eq};
767 ///
768 /// let encoded = hex::encode(b"hello world");
769 /// let decoded = hex::decode(encoded.as_bytes()).unwrap();
770 ///
771 /// assert!(ct_eq(b"hello world", decoded));
772 /// ```
773 pub fn hex_decode_alloc(input: &[u8]) -> Result<Vec<u8>, HexError> {
774 let mut output = vec![0u8; input.len() >> 1];
775 hex_decode(input, output.as_mut_slice()).map(move |_| output)
776 }
777}
778
779#[cfg(any(test, kani, check))]
780const fn is_valid_hex(byte: u8) -> bool {
781 matches!(
782 byte,
783 b'A'..=b'Z'
784 | b'a'..=b'z'
785 | b'0'..=b'9'
786 )
787}
788
789#[cfg(any(test, kani, check))]
790const fn is_valid_hex_2(first: u8, second: u8) -> bool {
791 is_valid_hex(first) && is_valid_hex(second)
792}
793
794#[cfg(any(kani, check))]
795const fn is_valid_hex_4(a: u8, b: u8, c: u8, d: u8) -> bool {
796 is_valid_hex_2(a, b) && is_valid_hex_2(c, d)
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802
803 #[test]
804 fn cmp_slice_eq_smoke() {
805 let a = [3u8; 17];
806 let b = [3u8; 17];
807
808 assert_eq!(cmp_slice(a.as_slice(), b.as_slice()), 1);
809 }
810
811 macro_rules! str {
812 ($expr:expr) => {
813 core::str::from_utf8($expr).unwrap()
814 };
815 }
816
817 #[test]
818 fn hex_encode_works() {
819 let mut out = [0u8; 22];
820 let len = hex_encode(b"hello world", &mut out).unwrap();
821 assert_eq!(len, 22);
822 assert_eq!(str!(&out), "68656c6c6f20776f726c64");
823 }
824
825 #[test]
826 fn hex_encode_to_decode() {
827 let mut out = [0u8; 22];
828 let _len = hex_encode(b"hello world", &mut out).unwrap();
829
830 let mut dec = [0u8; 11];
831 let read = hex_decode(&out, &mut dec).unwrap();
832
833 assert_eq!(read, 11);
834 assert_eq!(&dec, b"hello world");
835 }
836
837 #[test]
838 fn invalid_hex() {
839 let mut out = [0; 69];
840 assert!(hex_decode(b"hello world I am not valid hex !!!", &mut out).is_err());
841 }
842}
843
844#[cfg(test)]
845mod property_tests {
846 use super::*;
847 use proptest::prelude::*;
848 use crate::aes::test_utils::BoundList;
849
850 proptest! {
851 #![proptest_config(ProptestConfig::with_cases(100_000))]
852
853 #[test]
854 fn enusre_ct_add_no_wrap(a in any::<u32>(), b in any::<u32>()) {
855 let (out, res) = add_no_wrap(a, b);
856
857 ensure!(( res.is_err() ) <==> ( a.checked_add(b).is_none() ));
858 ensure!(( out == a ) <==> ( res.is_err() || b == 0 ));
859 ensure!(( res.is_ok() ) <==> ( out != a || b == 0 ));
860 }
861 }
862
863 proptest! {
864 #![proptest_config(ProptestConfig::with_cases(10_000))]
865
866 #[test]
867 fn ensure_cmp_slice(a in any::<BoundList<1024>>(), b in any::<BoundList<1024>>()) {
868 let ct_res = cmp_slice(a.as_slice(), b.as_slice()) == 1;
869 let res = a.as_slice() == b.as_slice();
870
871 ensure!((ct_res) <==> (res));
872 ensure!((!ct_res) <==> (!res));
873 }
874 }
875
876 proptest! {
877 #![proptest_config(ProptestConfig::with_cases(50_000))]
878
879 #[test]
880 fn hex_encode_is_hex_crate(
881 bin in any::<BoundList<1024>>()
882 ) {
883 let output_len = hex_encode_len(bin.len());
884 let mut output = BoundList::<2048>::new_zeroes(output_len);
885
886 let res = hex::encode(bin.as_slice());
887 let len = hex_encode(bin.as_slice(), output.as_mut_slice()).unwrap();
888
889 prop_assert_eq!(res.len(), len);
890 prop_assert_eq!(res.as_bytes(), output.as_slice());
891 }
892
893 #[test]
894 fn hex_is_bijective(
895 bin in any::<BoundList<1024>>()
896 ) {
897 let output_len = hex_encode_len(bin.len());
898 let mut output = BoundList::<2048>::new_zeroes(output_len);
899
900 let len = hex_encode(bin.as_slice(), output.as_mut_slice()).unwrap();
901
902 prop_assert_eq!(len, output.len());
903
904 let mut decoded = bin.create_self();
905 let len = hex_decode(output.as_slice(), decoded.as_mut_slice()).unwrap();
906
907 prop_assert_eq!(len, bin.len());
908 prop_assert_eq!(decoded.as_slice(), bin.as_slice());
909 }
910 }
911
912 // I'd run tests in release.
913 proptest! {
914 #![proptest_config(ProptestConfig::with_cases(1_000_000))]
915
916 #[test]
917 fn hex_encode_is_valid_utf8(
918 bin in any::<BoundList<256>>()
919 ) {
920 let output_len = hex_encode_len(bin.len());
921 let mut output = BoundList::<512>::new_zeroes(output_len);
922
923 hex_encode(bin.as_slice(), output.as_mut_slice()).unwrap();
924
925 prop_assert!(core::str::from_utf8(output.as_slice()).is_ok());
926 }
927 }
928}
929
930#[cfg(kani)]
931mod verify {
932 use super::*;
933 use kani::proof;
934
935 #[proof]
936 fn check_ct_add_no_wrap() {
937 let a = kani::any();
938 let b = kani::any();
939
940 let (out, res) = add_no_wrap(a, b);
941
942 ensure!(( res.is_err() ) <==> ( a.checked_add(b).is_none() ));
943 ensure!(( out == a ) <==> ( res.is_err() || b == 0 ));
944 ensure!(( res.is_ok() ) <==> ( out != a || b == 0 ));
945 }
946
947 #[proof]
948 fn check_ct_gt() {
949 let a = kani::any();
950 let b = kani::any();
951
952 let is_gt = gt(a, b) == 1;
953
954 ensure!((is_gt) <==> (a > b));
955 ensure!((!is_gt) <==> (a <= b));
956 }
957
958 // This has crashed my computer multiple times due to kani using something like 160gb of memory.
959 // Had to dial it down significantly.
960 #[proof]
961 #[kani::unwind(12)]
962 fn check_slice_cmp() {
963 let a = kani::vec::any_vec::<u8, 7>();
964 let b = kani::vec::any_vec::<u8, 7>();
965
966 let res = a == b;
967 let ct_res = cmp_slice(a.as_slice(), b.as_slice()) == 1;
968
969 ensure!((res) <==> (ct_res));
970 ensure!((!res) <==> (!ct_res));
971 }
972
973 #[proof]
974 fn encode_byte_is_always_utf8() {
975 let byte: u8 = kani::any();
976 let mut out = [0u8; 2];
977
978 encode_byte(byte, &mut out);
979
980 kani::assert(
981 is_valid_hex_2(out[0], out[1]),
982 "For all bytes, the output must always be valid UTF8"
983 );
984 }
985
986 #[proof]
987 fn verify_encode_n_plus_1_bytes_symbolic() {
988 // This proof symbolically represents the inductive step:
989 // If encoding n bytes produces valid UTF-8, then encoding n+1 bytes also produces valid
990 // UTF-8.
991
992 let byte: u8 = kani::any();
993 let mut out = [0u8; 4];
994
995 encode_byte(byte, &mut out);
996
997 let byte: u8 = kani::any();
998 encode_byte(byte, &mut out[2..]);
999
1000 kani::assert(
1001 is_valid_hex_2(out[0], out[1]) && is_valid_hex_2(out[2], out[3]),
1002 "Encoding an additional byte preserves UTF-8 validity"
1003 );
1004 }
1005
1006 // Now, we simply re-apply what we have done using encode byte individually to symbolically
1007 // prove the inductive step to the actual hex_encode function. This function already has
1008 // postconditions which are checked via kani, so we do not need to rewrite any part of the
1009 // specification here.
1010 //
1011 // Again, this is a symbolic proof, as for all slices would crash a computer and is logically
1012 // equivalent to symbolically proving the inductive step.
1013 #[proof]
1014 fn verify_hex_encode_inductive_step_symbolic() {
1015 // no real harm in doing more as long as our PC can handle it. However, there is no benefit
1016 // to this.
1017 let input: [u8; 6] = kani::any();
1018 let mut output = [0u8; 12];
1019
1020 hex_encode(&input, &mut output).unwrap();
1021
1022 kani::assert(
1023 is_valid_hex_4(output[0], output[1], output[2], output[3])
1024 && is_valid_hex_4(output[4], output[5], output[6], output[7])
1025 && is_valid_hex_4(output[8], output[9], output[10], output[11]),
1026 "Encoding an additional byte preserves UTF-8 validity"
1027 )
1028 }
1029}