19 KiB
Phase 2B: SoulKey & Entropy Implementation
Status: 🔨 IN PROGRESS Objective: Implement core L1 identity primitives (pure Zig) Date Started: 2026-01-30 Critical Path: Unblocks Phase 2C (Identity Validation) and Phase 2D (DIDs)
Architecture Overview
┌───────────────────────────────────────────────────────────────┐
│ Phase 2B: SoulKey & Entropy (Pure Zig - NO Kyber yet) │
├───────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SoulKey (l1-identity/soulkey.zig) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ - Ed25519 keypair (signing) │ │
│ │ - X25519 keypair (ECDH key agreement) │ │
│ │ - ML-KEM-768 placeholder (Phase 3: PQXDH) │ │
│ │ - DID generation (blake3 hash of public keys) │ │
│ │ - Deterministic from seed (HKDF-SHA256) │ │
│ │ - Sign, verify, derive shared secrets │ │
│ │ - Serialize/deserialize for secure storage │ │
│ │ - Zeroize private key material (constant-time) │ │
│ │ │ │
│ │ Public Methods: │ │
│ │ ✅ fromSeed(seed: [32]u8) -> SoulKey │ │
│ │ ✅ generate() -> SoulKey (random seed) │ │
│ │ ✅ sign(message: []u8) -> [64]u8 │ │
│ │ ✅ verify(pubkey, msg, sig) -> bool │ │
│ │ ✅ deriveSharedSecret(peer_public) -> [32]u8 │ │
│ │ ✅ toBytes() / fromBytes() │ │
│ │ ✅ zeroize() │ │
│ │ ✅ didString() │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ EntropyStamp (l1-identity/entropy.zig) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ - Argon2id memory-hard PoW hashing │ │
│ │ - Configurable difficulty (leading zero bits) │ │
│ │ - Timestamp validation (freshness checks) │ │
│ │ - Service type domain separation │ │
│ │ - Kenya Rule: difficulty 8 < 100ms on ARM Cortex-A53 │ │
│ │ │ │
│ │ Configuration: │ │
│ │ - Memory: 2048 KiB (2MB) - mobile-friendly │ │
│ │ - Iterations: 2 (fast for mobile) │ │
│ │ - Parallelism: 1 (single-core) │ │
│ │ - Salt: 16 bytes (random, per-stamp) │ │
│ │ - Hash: 32 bytes (SHA256-compatible) │ │
│ │ │ │
│ │ Public Methods: │ │
│ │ ✅ mine(payload_hash, difficulty, service, max_iter) │ │
│ │ ✅ verify(payload_hash, min_diff, service, max_age) │ │
│ │ ✅ toBytes() / fromBytes() │ │
│ │ ✅ countLeadingZeros() │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ DID (Decentralized Identifier) - in soulkey.zig │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ - Generate from public keys (blake3 hash) │ │
│ │ - Format: did:libertaria:<hex_encoded> │ │
│ │ - 32-byte identifier space │ │
│ │ │ │
│ │ Public Methods: │ │
│ │ ✅ create(ed_pub, x_pub, mlkem_pub) -> DID │ │
│ │ ✅ hexString() -> "did:libertaria:..." │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────┘
File Structure
l1-identity/
├── soulkey.zig [UPDATED] SoulKey generation, signing, DIDs
├── entropy.zig [NEW] Entropy stamp mining and verification
├── crypto.zig [EXISTING] X25519, XChaCha20-Poly1305
├── argon2.zig [EXISTING] Argon2id FFI (C bindings)
├── pqxdh.zig [EXISTING - deferred to Phase 3] PQXDH stubs
└── tests.zig [NEW] Integration tests
Implementation Details
1. SoulKey: Core Identity Keypair
File: l1-identity/soulkey.zig
Structure:
pub const SoulKey = struct {
ed25519_private: [32]u8, // Signing private key
ed25519_public: [32]u8, // Signing public key
x25519_private: [32]u8, // ECDH private key
x25519_public: [32]u8, // ECDH public key
mlkem_private: [2400]u8, // Post-quantum (Phase 3)
mlkem_public: [1184]u8, // Post-quantum (Phase 3)
did: [32]u8, // DID (blake3 hash of publics)
created_at: u64, // Timestamp (unix seconds)
};
Key Methods:
-
fromSeed(seed: [32]u8) -> SoulKey- Deterministic key generation from seed
- HKDF-SHA256 for key derivation
- Domain separation: "libertaria-soulkey-{ed25519|x25519}-v1"
- Returns fully-formed identity
const seed = [_]u8{0x42} ** 32; const soulkey = try SoulKey.fromSeed(&seed); // soulkey.ed25519_public contains signing key // soulkey.x25519_public contains ECDH key // soulkey.did contains deterministic identifier -
generate() -> SoulKey- Random seed + fromSeed()
- Uses crypto.random.bytes()
- Secure memory handling (zeroize seed)
const soulkey = try SoulKey.generate(); -
sign(message: []u8) -> [64]u8- Ed25519 digital signature
- Returns 64-byte signature
const msg = "Hello, Libertaria!"; const sig = try soulkey.sign(msg); // sig: [64]u8 Ed25519 signature -
verify(pubkey: [32]u8, message: []u8, sig: [64]u8) -> bool- Static method for signature verification
- Constant-time comparison
- Returns true if valid, error if invalid
try SoulKey.verify(soulkey.ed25519_public, msg, sig); -
deriveSharedSecret(peer_public: [32]u8) -> [32]u8- X25519 elliptic curve key agreement
- Produces shared secret for symmetric encryption
const shared_secret = try soulkey.deriveSharedSecret(peer_public); // shared_secret: [32]u8 (use with XChaCha20-Poly1305) -
zeroize()- Constant-time secure erasure of private keys
- Uses crypto.utils.secureZero()
- Prevents timing attacks and memory leaks
var soulkey = try SoulKey.generate(); defer soulkey.zeroize(); // Private keys erased on defer -
toBytes() / fromBytes()- Serialization for secure storage
- Includes all key material (WARNING: exposes privates)
- Total size: 3,552 bytes (32+32+32+32+2400+1184+32+8)
DID Generation:
// Inside fromSeed():
var hasher = crypto.hash.blake3.Blake3.init(.{});
hasher.update(&ed25519_public);
hasher.update(&x25519_public);
hasher.update(&mlkem_public); // zeros for now
hasher.final(&did);
// did: [32]u8 (blake3 hash of all public keys)
String Representation:
const did_str = try soulkey.didString(allocator);
// Result: "did:libertaria:4242424242..."
2. Entropy Stamp: Proof-of-Work
File: l1-identity/entropy.zig
Structure:
pub const EntropyStamp = struct {
hash: [32]u8, // Argon2id hash output
difficulty: u8, // Leading zero bits required
memory_cost_kb: u16, // Memory used during mining (2048 KB)
timestamp_sec: u64, // Unix timestamp when created
service_type: u16, // Domain identifier (prevents replay)
};
Kenya Rule Configuration:
ARGON2_MEMORY_KB = 2048 // 2MB (fits on budget devices)
ARGON2_TIME_COST = 2 // 2 iterations (fast)
ARGON2_PARALLELISM = 1 // Single-threaded
SALT_LEN = 16 // Standard Argon2 salt
HASH_LEN = 32 // SHA256-compatible output
DEFAULT_MAX_AGE_SECONDS = 3600 // 1 hour TTL
Key Methods:
-
mine(payload_hash, difficulty, service_type, max_iterations) -> EntropyStamp- Proof-of-work computation
- Increments nonce until hash has enough leading zeros
- Uses Argon2id for memory-hard hashing
- Limits iterations to prevent DoS
const payload = "message to stamp"; var payload_hash: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(payload, &payload_hash, .{}); const stamp = try EntropyStamp.mine( &payload_hash, 8, // difficulty (8-14 for Kenya compliance) 0x0A00, // service_type (FEED_WORLD_POST) 1_000_000, // max_iterations ); // stamp.hash: [32]u8 with 8 leading zero bits // stamp.timestamp_sec: current unix time -
verify(payload_hash, min_difficulty, service_type, max_age) -> void- Checks timestamp freshness (±60 second clock skew)
- Verifies service type matches
- Validates difficulty (leading zero count)
- Throws error if invalid
try stamp.verify( &payload_hash, 8, // require at least 8 zero bits 0x0A00, // expected service 3600, // max age (1 hour) ); // Throws: error.ServiceMismatch if wrong service // Throws: error.StampExpired if too old // Throws: error.InsufficientDifficulty if not enough zeros -
toBytes() -> [58]u8/fromBytes([58]u8) -> EntropyStamp- Serialization for LWF payload inclusion
- Total size: 58 bytes (32+1+2+8+2+13 padding)
- Big-endian format (network byte order)
const bytes = stamp.toBytes(); // bytes: [58]u8 (fits in LWF trailer) const stamp2 = EntropyStamp.fromBytes(&bytes);
Mining Algorithm:
Input: payload_hash, difficulty, service_type, max_iterations
Output: stamp with proof-of-work
1. Generate random nonce [16]u8
2. For each iteration (0 to max_iterations):
a. Increment nonce (little-endian)
b. Compute input = payload_hash || nonce || timestamp || service_type
c. Call Argon2id(input, 2 iterations, 2MB memory, 1 thread)
d. Count leading zero bits in output
e. If zeros >= difficulty, return stamp
3. Throw MaxIterationsExceeded
Kenya Rule Compliance:
- Difficulty 8: ~256 Argon2id iterations on average
- Difficulty 10: ~1024 iterations on average
- Target: <100ms on ARM Cortex-A53 @ 1.4GHz
Performance (Estimated):
| Difficulty | Iterations | Time (ARM A53) | Memory |
|---|---|---|---|
| 4 | 16 | 5ms | 2MB |
| 6 | 64 | 20ms | 2MB |
| 8 | 256 | 80ms | 2MB |
| 10 | 1024 | 320ms | 2MB |
| 12 | 4096 | 1.3s | 2MB |
3. DID: Decentralized Identifier
Structure:
pub const DID = struct {
bytes: [32]u8; // blake3 hash of (ed25519_pub || x25519_pub || mlkem_pub)
};
Generation:
const did = DID.create(
soulkey.ed25519_public,
soulkey.x25519_public,
soulkey.mlkem_public,
);
// did.bytes: [32]u8 (deterministic from public keys)
String Format:
did:libertaria:4242424242424242424242424242424242424242424242424242424242424242
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64 hex characters (32 bytes)
Test Coverage
SoulKey Tests
test "soulkey generation" {
// Test random generation and field validation
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);
const key = try SoulKey.generate(seed);
// Validate all fields are present
try std.testing.expectEqual(@as(usize, 32), key.ed25519_public.len);
try std.testing.expectEqual(@as(usize, 32), key.x25519_public.len);
try std.testing.expectEqual(@as(usize, 32), key.did.len);
}
test "soulkey signature" {
// Test Ed25519 signing and verification
const key = try SoulKey.generate();
const message = "Hello, Libertaria!";
const signature = try key.sign(message);
const valid = try SoulKey.verify(key.ed25519_public, message, signature);
try std.testing.expect(valid);
}
test "soulkey deterministic" {
// Test HKDF seed derivation produces same keys
const seed = [_]u8{0x42} ** 32;
const key1 = try SoulKey.fromSeed(&seed);
const key2 = try SoulKey.fromSeed(&seed);
// Same seed → same keys
try std.testing.expectEqualSlices(u8, &key1.ed25519_public, &key2.ed25519_public);
try std.testing.expectEqualSlices(u8, &key1.x25519_public, &key2.x25519_public);
try std.testing.expectEqualSlices(u8, &key1.did, &key2.did);
}
test "soulkey serialization" {
// Test roundtrip encoding
const key = try SoulKey.generate();
const bytes = try key.toBytes(allocator);
defer allocator.free(bytes);
const key2 = try SoulKey.fromBytes(bytes);
try std.testing.expectEqualSlices(u8, &key.ed25519_public, &key2.ed25519_public);
}
Entropy Stamp Tests
test "entropy stamp: mining and difficulty" {
// Test proof-of-work generation
const payload = "test_payload";
var payload_hash: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(payload, &payload_hash, .{});
const stamp = try EntropyStamp.mine(&payload_hash, 8, 0x0A00, 100_000);
// Verify stamp has required difficulty
const zeros = countLeadingZeros(&stamp.hash);
try std.testing.expect(zeros >= 8);
}
test "entropy stamp: verification" {
// Test freshness and domain separation
const payload = "test";
var payload_hash: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(payload, &payload_hash, .{});
const stamp = try EntropyStamp.mine(&payload_hash, 8, 0x0A00, 100_000);
// Should verify
try stamp.verify(&payload_hash, 8, 0x0A00, 3600);
// Should fail with wrong service
const result = stamp.verify(&payload_hash, 8, 0x0B00, 3600);
try std.testing.expectError(error.ServiceMismatch, result);
}
test "entropy stamp: Kenya rule" {
// Test that difficulty 8 completes in reasonable time
const payload = "Kenya test";
var payload_hash: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(payload, &payload_hash, .{});
const start = std.time.milliTimestamp();
const stamp = try EntropyStamp.mine(&payload_hash, 8, 0x0A00, 1_000_000);
const elapsed = std.time.milliTimestamp() - start;
// Should complete quickly (soft guideline, not hard requirement)
_ = stamp;
_ = elapsed;
}
Dependencies
Pure Zig (std library)
std.crypto.sign.Ed25519- Signingstd.crypto.dh.X25519- Key agreementstd.crypto.hash.blake3- DID generationstd.crypto.hash.sha2- Entropy stamp input hashingstd.crypto.utils.secureZero- Key material destructionstd.crypto.random- Nonce/seed generationstd.time- Timestamp generationstd.mem- Memory utilities
C FFI (Compiled in build.zig)
argon2id_hash_raw- Memory-hard hashing from vendor/argon2/
NOT YET (Phase 3)
OQS_KEM_kyber768_*- Post-quantum KEM (deferred to PQXDH)
Binary Size Impact
| Component | Debug | ReleaseSmall | Status |
|---|---|---|---|
| soulkey.zig | ~20KB | ~4KB | ✅ |
| entropy.zig | ~25KB | ~5KB | ✅ |
| Argon2 C code | ~40KB | ~8KB | ✅ |
| Total L1 | ~85KB | ~17KB | ✅ Kenya Rule |
Security Considerations
Key Derivation (HKDF-SHA256)
- Uses domain separation ("libertaria-soulkey-{type}-v1")
- Prevents key material reuse across contexts
- Complies with NIST SP 800-56C
Signature Verification
- Constant-time Ed25519 verification
- No side-channel leakage of valid/invalid
- Prevents timing-based forging
Key Zeroization
crypto.utils.secureZero()overwrites all private key bytes- Constant-time operation (no early exits)
- Prevents memory disclosure attacks
Entropy Stamp Freshness
- ±60 second clock skew tolerance
- Service type domain separation (prevents cross-service replay)
- Timestamp prevents indefinite reuse
Entropy Stamp Difficulty
- Memory-hard (Argon2id = resistant to GPU attacks)
- Cost-based (thermodynamic limit on spam)
- Difficulty adjustable per application
Next Steps
Immediate (Phase 2B Complete)
- Implement SoulKey generation from seed
- Implement SoulKey signing/verification
- Implement entropy stamp mining
- Implement entropy stamp verification
- Run all tests and verify Kenya compliance
- Document API in docs/L1_IDENTITY_API.md
- Update build.zig to include entropy.zig tests
Phase 2C: Identity Validation
- Implement prekey bundle generation
- Implement prekey signed signature
- Implement one-time prekey rotation
Phase 3: PQXDH Handshake
- Replace ML-KEM placeholders with actual Kyber
- Implement PQXDH initiator flow
- Implement PQXDH responder flow
- Fix Zig-to-C linker (static library approach)
References
- RFC-0250: Larval Identity (SoulKey)
- RFC-0100: Entropy Stamp Schema (PoW)
- RFC-0830: PQXDH Handshake (Phase 3)
- NIST SP 800-56C: Key Derivation Function Specification
- Argon2 Paper: "Argon2: New Generation of Memory-Hard Password Hashing"
- FIPS 186-4: Digital Signature Standard (Ed25519)