Files
Luma/Tests/SCRAMDowngradeProtectionTests.swift
wt 1650145d4d
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
serverinfoview
2026-09-03 12:42:20 +07:00

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)
}
}