541 lines
19 KiB
Markdown
541 lines
19 KiB
Markdown
# 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:**
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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)
|
|
|
|
```zig
|
|
const soulkey = try SoulKey.generate();
|
|
```
|
|
|
|
3. **`sign(message: []u8) -> [64]u8`**
|
|
- Ed25519 digital signature
|
|
- Returns 64-byte signature
|
|
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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:**
|
|
```zig
|
|
// 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:**
|
|
```zig
|
|
const did_str = try soulkey.didString(allocator);
|
|
// Result: "did:libertaria:4242424242..."
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Entropy Stamp: Proof-of-Work
|
|
|
|
**File:** `l1-identity/entropy.zig`
|
|
|
|
**Structure:**
|
|
```zig
|
|
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:**
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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)
|
|
|
|
```zig
|
|
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:**
|
|
```zig
|
|
pub const DID = struct {
|
|
bytes: [32]u8; // blake3 hash of (ed25519_pub || x25519_pub || mlkem_pub)
|
|
};
|
|
```
|
|
|
|
**Generation:**
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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
|
|
|
|
```zig
|
|
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)
|
|
|
|
- [x] Implement SoulKey generation from seed
|
|
- [x] Implement SoulKey signing/verification
|
|
- [x] Implement entropy stamp mining
|
|
- [x] 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)
|
|
|