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/
vendor/liboqs/build/
vendor/liboqs/install/
vendor/argon2/build/
*.o
.zig-cache/
# Binaries & Executables
test_zig_sha3
test_zig_shake
*.exe
*.dll
*.so
*.dylib
*.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("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)
// ========================================================================
@ -56,6 +71,8 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
// SoulKey needs PQXDH for deterministic generation
l1_soulkey_mod.addImport("pqxdh", l1_pqxdh_mod);
const l1_entropy_mod = b.createModule(.{
.root_source_file = b.path("l1-identity/entropy.zig"),
@ -68,12 +85,14 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
l1_prekey_mod.addImport("pqxdh", l1_pqxdh_mod);
const l1_did_mod = b.createModule(.{
.root_source_file = b.path("l1-identity/did.zig"),
.target = target,
.optimize = optimize,
});
l1_did_mod.addImport("pqxdh", l1_pqxdh_mod);
// ========================================================================
// 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(.{
.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);
// L1 Entropy tests (Phase 2B)
@ -131,29 +152,17 @@ pub fn build(b: *std.Build) void {
const l1_prekey_tests = b.addTest(.{
.root_module = l1_prekey_mod,
});
l1_prekey_tests.linkLibC();
const run_l1_prekey_tests = b.addRunArtifact(l1_prekey_tests);
// L1 DID tests (Phase 2D)
const l1_did_tests = b.addTest(.{
.root_module = l1_did_mod,
});
l1_did_tests.linkLibC();
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
l1_mod.addImport("pqxdh", l1_pqxdh_mod);
// Tests (root is test_pqxdh.zig)
const l1_pqxdh_tests_mod = b.createModule(.{
@ -193,6 +202,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
l1_vector_mod.addImport("time", time_mod);
l1_vector_mod.addImport("pqxdh", l1_pqxdh_mod);
const l1_vector_tests = b.addTest(.{
.root_module = l1_vector_mod,
@ -218,7 +228,7 @@ pub fn build(b: *std.Build) void {
l1_vector_tests.linkLibC();
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)
const test_step = b.step("test", "Run SDK tests");

View File

@ -19,6 +19,7 @@
const std = @import("std");
const crypto = std.crypto;
const pqxdh = @import("pqxdh");
// ============================================================================
// Constants
@ -37,6 +38,57 @@ pub const DIDMethod = enum {
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);
}
};
// ============================================================================
// DID Identifier: Minimal Parsing
// ============================================================================

View File

@ -40,6 +40,61 @@ extern "c" fn OQS_KEM_ml_kem_768_decaps(
secret_key: ?*const u8,
) 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)
// ============================================================================

View File

@ -11,6 +11,7 @@
const std = @import("std");
const crypto = std.crypto;
const pqxdh = @import("pqxdh");
// ============================================================================
// Constants (Prekey Validity Periods)
@ -68,33 +69,10 @@ pub const SignedPrekey = struct {
@memcpy(message[0..32], &public_key);
std.mem.writeInt(u64, message[32..40][0..8], now, .big);
// Sign with identity key
// For Phase 2C: use placeholder signature
// Phase 3 will integrate full Ed25519 signing via SoulKey
var signature: [64]u8 = undefined;
// 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);
// Sign with identity key (Ed25519)
// identity_private is the seed
const kp = try crypto.sign.Ed25519.KeyPair.generateDeterministic(identity_private);
const signature = (try kp.sign(&message, null)).toBytes();
// Calculate expiration (30 days from now)
const expires_at = now + SIGNED_PREKEY_ROTATION_DAYS * 24 * 60 * 60;
@ -116,10 +94,7 @@ pub const SignedPrekey = struct {
identity_public: [32]u8,
max_age_seconds: i64,
) !void {
// Phase 2C: Check expiration only
// Phase 3 will integrate full Ed25519 signature verification
_ = identity_public;
// 1. Check expiration
const now: i64 = @intCast(std.time.timestamp());
const age: i64 = now - @as(i64, @intCast(self.created_at));
@ -131,6 +106,15 @@ pub const SignedPrekey = struct {
if (age < -60) {
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)
@ -243,7 +227,7 @@ pub const PrekeyBundle = struct {
signed_prekey_signature: [64]u8,
/// 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_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
.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
.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,
.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,

View File

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