libertaria-stack/docs/archive/PHASE_2B_IMPLEMENTATION.md

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:

  1. 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
    
  2. generate() -> SoulKey

    • Random seed + fromSeed()
    • Uses crypto.random.bytes()
    • Secure memory handling (zeroize seed)
    const soulkey = try SoulKey.generate();
    
  3. 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
    
  4. 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);
    
  5. 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)
    
  6. 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
    
  7. 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:

  1. 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
    
  2. 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
    
  3. 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 - Signing
  • std.crypto.dh.X25519 - Key agreement
  • std.crypto.hash.blake3 - DID generation
  • std.crypto.hash.sha2 - Entropy stamp input hashing
  • std.crypto.utils.secureZero - Key material destruction
  • std.crypto.random - Nonce/seed generation
  • std.time - Timestamp generation
  • std.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)