296 lines
11 KiB
Zig
296 lines
11 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;
|
|
|
|
// ============================================================================
|
|
// 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 when liboqs is linked)
|
|
mlkem_private: [2400]u8,
|
|
mlkem_public: [1184]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 ===
|
|
// 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, .{});
|
|
|
|
// === 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 (placeholder) ===
|
|
// TODO: Generate via liboqs when linked (Phase 3: PQXDH)
|
|
@memset(&key.mlkem_private, 0);
|
|
@memset(&key.mlkem_public, 0);
|
|
|
|
// === 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;
|
|
@memcpy(did_input[0..32], &key.ed25519_public);
|
|
@memcpy(did_input[32..64], &key.x25519_public);
|
|
@memcpy(did_input[64..1248], &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.utils.secureZero(u8, &seed);
|
|
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.
|
|
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;
|
|
|
|
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;
|
|
}
|
|
|
|
/// Verify a signature (HMAC-SHA256 for Phase 2C, full Ed25519 in Phase 3)
|
|
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);
|
|
|
|
// Verify second half of signature (HMAC with public key)
|
|
return std.mem.eql(u8, signature[32..64], &expected_hmac);
|
|
}
|
|
|
|
/// 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.utils.secureZero(u8, &self.ed25519_private);
|
|
crypto.utils.secureZero(u8, &self.x25519_private);
|
|
crypto.utils.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);
|
|
}
|