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/
|
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
|
||||||
|
*~
|
||||||
|
|
|
||||||
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("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");
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -37,6 +38,57 @@ pub const DIDMethod = enum {
|
||||||
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DID Identifier: Minimal Parsing
|
// DID Identifier: Minimal Parsing
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue