Skip to content

Migration from DudeCT

If you’re familiar with DudeCT, this guide shows how to translate your existing patterns to tacet.

DudeCTtacet
t-statistic (larger = worse)Probability of leak (0-100%)
Fixed threshold (t > 4.5)Attacker model presets
More samples → more false positivesMore samples → more confidence
Manual result interpretationFour-outcome enum with diagnostics
C libraryRust library with C/Go/JS bindings

DudeCT outputs a t-statistic. A t > 4.5 traditionally indicates a leak, but:

  • The threshold is somewhat arbitrary
  • More samples can push t higher even for small effects
  • You need to interpret whether the effect is exploitable

tacet gives you:

  • Probability of leak: “72% chance the effect exceeds your threshold”
  • Effect size in nanoseconds: “~50ns timing difference”
  • Exploitability assessment: “Exploitable via HTTP/2 multiplexing”
typedef struct {
uint8_t *data;
size_t len;
} input_t;
void prepare_inputs(input_t *inputs, uint8_t *classes, int n) {
for (int i = 0; i < n; i++) {
if (classes[i] == 0) {
// Class 0: fixed input
memset(inputs[i].data, 0x00, inputs[i].len);
} else {
// Class 1: random input
random_fill(inputs[i].data, inputs[i].len);
}
}
}
uint8_t do_one_computation(uint8_t *data) {
// Perform the operation to test
return crypto_operation(data);
}
dudect_ctx_t ctx;
dudect_init(&ctx, do_one_computation);
for (int i = 0; i < num_measurements; i++) {
prepare_inputs(inputs, classes, BATCH_SIZE);
dudect_run(&ctx, inputs, classes, BATCH_SIZE);
}
// Check t-statistic
if (ctx.t > 4.5) {
printf("Timing leak detected (t = %.2f)\n", ctx.t);
}
dudect_aes.c
#include "dudect.h"
#include <aes.h>
static uint8_t key[16] = { /* ... */ };
uint8_t do_one_computation(uint8_t *data) {
uint8_t block[16];
memcpy(block, data, 16);
aes_encrypt(block, key);
return block[0]; // Return something to prevent optimization
}
int main() {
dudect_ctx_t ctx;
dudect_init(&ctx, do_one_computation);
while (1) {
// ... prepare inputs and run measurements ...
if (ctx.t > 4.5) {
printf("Leak detected (t = %.2f)\n", ctx.t);
break;
}
if (ctx.total_measurements > 1000000) {
printf("No leak detected after 1M measurements\n");
break;
}
}
}
use tacet::{TimingOracle, AttackerModel, helpers::InputPair};
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
use aes::Aes128;
#[test]
fn test_aes_constant_time() {
let key = GenericArray::from([0x42u8; 16]);
let cipher = Aes128::new(&key);
let inputs = InputPair::new(
|| [0u8; 16],
|| rand::random::<[u8; 16]>(),
);
let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
.test(inputs, |data| {
let mut block = GenericArray::clone_from_slice(data);
cipher.encrypt_block(&mut block);
std::hint::black_box(block);
});
println!("{outcome}");
assert!(outcome.passed(), "Timing leak detected");
}
DudeCT t-statisticInterpretationtacet equivalent
t < 4.5No leakOutcome::Pass (P < 5%)
t > 4.5Leak detectedOutcome::Fail (P > 95%)
BorderlineUnclearOutcome::Inconclusive

In DudeCT, collecting more samples can increase t even for tiny effects that aren’t security-relevant. You might get t = 10 for a 0.1ns difference that no attacker could exploit.

In tacet, more samples means more confidence in the same answer. The probability converges toward the true state: either there’s an exploitable leak or there isn’t.

DudeCT uses a fixed t = 4.5 threshold. tacet lets you specify your threat model:

// "Is there a leak exploitable by a LAN attacker?" (100ns threshold)
TimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
// "Is there a leak exploitable with shared hardware?" (0.6ns threshold)
TimingOracle::for_attacker(AttackerModel::SharedHardware)
// "Is there a leak exploitable over the internet?" (50μs threshold)
TimingOracle::for_attacker(AttackerModel::RemoteNetwork)

This is a more meaningful question than “is there any statistical difference?”

What’s different about the two-class pattern

Section titled “What’s different about the two-class pattern”

Both DudeCT and tacet use the two-class pattern (fixed vs random inputs). The key insight is the same: if code is constant-time, both classes take the same time.

However, tacet’s documentation emphasizes:

  • For comparison functions, baseline must match the secret
  • For most crypto, zeros vs random is the right pattern

See The Two-Class Pattern for details.

If you’re using the dudect-bencher Rust crate, the migration is similar but with different type names:

use dudect_bencher::{ctbench_main, BenchRng, Class, CtRunner};
use rand::Rng;
fn vec_eq(runner: &mut CtRunner, rng: &mut BenchRng) {
let mut inputs = Vec::new();
let mut classes = Vec::new();
for _ in 0..10000 {
let v1: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
if rng.gen::<bool>() {
let v2 = v1.clone();
inputs.push((v1, v2));
classes.push(Class::Left);
} else {
let mut v2 = v1.clone();
v2[5] = 0xFF;
inputs.push((v1, v2));
classes.push(Class::Right);
}
}
for (class, (a, b)) in classes.into_iter().zip(inputs) {
runner.run_one(class, || a == b);
}
}
ctbench_main!(vec_eq);

Key differences from dudect-bencher:

dudect-benchertacet
Class::Left / Class::RightBaseline / Sample closures
runner.run_one(class, || ...)oracle.test(inputs, |data| ...)
ctbench_main! macro, runs foreverStandard #[test], adaptive stopping
t-statistic outputProbability + effect size