Adding Crypto Functions
This guide covers adding new cryptographic functions. Crypto code requires extra care for security and cross-language implementation.Prerequisites
- Understand Zig Patterns
- Understand Multi-Language Integration
- Know constant-time programming basics
Decision Tree
Before implementing, decide:- Pure Zig? Simple operations (hashing, encoding)
- Rust FFI? Complex curves (arkworks ecosystem)
- C library? Performance-critical with existing impl (blst, c-kzg)
Example: Adding a Hash Function
We’ll add a hypotheticalWhirlpool hash function.
Step 1: Create Directory
Copy
Ask AI
mkdir -p src/crypto/Whirlpool
Copy
Ask AI
src/crypto/Whirlpool/
├── whirlpool.zig # Core implementation
├── Whirlpool.js # TypeScript wrapper
├── Whirlpool.test.ts # Tests
├── Whirlpool.wasm.ts # WASM variant
├── Whirlpool.wasm.test.ts # WASM tests
├── index.ts # Exports
└── whirlpool.mdx # Documentation
Step 2: Implement in Zig
Copy
Ask AI
// whirlpool.zig
const std = @import("std");
pub const Whirlpool = struct {
const DIGEST_SIZE = 64;
const BLOCK_SIZE = 64;
state: [8]u64,
buffer: [BLOCK_SIZE]u8,
buffer_len: usize,
total_len: u64,
pub fn init() Whirlpool {
return Whirlpool{
.state = [_]u64{0} ** 8,
.buffer = [_]u8{0} ** BLOCK_SIZE,
.buffer_len = 0,
.total_len = 0,
};
}
pub fn update(self: *Whirlpool, data: []const u8) void {
var i: usize = 0;
// Fill buffer first
if (self.buffer_len > 0) {
const space = BLOCK_SIZE - self.buffer_len;
const copy_len = @min(space, data.len);
@memcpy(self.buffer[self.buffer_len..][0..copy_len], data[0..copy_len]);
self.buffer_len += copy_len;
i = copy_len;
if (self.buffer_len == BLOCK_SIZE) {
self.processBlock(&self.buffer);
self.buffer_len = 0;
}
}
// Process full blocks
while (i + BLOCK_SIZE <= data.len) : (i += BLOCK_SIZE) {
self.processBlock(data[i..][0..BLOCK_SIZE]);
}
// Store remainder
if (i < data.len) {
const remainder = data.len - i;
@memcpy(self.buffer[0..remainder], data[i..]);
self.buffer_len = remainder;
}
self.total_len += data.len;
}
pub fn final(self: *Whirlpool) [DIGEST_SIZE]u8 {
// Padding
self.buffer[self.buffer_len] = 0x80;
self.buffer_len += 1;
if (self.buffer_len > 32) {
@memset(self.buffer[self.buffer_len..], 0);
self.processBlock(&self.buffer);
self.buffer_len = 0;
}
@memset(self.buffer[self.buffer_len..], 0);
// Length in bits
const bit_len = self.total_len * 8;
std.mem.writeInt(u64, self.buffer[56..64], bit_len, .big);
self.processBlock(&self.buffer);
// Output
var digest: [DIGEST_SIZE]u8 = undefined;
for (self.state, 0..) |word, j| {
std.mem.writeInt(u64, digest[j * 8 ..][0..8], word, .big);
}
return digest;
}
fn processBlock(self: *Whirlpool, block: *const [BLOCK_SIZE]u8) void {
// Actual Whirlpool compression function
_ = self;
_ = block;
// ... implementation details ...
}
/// One-shot hash function
pub fn hash(data: []const u8) [DIGEST_SIZE]u8 {
var h = Whirlpool.init();
h.update(data);
return h.final();
}
};
// Tests
test "Whirlpool empty string" {
const digest = Whirlpool.hash("");
// Compare against known test vector
const expected = [_]u8{ 0x19, 0xFA, ... }; // Full vector
try std.testing.expectEqualSlices(u8, &expected, &digest);
}
test "Whirlpool 'abc'" {
const digest = Whirlpool.hash("abc");
const expected = [_]u8{ ... }; // Test vector for "abc"
try std.testing.expectEqualSlices(u8, &expected, &digest);
}
test "Whirlpool incremental equals one-shot" {
const data = "The quick brown fox jumps over the lazy dog";
const one_shot = Whirlpool.hash(data);
var incremental = Whirlpool.init();
incremental.update(data[0..10]);
incremental.update(data[10..]);
const inc_result = incremental.final();
try std.testing.expectEqualSlices(u8, &one_shot, &inc_result);
}
Step 3: TypeScript Wrapper
Copy
Ask AI
// Whirlpool.js
import { getWasm } from "../wasm-loader/loader.js";
/**
* Compute Whirlpool hash of data
* @param {Uint8Array | string} data
* @returns {Uint8Array}
*/
export function hash(data) {
const input = typeof data === "string"
? new TextEncoder().encode(data)
: data;
const wasm = getWasm();
return wasm.whirlpool_hash(input);
}
/**
* Compute Whirlpool hash and return as hex
* @param {Uint8Array | string} data
* @returns {string}
*/
export function hashHex(data) {
const digest = hash(data);
return "0x" + [...digest].map(b => b.toString(16).padStart(2, "0")).join("");
}
Copy
Ask AI
// index.ts
export { hash } from "./Whirlpool.js";
export { hashHex } from "./Whirlpool.js";
// Re-export for namespace usage
import { hash, hashHex } from "./Whirlpool.js";
export const Whirlpool = { hash, hashHex };
Step 4: Register in Module
Copy
Ask AI
// src/crypto/root.zig
pub const Keccak256 = @import("Keccak256/keccak256.zig").Keccak256;
pub const Whirlpool = @import("Whirlpool/whirlpool.zig").Whirlpool; // Add
Step 5: Tests
Copy
Ask AI
// Whirlpool.test.ts
import { describe, it, expect } from "vitest";
import * as Whirlpool from "./index.js";
describe("Whirlpool", () => {
// Test vectors from specification
const vectors = [
{ input: "", expected: "19fa61d75522a466..." },
{ input: "abc", expected: "..." },
{ input: "The quick brown fox...", expected: "..." },
];
describe("hash", () => {
it.each(vectors)("hashes '$input' correctly", ({ input, expected }) => {
const result = Whirlpool.hashHex(input);
expect(result).toBe(expected);
});
it("handles Uint8Array input", () => {
const input = new Uint8Array([0x61, 0x62, 0x63]); // "abc"
const result = Whirlpool.hash(input);
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBe(64);
});
});
});
Adding Curve Operations (Rust FFI)
For elliptic curve operations, use Rust with arkworks.Step 1: Add Rust Dependency
Copy
Ask AI
# Cargo.toml
[dependencies]
ark-ff = "0.4"
ark-ec = "0.4"
ark-whirlpool = "0.4" # Hypothetical
Step 2: Create Rust Wrapper
Copy
Ask AI
// src/rust/whirlpool.rs
use ark_whirlpool::{Curve, Point};
#[no_mangle]
pub extern "C" fn whirlpool_point_add(
ax: *const u8, ay: *const u8,
bx: *const u8, by: *const u8,
out_x: *mut u8, out_y: *mut u8,
) -> i32 {
// Implementation
}
Step 3: Zig FFI Bindings
Copy
Ask AI
// whirlpool_ffi.zig
const c = @cImport({
@cInclude("whirlpool.h");
});
pub fn pointAdd(a: Point, b: Point) !Point {
var result_x: [32]u8 = undefined;
var result_y: [32]u8 = undefined;
const status = c.whirlpool_point_add(
&a.x, &a.y,
&b.x, &b.y,
&result_x, &result_y,
);
if (status != 0) return error.CurveError;
return Point{ .x = result_x, .y = result_y };
}
Security Requirements
Constant-Time Operations
All crypto code must be constant-time to prevent timing attacks.
Copy
Ask AI
// ✅ Constant time comparison
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
pub fn insecureEquals(a: []const u8, b: []const u8) bool {
for (a, b) |x, y| {
if (x != y) return false; // Early return leaks timing
}
return true;
}
Memory Clearing
Clear sensitive data after use:Copy
Ask AI
pub fn signMessage(private_key: [32]u8, message: []const u8) ![64]u8 {
var key_copy = private_key;
defer std.crypto.utils.secureZero(u8, &key_copy); // Clear on exit
// ... signing logic ...
return signature;
}
Input Validation
Always validate inputs before processing:Copy
Ask AI
pub fn verifySignature(sig: []const u8, msg: []const u8, pubkey: []const u8) !bool {
// Validate lengths
if (sig.len != 64) return error.InvalidSignatureLength;
if (pubkey.len != 33 and pubkey.len != 65) return error.InvalidPublicKeyLength;
// Validate signature components (r, s in valid range)
const r = sig[0..32];
const s = sig[32..64];
if (!isValidScalar(r) or !isValidScalar(s)) return error.InvalidSignature;
// ... verification logic ...
}
Test Vectors
Every crypto function needs test vectors from official sources:Copy
Ask AI
test "Whirlpool official test vectors" {
// From ISO/IEC 10118-3:2004
const vectors = .{
.{ .input = "", .expected = "19FA61D75522A466..." },
.{ .input = "a", .expected = "8ACA2602792AEC6F..." },
.{ .input = "abc", .expected = "..." },
// ... more vectors
};
for (vectors) |v| {
const result = Whirlpool.hash(v.input);
const expected = try hexToBytes(v.expected);
try std.testing.expectEqualSlices(u8, &expected, &result);
}
}
Cross-Validation
Test against reference implementations:Copy
Ask AI
// Whirlpool.test.ts
import { whirlpool as nobleWhirlpool } from "@noble/hashes/whirlpool";
describe("cross-validation", () => {
it("matches noble implementation", () => {
const data = "test data";
const ours = Whirlpool.hash(data);
const noble = nobleWhirlpool(data);
expect(ours).toEqual(noble);
});
// Fuzz test
it("matches noble for random inputs", () => {
for (let i = 0; i < 1000; i++) {
const data = crypto.getRandomValues(new Uint8Array(Math.random() * 1000));
const ours = Whirlpool.hash(data);
const noble = nobleWhirlpool(data);
expect(ours).toEqual(noble);
}
});
});
Documentation
Copy
Ask AI
// docs/crypto/whirlpool/index.mdx
---
title: Whirlpool
description: Whirlpool cryptographic hash function
---
# Whirlpool
Whirlpool is a cryptographic hash function that produces a 512-bit digest.
<Warning>
Whirlpool is not commonly used in Ethereum. Consider using [Keccak256](/crypto/keccak256) for Ethereum operations.
</Warning>
## Usage
```typescript
import * as Whirlpool from "@voltaire/crypto/Whirlpool";
// Hash a string
const hash = Whirlpool.hash("hello world");
// Hash as hex
const hex = Whirlpool.hashHex("hello world");
Security
- 512-bit output
- Designed by Vincent Rijmen and Paulo Barreto
- Standardized in ISO/IEC 10118-3:2004
Test Vectors
| Input | Output (truncated) |
|---|---|
"" | 0x19FA61D75522A466... |
"abc" | 0x... |
Copy
Ask AI
## Checklist
Before submitting crypto code:
- [ ] Zig implementation with inline tests
- [ ] All operations constant-time where needed
- [ ] Input validation on all public functions
- [ ] Test vectors from official specification
- [ ] Cross-validation against reference implementation
- [ ] TypeScript wrapper with types
- [ ] WASM variant working
- [ ] Memory cleared after sensitive operations
- [ ] Documentation with security notes
- [ ] Registered in `crypto/root.zig`
- [ ] All tests passing

