You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Musubi/Packages/MusubiKit/Tests/IntercomTests/DTLSInteropTests.swift

115 lines
5.5 KiB

import CryptoKit
import Darwin
import Foundation
import Testing
@testable import Intercom
/// Handshake against `openssl s_client` (development tool only, not shipped).
/// Skipped when Homebrew OpenSSL is missing.
private let opensslPath = "/opt/homebrew/bin/openssl"
@Suite(.enabled(if: FileManager.default.isExecutableFile(atPath: opensslPath)))
struct DTLSInteropTests {
static let openssl = opensslPath
@Test(.timeLimit(.minutes(1)), arguments: [true, false])
func handshakeChecksFingerprintAndExportsSameSRTPKeys(fingerprintMatches: Bool) throws {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("musubi-dtls-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
let keyPath = dir.appendingPathComponent("client.key").path
let certPath = dir.appendingPathComponent("client.pem").path
try run([Self.openssl, "req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:P-256", "-nodes",
"-keyout", keyPath, "-out", certPath, "-subj", "/CN=browser", "-days", "1"])
let der = try run([Self.openssl, "x509", "-in", certPath, "-outform", "DER"])
let expected = fingerprintMatches ? DTLSCertificate.fingerprint(Array(der)) : try DTLSCertificate().fingerprint
let server = DTLSServer(certificate: try DTLSCertificate(), expectedFingerprint: expected)
let socket = Darwin.socket(AF_INET, SOCK_DGRAM, 0)
defer { close(socket) }
var address = sockaddr_in()
address.sin_family = sa_family_t(AF_INET)
address.sin_addr.s_addr = inet_addr("127.0.0.1")
var length = socklen_t(MemoryLayout<sockaddr_in>.size)
_ = withUnsafePointer(to: &address) { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { bind(socket, $0, length) } }
_ = withUnsafeMutablePointer(to: &address) { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { getsockname(socket, $0, &length) } }
var timeout = timeval(tv_sec: 0, tv_usec: 200_000)
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, socklen_t(MemoryLayout<timeval>.size))
let port = UInt16(bigEndian: address.sin_port)
let client = Process()
client.executableURL = URL(fileURLWithPath: Self.openssl)
client.arguments = ["s_client", "-dtls1_2", "-connect", "127.0.0.1:\(port)",
"-cipher", "ECDHE-ECDSA-AES128-GCM-SHA256", "-use_srtp", "SRTP_AES128_CM_SHA1_80",
"-cert", certPath, "-key", keyPath,
"-keymatexport", "EXTRACTOR-dtls_srtp", "-keymatexportlen", "60"]
let stdin = Pipe()
let stdout = Pipe()
client.standardInput = stdin
client.standardOutput = stdout
client.standardError = stdout
try client.run()
defer { client.terminate() }
let output = OutputCollector(stdout.fileHandleForReading)
var buffer = [UInt8](repeating: 0, count: 2048)
let deadline = Date().addingTimeInterval(fingerprintMatches ? 20 : 3)
while Date() < deadline, !output.text.contains("Keying material") {
if case .failed = server.state { break }
var peer = sockaddr_storage()
var peerLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
let n = withUnsafeMutablePointer(to: &peer) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { recvfrom(socket, &buffer, buffer.count, 0, $0, &peerLength) }
}
guard n > 0 else { continue }
for reply in server.handle(Array(buffer[0..<n])) {
_ = withUnsafePointer(to: &peer) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) { sendto(socket, reply, reply.count, 0, $0, peerLength) }
}
}
}
guard fingerprintMatches else {
#expect(server.state == .failed("Client certificate does not match the SDP fingerprint"))
#expect(server.keyMaterial == nil)
return
}
#expect(server.state == .established)
let text = output.text
let line = try #require(text.split(separator: "\n").first { $0.contains("Keying material:") })
let exported = String(line.split(separator: ":").last!).trimmingCharacters(in: .whitespaces).lowercased()
let keys = try #require(server.keyMaterial)
#expect(exported == (keys.clientKey + keys.serverKey + keys.clientSalt + keys.serverSalt).hex)
#expect(text.contains("SRTP Extension negotiated, profile=SRTP_AES128_CM_SHA1_80"))
}
@discardableResult
private func run(_ arguments: [String]) throws -> Data {
let process = Process()
process.executableURL = URL(fileURLWithPath: arguments[0])
process.arguments = Array(arguments.dropFirst())
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = FileHandle.nullDevice
try process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
return data
}
}
private final class OutputCollector: @unchecked Sendable {
private let lock = NSLock()
private var data = Data()
init(_ handle: FileHandle) {
handle.readabilityHandler = { [weak self] h in
let chunk = h.availableData
self?.lock.withLock { self?.data.append(chunk) }
}
}
var text: String { lock.withLock { String(decoding: data, as: UTF8.self) } }
}

Powered by TurnKey Linux.