118 lines
3.9 KiB
Swift
118 lines
3.9 KiB
Swift
//
|
|||
|
|
// 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")
|
||
|
|
}
|
||
|
|
}
|