This commit is contained in:
2026-09-13 12:15:36 -07:00
commit e473d00f4f
104 changed files with 16080 additions and 0 deletions
@@ -0,0 +1,117 @@
//
// 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")
}
}