Proof-of-process primitive using timing jitter for human authorship verification
- Overview
- Key Features
- Installation
- Quick Start
- Architecture
- Security Models
- Human Validation
- Evidence Chain
- API Reference
- Performance
- Configuration
- Testing
- FAQ
- Troubleshooting
- Comparison with Alternatives
- Roadmap
- Contributing
- License
- Acknowledgments
physjitter provides cryptographic proof-of-process through timing jitter, enabling verification that content was created through a human typing process rather than generated or pasted. It creates tamper-evident records that serve as evidence of authorship process.
In an era of AI-generated content, proving that text was actually typed by a human—keystroke by keystroke—has become valuable. physjitter addresses this by:
- Recording timing evidence for each input event (keystroke, edit, etc.)
- Binding evidence to hardware when available (physics-based security)
- Validating against human patterns using statistical models from real typing data
- Creating verifiable proof chains that can be independently validated
| Use Case | Description |
|---|---|
| Authorship Verification | Prove a document was typed, not pasted or generated |
| Academic Integrity | Evidence that essays were written by the student |
| Legal Documentation | Proof of process for contracts and agreements |
| Content Authenticity | Distinguish human-written from AI-generated content |
| Anti-Fraud | Detect automated form submissions |
| Feature | Description |
|---|---|
| Dual Security Models | Economic (HMAC-based) and Physics (hardware entropy) |
| Automatic Fallback | Uses hardware when available, gracefully degrades in VMs |
| Human Validation | Statistical model trained on 136M real keystrokes |
| Evidence Chain | Cryptographically-linked, serializable proof records |
| Zero Unsafe Code | Pure safe Rust implementation (#![forbid(unsafe_code)]) |
no_std Support |
Works in embedded and WASM environments |
| SLSA Level 3 | Supply chain security with provenance attestation |
| Minimal Dependencies | Only well-audited RustCrypto crates |
| Cross-Platform | Linux, macOS, Windows, WebAssembly |
Add to your Cargo.toml:
[dependencies]
physjitter = "0.2"Or install with cargo:
cargo add physjitter| Feature | Description | Default |
|---|---|---|
std |
Standard library support (Session, timing, JSON) | Yes |
hardware |
Enable TSC/hardware entropy collection | No |
rand |
Enable random secret generation | No |
# Enable all features
physjitter = { version = "0.2", features = ["hardware", "rand"] }
# no_std (embedded/WASM compatible)
physjitter = { version = "0.2", default-features = false }| Platform | Pure Jitter | Hardware Entropy | Notes |
|---|---|---|---|
| Linux x86_64 | Yes | Yes | Full support |
| Linux aarch64 | Yes | Yes | Full support |
| macOS x86_64 | Yes | Yes | Full support |
| macOS aarch64 | Yes | Yes | Full support (Apple Silicon) |
| Windows x86_64 | Yes | Yes | Full support |
| WebAssembly | Yes | No | Pure jitter only (no_std) |
Embedded (no_std) |
Yes | No | Core functionality only |
| Docker/VMs | Yes | Varies | May fall back to pure jitter |
use physjitter::{Session, Error};
fn main() -> Result<(), Error> {
// Create a session with your secret key
// IMPORTANT: Use proper key derivation in production!
let secret = [0u8; 32];
let mut session = Session::new(secret);
// Sample jitter for each keystroke/input event
let keystrokes = ["H", "e", "l", "l", "o", " ", "W", "o", "r", "l", "d"];
for keystroke in keystrokes {
// Get jitter delay for this input
let jitter_us = session.sample(keystroke.as_bytes())?;
// Apply the jitter delay (creates timing evidence)
std::thread::sleep(std::time::Duration::from_micros(jitter_us as u64));
}
// Validate the session against human typing model
let result = session.validate();
println!("Human: {}, Confidence: {:.2}", result.is_human, result.confidence);
// Export evidence chain for storage/verification
let evidence_json = session.export_json()?;
println!("Evidence records: {}", session.evidence().records.len());
println!("Physics ratio: {:.1}%", session.phys_ratio() * 100.0);
Ok(())
}use physjitter::{HybridEngine, Evidence, Error};
fn main() -> Result<(), Error> {
// Create hybrid engine (auto-selects best entropy source)
let engine = HybridEngine::default();
let secret = [42u8; 32];
// Sample jitter with evidence
let (jitter, evidence) = engine.sample(&secret, b"keystroke-a")?;
match &evidence {
Evidence::Phys { phys_hash, .. } => {
println!("Hardware entropy captured: {:02x?}...", &phys_hash[..4]);
}
Evidence::Pure { .. } => {
println!("Using HMAC fallback (VM/container detected)");
}
}
println!("Jitter delay: {}μs", jitter);
Ok(())
}use physjitter::Session;
fn main() {
// Requires "rand" feature
#[cfg(feature = "rand")]
{
let mut session = Session::random();
// ... use session
}
}┌─────────────────────────────────────────────────────────────────┐
│ Application │
├─────────────────────────────────────────────────────────────────┤
│ Session │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Secret │ │ HybridEngine │ │ EvidenceChain │ │
│ │ [u8; 32] │ │ │ │ ┌──────┐ ┌──────┐ │ │
│ └─────────────┘ │ ┌────────┐ │ │ │ Phys │→│ Pure │→... │ │
│ │ │PhysJit │ │ │ └──────┘ └──────┘ │ │
│ │ │ ter │ │ └────────────────────────┘ │
│ │ └───┬────┘ │ │
│ │ │ │ ┌────────────────────────┐ │
│ │ ┌───▼────┐ │ │ HumanModel │ │
│ │ │PureJit │ │ │ (Aalto 136M dataset) │ │
│ │ │ ter │ │ └────────────────────────┘ │
│ │ └────────┘ │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
/// Source of physical entropy from hardware or environment.
pub trait EntropySource {
/// Collect entropy sample, mixing with provided inputs.
fn sample(&self, inputs: &[u8]) -> Result<PhysHash, Error>;
/// Validate that a captured hash meets statistical requirements.
fn validate(&self, hash: PhysHash) -> bool;
}
/// Engine that computes jitter delays from entropy.
pub trait JitterEngine {
/// Compute jitter delay from secret, inputs, and entropy.
fn compute_jitter(&self, secret: &[u8; 32], inputs: &[u8], entropy: PhysHash) -> Jitter;
}| Module | Description |
|---|---|
lib.rs |
Main entry point, Session, HybridEngine |
traits.rs |
Core traits EntropySource, JitterEngine |
pure.rs |
PureJitter - HMAC-based economic security |
phys.rs |
PhysJitter - Hardware entropy collection |
evidence.rs |
Evidence, EvidenceChain - Proof records |
model.rs |
HumanModel - Statistical validation |
| Engine | Trait | Security Model | Requirements |
|---|---|---|---|
[PureJitter] |
JitterEngine |
Economic | None |
[PhysJitter] |
EntropySource + JitterEngine |
Physics | Hardware access |
[HybridEngine] |
(composite) | Both | Auto-detect |
Security relies on the economic cost of reproducing the exact input sequence. An attacker would need to retype content character-by-character with identical timing to reproduce the jitter chain.
use physjitter::{PureJitter, JitterEngine};
let engine = PureJitter::new(500, 2500); // jmin=500μs, range=2500μs
let secret = [0u8; 32];
let entropy = [0u8; 32]; // Unused in pure mode
let jitter = engine.compute_jitter(&secret, b"keystroke", entropy);
assert!(jitter >= 500 && jitter < 3000);Properties:
| Property | Value |
|---|---|
| Deterministic | Yes - same inputs always produce same jitter |
| Portable | Works everywhere (VMs, containers, WASM) |
| Performance | ~200ns per computation |
| Secret dependency | Full - compromise defeats security |
When to use:
- Virtualized environments (Docker, VMs, cloud)
- WebAssembly targets
- When hardware entropy is unavailable
- Lower-stakes verification scenarios
Security relies on hardware entropy that cannot be perfectly simulated. Uses TSC (Time Stamp Counter) and timing variations unique to the physical device.
use physjitter::{PhysJitter, EntropySource, JitterEngine, Error};
fn main() -> Result<(), Error> {
let phys = PhysJitter::new(8); // Require 8 bits minimum entropy
// Collect hardware entropy
let entropy = phys.sample(b"inputs")?;
// Compute jitter using hardware entropy
let secret = [42u8; 32];
let jitter = phys.compute_jitter(&secret, b"inputs", entropy);
// Verify entropy meets requirements
assert!(phys.validate(entropy));
Ok(())
}Properties:
| Property | Value |
|---|---|
| Deterministic | No - hardware noise provides true randomness |
| Device-bound | Entropy tied to specific hardware |
| Tamper-evident | Replay attacks detectable |
| Requirements | Physical hardware access |
When to use:
- Native desktop applications
- High-stakes verification
- When hardware is trusted
- Maximum security requirements
Combines both models: uses physics when available, falls back to pure jitter in virtualized environments. Evidence records which mode was used.
use physjitter::{HybridEngine, Evidence, Error};
fn main() -> Result<(), Error> {
let engine = HybridEngine::default()
.with_min_entropy(8); // Require 8 bits for physics mode
let secret = [42u8; 32];
let (jitter, evidence) = engine.sample(&secret, b"input")?;
// Check which mode was used
match &evidence {
Evidence::Phys { phys_hash, .. } => {
println!("Hardware entropy: {:02x?}...", &phys_hash[..4]);
}
Evidence::Pure { .. } => {
println!("HMAC fallback (VM/low entropy detected)");
}
}
// Check if physics mode is available
if engine.phys_available() {
println!("Hardware entropy source detected");
}
Ok(())
}This is the recommended engine for production use.
The HumanModel validates jitter sequences against statistical patterns derived from the Aalto 136M keystroke dataset.
use physjitter::{Session, HumanModel, Jitter};
fn main() {
let secret = [0u8; 32];
let mut session = Session::new(secret);
// Simulate typing (in real use, this comes from actual keystrokes)
for i in 0..50 {
let input = format!("key{}", i);
let _ = session.sample(input.as_bytes());
}
// Validate against human model
let result = session.validate();
println!("Is human: {}", result.is_human);
println!("Confidence: {:.2}", result.confidence);
println!("Anomalies: {}", result.anomalies.len());
// Examine statistics
println!("Mean jitter: {:.2}μs", result.stats.mean);
println!("Std dev: {:.2}μs", result.stats.std_dev);
println!("Range: [{}, {}]μs", result.stats.min, result.stats.max);
}use physjitter::HumanModel;
// Load default model (based on Aalto dataset)
let model = HumanModel::default();
// Load embedded baseline
let baseline = HumanModel::baseline();
// Or create custom model
let custom = HumanModel {
iki_min_us: 30_000, // 30ms minimum IKI
iki_max_us: 2_000_000, // 2s maximum IKI
iki_mean_us: 200_000, // 200ms mean IKI
iki_std_us: 80_000, // 80ms std dev
jitter_min_us: 500, // Match engine jmin
jitter_max_us: 3000, // Match engine jmin + range
min_sequence_length: 20, // Minimum samples for validation
max_perfect_ratio: 0.05, // Max 5% identical consecutive values
};| Anomaly | Description | Indicates |
|---|---|---|
PerfectTiming |
Too many identical consecutive values | Automation, replay attack |
LowVariance |
Unnaturally consistent timing | Scripted input, bot |
RepeatingPattern |
Periodic patterns in sequence | Macro, automation |
OutOfRange |
Values outside human typing range | Invalid data, tampering |
DistributionMismatch |
Statistical distribution anomaly | Non-human origin |
use physjitter::{Session, ValidationResult, AnomalyKind};
fn interpret_validation(result: &ValidationResult) {
match (result.is_human, result.confidence) {
(true, c) if c > 0.9 => println!("High confidence human"),
(true, c) if c > 0.7 => println!("Likely human"),
(true, _) => println!("Possibly human (low confidence)"),
(false, _) => {
println!("Likely automated. Anomalies:");
for anomaly in &result.anomalies {
match anomaly.kind {
AnomalyKind::PerfectTiming =>
println!(" - Perfect timing detected (replay?)"),
AnomalyKind::LowVariance =>
println!(" - Too consistent (bot?)"),
AnomalyKind::RepeatingPattern =>
println!(" - Pattern detected (macro?)"),
AnomalyKind::OutOfRange =>
println!(" - Invalid values (tampering?)"),
AnomalyKind::DistributionMismatch =>
println!(" - Statistical anomaly"),
}
}
}
}
}Evidence is accumulated in an append-only chain with cryptographic integrity. Each record is hashed into a running chain hash, making tampering detectable.
use physjitter::{EvidenceChain, Evidence};
fn main() {
let mut chain = EvidenceChain::new();
// Append evidence records
chain.append(Evidence::phys([1u8; 32], 1500));
chain.append(Evidence::pure(2000));
chain.append(Evidence::phys([2u8; 32], 1800));
// Chain statistics
println!("Total records: {}", chain.records.len());
println!("Physics records: {}", chain.phys_count());
println!("Pure records: {}", chain.pure_count());
println!("Physics ratio: {:.1}%", chain.phys_ratio() * 100.0);
// Chain integrity hash
println!("Chain hash: {:02x?}...", &chain.chain_hash[..8]);
}use physjitter::{EvidenceChain, Evidence};
fn main() -> Result<(), serde_json::Error> {
let mut chain = EvidenceChain::new();
chain.append(Evidence::phys([1u8; 32], 1500));
chain.append(Evidence::pure(2000));
// Serialize to JSON
let json = serde_json::to_string_pretty(&chain)?;
println!("{}", json);
// Deserialize from JSON
let restored: EvidenceChain = serde_json::from_str(&json)?;
assert_eq!(restored.records.len(), 2);
Ok(())
}{
"version": 1,
"records": [
{
"type": "Phys",
"phys_hash": [1, 2, 3, "...32 bytes..."],
"jitter": 1500,
"timestamp_us": 1706745600000000
},
{
"type": "Pure",
"jitter": 2000,
"timestamp_us": 1706745600100000
}
],
"chain_hash": ["...32 bytes..."]
}use physjitter::{EvidenceChain, Evidence, PureJitter, JitterEngine};
fn main() {
let engine = PureJitter::default();
let secret = [42u8; 32];
let inputs: Vec<&[u8]> = vec![b"key1", b"key2", b"key3"];
// Build chain with known inputs
let mut chain = EvidenceChain::new();
for input in &inputs {
let jitter = engine.compute_jitter(&secret, input, [0u8; 32]);
chain.append(Evidence::pure(jitter));
}
// Verify chain against inputs
assert!(chain.verify_chain(&secret, &inputs, &engine));
// Fails with wrong inputs
let wrong_inputs: Vec<&[u8]> = vec![b"wrong1", b"wrong2", b"wrong3"];
assert!(!chain.verify_chain(&secret, &wrong_inputs, &engine));
}| Type | Description |
|---|---|
PhysHash |
[u8; 32] - SHA-256 hash output |
Jitter |
u32 - Jitter delay in microseconds |
pub enum Error {
/// Insufficient entropy collected from hardware.
InsufficientEntropy { required: u8, found: u8 },
/// Hardware entropy source not available.
HardwareUnavailable { reason: String },
/// Invalid input provided.
InvalidInput(String),
}| Struct | Description |
|---|---|
Session |
High-level session manager with evidence tracking |
HybridEngine |
Combines physics and pure jitter with fallback |
PureJitter |
HMAC-based jitter engine (economic security) |
PhysJitter |
Hardware entropy-based engine (physics security) |
EvidenceChain |
Append-only chain of evidence records |
Evidence |
Single evidence record (Phys or Pure) |
HumanModel |
Statistical model for validation |
ValidationResult |
Result of human validation |
For complete API documentation, see docs.rs/physjitter.
Benchmarked on Apple M1 Pro:
| Operation | Time | Throughput |
|---|---|---|
PureJitter::compute_jitter |
~200ns | 5M ops/sec |
PhysJitter::sample |
~10μs | 100K ops/sec |
HybridEngine::sample |
~12μs | 83K ops/sec |
HumanModel::validate (1000 samples) |
~50μs | — |
EvidenceChain::append |
~1μs | 1M ops/sec |
| Evidence JSON serialization | ~5μs | 200K ops/sec |
| Component | Memory |
|---|---|
Session |
~500 bytes + evidence |
Evidence record |
~80 bytes |
EvidenceChain (1000 records) |
~80KB |
use physjitter::{Session, HybridEngine, PhysJitter, PureJitter};
// Default configuration
let session = Session::new([0u8; 32]);
// Custom hybrid engine
let phys = PhysJitter::new(8); // 8 bits minimum entropy
let pure = PureJitter::new(500, 2500); // 500-3000μs range
let engine = HybridEngine::new(phys, pure)
.with_min_entropy(8);
// Note: Session uses default HybridEngine
// For custom engine, use HybridEngine directlyuse physjitter::PureJitter;
// Default: 500-3000μs range
let default = PureJitter::default();
// Custom range: 1000-5000μs
let custom = PureJitter::new(1000, 4000);use physjitter::PhysJitter;
// Default: 0 bits minimum (accept all)
let default = PhysJitter::default();
// Require 8 bits minimum entropy
let strict = PhysJitter::new(8);
// Very strict: 16 bits minimum
let very_strict = PhysJitter::new(16);# All tests
cargo test
# All tests with all features
cargo test --all-features
# Specific test
cargo test test_human_validation
# With output
cargo test -- --nocapturecargo bench# Default features only
cargo test
# Hardware feature
cargo test --features hardware
# All features
cargo test --all-features
# No default features (minimal)
cargo test --no-default-featuresThis crate includes fuzzing targets using cargo-fuzz to find edge cases and potential bugs:
| Target | Description |
|---|---|
fuzz_evidence_json |
JSON deserialization of Evidence and EvidenceChain |
fuzz_human_model |
HumanModel validation with arbitrary jitter/IKI values |
fuzz_evidence_verify |
Evidence verification and chain integrity |
fuzz_jitter_compute |
Jitter computation with various parameters |
# Install cargo-fuzz (requires nightly)
cargo install cargo-fuzz
# Run a specific fuzz target
cargo +nightly fuzz run fuzz_evidence_json
# Run with a time limit (60 seconds)
cargo +nightly fuzz run fuzz_human_model -- -max_total_time=60
# Run all fuzz targets briefly
for target in fuzz_evidence_json fuzz_human_model fuzz_evidence_verify fuzz_jitter_compute; do
cargo +nightly fuzz run $target -- -max_total_time=10
doneQ: What makes this different from just recording timestamps?
A: physjitter combines:
- Cryptographic binding (HMAC) to a session secret
- Hardware entropy when available (non-reproducible)
- Statistical validation against real human typing data
- Tamper-evident chain hashing
Plain timestamps can be easily forged. physjitter creates evidence that's cryptographically bound to both the secret and the hardware.
Q: Can this be fooled by typing very slowly?
A: The human model has upper bounds (~2 seconds) for inter-key intervals. Extremely slow typing may pass validation but would be impractical for generating significant content. The primary defense is against automation, not against dedicated human efforts to create false evidence.
Q: Does this prove who typed the content?
A: No. It proves that someone typed the content through a human-like process. Authentication (proving identity) requires separate mechanisms.
Q: What if the secret is compromised?
A: In pure jitter mode, a compromised secret allows an attacker to compute valid jitter values. However:
- They still need to match the exact input sequence
- Physics-bound evidence remains valid (hardware entropy can't be reproduced)
- Timestamps provide additional context
For high-stakes applications, use hardware entropy mode and rotate secrets regularly.
Q: Is the HMAC computation constant-time?
A: Yes, we use the hmac crate from RustCrypto which provides constant-time operations.
Q: Can an attacker replay evidence?
A: Evidence includes timestamps, making replay detectable. Physics-bound evidence includes hardware entropy that varies per capture. Applications should also bind evidence to session context (user ID, document ID, etc.).
Q: Why does physics mode fail in my VM?
A: VMs often don't provide accurate TSC (Time Stamp Counter) readings. The hybrid engine automatically detects this and falls back to pure jitter mode. Check evidence.is_phys() to see which mode was used.
Q: What's the minimum sequence length for validation?
A: Default is 20 samples. You can adjust this in HumanModel::min_sequence_length. Shorter sequences have higher false positive/negative rates.
Q: Can I use this in WebAssembly?
A: Yes, but only pure jitter mode is available (no hardware entropy). Compile without the hardware feature.
"InsufficientEntropy" error:
Error: Insufficient entropy: required 8 bits, found 2
This occurs when hardware entropy doesn't meet the minimum threshold. Solutions:
- Use
HybridEnginefor automatic fallback - Lower the entropy requirement:
PhysJitter::new(2) - Check if running in a VM (hardware entropy may be unavailable)
All evidence is "Pure" even on native hardware:
Check that:
- The
hardwarefeature is enabled - You're not running in a container/VM
engine.phys_available()returnstrue
Validation always returns is_human: false:
Check for:
- Sufficient sequence length (minimum 20 by default)
- Varied input values (not all identical)
- Reasonable timing variation
// Debug validation
let result = session.validate();
for anomaly in &result.anomalies {
println!("Anomaly: {:?} at {}: {}",
anomaly.kind, anomaly.position, anomaly.detail);
}High memory usage with large evidence chains:
Evidence chains grow linearly. For long sessions:
- Export and archive evidence periodically
- Start new sessions for new documents
- Consider summarizing older evidence
| Feature | physjitter | Timestamp logging | Behavioral biometrics |
|---|---|---|---|
| Hardware binding | Yes | No | Varies |
| Cryptographic proof | Yes | No | No |
| Human validation | Yes | No | Yes |
| Privacy preserving | Yes | No | No |
| Offline verification | Yes | Yes | No |
| No external service | Yes | Yes | No |
- WASM-optimized builds
- Additional statistical models
- Batch verification API
- Hardware attestation integration
- Language-specific typing patterns
See GitHub Issues for detailed roadmap.
- witnessd — Cryptographic authorship witnessing daemon (uses physjitter)
We welcome contributions! Please see our Contributing Guidelines before submitting PRs.
All releases include SLSA Level 3 provenance attestations:
# Install slsa-verifier
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
# Download and verify
curl -LO https://github.com/writerslogic/physjitter/releases/download/v0.1.0/...
slsa-verifier verify-artifact artifact.tar.gz \
--provenance-path multiple.intoto.jsonl \
--source-uri github.com/writerslogic/physjitterLicensed under the Apache License, Version 2.0 (LICENSE).
- Aalto University for the 136M keystroke dataset
- The RustCrypto team for cryptographic primitives
- The Rust community for foundational tooling
Built with care by WritersLogic