montana/macOS/MontanaPresence/Crypto/MLDSA65.swift

337 lines
12 KiB
Swift

//
// MLDSA65.swift
// Montana Protocol macOS
//
// ML-DSA-65 (FIPS 204) Post-Quantum Signatures via liboqs
// Layer 1: Keys, Addresses, Signatures
//
// Key sizes:
// Private key: 4032 bytes
// Public key: 1952 bytes
// Signature: 3309 bytes
//
// Address format: mt + SHA256(pubkey)[:18].hex() + checksum(4 hex) = 42 chars
// Checksum: SHA256(SHA256("mt" + payload))[:2].hex()
//
import Foundation
/// ML-DSA-65 Post-Quantum Signature Scheme
/// Self-Custody implementation using liboqs
public struct MLDSA65 {
// MARK: - Constants (FIPS 204)
/// Private key size: 4032 bytes
public static let privateKeySize = 4032
/// Public key size: 1952 bytes
public static let publicKeySize = 1952
/// Signature size: 3309 bytes
public static let signatureSize = 3309
/// Address length: 42 characters (mt + 36 hex payload + 4 hex checksum)
public static let addressLength = 42
/// Serial queue for thread-safe liboqs operations
private static let cryptoQueue = DispatchQueue(label: "network.montana.crypto", qos: .userInitiated)
/// Thread-safe OQS initialization (dispatch_once semantics via Swift static let)
private static let oqsInit: Void = {
OQS_init()
}()
/// Global deterministic RNG state (protected by cryptoQueue)
private static var deterministicRNG: DeterministicCTRDRBG?
// MARK: - Deterministic RNG (SHA256-CTR DRBG)
/// Counter-mode DRBG using SHA-256 for deterministic key generation
private class DeterministicCTRDRBG {
private var seed: [UInt8]
private var counter: UInt64 = 0
init(seed: Data) {
self.seed = [UInt8](seed)
}
func generate(buf: UnsafeMutablePointer<UInt8>, count: Int) {
var offset = 0
while offset < count {
// block = SHA256(seed || counter_be)
var input = seed
withUnsafeBytes(of: counter.bigEndian) { input.append(contentsOf: $0) }
var hash = [UInt8](repeating: 0, count: 32)
input.withUnsafeBufferPointer { ptr in
CC_SHA256(ptr.baseAddress, CC_LONG(input.count), &hash)
}
let needed = min(32, count - offset)
for i in 0..<needed {
buf[offset + i] = hash[i]
}
offset += needed
counter += 1
}
}
func reset() {
// Zero the seed
for i in 0..<seed.count { seed[i] = 0 }
counter = 0
}
}
/// C callback for liboqs custom RNG
private static let deterministicRNGCallback: @convention(c) (UnsafeMutablePointer<UInt8>?, Int) -> Void = { buf, len in
guard let buf = buf else { return }
MLDSA65.deterministicRNG?.generate(buf: buf, count: len)
}
/// C callback that returns all zeros (for deterministic signing, rnd=0)
private static let zeroRNGCallback: @convention(c) (UnsafeMutablePointer<UInt8>?, Int) -> Void = { buf, len in
guard let buf = buf else { return }
memset(buf, 0, len)
}
// MARK: - Key Generation (Random)
/// Generate ML-DSA-65 keypair using system secure RNG
/// - Returns: (privateKey, publicKey) tuple
public static func generateKeypair() -> (privateKey: Data, publicKey: Data)? {
return cryptoQueue.sync {
_ = oqsInit
guard let sig = OQS_SIG_new(OQS_SIG_alg_ml_dsa_65) else {
return nil
}
defer { OQS_SIG_free(sig) }
var publicKey = Data(count: Int(sig.pointee.length_public_key))
var privateKey = Data(count: Int(sig.pointee.length_secret_key))
let result = publicKey.withUnsafeMutableBytes { pubPtr in
privateKey.withUnsafeMutableBytes { privPtr in
OQS_SIG_keypair(sig,
pubPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
privPtr.baseAddress?.assumingMemoryBound(to: UInt8.self))
}
}
guard result == OQS_SUCCESS else {
return nil
}
return (privateKey, publicKey)
}
}
// MARK: - Key Generation (Deterministic from Seed)
/// Generate ML-DSA-65 keypair deterministically from a 64-byte seed
/// Injects SHA256-CTR DRBG into liboqs, generates keypair, restores system RNG
/// - Parameter seed: 64-byte seed (from PBKDF2)
/// - Returns: (privateKey, publicKey) tuple always identical for same seed
public static func generateKeypairFromSeed(seed: Data) -> (privateKey: Data, publicKey: Data)? {
guard seed.count == 64 else { return nil }
return cryptoQueue.sync {
_ = oqsInit
// Inject deterministic RNG
deterministicRNG = DeterministicCTRDRBG(seed: seed)
OQS_randombytes_custom_algorithm(deterministicRNGCallback)
defer {
// Restore system RNG and zero state
OQS_randombytes_switch_algorithm(OQS_RAND_alg_system)
deterministicRNG?.reset()
deterministicRNG = nil
}
guard let sig = OQS_SIG_new(OQS_SIG_alg_ml_dsa_65) else {
return nil
}
defer { OQS_SIG_free(sig) }
var publicKey = Data(count: Int(sig.pointee.length_public_key))
var privateKey = Data(count: Int(sig.pointee.length_secret_key))
let result = publicKey.withUnsafeMutableBytes { pubPtr in
privateKey.withUnsafeMutableBytes { privPtr in
OQS_SIG_keypair(sig,
pubPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
privPtr.baseAddress?.assumingMemoryBound(to: UInt8.self))
}
}
guard result == OQS_SUCCESS else {
return nil
}
return (privateKey, publicKey)
}
}
// MARK: - Signing (Deterministic FIPS 204 rnd=0)
/// Sign message deterministically with ML-DSA-65 private key
/// Same message + same key = identical signature (FIPS 204 deterministic mode)
/// - Parameters:
/// - message: Message to sign (must not be empty)
/// - privateKey: ML-DSA-65 private key (4032 bytes)
/// - Returns: Signature (3309 bytes)
public static func sign(message: Data, privateKey: Data) -> Data? {
guard !message.isEmpty else {
return nil
}
guard privateKey.count == privateKeySize else {
return nil
}
return cryptoQueue.sync {
_ = oqsInit
// Inject zero RNG deterministic signing (rnd = 0x00...00)
OQS_randombytes_custom_algorithm(zeroRNGCallback)
defer {
// Restore system RNG
OQS_randombytes_switch_algorithm(OQS_RAND_alg_system)
}
guard let sig = OQS_SIG_new(OQS_SIG_alg_ml_dsa_65) else {
return nil
}
defer { OQS_SIG_free(sig) }
var signature = Data(count: Int(sig.pointee.length_signature))
var signatureLen: Int = 0
let result = signature.withUnsafeMutableBytes { sigPtr in
message.withUnsafeBytes { msgPtr in
privateKey.withUnsafeBytes { keyPtr in
OQS_SIG_sign(sig,
sigPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
&signatureLen,
msgPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
message.count,
keyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self))
}
}
}
guard result == OQS_SUCCESS else {
return nil
}
signature = signature.prefix(signatureLen)
return signature
}
}
// MARK: - Verification
/// Verify ML-DSA-65 signature
/// - Parameters:
/// - message: Original message (must not be empty)
/// - signature: Signature to verify (3309 bytes)
/// - publicKey: ML-DSA-65 public key (1952 bytes)
/// - Returns: true if signature is valid
public static func verify(message: Data, signature: Data, publicKey: Data) -> Bool {
guard !message.isEmpty else {
return false
}
guard signature.count == signatureSize else {
return false
}
guard publicKey.count == publicKeySize else {
return false
}
return cryptoQueue.sync {
_ = oqsInit
guard let sig = OQS_SIG_new(OQS_SIG_alg_ml_dsa_65) else {
return false
}
defer { OQS_SIG_free(sig) }
let result = message.withUnsafeBytes { msgPtr in
signature.withUnsafeBytes { sigPtr in
publicKey.withUnsafeBytes { keyPtr in
OQS_SIG_verify(sig,
msgPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
message.count,
sigPtr.baseAddress?.assumingMemoryBound(to: UInt8.self),
signature.count,
keyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self))
}
}
}
return result == OQS_SUCCESS
}
}
// MARK: - Address Generation (with Checksum)
/// Generate Montana address from public key
/// Format: mt + SHA256(pubkey)[:18].hex() + checksum = 42 chars
/// Checksum: SHA256(SHA256("mt" + payload))[:2].hex()
public static func generateAddress(from publicKey: Data) -> String {
let hash = sha256(publicKey)
let payload = hash.prefix(18).map { String(format: "%02x", $0) }.joined()
let checksum = computeChecksum("mt" + payload)
return "mt" + payload + checksum
}
/// Validate a Montana address (format + checksum)
/// - Parameter address: Address string to validate
/// - Returns: true if format and checksum are valid
public static func validateAddress(_ address: String) -> Bool {
guard address.count == addressLength else { return false }
guard address.hasPrefix("mt") else { return false }
let body = String(address.dropFirst(2))
guard body.allSatisfy({ "0123456789abcdef".contains($0) }) else { return false }
let payload = String(body.prefix(36))
let checksum = String(body.suffix(4))
let expected = computeChecksum("mt" + payload)
return checksum == expected
}
/// Compute 4-char hex checksum: SHA256(SHA256(prefix + payload))[:2]
private static func computeChecksum(_ input: String) -> String {
guard let data = input.data(using: .utf8) else { return "0000" }
let hash1 = sha256(data)
let hash2 = sha256(hash1)
return hash2.prefix(2).map { String(format: "%02x", $0) }.joined()
}
// MARK: - Memory Safety
/// Zero out sensitive data in place
public static func zeroMemory(_ data: inout Data) {
data.withUnsafeMutableBytes { ptr in
if let baseAddress = ptr.baseAddress {
memset(baseAddress, 0, ptr.count)
}
}
data = Data()
}
// MARK: - Helpers
/// SHA-256 hash (internal, used for address + checksum)
static func sha256(_ data: Data) -> Data {
var hash = [UInt8](repeating: 0, count: 32)
data.withUnsafeBytes {
_ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
}
return Data(hash)
}
}