Skip to content

Go bindings for tacet.

package main
import (
"fmt"
"log"
"time"
tacet "github.com/agucova/tacet/crates/tacet-go"
)
func main() {
result, err := tacet.Test(
tacet.NewZeroGenerator(0), // Baseline: all zeros
tacet.FuncOperation(func(input []byte) {
myCryptoFunction(input)
}),
32, // Input size in bytes
tacet.WithAttacker(tacet.AdjacentNetwork),
tacet.WithTimeBudget(30 * time.Second),
)
if err != nil {
log.Fatal(err)
}
switch result.Outcome {
case tacet.Pass:
fmt.Printf("No leak: P(leak)=%.1f%%\n", result.LeakProbability*100)
case tacet.Fail:
fmt.Printf("Leak: %.1f ns (%s), %s\n",
result.Effect.MaxEffectNs,
result.Effect.TailDiagnostics.PatternLabel,
result.Exploitability)
case tacet.Inconclusive:
fmt.Printf("Inconclusive: %s\n", result.InconclusiveReason)
case tacet.Unmeasurable:
fmt.Printf("Too fast: %s\n", result.Recommendation)
}
}

Terminal window
go get github.com/agucova/tacet/crates/tacet-go
go run github.com/agucova/tacet/crates/tacet-go/cmd/tacet-install@latest

The install command downloads the pre-built static library for your platform (~12MB) and places it in the module cache where CGo can find it. This only needs to be run once.

Requirements: Go 1.22+ with CGo enabled.

Troubleshooting: If you see linker errors about missing tacet symbols, run the install command above. The native library must be installed before building.

PlatformArchitectureStatus
macOSARM64 (Apple Silicon)✅ Supported
macOSAMD64 (Intel)✅ Supported
LinuxARM64✅ Supported
LinuxAMD64✅ Supported

The library is statically linked, so binaries are self-contained with no runtime dependencies.


If you prefer to build the native library yourself instead of downloading pre-built binaries:

  • Rust toolchain (stable)
  • Go 1.22+ with CGo enabled
  • C compiler (clang or gcc)
Terminal window
# Clone the repository
git clone https://github.com/agucova/tacet
cd tacet
# Build the C library
cargo build -p tacet-c --release
# Strip debug symbols (reduces size from ~26MB to ~12MB)
strip -S target/release/libtacet_c.a # macOS
# or: strip --strip-debug target/release/libtacet_c.a # Linux
# Copy to the appropriate platform directory
mkdir -p crates/tacet-go/internal/ffi/lib/$(go env GOOS)_$(go env GOARCH)
cp target/release/libtacet_c.a \
crates/tacet-go/internal/ffi/lib/$(go env GOOS)_$(go env GOARCH)/
# Verify it works
cd crates/tacet-go
go test -v -short -run TestTimerWorks

To install a specific version of the library:

Terminal window
go run github.com/agucova/tacet/crates/tacet-go/cmd/tacet-install@latest -version=v0.2.3

func Test(gen Generator, op Operation, inputSize int, opts ...Option) (*Result, error)
ParameterTypeDescription
genGeneratorInput generator
opOperationOperation to test
inputSizeintInput buffer size
opts...OptionConfiguration options

func WithAttacker(model AttackerModel) Option
ModelThresholdUse Case
SharedHardware0.6 nsSGX, containers
PostQuantum3.3 nsPost-quantum crypto
AdjacentNetwork100 nsLAN, HTTP/2
RemoteNetwork50 μsInternet
Research0Detect any difference
func WithTimeBudget(d time.Duration) Option
func WithMaxSamples(n int) Option
OptionDefaultDescription
WithTimeBudget30sMaximum test time
WithMaxSamples100,000Max samples per class
func WithPassThreshold(p float64) Option // Default: 0.05
func WithFailThreshold(p float64) Option // Default: 0.95
func WithCustomThreshold(ns float64) Option

type Generator interface {
Generate(isBaseline bool, output []byte)
}
// Fixed zero bytes for baseline, random for sample
tacet.NewZeroGenerator(seed)
// Fixed value for baseline, random for sample
tacet.NewFixedGenerator(fixedValue, seed)
// Custom generator from function
tacet.FuncGenerator(func(isBaseline bool, output []byte) {
// ...
})
type Operation interface {
Execute(input []byte)
}
// From function
tacet.FuncOperation(func(input []byte) {
myCryptoFunction(input)
})

type Outcome int
const (
Pass Outcome = iota
Fail
Inconclusive
Unmeasurable
)
type Result struct {
Outcome Outcome
LeakProbability float64
Effect Effect
Quality Quality
Exploitability Exploitability
InconclusiveReason InconclusiveReason
SamplesUsed int
Recommendation string
}
type Effect struct {
MaxEffectNs float64 // L∞ norm of W₁ distance
CredibleInterval [2]float64 // 95% credible interval
TailDiagnostics TailDiagnostics // Shift-vs-tail decomposition
}

The TailDiagnostics structure decomposes the timing effect into uniform shift and tail components:

type TailDiagnostics struct {
ShiftNs float64 // Median of rank-matched differences
TailNs float64 // Mean absolute deviation from shift
TailShare float64 // Fraction of effect from tail (0.0-1.0)
TailSlowShare float64 // Fraction of tail mass that's positive
QuantileShifts QuantileShifts // Per-quantile timing differences
PatternLabel EffectPattern // Automatic classification
}
type QuantileShifts struct {
P50Ns float64 // Median shift
P90Ns float64 // 90th percentile shift
P95Ns float64 // 95th percentile shift
P99Ns float64 // 99th percentile shift
}
type EffectPattern int
const (
TailEffect EffectPattern = iota // >50% of effect in slow outliers
UniformShift // <30% of effect in tail
Mixed // 30-50% in tail
Negligible // Total effect below measurement floor
)
func (p EffectPattern) String() string // "tail effect", "uniform shift", "mixed", "negligible"

Understanding the decomposition:

  • ShiftNs: Median timing difference across all observations (aligned by rank). Non-zero values indicate consistent timing differences across the entire distribution.
  • TailNs: Measures how much individual measurements deviate from the uniform shift. High values indicate some operations are disproportionately slow.
  • TailShare: Fraction of total W₁ distance from tail deviations. Values above 0.5 indicate tail-dominated effects.

Example: Accessing effect details

if result.Outcome == tacet.Fail {
fmt.Printf("Max effect: %.1f ns\n", result.Effect.MaxEffectNs)
fmt.Printf("Pattern: %s\n", result.Effect.TailDiagnostics.PatternLabel)
tail := result.Effect.TailDiagnostics
fmt.Printf("Shift: %.1f ns, Tail: %.1f ns\n", tail.ShiftNs, tail.TailNs)
// Interpret the effect pattern
switch tail.PatternLabel {
case tacet.TailEffect:
fmt.Printf("Tail-heavy leak: %.0f%% from slow outliers\n", tail.TailShare*100)
fmt.Printf("p99: %.1f ns vs median: %.1f ns\n",
tail.QuantileShifts.P99Ns, tail.QuantileShifts.P50Ns)
case tacet.UniformShift:
fmt.Printf("Uniform timing shift: %.1f ns across distribution\n", tail.ShiftNs)
case tacet.Mixed:
fmt.Printf("Mixed effect: %.1f ns shift + %.1f ns tail\n",
tail.ShiftNs, tail.TailNs)
case tacet.Negligible:
fmt.Println("Effect below measurement floor")
}
}
type Quality int
const (
Excellent Quality = iota // MDE < 5 ns
Good // MDE 5-20 ns
Poor // MDE 20-100 ns
TooNoisy // MDE > 100 ns
)
type Exploitability int
const (
SharedHardwareOnly Exploitability = iota // < 10 ns
Http2Multiplexing // 10-100 ns
StandardRemote // 100 ns - 10 us
ObviousLeak // > 10 us
)

package main
import (
"crypto/subtle"
"fmt"
"time"
"github.com/agucova/tacet/crates/tacet-go"
)
func main() {
secret := make([]byte, 32)
copy(secret, []byte("my-secret-key-here-32-bytes-long"))
result, _ := tacet.Test(
tacet.NewFixedGenerator(secret, 42),
tacet.FuncOperation(func(input []byte) {
subtle.ConstantTimeCompare(secret, input)
}),
32,
tacet.WithAttacker(tacet.AdjacentNetwork),
tacet.WithTimeBudget(30*time.Second),
)
switch result.Outcome {
case tacet.Pass:
fmt.Println("Constant-time comparison verified")
case tacet.Fail:
fmt.Printf("Timing leak detected! Effect: %.1f ns (%s)\n",
result.Effect.MaxEffectNs,
result.Effect.TailDiagnostics.PatternLabel)
default:
fmt.Printf("Test incomplete: %v\n", result.Outcome)
}
}