libertaria-stack/core/l1-identity/soulkey.zig

326 lines
12 KiB
Zig

//! RFC-0250: Larval Identity / SoulKey
//!
//! This module implements SoulKey - the core identity keypair for Libertaria.
//!
//! A SoulKey is a cryptographic identity consisting of three keypairs:
//! 1. Ed25519 - Digital signatures (sign messages)
//! 2. X25519 - Elliptic curve key agreement (ECDH)
//! 3. ML-KEM-768 - Post-quantum key encapsulation (hybrid)
//!
//! The identity is cryptographically bound to a DID (Decentralized Identifier)
//! via a SHA256 hash of the public keys.
//!
//! Storage: Private keys MUST be protected (hardware wallet, TPM, or secure enclave)
const std = @import("std");
const crypto = std.crypto;
const pqxdh = @import("pqxdh");
// ============================================================================
// SoulKey: Core Identity Keypair
// ============================================================================
pub const SoulKey = struct {
/// Ed25519 signing keypair
ed25519_private: [32]u8,
ed25519_public: [32]u8,
/// X25519 key agreement keypair
x25519_private: [32]u8,
x25519_public: [32]u8,
/// ML-KEM-768 post-quantum keypair
/// (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,
/// Generation timestamp (unix seconds)
created_at: u64,
// === Methods ===
/// Generate a new SoulKey from seed (deterministic, BIP-39 compatible)
pub fn fromSeed(seed: *const [32]u8) !SoulKey {
var key: SoulKey = undefined;
// === Ed25519 generation ===
// 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
var x25519_seed: [32]u8 = undefined;
// Simple domain separation: hash seed || domain string
// String "libertaria-soulkey-x25519-v1" is 28 bytes
var input_with_domain: [32 + 28]u8 = undefined;
@memcpy(input_with_domain[0..32], seed);
@memcpy(input_with_domain[32..60], "libertaria-soulkey-x25519-v1");
crypto.hash.sha2.Sha256.hash(&input_with_domain, &x25519_seed, .{});
key.x25519_private = x25519_seed;
key.x25519_public = try crypto.dh.X25519.recoverPublicKey(x25519_seed);
// === 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
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..], &key.mlkem_public);
crypto.hash.sha2.Sha256.hash(&did_input, &key.did, .{});
key.created_at = @intCast(std.time.timestamp());
return key;
}
/// Generate a new SoulKey with random seed
pub fn generate() !SoulKey {
var seed: [32]u8 = undefined;
crypto.random.bytes(&seed);
defer crypto.secureZero(u8, &seed);
return fromSeed(&seed);
}
/// Sign a message using Ed25519
pub fn sign(self: *const SoulKey, message: []const u8) ![64]u8 {
// 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)
const signature = try kp.sign(message, null);
return signature.toBytes();
}
/// Verify a signature using Ed25519
pub fn verify(public_key: [32]u8, message: []const u8, signature: [64]u8) !bool {
const sig = crypto.sign.Ed25519.Signature.fromBytes(signature);
const pk = crypto.sign.Ed25519.PublicKey.fromBytes(public_key) catch return false;
sig.verify(message, pk) catch return false;
return true;
}
/// Derive a shared secret via X25519 key agreement
pub fn deriveSharedSecret(self: *const SoulKey, peer_public: [32]u8) ![32]u8 {
return crypto.dh.X25519.scalarmult(self.x25519_private, peer_public);
}
/// Serialize SoulKey to bytes (includes all key material)
/// WARNING: This exposes private keys! Only use for secure storage.
pub fn toBytes(self: *const SoulKey, allocator: std.mem.Allocator) ![]u8 {
const total_size = 32 + 32 + 32 + 32 + 2400 + 1184 + 32 + 8;
var buffer = try allocator.alloc(u8, total_size);
var offset: usize = 0;
@memcpy(buffer[offset .. offset + 32], &self.ed25519_private);
offset += 32;
@memcpy(buffer[offset .. offset + 32], &self.ed25519_public);
offset += 32;
@memcpy(buffer[offset .. offset + 32], &self.x25519_private);
offset += 32;
@memcpy(buffer[offset .. offset + 32], &self.x25519_public);
offset += 32;
@memcpy(buffer[offset .. offset + 2400], &self.mlkem_private);
offset += 2400;
@memcpy(buffer[offset .. offset + 1184], &self.mlkem_public);
offset += 1184;
@memcpy(buffer[offset .. offset + 32], &self.did);
offset += 32;
@memcpy(
buffer[offset .. offset + 8],
std.mem.asBytes(&std.mem.nativeToBig(u64, self.created_at)),
);
return buffer;
}
/// Deserialize SoulKey from bytes
pub fn fromBytes(data: []const u8) !SoulKey {
const expected_size = 32 + 32 + 32 + 32 + 2400 + 1184 + 32 + 8;
if (data.len != expected_size) return error.InvalidSoulKeySize;
var key: SoulKey = undefined;
var offset: usize = 0;
@memcpy(&key.ed25519_private, data[offset .. offset + 32]);
offset += 32;
@memcpy(&key.ed25519_public, data[offset .. offset + 32]);
offset += 32;
@memcpy(&key.x25519_private, data[offset .. offset + 32]);
offset += 32;
@memcpy(&key.x25519_public, data[offset .. offset + 32]);
offset += 32;
@memcpy(&key.mlkem_private, data[offset .. offset + 2400]);
offset += 2400;
@memcpy(&key.mlkem_public, data[offset .. offset + 1184]);
offset += 1184;
@memcpy(&key.did, data[offset .. offset + 32]);
offset += 32;
key.created_at = std.mem.readInt(u64, data[offset .. offset + 8][0..8], .big);
return key;
}
/// Zeroize private key material (constant-time)
pub fn zeroize(self: *SoulKey) void {
crypto.secureZero(u8, &self.ed25519_private);
crypto.secureZero(u8, &self.x25519_private);
crypto.secureZero(u8, &self.mlkem_private);
}
/// Get the DID string (base58 or hex)
pub fn didString(self: *const SoulKey, allocator: std.mem.Allocator) ![]u8 {
// For now, return hex-encoded DID
return std.fmt.allocPrint(allocator, "did:libertaria:{s}", .{std.fmt.fmtSliceHexLower(&self.did)});
}
};
// ============================================================================
// DID: Decentralized Identifier
// ============================================================================
pub const DID = struct {
/// Raw DID bytes (32-byte SHA256 hash of all public keys)
bytes: [32]u8,
/// Create DID from public keys
/// Hash: SHA256(ed25519_public || x25519_public || mlkem_public)
pub fn create(ed25519_public: [32]u8, x25519_public: [32]u8, mlkem_public: [1184]u8) DID {
var did_input: [32 + 32 + 1184]u8 = undefined;
@memcpy(did_input[0..32], &ed25519_public);
@memcpy(did_input[32..64], &x25519_public);
@memcpy(did_input[64..1248], &mlkem_public);
var bytes: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(&did_input, &bytes, .{});
return .{ .bytes = bytes };
}
/// Hex-encode DID for display
pub fn hexString(self: *const DID, allocator: std.mem.Allocator) ![]u8 {
return std.fmt.allocPrint(allocator, "did:libertaria:{s}", .{std.fmt.fmtSliceHexLower(&self.bytes)});
}
};
// ============================================================================
// Tests
// ============================================================================
test "soulkey generation" {
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);
const key = try SoulKey.fromSeed(&seed);
try std.testing.expectEqual(@as(usize, 32), key.ed25519_public.len);
try std.testing.expectEqual(@as(usize, 32), key.x25519_public.len);
try std.testing.expectEqual(@as(usize, 32), key.did.len);
}
test "soulkey signature" {
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);
const key = try SoulKey.fromSeed(&seed);
const message = "Hello, Libertaria!";
const signature = try key.sign(message);
const valid = try SoulKey.verify(key.ed25519_public, message, signature);
try std.testing.expect(valid);
}
test "soulkey serialization" {
const allocator = std.testing.allocator;
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);
const key = try SoulKey.fromSeed(&seed);
const bytes = try key.toBytes(allocator);
defer allocator.free(bytes);
const key2 = try SoulKey.fromBytes(bytes);
try std.testing.expectEqualSlices(u8, &key.ed25519_public, &key2.ed25519_public);
try std.testing.expectEqualSlices(u8, &key.x25519_public, &key2.x25519_public);
try std.testing.expectEqualSlices(u8, &key.did, &key2.did);
}
test "did creation" {
var seed: [32]u8 = undefined;
std.crypto.random.bytes(&seed);
const key = try SoulKey.fromSeed(&seed);
const did = DID.create(key.ed25519_public, key.x25519_public, key.mlkem_public);
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);
}