|
| 1 | +// Verifies the confidential candidate exchange in the ACTUAL CandidateOffer.swift (+ Ice + |
| 2 | +// MeshCrypto). Only static room-key derivation is used, so no MeshCrypto() is constructed and |
| 3 | +// the Keychain is never touched (the #41 hang cannot recur here). The clock is passed in, so |
| 4 | +// expiry is deterministic. |
| 5 | +// swiftc MeshCrypto.swift HolePunch.swift IceSession.swift CandidateOffer.swift candidate_offer.swift -o /tmp/co && /tmp/co |
| 6 | +import Foundation |
| 7 | +import CryptoKit |
| 8 | + |
| 9 | +var fails = 0 |
| 10 | +func check(_ c: Bool, _ l: String) { print("\(c ? "PASS" : "FAIL") \(l)"); if !c { fails += 1 } } |
| 11 | + |
| 12 | +let cands = [Ice.Candidate(ip: "192.168.1.50", port: 5000, kind: .host), |
| 13 | + Ice.Candidate(ip: "203.0.113.50", port: 61000, kind: .srflx)] |
| 14 | +let t0 = Date(timeIntervalSince1970: 1_700_000_000) // fixed clock |
| 15 | + |
| 16 | +print("== honest round-trip under the same room ==") |
| 17 | +do { |
| 18 | + let offer = CandidateOffer.make(candidates: cands, tiebreaker: 0xAABB_CCDD, room: "team-alpha", ttlMs: 30_000, now: t0) |
| 19 | + let got = CandidateOffer.open(offer, room: "team-alpha", now: t0.addingTimeInterval(5)) |
| 20 | + check(got?.candidates == cands, "the peer's candidate list is recovered intact") |
| 21 | + check(got?.tiebreaker == 0xAABB_CCDD, "the ICE tiebreaker is recovered") |
| 22 | + check(offer.count > 17, "the offer is sealed (nonce+ciphertext+tag), not plaintext") |
| 23 | +} |
| 24 | + |
| 25 | +print("== a rendezvous without the room passphrase can neither read nor forge ==") |
| 26 | +do { |
| 27 | + let offer = CandidateOffer.make(candidates: cands, tiebreaker: 1, room: "team-alpha", ttlMs: 30_000, now: t0) |
| 28 | + check(CandidateOffer.open(offer, room: "team-beta", now: t0) == nil, "a different room passphrase -> nil (cannot read)") |
| 29 | + var tampered = [UInt8](offer); tampered[tampered.count - 1] ^= 0xFF |
| 30 | + check(CandidateOffer.open(Data(tampered), room: "team-alpha", now: t0) == nil, "flipped auth-tag byte -> nil (cannot forge a candidate)") |
| 31 | + // the offer key is domain-separated from the raw invite key: opening under it must fail |
| 32 | + let underInviteKey = { () -> Bool in |
| 33 | + guard let box = try? ChaChaPoly.SealedBox(combined: offer), |
| 34 | + let _ = try? ChaChaPoly.open(box, using: MeshCrypto.inviteAuthKey(room: "team-alpha")) else { return false } |
| 35 | + return true |
| 36 | + }() |
| 37 | + check(!underInviteKey, "the offer does NOT open under the raw invite key (domain separation holds)") |
| 38 | +} |
| 39 | + |
| 40 | +print("== a captured offer cannot be replayed after it expires ==") |
| 41 | +do { |
| 42 | + let offer = CandidateOffer.make(candidates: cands, tiebreaker: 1, room: "r", ttlMs: 10_000, now: t0) |
| 43 | + check(CandidateOffer.open(offer, room: "r", now: t0.addingTimeInterval(9)) != nil, "within TTL -> accepted") |
| 44 | + check(CandidateOffer.open(offer, room: "r", now: t0.addingTimeInterval(11)) == nil, "past TTL -> rejected (stale, un-replayable)") |
| 45 | +} |
| 46 | + |
| 47 | +print("== ICE role resolves deterministically and oppositely for the two peers ==") |
| 48 | +do { |
| 49 | + check(CandidateOffer.isControlling(mine: 100, peer: 50), "higher tiebreaker -> controlling") |
| 50 | + check(!CandidateOffer.isControlling(mine: 50, peer: 100), "lower tiebreaker -> controlled") |
| 51 | + check(CandidateOffer.isControlling(mine: 100, peer: 50) != CandidateOffer.isControlling(mine: 50, peer: 100), |
| 52 | + "the two peers pick OPPOSITE roles (no double-controlling)") |
| 53 | +} |
| 54 | + |
| 55 | +print("== garbage / truncated input never crashes ==") |
| 56 | +do { |
| 57 | + check(CandidateOffer.open(Data(), room: "r", now: t0) == nil, "empty -> nil") |
| 58 | + check(CandidateOffer.open(Data([1, 2, 3, 4, 5]), room: "r", now: t0) == nil, "junk -> nil") |
| 59 | +} |
| 60 | + |
| 61 | +print("\n\(fails == 0 ? "ALL PASS" : "\(fails) FAILURE(S)")") |
| 62 | +exit(fails == 0 ? 0 : 1) |
0 commit comments