blockblaz/hash-zig
A pure zig implementation of hash based signatures inspired from the rust implementation https://github.com/b-wagn/hash-sig
A pure Zig implementation of hash-based signatures using Poseidon2 and SHA3 with incomparable encodings. This library implements Generalized XMSS signatures based on the framework from this paper, with exact compatibility with the hash-sig Rust implementation. Features include PRF-based key derivation, epoch management, encoding randomness, and full Merkle tree storage. Poseidon2 targets the KoalaBear 31βbit field with Montgomery arithmetic (compatible with plonky3 constants).
rho
(encoding randomness) in signatures for securityAdd to your build.zig.zon
:
.{
.name = .my_project,
.version = "0.1.0",
.dependencies = .{
.@"hash-zig" = .{
.url = "https://github.com/ch4r10t33r/hash-zig/archive/refs/tags/v0.1.0.tar.gz",
.hash = "1220...", // Will be generated by zig build
},
},
}
In your build.zig
:
const hash_zig_dep = b.dependency("hash-zig", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("hash-zig", hash_zig_dep.module("hash-zig"));
git clone https://github.com/ch4r10t33r/hash-zig.git
cd hash-zig
zig build test
const std = @import("std");
const hash_zig = @import("hash-zig");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Initialize with medium lifetime (2^16 signatures)
// Only 128-bit security is supported
const params = hash_zig.Parameters.initHypercube(.lifetime_2_16);
var sig_scheme = try hash_zig.HashSignature.init(allocator, params);
defer sig_scheme.deinit();
// Generate a random seed for key generation (32 bytes required)
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);
// Generate keypair from seed with epoch management
// Parameters: seed, activation_epoch, num_active_epochs (0 = use full lifetime)
var keypair = try sig_scheme.generateKeyPair(allocator, &seed, 0, 0);
defer keypair.deinit(allocator);
// Sign a message (epoch must be tracked by your application!)
const message = "Hello, hash-based signatures!";
const epoch: u64 = 0; // YOUR APP must track this and never reuse!
// RNG seed for encoding randomness (rho)
var rng_seed: [32]u8 = undefined;
std.crypto.random.bytes(&rng_seed);
var signature = try sig_scheme.sign(allocator, message, &keypair.secret_key, epoch, &rng_seed);
defer signature.deinit(allocator);
// Verify signature
const is_valid = try sig_scheme.verify(allocator, message, signature, &keypair.public_key);
std.debug.print("Signature valid: {}\n", .{is_valid});
}
The hash-zig library includes several built-in programs for demonstration, testing, and performance analysis:
hash-zig-example
)Purpose: Demonstrates basic usage of the hash-zig library
Command: zig build example
or zig build run
Description: Shows how to generate keypairs, sign messages, and verify signatures using the Rust-compatible implementation. Includes timing measurements, displays key information (PRF key, tree structure, epoch management), and demonstrates encoding randomness. Perfect for understanding the library's core functionality.
hash-zig-benchmark
)Purpose: Comprehensive performance benchmarking
Command: zig build benchmark
Description: Runs standardized performance tests across different key lifetimes (2^10 and 2^16). Measures key generation (with on-demand key derivation from PRF), signing (with encoding randomness), and verification times with detailed metrics. Outputs results in CI-friendly format for automated testing. Uses the Rust-compatible implementation.
hash-zig-simd-benchmark
)Purpose: Tests SIMD-optimized implementations
Command: zig build simd-benchmark
Description: Benchmarks SIMD-optimized versions of the hash-based signature scheme. Tests both 2^10 and 2^16 lifetimes with SIMD acceleration. Useful for measuring performance with vectorization. Uses the same Rust-compatible architecture as the standard implementation but with SIMD optimizations.
hash-zig-compare
)Purpose: Compare Standard vs SIMD implementations
Command: zig build compare
Description: Compares the performance and correctness of the standard (scalar) vs SIMD implementations. Both use identical Rust-compatible architecture (PRF-based key derivation, epoch management, encoding randomness) and hypercube parameters (64 chains Γ 8 length). Shows performance speedups from SIMD optimizations and verifies public key consistency.
# Build all executables
zig build
# Run specific programs
zig build example # Basic usage demo (Rust-compatible)
zig build benchmark # Standard benchmark
zig build simd-benchmark # SIMD benchmark
zig build compare # Compare Standard vs SIMD
All programs provide detailed timing information and can be used for:
const hash_zig = @import("hash-zig");
// Configure parameters with Poseidon2 (default)
// 128-bit security with hypercube parameters: 64 chains of length 8 (w=3)
const params = hash_zig.Parameters.initHypercube(.lifetime_2_16);
var sig = try hash_zig.HashSignature.init(allocator, params);
defer sig.deinit();
// Generate a random seed (32 bytes required)
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);
// Generate keys from seed with epoch management
// activation_epoch: 0 (start from beginning)
// num_active_epochs: 0 (use full lifetime)
var keypair = try sig.generateKeyPair(allocator, &seed, 0, 0);
defer keypair.deinit(allocator);
// Sign (YOU must track epochs and never reuse!)
const epoch: u64 = 0; // Track this in your app's database!
// Generate RNG seed for encoding randomness (rho)
var rng_seed: [32]u8 = undefined;
std.crypto.random.bytes(&rng_seed);
var signature = try sig.sign(allocator, "message", &keypair.secret_key, epoch, &rng_seed);
defer signature.deinit(allocator);
// Verify
const valid = try sig.verify(allocator, "message", signature, &keypair.public_key);
The generateKeyPair
function requires a 32-byte seed. You can use this for:
std.crypto.random.bytes(&seed)
// Random key generation (default approach)
var random_seed: [32]u8 = undefined;
std.crypto.random.bytes(&random_seed);
var keypair = try sig.generateKeyPair(allocator, &random_seed, 0, 0);
// Deterministic key generation from a known seed
const deterministic_seed: [32]u8 = .{1} ** 32; // Use a proper KDF in production!
var keypair2 = try sig.generateKeyPair(allocator, &deterministic_seed, 0, 0);
// Same seed will always generate the same keypair
// Epoch management example
var keypair_limited = try sig.generateKeyPair(
allocator,
&seed,
100, // activation_epoch: first valid epoch is 100
50 // num_active_epochs: can sign epochs 100-149
);
// Initialize with SHA3-256 instead of Poseidon2
const params = hash_zig.Parameters.initWithSha3(.lifetime_2_16);
// Everything else works the same way
var sig = try hash_zig.HashSignature.init(allocator, params);
defer sig.deinit();
// Poseidon2 with hypercube parameters (default) - optimized for ZK proof systems
const params_p2 = hash_zig.Parameters.initHypercube(.lifetime_2_16);
// SHA3-256 - NIST standard
const params_sha3 = hash_zig.Parameters.initWithSha3(.lifetime_2_16);
// Default parameters (Poseidon2 with lifetime_2_16)
const params_default = hash_zig.Parameters.initDefault();
// lifetime_2_10: 2^10 = 1,024 signatures
const params_short = hash_zig.Parameters.initHypercube(.lifetime_2_10);
// lifetime_2_16: 2^16 = 65,536 signatures (default)
const params_medium = hash_zig.Parameters.initHypercube(.lifetime_2_16);
// lifetime_2_18: 2^18 = 262,144 signatures (for benchmarking against Rust impl)
const params_benchmark = hash_zig.Parameters.initHypercube(.lifetime_2_18);
// lifetime_2_20: 2^20 = 1,048,576 signatures
const params_long = hash_zig.Parameters.initHypercube(.lifetime_2_20);
// lifetime_2_28: 2^28 = 268,435,456 signatures
const params_very_long = hash_zig.Parameters.initHypercube(.lifetime_2_28);
// lifetime_2_32: 2^32 = 4,294,967,296 signatures
const params_extreme = hash_zig.Parameters.initHypercube(.lifetime_2_32);
Using hypercube parameters as specified in hypercube-hashsig-parameters:
Parameter | Value | Notes |
---|---|---|
Security Level | 128-bit | Post-quantum secure |
Hash Output | 32 bytes | 256-bit hash for 128-bit security |
Encoding | Binary | Incomparable binary encoding |
Winternitz w | 3 | Chain length 8 (2^3 = 8) |
Number of Chains | 64 | Hypercube parameter specification |
Chain Length | 8 | Winternitz parameter w=3 |
Poseidon2 Width | 16 | Rust-compatible (external_rounds=8, internal_rounds=20, sbox_degree=3) |
Note: These parameters use the hypercube configuration (64 chains of length 8) as specified in the hypercube-hashsig-parameters repository, with Rust-compatible Poseidon2 parameters for interoperability.
Function | Security | Output Size | Use Case |
---|---|---|---|
Poseidon2 | 128-bit | 32 bytes | ZK proofs, arithmetic circuits |
SHA3-256 | 128-bit | 32 bytes | NIST standard, general crypto |
Both hash functions provide 128-bit post-quantum security with 32-byte (256-bit) output.
Lifetime | Tree Height | Max Signatures | Memory Required* |
---|---|---|---|
lifetime_2_10 | 10 | 1,024 | ~32 KB |
lifetime_2_16 | 16 | 65,536 | ~2 MB |
lifetime_2_18 | 18 | 262,144 | ~8.4 MB |
lifetime_2_20 | 20 | 1,048,576 | ~33 MB |
lifetime_2_28 | 28 | 268,435,456 | ~8.6 GB |
lifetime_2_32 | 32 | 4,294,967,296 | ~137 GB |
*Memory estimates based on 32-byte hashes and cached leaves. Actual memory usage may vary.
hash-zig/
βββ src/
β βββ root.zig # Main module entry point
β βββ params.zig # Configuration and parameters
β βββ poseidon2/
β β βββ fields/
β β β βββ generic_montgomery.zig # Generic 31-bit Montgomery arithmetic
β β β βββ koalabear/montgomery.zig # KoalaBear field instance
β β βββ instances/koalabear16.zig # Width-16 Poseidon2 instance (plonky3 constants)
β β βββ poseidon2.zig # Rust-compatible Poseidon2 implementation
β β βββ generic_poseidon2.zig # Generic Poseidon2 constructor (Montgomery)
β βββ sha3.zig # SHA3 hash implementation
β βββ encoding.zig # Incomparable encodings
β βββ tweakable_hash.zig # Domain-separated hashing
β βββ winternitz.zig # Winternitz OTS
β βββ merkle.zig # Merkle tree construction
β βββ signature.zig # Main signature scheme
βββ examples/
β βββ basic_usage.zig
βββ test/
β βββ integration_test.zig
βββ build.zig
This implementation is benchmarked against the reference Rust implementation using the hash-sig-benchmarks suite.
The benchmark repository provides:
Benchmark the implementations yourself:
git clone https://github.com/ch4r10t33r/hash-sig-benchmarks.git
cd hash-sig-benchmarks
python3 benchmark.py 3 # Run 3 iterations
The benchmark suite automatically:
Measured on Apple M2 with Zig 0.14.1, using Poseidon2 hash and level_128 security:
Operation | Time | Notes |
---|---|---|
Key Generation | ~110 seconds | Parallel multi-threaded, generates 1024 leaves + full tree (2047 nodes) |
Sign | ~370 ms | Derives OTS keys from PRF, includes encoding randomness |
Verify | ~93 ms | Reconstructs leaf, verifies Merkle path |
Key Sizes (Matching Rust):
Performance Notes:
All projections based on Apple M2 Mac (8 cores) with parallel implementation - actual times will vary by hardware.
Lifetime | Signatures | Tree Height | Estimated Time* | Memory Required |
---|---|---|---|---|
lifetime_2_10 | 1,024 | 10 | ~7 min (measured on M2, hypercube parameters) | ~33 KB |
lifetime_2_16 | 65,536 | 16 | ~34 minutes | ~2.1 MB |
lifetime_2_18 | 262,144 | 18 | ~2.2 hours | ~8.4 MB |
lifetime_2_20 | 1,048,576 | 20 | ~9 hours | ~34 MB |
lifetime_2_28 | 268,435,456 | 28 | ~97 days | ~8.6 GB |
lifetime_2_32 | 4,294,967,296 | 32 | ~4.1 years | ~137 GB |
*Projected by linear scaling from M2 parallel measurements with hypercube parameters: (signatures / 1024) Γ 7 min. Key generation scales O(n) with number of signatures. Performance will vary based on CPU core count and speed.
Operation | Time | Complexity |
---|---|---|
Sign | ~50 ms | O(log n) - constant across lifetimes |
Verify | ~25 ms | O(log n) - constant across lifetimes |
Note: Signing and verification times remain nearly constant across all lifetimes because they only process the authentication path (length = tree height). Only key generation scales with the number of signatures.
Important: This library does NOT manage signature state. Your application MUST:
sign()
Example state management pattern:
// Pseudo-code for safe state management
fn signMessage(db: *Database, sig_scheme: *HashSignature, message: []const u8, secret_key: []const u8) !Signature {
// 1. Get and increment index atomically
const index = try db.getAndIncrementIndex();
// 2. Persist the new index BEFORE signing
try db.saveIndex(index + 1);
try db.flush(); // Ensure it's on disk
// 3. Now safe to sign
return sig_scheme.sign(allocator, message, secret_key, index);
}
The implementation matches Rust's GeneralizedXMSSSignatureScheme
exactly:
// Public Key (matches Rust GeneralizedXMSSPublicKey)
pub const PublicKey = struct {
root: []u8, // Merkle root hash
parameter: Parameters, // Hash function parameters
};
// Secret Key (matches Rust GeneralizedXMSSSecretKey)
pub const SecretKey = struct {
prf_key: [32]u8, // PRF key for key derivation
tree: [][]u8, // Full Merkle tree (all nodes)
tree_height: u32,
parameter: Parameters, // Hash function parameters
activation_epoch: u64, // First valid epoch
num_active_epochs: u64, // Number of valid epochs
};
// Signature (matches Rust GeneralizedXMSSSignature)
pub const Signature = struct {
epoch: u64, // Signature epoch/index
auth_path: [][]u8, // Merkle authentication path
rho: [32]u8, // Encoding randomness
hashes: [][]u8, // OTS signature values
};
// Initialize signature scheme
var sig_scheme = try hash_zig.HashSignature.init(allocator, params);
defer sig_scheme.deinit();
// Generate keypair (Rust: key_gen)
// activation_epoch: first valid epoch (0 = start)
// num_active_epochs: number of epochs (0 = use full lifetime)
var keypair = try sig_scheme.generateKeyPair(
allocator,
&seed, // 32-byte seed
0, // activation_epoch
0 // num_active_epochs (0 = all)
);
defer keypair.deinit(allocator);
// Sign a message (Rust: sign)
var signature = try sig_scheme.sign(
allocator,
message, // Message to sign
&keypair.secret_key, // Secret key reference
epoch, // Epoch index (0 to lifetime-1)
&rng_seed // RNG seed for encoding randomness
);
defer signature.deinit(allocator);
// Verify signature (Rust: verify)
const is_valid = try sig_scheme.verify(
allocator,
message, // Message to verify
signature, // Signature to check
&keypair.public_key // Public key reference
);
// Poseidon2 with hypercube parameters (recommended)
const params = hash_zig.Parameters.initHypercube(.lifetime_2_16);
// SHA3-256
const params_sha3 = hash_zig.Parameters.initWithSha3(.lifetime_2_16);
// Default (Poseidon2, lifetime_2_16, 128-bit security)
const params_default = hash_zig.Parameters.initDefault();
pub const SecurityLevel = enum { level_128 }; // Only 128-bit supported
pub const HashFunction = enum { poseidon2, sha3 };
pub const KeyLifetime = enum {
lifetime_2_10, // 1,024 signatures
lifetime_2_16, // 65,536 signatures
lifetime_2_18, // 262,144 signatures
lifetime_2_20, // 1,048,576 signatures
lifetime_2_28, // 268,435,456 signatures
lifetime_2_32 // 4,294,967,296 signatures
};
pub const EncodingType = enum { binary }; // Only binary encoding supported
zig build test
zig build lint
Note: The linter (zlinter) is a dev-time tool for this repository. Consumers of hash-zig
do not need to depend on zlinter unless they want to run our lint target in their own CI.
zig build
zig build example
zig build docs
This will generate HTML documentation in zig-out/docs/
. Open zig-out/docs/index.html
in your browser to view the API documentation.
Poseidon2:
test "poseidon2 hashing" {
const allocator = std.testing.allocator;
const params = hash_zig.Parameters.initHypercube(.lifetime_2_16);
var hash = try hash_zig.TweakableHash.init(allocator, params);
defer hash.deinit();
const result = try hash.hash(allocator, "test data", 0);
defer allocator.free(result);
try std.testing.expect(result.len == 32);
}
SHA3:
test "sha3 hashing" {
const allocator = std.testing.allocator;
const params = hash_zig.Parameters.initWithSha3(.lifetime_2_16);
var hash = try hash_zig.TweakableHash.init(allocator, params);
defer hash.deinit();
const result = try hash.hash(allocator, "test data", 0);
defer allocator.free(result);
try std.testing.expect(result.len == 32); // SHA3-256
}
Comparison:
test "compare hash functions" {
const allocator = std.testing.allocator;
// Poseidon2
const params_p2 = hash_zig.Parameters.initHypercube(.lifetime_2_16);
var hash_p2 = try hash_zig.TweakableHash.init(allocator, params_p2);
defer hash_p2.deinit();
// SHA3-256
const params_sha3 = hash_zig.Parameters.initWithSha3(.lifetime_2_16);
var hash_sha3 = try hash_zig.TweakableHash.init(allocator, params_sha3);
defer hash_sha3.deinit();
const data = "test";
const h1 = try hash_p2.hash(allocator, data, 0);
defer allocator.free(h1);
const h2 = try hash_sha3.hash(allocator, data, 0);
defer allocator.free(h2);
// Different hash functions produce different outputs
try std.testing.expect(!std.mem.eql(u8, h1, h2));
}
Contributions welcome! Please:
zig build test
)zig build lint
)GitHub Actions automatically runs on pushes/PRs to main
, master
, or develop
:
See .github/workflows/ci.yml
for details.
Note: The project currently requires Zig 0.14.1 because zlinter only supports the 0.14.x branch. Once zlinter adds support for Zig 0.15+, we'll update to the latest version.
Apache License 2.0 - see LICENSE file.
β οΈ IMPORTANT DISCLAIMER: This is a prototype implementation for research and experimentation. This code has NOT been audited and should NOT be used in production systems. Applications using this library MUST implement proper state management to prevent signature index reuse - the library does not enforce this.