92 lines
3.9 KiB
Swift
92 lines
3.9 KiB
Swift
import XCTest
|
|
@testable import Luma
|
|
|
|
final class SCRAMDowngradeProtectionTests: XCTestCase {
|
|
/// Reference vector from XEP-0474 §6.3: the server's `h` attribute is
|
|
/// base64(SHA-1('SCRAM-SHA-1\u{1E}SCRAM-SHA-1-PLUS\u{1F}tls-exporter\u{1E}tls-server-end-point')).
|
|
func testHashMatchesXEP0474Example() {
|
|
let hash = SCRAMDowngradeProtection.hash(
|
|
mechanisms: ["SCRAM-SHA-1", "SCRAM-SHA-1-PLUS"],
|
|
channelBindingTypes: ["tls-exporter", "tls-server-end-point"],
|
|
using: .sha1
|
|
)
|
|
XCTAssertEqual(hash.base64EncodedString(), "G6k/rBLDqgOhRRaCuuatSDFkJ08=")
|
|
}
|
|
|
|
func testHashSortsListsPerIOctetCollation() {
|
|
// XEP-0474 §5.1 requires i;octet-sorted lists, so advertisement order
|
|
// must not affect the hash.
|
|
let advertisedOrder = SCRAMDowngradeProtection.hash(
|
|
mechanisms: ["SCRAM-SHA-1", "SCRAM-SHA-1-PLUS"],
|
|
channelBindingTypes: ["tls-exporter", "tls-server-end-point"],
|
|
using: .sha1
|
|
)
|
|
let reordered = SCRAMDowngradeProtection.hash(
|
|
mechanisms: ["SCRAM-SHA-1-PLUS", "SCRAM-SHA-1"],
|
|
channelBindingTypes: ["tls-server-end-point", "tls-exporter"],
|
|
using: .sha1
|
|
)
|
|
XCTAssertEqual(advertisedOrder, reordered)
|
|
}
|
|
|
|
func testHashOmitsBindingSectionWhenNoTypesAdvertised() {
|
|
let withBindings = SCRAMDowngradeProtection.hash(
|
|
mechanisms: ["SCRAM-SHA-1"],
|
|
channelBindingTypes: ["tls-exporter"],
|
|
using: .sha1
|
|
)
|
|
let withoutBindings = SCRAMDowngradeProtection.hash(
|
|
mechanisms: ["SCRAM-SHA-1"],
|
|
channelBindingTypes: [],
|
|
using: .sha1
|
|
)
|
|
XCTAssertNotEqual(withBindings, withoutBindings)
|
|
// Without bindings the input is exactly the sorted mechanisms with no
|
|
// trailing 0x1F delimiter.
|
|
let expected = SCRAMHash.sha1.hash(data: Data("SCRAM-SHA-1".utf8))
|
|
XCTAssertEqual(withoutBindings, expected)
|
|
}
|
|
|
|
func testServerFirstParsesDowngradeProtectionHash() throws {
|
|
let message = "r=12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6a09117a6-ac50-4f2f-93f1-93799c2bddf6,s=QSXCR+Q6sek8bf92,i=4096,h=G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
|
let parsed = try SCRAMSHA512.parseServerFirst(
|
|
message,
|
|
expectedNoncePrefix: "12C4CD5C-E38E-4A98-8F6D-15C38F51CCC6"
|
|
)
|
|
XCTAssertEqual(parsed.downgradeProtectionHash, "G6k/rBLDqgOhRRaCuuatSDFkJ08=")
|
|
}
|
|
|
|
func testServerFirstWithoutHashParsesCleanly() throws {
|
|
let message = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"
|
|
let parsed = try SCRAMSHA512.parseServerFirst(
|
|
message,
|
|
expectedNoncePrefix: "fyko+d2lbbFgONRv9qkxdawL"
|
|
)
|
|
XCTAssertNil(parsed.downgradeProtectionHash)
|
|
}
|
|
|
|
func testClientFinalIncludesXAttributeWhenHashPresent() throws {
|
|
let exchange = SCRAMPlusExchange(
|
|
hash: .sha1,
|
|
username: "user",
|
|
password: "pencil",
|
|
channelBindingType: "tls-server-end-point",
|
|
channelBindingData: Data(0..<32),
|
|
clientNonce: "fyko+d2lbbFgONRv9qkxdawL"
|
|
)
|
|
let serverFirst = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096,h=G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
|
let final = try exchange.clientFinalMessage(
|
|
serverFirst: serverFirst,
|
|
downgradeProtectionHashBase64: "G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
|
)
|
|
XCTAssertTrue(final.contains(",x=G6k/rBLDqgOhRRaCuuatSDFkJ08=,"))
|
|
// The same x-attribute must be part of the expected signature's
|
|
// auth message (server reconstructs client-final-without-proof).
|
|
let expected = try exchange.expectedServerSignature(
|
|
serverFirst: serverFirst,
|
|
downgradeProtectionHashBase64: "G6k/rBLDqgOhRRaCuuatSDFkJ08="
|
|
)
|
|
XCTAssertFalse(expected.isEmpty)
|
|
}
|
|
}
|