feat(l1-identity): integrate ML-KEM-768 post-quantum key and fix Zig 0.13 compatibility

This commit is contained in:
Markus Maiwald 2026-01-31 00:07:55 +01:00
parent c8ba5ea532
commit e1df4b89c9
6 changed files with 252 additions and 99 deletions

32
.gitignore vendored
View File

@ -1,7 +1,29 @@
zig-cache/ # Zig
zig-out/ zig-out/
vendor/liboqs/build/ .zig-cache/
vendor/liboqs/install/
vendor/argon2/build/ # Binaries & Executables
*.o test_zig_sha3
test_zig_shake
*.exe
*.dll
*.so
*.dylib
*.a *.a
*.lib
# Operational Reports & Stories
REPORTS/
STORIES/
*.report.md
*.story.md
logs/
*.log
# Editor & OS
.DS_Store
.idea/
.vscode/
*.swp
*.swo
*~

View File

@ -48,6 +48,21 @@ pub fn build(b: *std.Build) void {
l1_mod.addImport("shake", crypto_shake_mod); l1_mod.addImport("shake", crypto_shake_mod);
l1_mod.addImport("fips202_bridge", crypto_fips202_mod); l1_mod.addImport("fips202_bridge", crypto_fips202_mod);
// ========================================================================
// L1 PQXDH Module (Phase 3) - Core Dependency
// ========================================================================
const l1_pqxdh_mod = b.createModule(.{
.root_source_file = b.path("l1-identity/pqxdh.zig"),
.target = target,
.optimize = optimize,
});
l1_pqxdh_mod.addIncludePath(b.path("vendor/liboqs/install/include"));
l1_pqxdh_mod.addLibraryPath(b.path("vendor/liboqs/install/lib"));
l1_pqxdh_mod.linkSystemLibrary("oqs", .{ .needed = true });
// Ensure l1_mod uses PQXDH
l1_mod.addImport("pqxdh", l1_pqxdh_mod);
// ======================================================================== // ========================================================================
// L1 Modules: SoulKey, Entropy, Prekey (Phase 2B + 2C) // L1 Modules: SoulKey, Entropy, Prekey (Phase 2B + 2C)
// ======================================================================== // ========================================================================
@ -56,6 +71,8 @@ pub fn build(b: *std.Build) void {
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
// SoulKey needs PQXDH for deterministic generation
l1_soulkey_mod.addImport("pqxdh", l1_pqxdh_mod);
const l1_entropy_mod = b.createModule(.{ const l1_entropy_mod = b.createModule(.{
.root_source_file = b.path("l1-identity/entropy.zig"), .root_source_file = b.path("l1-identity/entropy.zig"),
@ -68,12 +85,14 @@ pub fn build(b: *std.Build) void {
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
l1_prekey_mod.addImport("pqxdh", l1_pqxdh_mod);
const l1_did_mod = b.createModule(.{ const l1_did_mod = b.createModule(.{
.root_source_file = b.path("l1-identity/did.zig"), .root_source_file = b.path("l1-identity/did.zig"),
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
l1_did_mod.addImport("pqxdh", l1_pqxdh_mod);
// ======================================================================== // ========================================================================
// Tests (with C FFI support for Argon2 + liboqs) // Tests (with C FFI support for Argon2 + liboqs)
@ -101,6 +120,8 @@ pub fn build(b: *std.Build) void {
const l1_soulkey_tests = b.addTest(.{ const l1_soulkey_tests = b.addTest(.{
.root_module = l1_soulkey_mod, .root_module = l1_soulkey_mod,
}); });
// Tests linking liboqs effectively happen via the module now, but we also link LibC
l1_soulkey_tests.linkLibC();
const run_l1_soulkey_tests = b.addRunArtifact(l1_soulkey_tests); const run_l1_soulkey_tests = b.addRunArtifact(l1_soulkey_tests);
// L1 Entropy tests (Phase 2B) // L1 Entropy tests (Phase 2B)
@ -131,29 +152,17 @@ pub fn build(b: *std.Build) void {
const l1_prekey_tests = b.addTest(.{ const l1_prekey_tests = b.addTest(.{
.root_module = l1_prekey_mod, .root_module = l1_prekey_mod,
}); });
l1_prekey_tests.linkLibC();
const run_l1_prekey_tests = b.addRunArtifact(l1_prekey_tests); const run_l1_prekey_tests = b.addRunArtifact(l1_prekey_tests);
// L1 DID tests (Phase 2D) // L1 DID tests (Phase 2D)
const l1_did_tests = b.addTest(.{ const l1_did_tests = b.addTest(.{
.root_module = l1_did_mod, .root_module = l1_did_mod,
}); });
l1_did_tests.linkLibC();
const run_l1_did_tests = b.addRunArtifact(l1_did_tests); const run_l1_did_tests = b.addRunArtifact(l1_did_tests);
// ========================================================================
// L1 PQXDH tests (Phase 3)
// ========================================================================
const l1_pqxdh_mod = b.createModule(.{
.root_source_file = b.path("l1-identity/pqxdh.zig"),
.target = target,
.optimize = optimize,
});
l1_pqxdh_mod.addIncludePath(b.path("vendor/liboqs/install/include"));
l1_pqxdh_mod.addLibraryPath(b.path("vendor/liboqs/install/lib"));
l1_pqxdh_mod.linkSystemLibrary("oqs", .{ .needed = true });
// Consuming artifacts must linkLibC()
// Import PQXDH into main L1 module // Import PQXDH into main L1 module
l1_mod.addImport("pqxdh", l1_pqxdh_mod);
// Tests (root is test_pqxdh.zig) // Tests (root is test_pqxdh.zig)
const l1_pqxdh_tests_mod = b.createModule(.{ const l1_pqxdh_tests_mod = b.createModule(.{
@ -193,6 +202,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .optimize = optimize,
}); });
l1_vector_mod.addImport("time", time_mod); l1_vector_mod.addImport("time", time_mod);
l1_vector_mod.addImport("pqxdh", l1_pqxdh_mod);
const l1_vector_tests = b.addTest(.{ const l1_vector_tests = b.addTest(.{
.root_module = l1_vector_mod, .root_module = l1_vector_mod,
@ -218,7 +228,7 @@ pub fn build(b: *std.Build) void {
l1_vector_tests.linkLibC(); l1_vector_tests.linkLibC();
const run_l1_vector_tests = b.addRunArtifact(l1_vector_tests); const run_l1_vector_tests = b.addRunArtifact(l1_vector_tests);
// NOTE: Phase 3 PQXDH uses stubbed ML-KEM. Real liboqs integration pending. // NOTE: Phase 3 PQXDH uses ML-KEM-768 via liboqs (integrated).
// Test step (runs Phase 2B + 2C + 2D + 3C SDK tests) // Test step (runs Phase 2B + 2C + 2D + 3C SDK tests)
const test_step = b.step("test", "Run SDK tests"); const test_step = b.step("test", "Run SDK tests");

View File

@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const crypto = std.crypto; const crypto = std.crypto;
const pqxdh = @import("pqxdh");
// ============================================================================ // ============================================================================
// Constants // Constants
@ -32,9 +33,60 @@ pub const DEFAULT_CACHE_TTL_SECONDS: u64 = 3600;
/// Supported DID methods /// Supported DID methods
pub const DIDMethod = enum { pub const DIDMethod = enum {
mosaic, // did:mosaic:* mosaic, // did:mosaic:*
libertaria, // did:libertaria:* libertaria, // did:libertaria:*
other, // Future methods, opaque handling other, // Future methods, opaque handling
};
// ============================================================================
// DID Document: Public Identity State
// ============================================================================
/// Represents the resolved public state of an identity
pub const DIDDocument = struct {
/// The DID identifier (hash of keys)
id: DIDIdentifier,
/// Public Keys (Must match hash in ID)
ed25519_public: [32]u8,
x25519_public: [32]u8,
mlkem_public: [pqxdh.ML_KEM_768.PUBLIC_KEY_SIZE]u8,
/// Metadata
created_at: u64,
version: u32 = 1,
/// Self-signature by Ed25519 key (binds ID to keys)
/// Signed data: id.method_specific_id || created_at || version
signature: [64]u8,
/// Verify that this document is valid (hash matches ID, signature valid)
pub fn verify(self: *const DIDDocument) !void {
// 1. Verify ID hash
var did_input: [32 + 32 + pqxdh.ML_KEM_768.PUBLIC_KEY_SIZE]u8 = undefined;
@memcpy(did_input[0..32], &self.ed25519_public);
@memcpy(did_input[32..64], &self.x25519_public);
@memcpy(did_input[64..], &self.mlkem_public);
var calculated_hash: [32]u8 = undefined;
crypto.hash.sha2.Sha256.hash(&did_input, &calculated_hash, .{});
if (!std.mem.eql(u8, &calculated_hash, &self.id.method_specific_id)) {
return error.InvalidDIDHash;
}
// 2. Verify Signature
// Data: method_specific_id (32) + created_at (8) + version (4)
var sig_data: [32 + 8 + 4]u8 = undefined;
@memcpy(sig_data[0..32], &self.id.method_specific_id);
std.mem.writeInt(u64, sig_data[32..40], self.created_at, .little);
std.mem.writeInt(u32, sig_data[40..44], self.version, .little);
// Verification (using Ed25519)
const sig = crypto.sign.Ed25519.Signature.fromBytes(self.signature);
const pk = try crypto.sign.Ed25519.PublicKey.fromBytes(self.ed25519_public);
try sig.verify(&sig_data, pk);
}
}; };
// ============================================================================ // ============================================================================
@ -136,9 +188,9 @@ pub const DIDIdentifier = struct {
pub const DIDCacheEntry = struct { pub const DIDCacheEntry = struct {
did: DIDIdentifier, did: DIDIdentifier,
metadata: []const u8, // Opaque bytes (method-specific) metadata: []const u8, // Opaque bytes (method-specific)
ttl_seconds: u64, // Entry TTL ttl_seconds: u64, // Entry TTL
created_at: u64, // Unix timestamp created_at: u64, // Unix timestamp
/// Check if this cache entry has expired /// Check if this cache entry has expired
pub fn isExpired(self: *const DIDCacheEntry, now: u64) bool { pub fn isExpired(self: *const DIDCacheEntry, now: u64) bool {
@ -359,8 +411,8 @@ test "DID cache pruning" {
const did1 = try DIDIdentifier.parse("did:mosaic:prune1"); const did1 = try DIDIdentifier.parse("did:mosaic:prune1");
const did2 = try DIDIdentifier.parse("did:mosaic:prune2"); const did2 = try DIDIdentifier.parse("did:mosaic:prune2");
try cache.store(&did1, "data1", 1); // Short TTL try cache.store(&did1, "data1", 1); // Short TTL
try cache.store(&did2, "data2", 3600); // Long TTL try cache.store(&did2, "data2", 3600); // Long TTL
const initial_count = cache.count(); const initial_count = cache.count();
try std.testing.expect(initial_count == 2); try std.testing.expect(initial_count == 2);

View File

@ -40,6 +40,61 @@ extern "c" fn OQS_KEM_ml_kem_768_decaps(
secret_key: ?*const u8, secret_key: ?*const u8,
) c_int; ) c_int;
/// Switch liboqs RNG algorithm (e.g., "system", "nist-kat")
extern "c" fn OQS_randombytes_switch_algorithm(algorithm: ?[*:0]const u8) c_int;
/// Set custom RNG callback
extern "c" fn OQS_randombytes_custom_algorithm(algorithm_ptr: *const fn ([*]u8, usize) callconv(.c) void) void;
/// Global mutex to protect RNG state during deterministic generation
var rng_mutex = std.Thread.Mutex{};
/// Global SHAKE256 state for deterministic RNG
var deterministic_rng: std.crypto.hash.sha3.Shake256 = undefined;
/// Custom RNG callback for liboqs -> uses global SHAKE256 state
fn custom_rng_callback(dest: [*]u8, len: usize) callconv(.c) void {
deterministic_rng.squeeze(dest[0..len]);
}
pub const KeyPair = struct {
public_key: [ML_KEM_768.PUBLIC_KEY_SIZE]u8,
secret_key: [ML_KEM_768.SECRET_KEY_SIZE]u8,
};
/// Generate ML-KEM-768 keypair deterministically from a 32-byte seed
/// Thread-safe via global mutex (liboqs RNG is global state)
pub fn generateKeypairFromSeed(seed: [32]u8) !KeyPair {
rng_mutex.lock();
defer rng_mutex.unlock();
// 1. Initialize deterministic RNG with seed
deterministic_rng = std.crypto.hash.sha3.Shake256.init(.{});
// Use domain separation for ML-KEM seed
const domain = "Libertaria_ML-KEM-768_Seed_v1";
deterministic_rng.update(domain);
deterministic_rng.update(&seed);
// 2. Switch liboqs to use our custom callback
OQS_randombytes_custom_algorithm(custom_rng_callback);
// 3. Generate keypair
var kp: KeyPair = undefined;
// Call liboqs key generation
// Note: liboqs keygen consumes randomness from the RNG we set
if (OQS_KEM_ml_kem_768_keypair(&kp.public_key[0], &kp.secret_key[0]) != 0) {
// Reset RNG before error return
_ = OQS_randombytes_switch_algorithm("system");
return error.KeyGenerationFailed;
}
// 4. Restore system RNG (important!)
_ = OQS_randombytes_switch_algorithm("system");
return kp;
}
// ============================================================================ // ============================================================================
// ML-KEM-768 Parameters (NIST FIPS 203) // ML-KEM-768 Parameters (NIST FIPS 203)
// ============================================================================ // ============================================================================

View File

@ -11,6 +11,7 @@
const std = @import("std"); const std = @import("std");
const crypto = std.crypto; const crypto = std.crypto;
const pqxdh = @import("pqxdh");
// ============================================================================ // ============================================================================
// Constants (Prekey Validity Periods) // Constants (Prekey Validity Periods)
@ -68,33 +69,10 @@ pub const SignedPrekey = struct {
@memcpy(message[0..32], &public_key); @memcpy(message[0..32], &public_key);
std.mem.writeInt(u64, message[32..40][0..8], now, .big); std.mem.writeInt(u64, message[32..40][0..8], now, .big);
// Sign with identity key // Sign with identity key (Ed25519)
// For Phase 2C: use placeholder signature // identity_private is the seed
// Phase 3 will integrate full Ed25519 signing via SoulKey const kp = try crypto.sign.Ed25519.KeyPair.generateDeterministic(identity_private);
var signature: [64]u8 = undefined; const signature = (try kp.sign(&message, null)).toBytes();
// Create a deterministic signature-like value for Phase 2C
// This is NOT a real cryptographic signature; just a placeholder
// Phase 3 will replace this with proper Ed25519 signatures
var combined: [32 + 40 + 8]u8 = undefined;
@memcpy(combined[0..32], &identity_private);
@memcpy(combined[32..72], &message);
std.mem.writeInt(u64, combined[72..80][0..8], now, .big);
// Hash the combined material to get signature-like bytes
var hash1: [32]u8 = undefined;
crypto.hash.sha2.Sha256.hash(combined[0..80], &hash1, .{});
var hash2: [32]u8 = undefined;
// Use second hash of rotated input
var combined2: [80]u8 = undefined;
@memcpy(combined2[0..72], combined[8..]);
@memcpy(combined2[72..80], combined[0..8]);
crypto.hash.sha2.Sha256.hash(&combined2, &hash2, .{});
// Combine hashes into 64-byte signature
@memcpy(signature[0..32], &hash1);
@memcpy(signature[32..64], &hash2);
// Calculate expiration (30 days from now) // Calculate expiration (30 days from now)
const expires_at = now + SIGNED_PREKEY_ROTATION_DAYS * 24 * 60 * 60; const expires_at = now + SIGNED_PREKEY_ROTATION_DAYS * 24 * 60 * 60;
@ -116,10 +94,7 @@ pub const SignedPrekey = struct {
identity_public: [32]u8, identity_public: [32]u8,
max_age_seconds: i64, max_age_seconds: i64,
) !void { ) !void {
// Phase 2C: Check expiration only // 1. Check expiration
// Phase 3 will integrate full Ed25519 signature verification
_ = identity_public;
const now: i64 = @intCast(std.time.timestamp()); const now: i64 = @intCast(std.time.timestamp());
const age: i64 = now - @as(i64, @intCast(self.created_at)); const age: i64 = now - @as(i64, @intCast(self.created_at));
@ -131,6 +106,15 @@ pub const SignedPrekey = struct {
if (age < -60) { if (age < -60) {
return error.SignedPrekeyFromFuture; return error.SignedPrekeyFromFuture;
} }
// 2. Verify signature
var message: [32 + 8]u8 = undefined;
@memcpy(message[0..32], &self.public_key);
std.mem.writeInt(u64, message[32..40][0..8], self.created_at, .big);
crypto.sign.Ed25519.verify(self.signature, &message, identity_public) catch {
return error.InvalidSignature;
};
} }
/// Check if prekey is approaching expiration (within grace period) /// Check if prekey is approaching expiration (within grace period)
@ -243,7 +227,7 @@ pub const PrekeyBundle = struct {
signed_prekey_signature: [64]u8, signed_prekey_signature: [64]u8,
/// Kyber-768 public key (post-quantum, optional) /// Kyber-768 public key (post-quantum, optional)
kyber_public: [1184]u8, mlkem_public: [pqxdh.ML_KEM_768.PUBLIC_KEY_SIZE]u8,
/// One-time prekeys (array of X25519 keys) /// One-time prekeys (array of X25519 keys)
one_time_keys: std.ArrayList(OneTimePrekey), one_time_keys: std.ArrayList(OneTimePrekey),
@ -289,7 +273,7 @@ pub const PrekeyBundle = struct {
.identity_key = [32]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // placeholder .identity_key = [32]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // placeholder
.signed_prekey = signed_prekey, .signed_prekey = signed_prekey,
.signed_prekey_signature = [64]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // placeholder .signed_prekey_signature = [64]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // placeholder
.kyber_public = [1184]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } ** 1, // placeholder .mlkem_public = [1]u8{0} ** pqxdh.ML_KEM_768.PUBLIC_KEY_SIZE, // placeholder
.one_time_keys = one_time_keys, .one_time_keys = one_time_keys,
.did = [32]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // placeholder .did = [32]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // placeholder
.created_at = now, .created_at = now,

View File

@ -14,6 +14,7 @@
const std = @import("std"); const std = @import("std");
const crypto = std.crypto; const crypto = std.crypto;
const pqxdh = @import("pqxdh");
// ============================================================================ // ============================================================================
// SoulKey: Core Identity Keypair // SoulKey: Core Identity Keypair
@ -29,9 +30,9 @@ pub const SoulKey = struct {
x25519_public: [32]u8, x25519_public: [32]u8,
/// ML-KEM-768 post-quantum keypair /// ML-KEM-768 post-quantum keypair
/// (populated when liboqs is linked) /// (populated deterministically from seed via liboqs)
mlkem_private: [2400]u8, mlkem_private: [pqxdh.ML_KEM_768.SECRET_KEY_SIZE]u8,
mlkem_public: [1184]u8, mlkem_public: [pqxdh.ML_KEM_768.PUBLIC_KEY_SIZE]u8,
/// DID: SHA256 hash of (ed25519_public || x25519_public || mlkem_public) /// DID: SHA256 hash of (ed25519_public || x25519_public || mlkem_public)
did: [32]u8, did: [32]u8,
@ -46,12 +47,11 @@ pub const SoulKey = struct {
var key: SoulKey = undefined; var key: SoulKey = undefined;
// === Ed25519 generation === // === Ed25519 generation ===
// Direct seed keypair (per Ed25519 spec) // Properly derive keypair from seed using standard Ed25519
key.ed25519_private = seed.*; const ed_kp = try crypto.sign.Ed25519.KeyPair.generateDeterministic(seed.*);
// ed_kp.secret_key.seed() returns the seed used.
// For Ed25519: seed is the private key, derive public key via hashing key.ed25519_private = ed_kp.secret_key.seed();
// This is simplified; Phase 3 will use proper Ed25519 key derivation key.ed25519_public = ed_kp.public_key.bytes;
crypto.hash.sha2.Sha256.hash(seed, &key.ed25519_public, .{});
// === X25519 generation === // === X25519 generation ===
// Derive X25519 private from seed via domain-separated hashing // Derive X25519 private from seed via domain-separated hashing
@ -65,18 +65,27 @@ pub const SoulKey = struct {
key.x25519_private = x25519_seed; key.x25519_private = x25519_seed;
key.x25519_public = try crypto.dh.X25519.recoverPublicKey(x25519_seed); key.x25519_public = try crypto.dh.X25519.recoverPublicKey(x25519_seed);
// === ML-KEM-768 generation (placeholder) === // === ML-KEM-768 generation ===
// TODO: Generate via liboqs when linked (Phase 3: PQXDH) // Derive dedicated seed for ML-KEM to ensure domain separation
@memset(&key.mlkem_private, 0); var mlkem_seed: [32]u8 = undefined;
@memset(&key.mlkem_public, 0); var mlkem_input: [32 + 30]u8 = undefined;
@memcpy(mlkem_input[0..32], seed);
@memcpy(mlkem_input[32..62], "libertaria-soulkey-mlkem768-v1");
crypto.hash.sha2.Sha256.hash(&mlkem_input, &mlkem_seed, .{});
// Use custom thread-safe deterministic generation (via liboqs RNG override)
// Note: This relies on liboqs being linked via build.zig
const kp = try pqxdh.generateKeypairFromSeed(mlkem_seed);
key.mlkem_public = kp.public_key;
key.mlkem_private = kp.secret_key;
// === DID generation === // === DID generation ===
// Hash all public keys together: ed25519 || x25519 || mlkem // Hash all public keys together: ed25519 || x25519 || mlkem
// Using SHA256 (Blake3 unavailable in Zig stdlib) // Using SHA256
var did_input: [32 + 32 + 1184]u8 = undefined; var did_input: [32 + 32 + pqxdh.ML_KEM_768.PUBLIC_KEY_SIZE]u8 = undefined;
@memcpy(did_input[0..32], &key.ed25519_public); @memcpy(did_input[0..32], &key.ed25519_public);
@memcpy(did_input[32..64], &key.x25519_public); @memcpy(did_input[32..64], &key.x25519_public);
@memcpy(did_input[64..1248], &key.mlkem_public); @memcpy(did_input[64..], &key.mlkem_public);
crypto.hash.sha2.Sha256.hash(&did_input, &key.did, .{}); crypto.hash.sha2.Sha256.hash(&did_input, &key.did, .{});
key.created_at = @intCast(std.time.timestamp()); key.created_at = @intCast(std.time.timestamp());
@ -92,34 +101,24 @@ pub const SoulKey = struct {
return fromSeed(&seed); return fromSeed(&seed);
} }
/// Sign a message (HMAC-SHA256 for Phase 2C, full Ed25519 in Phase 3) /// Sign a message using Ed25519
/// Phase 2C uses simplified signing with 32-byte seed.
/// Phase 3 will upgrade to proper Ed25519 signatures.
pub fn sign(self: *const SoulKey, message: []const u8) ![64]u8 { pub fn sign(self: *const SoulKey, message: []const u8) ![64]u8 {
var signature: [64]u8 = undefined; // Reconstruct KeyPair from stored seed/public
// Use HMAC-SHA256 for simplified signing in Phase 2C // Note: Ed25519.KeyPair can be formed from just seed if needed, but we have both.
// Signature: HMAC-SHA256(private_key, message) || HMAC-SHA256(public_key, message) const kp = try crypto.sign.Ed25519.KeyPair.generateDeterministic(self.ed25519_private);
var hmac1: [32]u8 = undefined; // Verify public matches? (Optional sanity check)
var hmac2: [32]u8 = undefined;
crypto.auth.hmac.sha2.HmacSha256.create(&hmac1, message, &self.ed25519_private); const signature = try kp.sign(message, null);
crypto.auth.hmac.sha2.HmacSha256.create(&hmac2, message, &self.ed25519_public); return signature.toBytes();
@memcpy(signature[0..32], &hmac1);
@memcpy(signature[32..64], &hmac2);
return signature;
} }
/// Verify a signature (HMAC-SHA256 for Phase 2C, full Ed25519 in Phase 3) /// Verify a signature using Ed25519
pub fn verify(public_key: [32]u8, message: []const u8, signature: [64]u8) !bool { pub fn verify(public_key: [32]u8, message: []const u8, signature: [64]u8) !bool {
// Phase 2C verification: check that signature matches HMAC pattern const sig = crypto.sign.Ed25519.Signature.fromBytes(signature);
// In Phase 3, this will be upgraded to Ed25519 verification const pk = crypto.sign.Ed25519.PublicKey.fromBytes(public_key) catch return false;
var expected_hmac: [32]u8 = undefined;
crypto.auth.hmac.sha2.HmacSha256.create(&expected_hmac, message, &public_key);
// Verify second half of signature (HMAC with public key) sig.verify(message, pk) catch return false;
return std.mem.eql(u8, signature[32..64], &expected_hmac); return true;
} }
/// Derive a shared secret via X25519 key agreement /// Derive a shared secret via X25519 key agreement
@ -293,3 +292,34 @@ test "did creation" {
try std.testing.expectEqualSlices(u8, &key.did, &did.bytes); try std.testing.expectEqualSlices(u8, &key.did, &did.bytes);
} }
test "SoulKey deterministic generation" {
var seed: [32]u8 = [_]u8{0x42} ** 32;
const key1 = try SoulKey.fromSeed(&seed);
const key2 = try SoulKey.fromSeed(&seed);
try std.testing.expectEqualSlices(u8, &key1.ed25519_private, &key2.ed25519_private);
try std.testing.expectEqualSlices(u8, &key1.ed25519_public, &key2.ed25519_public);
try std.testing.expectEqualSlices(u8, &key1.x25519_private, &key2.x25519_private);
try std.testing.expectEqualSlices(u8, &key1.x25519_public, &key2.x25519_public);
try std.testing.expectEqualSlices(u8, &key1.mlkem_private, &key2.mlkem_private);
try std.testing.expectEqualSlices(u8, &key1.mlkem_public, &key2.mlkem_public);
try std.testing.expectEqualSlices(u8, &key1.did, &key2.did);
}
test "SoulKey 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);
// Check invalid signature
var invalid_sig = signature;
invalid_sig[0] ^= 0xFF; // Flip a bit
const invalid = try SoulKey.verify(key.ed25519_public, message, invalid_sig);
try std.testing.expect(!invalid);
}