285 lines
13 KiB
Swift
285 lines
13 KiB
Swift
import Foundation
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// Montana Protocol — Layer 1 Cryptography: 4 Checks + 16 Tests
|
|
// ML-DSA-65 (FIPS 204) Post-Quantum Signatures
|
|
//
|
|
// Check 1: Generation (seed → deterministic keys)
|
|
// Check 2: Addressing (pubkey → mt... with checksum)
|
|
// Check 3: Sign & Verify (deterministic signatures)
|
|
// Check 4: Isolation (private key never exposed)
|
|
//
|
|
// Servers: Amsterdam (72.56.102.240), Almaty (91.200.148.93)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
@main
|
|
struct CryptoTest {
|
|
static var passed = 0
|
|
static var failed = 0
|
|
static var total = 16
|
|
|
|
static func test(_ num: Int, _ name: String, _ check: () -> Bool) {
|
|
let result = check()
|
|
if result {
|
|
print(" [\(num)/\(total)] PASS: \(name)")
|
|
passed += 1
|
|
} else {
|
|
print(" [\(num)/\(total)] FAIL: \(name)")
|
|
failed += 1
|
|
}
|
|
}
|
|
|
|
// HTTP helper for server tests
|
|
static func httpRequest(url: String, method: String = "GET", body: [String: Any]? = nil, headers: [String: String] = [:]) -> (data: Data?, statusCode: Int) {
|
|
guard let u = URL(string: url) else { return (nil, 0) }
|
|
var req = URLRequest(url: u, timeoutInterval: 10)
|
|
req.httpMethod = method
|
|
for (k, v) in headers {
|
|
req.setValue(v, forHTTPHeaderField: k)
|
|
}
|
|
if let body = body {
|
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
req.httpBody = try? JSONSerialization.data(withJSONObject: body)
|
|
}
|
|
|
|
let sem = DispatchSemaphore(value: 0)
|
|
var respData: Data?
|
|
var statusCode = 0
|
|
|
|
let task = URLSession.shared.dataTask(with: req) { data, response, _ in
|
|
respData = data
|
|
statusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
|
|
sem.signal()
|
|
}
|
|
task.resume()
|
|
_ = sem.wait(timeout: .now() + 15)
|
|
return (respData, statusCode)
|
|
}
|
|
|
|
static func main() {
|
|
print("═══════════════════════════════════════════════════════")
|
|
print(" Montana Layer 1 — 4 Checks, 16 Mainnet Tests")
|
|
print(" Algorithm: ML-DSA-65 (FIPS 204)")
|
|
print(" Key sizes: priv=4032, pub=1952, sig=3309")
|
|
print("═══════════════════════════════════════════════════════")
|
|
|
|
// ═════════════════════════════════════════════════════════
|
|
// CHECK 1: GENERATION — seed → deterministic keys
|
|
// ═════════════════════════════════════════════════════════
|
|
print("\n══ CHECK 1: GENERATION ══")
|
|
|
|
// TEST 1: Generate 24-word mnemonic
|
|
print("\n[TEST 1] Generate Mnemonic")
|
|
guard let words = MontanaSeed.generateMnemonic() else {
|
|
print(" FATAL: generateMnemonic() returned nil")
|
|
Foundation.exit(1)
|
|
}
|
|
test(1, "Mnemonic: 24 words generated") {
|
|
words.count == 24
|
|
}
|
|
|
|
// TEST 2: Validate mnemonic
|
|
print("\n[TEST 2] Validate Mnemonic")
|
|
test(2, "Mnemonic checksum valid") {
|
|
MontanaSeed.validateMnemonic(words)
|
|
}
|
|
|
|
// TEST 3: Derive keypair from mnemonic
|
|
print("\n[TEST 3] Derive Keypair from Mnemonic")
|
|
guard let kp1 = MontanaSeed.keypairFromMnemonic(words) else {
|
|
print(" FATAL: keypairFromMnemonic() returned nil")
|
|
Foundation.exit(1)
|
|
}
|
|
test(3, "Keygen from seed: priv=4032, pub=1952") {
|
|
kp1.privateKey.count == 4032 && kp1.publicKey.count == 1952
|
|
}
|
|
|
|
// TEST 4: DETERMINISTIC — same seed = same keys (CRITICAL)
|
|
print("\n[TEST 4] Deterministic Keys — Same Seed = Same Keys")
|
|
guard let kp2 = MontanaSeed.keypairFromMnemonic(words) else {
|
|
print(" FATAL: second keypairFromMnemonic() returned nil")
|
|
Foundation.exit(1)
|
|
}
|
|
test(4, "Same mnemonic → identical privateKey AND publicKey") {
|
|
kp1.privateKey == kp2.privateKey && kp1.publicKey == kp2.publicKey
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════
|
|
// CHECK 2: ADDRESSING — pubkey → mt... with checksum
|
|
// ═════════════════════════════════════════════════════════
|
|
print("\n══ CHECK 2: ADDRESSING ══")
|
|
|
|
// TEST 5: Address format
|
|
print("\n[TEST 5] Address Format")
|
|
let addr = MLDSA65.generateAddress(from: kp1.publicKey)
|
|
test(5, "Address: mt prefix, 42 chars, lowercase hex") {
|
|
addr.hasPrefix("mt") &&
|
|
addr.count == 42 &&
|
|
String(addr.dropFirst(2)).allSatisfy { "0123456789abcdef".contains($0) }
|
|
}
|
|
|
|
// TEST 6: Deterministic address
|
|
print("\n[TEST 6] Deterministic Address")
|
|
let addr2 = MLDSA65.generateAddress(from: kp1.publicKey)
|
|
test(6, "Same pubkey → same address") {
|
|
addr == addr2
|
|
}
|
|
|
|
// TEST 7: Address checksum validation
|
|
print("\n[TEST 7] Address Checksum Validation")
|
|
test(7, "Valid address passes checksum validation") {
|
|
MLDSA65.validateAddress(addr)
|
|
}
|
|
|
|
// TEST 8: Tampered address fails validation
|
|
print("\n[TEST 8] Tampered Address Rejection")
|
|
var tamperedAddr = addr
|
|
// Change one character in the middle
|
|
let idx = tamperedAddr.index(tamperedAddr.startIndex, offsetBy: 10)
|
|
let oldChar = tamperedAddr[idx]
|
|
let newChar: Character = oldChar == "a" ? "b" : "a"
|
|
tamperedAddr.replaceSubrange(idx...idx, with: String(newChar))
|
|
test(8, "Changed 1 char → checksum validation fails") {
|
|
!MLDSA65.validateAddress(tamperedAddr)
|
|
}
|
|
|
|
// TEST 9: Cannot reverse address → pubkey
|
|
print("\n[TEST 9] Address Irreversibility")
|
|
guard let kp3 = MLDSA65.generateKeypair() else {
|
|
print(" FATAL: random keygen failed")
|
|
Foundation.exit(1)
|
|
}
|
|
let addr3 = MLDSA65.generateAddress(from: kp3.publicKey)
|
|
test(9, "Different keys → different addresses, both valid") {
|
|
addr != addr3 && MLDSA65.validateAddress(addr3)
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════
|
|
// CHECK 3: SIGN & VERIFY — deterministic signatures
|
|
// ═════════════════════════════════════════════════════════
|
|
print("\n══ CHECK 3: SIGN & VERIFY ══")
|
|
|
|
// TEST 10: Sign + Verify roundtrip
|
|
print("\n[TEST 10] Sign + Verify Roundtrip")
|
|
let genesisMsg = "Montana Genesis".data(using: .utf8)!
|
|
guard let sig1 = MLDSA65.sign(message: genesisMsg, privateKey: kp1.privateKey) else {
|
|
print(" FATAL: sign() returned nil")
|
|
Foundation.exit(1)
|
|
}
|
|
test(10, "Sign(Montana Genesis) → Verify = true, sig=3309") {
|
|
MLDSA65.verify(message: genesisMsg, signature: sig1, publicKey: kp1.publicKey) &&
|
|
sig1.count == 3309
|
|
}
|
|
|
|
// TEST 11: DETERMINISTIC SIGNATURE — same key + same message = identical signature
|
|
print("\n[TEST 11] Deterministic Signature — Same Key + Same Message = Identical")
|
|
guard let sig2 = MLDSA65.sign(message: genesisMsg, privateKey: kp1.privateKey) else {
|
|
print(" FATAL: second sign() returned nil")
|
|
Foundation.exit(1)
|
|
}
|
|
test(11, "Two signatures of same message are byte-identical") {
|
|
sig1 == sig2
|
|
}
|
|
|
|
// TEST 12: THE CANONICAL TEST
|
|
// Seed → keys → sign → "delete" → recover from same seed → sign → identical
|
|
print("\n[TEST 12] CANONICAL: Seed → Sign → Delete → Recover → Sign = Identical")
|
|
let sig1Hex = sig1.map { String(format: "%02x", $0) }.joined()
|
|
// "Delete" keys (forget them)
|
|
// Recover from same mnemonic
|
|
guard let kpRecovered = MontanaSeed.keypairFromMnemonic(words) else {
|
|
print(" FATAL: recovery keypairFromMnemonic() returned nil")
|
|
Foundation.exit(1)
|
|
}
|
|
guard let sigRecovered = MLDSA65.sign(message: genesisMsg, privateKey: kpRecovered.privateKey) else {
|
|
print(" FATAL: recovered sign() returned nil")
|
|
Foundation.exit(1)
|
|
}
|
|
let sigRecoveredHex = sigRecovered.map { String(format: "%02x", $0) }.joined()
|
|
test(12, "Recovered signature is byte-identical to original") {
|
|
sig1Hex == sigRecoveredHex &&
|
|
kpRecovered.publicKey == kp1.publicKey &&
|
|
kpRecovered.privateKey == kp1.privateKey
|
|
}
|
|
|
|
// TEST 13: Wrong key rejection
|
|
print("\n[TEST 13] Wrong Key Rejection")
|
|
test(13, "Verify with wrong pubkey = false") {
|
|
!MLDSA65.verify(message: genesisMsg, signature: sig1, publicKey: kp3.publicKey)
|
|
}
|
|
|
|
// TEST 14: Tampered message rejection
|
|
print("\n[TEST 14] Tampered Message Rejection")
|
|
let tampered = "Montana Genesi5".data(using: .utf8)!
|
|
test(14, "Verify tampered message = false") {
|
|
!MLDSA65.verify(message: tampered, signature: sig1, publicKey: kp1.publicKey)
|
|
}
|
|
|
|
// ═════════════════════════════════════════════════════════
|
|
// CHECK 4: ISOLATION + SERVER VERIFICATION
|
|
// ═════════════════════════════════════════════════════════
|
|
print("\n══ CHECK 4: ISOLATION + SERVERS ══")
|
|
|
|
// TEST 15: Server Health — Amsterdam & Almaty
|
|
print("\n[TEST 15] Server Health")
|
|
let (_, amsStatus) = httpRequest(url: "http://72.56.102.240/api/health")
|
|
let (_, almStatus) = httpRequest(url: "http://91.200.148.93/api/health")
|
|
test(15, "Amsterdam=200, Almaty=200") {
|
|
amsStatus == 200 && almStatus == 200
|
|
}
|
|
|
|
// TEST 16: Server PQ-Signed Balance — Amsterdam
|
|
print("\n[TEST 16] Server PQ-Signed Balance — Amsterdam")
|
|
let ts = Int(Date().timeIntervalSince1970)
|
|
let balanceMsg = "BALANCE:\(addr):\(ts)"
|
|
guard let balanceData = balanceMsg.data(using: .utf8),
|
|
let balanceSig = MLDSA65.sign(message: balanceData, privateKey: kp1.privateKey) else {
|
|
print(" FATAL: balance sign failed")
|
|
Foundation.exit(1)
|
|
}
|
|
let pubHex = kp1.publicKey.map { String(format: "%02x", $0) }.joined()
|
|
let sigHex = balanceSig.map { String(format: "%02x", $0) }.joined()
|
|
|
|
let (balRespData, balRespStatus) = httpRequest(
|
|
url: "http://72.56.102.240/api/balance/\(addr)",
|
|
method: "GET",
|
|
headers: [
|
|
"X-Address": addr,
|
|
"X-Timestamp": "\(ts)",
|
|
"X-Signature": sigHex,
|
|
"X-Public-Key": pubHex
|
|
]
|
|
)
|
|
|
|
var pqVerified = false
|
|
if let data = balRespData,
|
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
pqVerified = json["pq_verified"] as? Bool ?? false
|
|
let bal = json["balance"] as? Double ?? -1
|
|
print(" Response: balance=\(bal), pq_verified=\(pqVerified)")
|
|
}
|
|
|
|
test(16, "Amsterdam PQ balance: status=200, pq_verified=true") {
|
|
balRespStatus == 200 && pqVerified
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// RESULTS
|
|
// ═══════════════════════════════════════════════════════
|
|
print("\n" + String(repeating: "═", count: 55))
|
|
print(" Results: \(passed)/\(total) passed, \(failed) failed")
|
|
print(String(repeating: "═", count: 55))
|
|
|
|
if failed == 0 {
|
|
print("\n ML-DSA-65 LAYER 1: ALL \(total) TESTS PASSED")
|
|
print(" Mnemonic: \(words.prefix(3).joined(separator: " "))... (\(words.count) words)")
|
|
print(" Address: \(addr)")
|
|
print(" Checksum: \(MLDSA65.validateAddress(addr) ? "VALID" : "INVALID")")
|
|
print(" Deterministic: sig1 == sigRecovered = \(sig1Hex == sigRecoveredHex)")
|
|
} else {
|
|
print("\n FAILED — \(failed) test(s)")
|
|
Foundation.exit(1)
|
|
}
|
|
}
|
|
}
|