Documentation Index
Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
Security
Voltaire handles cryptographic operations and sensitive data. This guide covers security requirements.
Threat Model
What We Protect Against
- Timing attacks: Side-channel leaks via execution time
- Memory disclosure: Sensitive data remaining in memory
- Input validation failures: Malformed data causing crashes or miscomputation
- Type confusion: Wrong types passed to crypto functions
What We Don’t Protect Against
- Compromised runtime (Zig, Node, browser)
- Hardware attacks (Spectre, Rowhammer)
- Malicious dependencies (supply chain)
Constant-Time Operations
All cryptographic comparisons and key operations must be constant-time.
Comparison
// ✅ Constant time - always iterates entire array
pub fn secureEquals(a: []const u8, b: []const u8) bool {
if (a.len != b.len) return false;
var result: u8 = 0;
for (a, b) |x, y| {
result |= x ^ y;
}
return result == 0;
}
// ❌ Timing leak - early return reveals mismatch position
pub fn insecureEquals(a: []const u8, b: []const u8) bool {
if (a.len != b.len) return false;
for (a, b) |x, y| {
if (x != y) return false;
}
return true;
}
Selection
// ✅ Constant time selection
pub fn select(condition: bool, a: u8, b: u8) u8 {
const mask = @as(u8, 0) -% @intFromBool(condition);
return (a & mask) | (b & ~mask);
}
// ❌ Branch-based selection
pub fn insecureSelect(condition: bool, a: u8, b: u8) u8 {
if (condition) return a else return b;
}
TypeScript
// ✅ Constant time in JS
export function secureEquals(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
Memory Handling
Clearing Sensitive Data
const std = @import("std");
pub fn signMessage(private_key: [32]u8, message: []const u8) ![64]u8 {
// Copy key to stack
var key = private_key;
// Ensure key is zeroed on any exit
defer std.crypto.utils.secureZero(u8, &key);
// ... signing logic ...
return signature;
}
Avoid Copying Secrets
// ✅ Pass by pointer, don't copy
pub fn derivePublicKey(private_key: *const [32]u8) [33]u8 {
// Use directly without copying
return computePublicKey(private_key.*);
}
// ❌ Unnecessary copy
pub fn derivePublicKeyBad(private_key: [32]u8) [33]u8 {
// private_key is copied to stack
return computePublicKey(private_key);
}
TypeScript Memory Limits
JavaScript doesn’t guarantee memory clearing:
// Best effort clearing
export function clearKey(key: Uint8Array): void {
crypto.getRandomValues(key); // Overwrite with random
key.fill(0); // Then zero
}
// Usage
const privateKey = generatePrivateKey();
try {
const signature = sign(privateKey, message);
return signature;
} finally {
clearKey(privateKey);
}
Validate Before Processing
pub fn verifySignature(
signature: []const u8,
message: []const u8,
public_key: []const u8,
) !bool {
// Length checks first
if (signature.len != 64) return error.InvalidSignatureLength;
if (public_key.len != 33 and public_key.len != 65) {
return error.InvalidPublicKeyLength;
}
// Range checks
const r = signature[0..32];
const s = signature[32..64];
if (!isValidScalar(r)) return error.InvalidR;
if (!isValidScalar(s)) return error.InvalidS;
// Point validation
const point = try decodePoint(public_key);
if (!point.isOnCurve()) return error.InvalidPublicKey;
// Now safe to verify
return internalVerify(signature, message, point);
}
Reject Invalid Early
pub fn fromHex(hex: []const u8) !Address {
// Reject immediately if wrong length
if (hex.len != 42) return error.InvalidLength;
if (hex[0] != '0' or hex[1] != 'x') return error.MissingPrefix;
// Validate characters before parsing
for (hex[2..]) |c| {
switch (c) {
'0'...'9', 'a'...'f', 'A'...'F' => {},
else => return error.InvalidCharacter,
}
}
// Now safe to parse
return parseValidatedHex(hex);
}
Error Handling
// ✅ Generic error, no secret info
pub fn decrypt(key: [32]u8, ciphertext: []const u8) ![]u8 {
// ...
if (!verifyMac(ciphertext)) {
return error.DecryptionFailed; // Generic
}
// ...
}
// ❌ Leaks information
pub fn decryptBad(key: [32]u8, ciphertext: []const u8) ![]u8 {
if (!verifyMac(ciphertext)) {
return error.MacVerificationFailed; // Reveals MAC failed specifically
}
if (!checkPadding(plaintext)) {
return error.PaddingInvalid; // Padding oracle attack!
}
}
Avoid Panics in Crypto Code
// ✅ Return error
pub fn parsePrivateKey(bytes: []const u8) !PrivateKey {
if (bytes.len != 32) return error.InvalidLength;
if (isZero(bytes)) return error.ZeroKey;
return PrivateKey{ .bytes = bytes[0..32].* };
}
// ❌ Panic exposes internal state
pub fn parsePrivateKeyBad(bytes: []const u8) PrivateKey {
if (bytes.len != 32) @panic("invalid key length"); // Bad
return PrivateKey{ .bytes = bytes[0..32].* };
}
Test Vectors
Use Official Vectors
test "secp256k1 sign - official vectors" {
// From https://www.secg.org/sec2-v2.pdf
const vectors = .{
.{
.private_key = "0x0000000000000000000000000000000000000000000000000000000000000001",
.message = "0x...",
.expected_sig = "0x...",
},
// ... more vectors
};
for (vectors) |v| {
const key = try PrivateKey.fromHex(v.private_key);
const msg = try hexToBytes(v.message);
const sig = try sign(key, msg);
try testing.expectEqualSlices(u8, v.expected_sig, &sig);
}
}
Edge Cases
test "signature validation edge cases" {
// Zero signature
const zero_sig = [_]u8{0} ** 64;
try testing.expectError(error.InvalidSignature, verify(zero_sig, msg, pubkey));
// Max value signature
const max_sig = [_]u8{0xff} ** 64;
try testing.expectError(error.InvalidSignature, verify(max_sig, msg, pubkey));
// s > n/2 (malleable signature)
const malleable_sig = createMalleableSig();
try testing.expectError(error.InvalidSignature, verify(malleable_sig, msg, pubkey));
}
test "rejects malformed public keys" {
const cases = .{
&[_]u8{}, // Empty
&[_]u8{0x04} ++ [_]u8{0} ** 63, // Wrong length
&[_]u8{0x05} ++ [_]u8{0} ** 64, // Invalid prefix
&[_]u8{0x04} ++ [_]u8{0xff} ** 64, // Point not on curve
};
for (cases) |case| {
try testing.expectError(error.InvalidPublicKey, parsePublicKey(case));
}
}
Cross-Validation
Against Reference Implementations
import { secp256k1 } from "@noble/curves/secp256k1";
import * as Secp256k1 from "./index.js";
describe("cross-validation", () => {
it("matches noble for signatures", () => {
const privateKey = new Uint8Array(32);
crypto.getRandomValues(privateKey);
const message = new TextEncoder().encode("test");
const hash = keccak256(message);
const ourSig = Secp256k1.sign(privateKey, hash);
const nobleSig = secp256k1.sign(hash, privateKey);
expect(ourSig.r).toEqual(nobleSig.r);
expect(ourSig.s).toEqual(nobleSig.s);
});
});
Fuzz Testing
describe("fuzz", () => {
it("never crashes on random input", () => {
for (let i = 0; i < 100000; i++) {
const len = Math.floor(Math.random() * 1000);
const data = crypto.getRandomValues(new Uint8Array(len));
try {
Signature.fromBytes(data);
} catch (e) {
// Expected to throw for invalid input
// But should never crash
}
}
});
});
Security Checklist
Before merging crypto code:
Implementation
Validation
Testing
Error Handling
Reporting Vulnerabilities
Found a security issue? Contact security@tevm.sh with:
- Description of vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We aim to respond within 48 hours.