feat(l1-identity): integrate ML-KEM-768 post-quantum key and fix Zig 0.13 compatibility
This commit is contained in:
parent
c8ba5ea532
commit
e1df4b89c9
|
|
@ -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
|
||||
*~
|
||||
|
|
|
|||
40
build.zig
40
build.zig
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue