Skip to main content

Adding Crypto Functions

This guide covers adding new cryptographic functions. Crypto code requires extra care for security and cross-language implementation.

Prerequisites

Decision Tree

Before implementing, decide:
  1. Pure Zig? Simple operations (hashing, encoding)
  2. Rust FFI? Complex curves (arkworks ecosystem)
  3. C library? Performance-critical with existing impl (blst, c-kzg)

Example: Adding a Hash Function

We’ll add a hypothetical Whirlpool hash function.

Step 1: Create Directory

mkdir -p src/crypto/Whirlpool
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

// 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

// 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("");
}
// 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

// src/crypto/root.zig
pub const Keccak256 = @import("Keccak256/keccak256.zig").Keccak256;
pub const Whirlpool = @import("Whirlpool/whirlpool.zig").Whirlpool;  // Add

Step 5: Tests

// 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

# Cargo.toml
[dependencies]
ark-ff = "0.4"
ark-ec = "0.4"
ark-whirlpool = "0.4"  # Hypothetical

Step 2: Create Rust Wrapper

// 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

// 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.
// ✅ 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:
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:
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:
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:
// 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

// 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

```zig
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

InputOutput (truncated)
""0x19FA61D75522A466...
"abc"0x...

## 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