// // ChannelBindingTests.swift // PortalKitTests // // Verification of cryptographic TLS channel binding in SRP-6a M1 and M2. // import XCTest @testable import PortalKit final class ChannelBindingTests: XCTestCase { let pin = "481516" let genuineCertHash = SrpGroup.rfc5054_2048.hashAlgorithm.hash("GENUINE_PORTAL_CERT_DER".data(using: .utf8)!) let rogueCertHash = SrpGroup.rfc5054_2048.hashAlgorithm.hash("ROGUE_PROXY_CERT_DER".data(using: .utf8)!) func testMatchingCertHashSucceeds() throws { let server = SrpServerMock(pin: pin) let client = PortalSrpClient() // Both use the same genuine TLS cert hash let m1Hex = try client.computeM1( saltHex: server.saltHex, pubBHex: server.pubBHex, pin: pin, tlsCertSha256: genuineCertHash ) let serverRes = server.verifyClient( pubAHex: client.pubAHex, clientM1Hex: m1Hex, serverTlsCertSha256: genuineCertHash ) XCTAssertTrue(serverRes.success) XCTAssertNotNil(serverRes.M2Hex) XCTAssertNoThrow(try client.verifyServerM2(serverM2Hex: serverRes.M2Hex!)) } func testAlteredCertHashFailsServerM1Verification() throws { let server = SrpServerMock(pin: pin) let client = PortalSrpClient() // Client is tricked into connecting through a MITM proxy presenting rogue cert let m1Hex = try client.computeM1( saltHex: server.saltHex, pubBHex: server.pubBHex, pin: pin, tlsCertSha256: rogueCertHash // Client uses rogue proxy cert hash ) // Portal TV verifies against its real local certificate hash let serverRes = server.verifyClient( pubAHex: client.pubAHex, clientM1Hex: m1Hex, serverTlsCertSha256: genuineCertHash // Server uses genuine cert hash ) XCTAssertFalse(serverRes.success, "Server must reject client M1 when TLS cert hashes differ") XCTAssertNil(serverRes.token) XCTAssertNil(serverRes.M2Hex) } func testAlteredCertHashFailsClientM2Verification() throws { let server = SrpServerMock(pin: pin) let client = PortalSrpClient() // Client computes M1 bound to genuine cert _ = try client.computeM1( saltHex: server.saltHex, pubBHex: server.pubBHex, pin: pin, tlsCertSha256: genuineCertHash ) // Attacker attempts to forge M2 or server computes M2 bound to a different cert hash let aPadded = client.A.toPadded256Data() let fakeM2 = SrpGroup.rfc5054_2048.hashAlgorithm.hash( aPadded, client.clientM1!, client.sessionKey!, rogueCertHash // Mismatched cert hash in M2 ) let fakeM2Hex = SrpFormat.bytesToHex(fakeM2) XCTAssertThrowsError(try client.verifyServerM2(serverM2Hex: fakeM2Hex)) { error in guard case SrpError.verificationFailed(let msg) = error else { XCTFail("Unexpected error type: \(error)") return } XCTAssertTrue(msg.contains("Server evidence M2 does not match")) } } func testSingleBitFlipInCertHashFails() throws { let server = SrpServerMock(pin: pin) let client = PortalSrpClient() // Flip 1 bit in the genuine cert hash var tamperedCertHash = genuineCertHash tamperedCertHash[0] ^= 0x01 let m1Hex = try client.computeM1( saltHex: server.saltHex, pubBHex: server.pubBHex, pin: pin, tlsCertSha256: tamperedCertHash ) let serverRes = server.verifyClient( pubAHex: client.pubAHex, clientM1Hex: m1Hex, serverTlsCertSha256: genuineCertHash ) XCTAssertFalse(serverRes.success, "Single-bit flip in TLS cert hash must fail verification") } }