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