Building blocks for a minimal WebRTC stack on CryptoKit/CommonCrypto, verified against RFC 5769, RFC 3711 and OpenSSL vectors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
2527b18e8d
commit
0ff5e2f907
@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
|
||||
/// Big-endian byte writer for the wire formats (STUN, DTLS, RTP).
|
||||
struct ByteWriter {
|
||||
private(set) var bytes: [UInt8] = []
|
||||
|
||||
init(capacity: Int = 64) { bytes.reserveCapacity(capacity) }
|
||||
|
||||
mutating func u8(_ v: UInt8) { bytes.append(v) }
|
||||
mutating func u16(_ v: UInt16) { bytes += [UInt8(v >> 8), UInt8(v & 0xFF)] }
|
||||
mutating func u24(_ v: Int) { bytes += [UInt8((v >> 16) & 0xFF), UInt8((v >> 8) & 0xFF), UInt8(v & 0xFF)] }
|
||||
mutating func u32(_ v: UInt32) { for shift in stride(from: 24, through: 0, by: -8) { bytes.append(UInt8((v >> UInt32(shift)) & 0xFF)) } }
|
||||
mutating func u48(_ v: UInt64) { for shift in stride(from: 40, through: 0, by: -8) { bytes.append(UInt8((v >> UInt64(shift)) & 0xFF)) } }
|
||||
mutating func u64(_ v: UInt64) { for shift in stride(from: 56, through: 0, by: -8) { bytes.append(UInt8((v >> UInt64(shift)) & 0xFF)) } }
|
||||
mutating func append<C: Collection>(_ data: C) where C.Element == UInt8 { bytes += data }
|
||||
|
||||
/// Writes `body` prefixed by its length in `lengthBytes` bytes (1, 2 or 3).
|
||||
mutating func vector(_ lengthBytes: Int, _ body: (inout ByteWriter) -> Void) {
|
||||
var inner = ByteWriter()
|
||||
body(&inner)
|
||||
let n = inner.bytes.count
|
||||
switch lengthBytes {
|
||||
case 1: u8(UInt8(n))
|
||||
case 2: u16(UInt16(n))
|
||||
default: u24(n)
|
||||
}
|
||||
bytes += inner.bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// Big-endian reader that throws instead of trapping on short input.
|
||||
struct ByteReader {
|
||||
struct OutOfBounds: Error {}
|
||||
|
||||
let bytes: [UInt8]
|
||||
private(set) var offset: Int
|
||||
|
||||
init<C: Collection>(_ bytes: C, offset: Int = 0) where C.Element == UInt8 {
|
||||
self.bytes = Array(bytes)
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
var remaining: Int { bytes.count - offset }
|
||||
var isAtEnd: Bool { offset >= bytes.count }
|
||||
|
||||
mutating func u8() throws -> UInt8 {
|
||||
guard remaining >= 1 else { throw OutOfBounds() }
|
||||
defer { offset += 1 }
|
||||
return bytes[offset]
|
||||
}
|
||||
|
||||
mutating func u16() throws -> UInt16 { try UInt16(read(2)) }
|
||||
mutating func u24() throws -> Int { Int(try read(3)) }
|
||||
mutating func u32() throws -> UInt32 { try UInt32(read(4)) }
|
||||
mutating func u48() throws -> UInt64 { try read(6) }
|
||||
mutating func u64() throws -> UInt64 { try read(8) }
|
||||
|
||||
mutating func take(_ n: Int) throws -> [UInt8] {
|
||||
guard n >= 0, remaining >= n else { throw OutOfBounds() }
|
||||
defer { offset += n }
|
||||
return Array(bytes[offset..<offset + n])
|
||||
}
|
||||
|
||||
mutating func skip(_ n: Int) throws { _ = try take(n) }
|
||||
|
||||
/// Reads a length-prefixed vector (`lengthBytes` = 1, 2 or 3).
|
||||
mutating func vector(_ lengthBytes: Int) throws -> [UInt8] {
|
||||
let n: Int = switch lengthBytes {
|
||||
case 1: Int(try u8())
|
||||
case 2: Int(try u16())
|
||||
default: try u24()
|
||||
}
|
||||
return try take(n)
|
||||
}
|
||||
|
||||
private mutating func read(_ n: Int) throws -> UInt64 {
|
||||
guard remaining >= n else { throw OutOfBounds() }
|
||||
var v: UInt64 = 0
|
||||
for i in 0..<n { v = (v << 8) | UInt64(bytes[offset + i]) }
|
||||
offset += n
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == UInt8 {
|
||||
init?(hex: String) {
|
||||
let clean = hex.filter { !$0.isWhitespace && $0 != ":" }
|
||||
guard clean.count.isMultiple(of: 2) else { return nil }
|
||||
var out: [UInt8] = []
|
||||
var index = clean.startIndex
|
||||
while index < clean.endIndex {
|
||||
let next = clean.index(index, offsetBy: 2)
|
||||
guard let b = UInt8(clean[index..<next], radix: 16) else { return nil }
|
||||
out.append(b)
|
||||
index = next
|
||||
}
|
||||
self = out
|
||||
}
|
||||
|
||||
var hex: String { map { String(format: "%02x", $0) }.joined() }
|
||||
}
|
||||
|
||||
/// Compares in constant time (MACs, Finished messages).
|
||||
func constantTimeEqual(_ a: [UInt8], _ b: [UInt8]) -> Bool {
|
||||
guard a.count == b.count else { return false }
|
||||
var diff: UInt8 = 0
|
||||
for i in a.indices { diff |= a[i] ^ b[i] }
|
||||
return diff == 0
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
/// TLS 1.2 PRF with SHA-256 (RFC 5246 section 5).
|
||||
enum TLSPRF {
|
||||
static func compute(secret: [UInt8], label: String, seed: [UInt8], length: Int) -> [UInt8] {
|
||||
let key = SymmetricKey(data: secret)
|
||||
let fullSeed = Array(label.utf8) + seed
|
||||
var a = Array(HMAC<SHA256>.authenticationCode(for: fullSeed, using: key))
|
||||
var out: [UInt8] = []
|
||||
out.reserveCapacity(length + 32)
|
||||
while out.count < length {
|
||||
out += HMAC<SHA256>.authenticationCode(for: a + fullSeed, using: key)
|
||||
a = Array(HMAC<SHA256>.authenticationCode(for: a, using: key))
|
||||
}
|
||||
return Array(out.prefix(length))
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal DER encoding/decoding for one self-signed ECDSA certificate and for reading peer keys.
|
||||
enum DER {
|
||||
static func tlv(_ tag: UInt8, _ content: [UInt8]) -> [UInt8] {
|
||||
var out = [tag]
|
||||
let n = content.count
|
||||
if n < 0x80 {
|
||||
out.append(UInt8(n))
|
||||
} else if n <= 0xFF {
|
||||
out += [0x81, UInt8(n)]
|
||||
} else {
|
||||
out += [0x82, UInt8(n >> 8), UInt8(n & 0xFF)]
|
||||
}
|
||||
return out + content
|
||||
}
|
||||
|
||||
static func sequence(_ items: [UInt8]...) -> [UInt8] { tlv(0x30, items.flatMap { $0 }) }
|
||||
static func set(_ items: [UInt8]...) -> [UInt8] { tlv(0x31, items.flatMap { $0 }) }
|
||||
|
||||
static func integer(_ bytes: [UInt8]) -> [UInt8] {
|
||||
var b = Array(bytes.drop { $0 == 0 })
|
||||
if b.isEmpty { b = [0] }
|
||||
if b[0] & 0x80 != 0 { b.insert(0, at: 0) }
|
||||
return tlv(0x02, b)
|
||||
}
|
||||
|
||||
static func oid(_ components: [UInt]) -> [UInt8] {
|
||||
var body = [UInt8(components[0] * 40 + components[1])]
|
||||
for c in components.dropFirst(2) {
|
||||
var chunk = [UInt8(c & 0x7F)]
|
||||
var v = c >> 7
|
||||
while v > 0 {
|
||||
chunk.insert(UInt8(v & 0x7F) | 0x80, at: 0)
|
||||
v >>= 7
|
||||
}
|
||||
body += chunk
|
||||
}
|
||||
return tlv(0x06, body)
|
||||
}
|
||||
|
||||
static func utf8(_ s: String) -> [UInt8] { tlv(0x0C, Array(s.utf8)) }
|
||||
static func bitString(_ bytes: [UInt8]) -> [UInt8] { tlv(0x03, [0] + bytes) }
|
||||
|
||||
static func utcTime(_ date: Date) -> [UInt8] {
|
||||
let f = DateFormatter()
|
||||
f.locale = Locale(identifier: "en_US_POSIX")
|
||||
f.timeZone = TimeZone(identifier: "UTC")
|
||||
f.dateFormat = "yyMMddHHmmss'Z'"
|
||||
return tlv(0x17, Array(f.string(from: date).utf8))
|
||||
}
|
||||
|
||||
/// One parsed element: tag and the full encoding range inside the source buffer.
|
||||
struct Element {
|
||||
let tag: UInt8
|
||||
let header: Int
|
||||
let range: Range<Int>
|
||||
var contentRange: Range<Int> { range.lowerBound + header..<range.upperBound }
|
||||
}
|
||||
|
||||
static func elements(_ bytes: [UInt8], in range: Range<Int>) -> [Element]? {
|
||||
var out: [Element] = []
|
||||
var i = range.lowerBound
|
||||
while i < range.upperBound {
|
||||
guard i + 2 <= range.upperBound else { return nil }
|
||||
let tag = bytes[i]
|
||||
var length = Int(bytes[i + 1])
|
||||
var header = 2
|
||||
if length & 0x80 != 0 {
|
||||
let count = length & 0x7F
|
||||
guard count <= 3, i + 2 + count <= range.upperBound else { return nil }
|
||||
length = 0
|
||||
for k in 0..<count { length = length << 8 | Int(bytes[i + 2 + k]) }
|
||||
header += count
|
||||
}
|
||||
guard i + header + length <= range.upperBound else { return nil }
|
||||
out.append(Element(tag: tag, header: header, range: i..<i + header + length))
|
||||
i += header + length
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/// SubjectPublicKeyInfo of an X.509 certificate in DER.
|
||||
static func subjectPublicKeyInfo(certificate: [UInt8]) -> [UInt8]? {
|
||||
guard let cert = elements(certificate, in: 0..<certificate.count)?.first, cert.tag == 0x30,
|
||||
let tbs = elements(certificate, in: cert.contentRange)?.first, tbs.tag == 0x30,
|
||||
let fields = elements(certificate, in: tbs.contentRange) else { return nil }
|
||||
// [0] version (optional), serial, signature, issuer, validity, subject, subjectPublicKeyInfo
|
||||
let start = fields.first?.tag == 0xA0 ? 1 : 0
|
||||
guard fields.count > start + 5 else { return nil }
|
||||
return Array(certificate[fields[start + 5].range])
|
||||
}
|
||||
}
|
||||
|
||||
/// The self-signed ECDSA P-256 certificate that identifies this Musubi in DTLS.
|
||||
/// Browsers only check it against the fingerprint in the SDP, not against a CA.
|
||||
struct DTLSCertificate: Sendable {
|
||||
let privateKey: P256.Signing.PrivateKey
|
||||
let der: [UInt8]
|
||||
|
||||
/// SHA-256 fingerprint as written in SDP (`AB:CD:…`).
|
||||
var fingerprint: String { Self.fingerprint(der) }
|
||||
|
||||
static func fingerprint(_ der: [UInt8]) -> String {
|
||||
SHA256.hash(data: der).map { String(format: "%02X", $0) }.joined(separator: ":")
|
||||
}
|
||||
|
||||
init(commonName: String = "Musubi", now: Date = Date()) throws {
|
||||
let key = P256.Signing.PrivateKey()
|
||||
let ecdsaWithSHA256 = DER.sequence(DER.oid([1, 2, 840, 10045, 4, 3, 2]))
|
||||
let name = DER.sequence(DER.set(DER.sequence(DER.oid([2, 5, 4, 3]), DER.utf8(commonName))))
|
||||
let validity = DER.sequence(DER.utcTime(now.addingTimeInterval(-86_400)), DER.utcTime(now.addingTimeInterval(30 * 86_400)))
|
||||
let spki = DER.sequence(
|
||||
DER.sequence(DER.oid([1, 2, 840, 10045, 2, 1]), DER.oid([1, 2, 840, 10045, 3, 1, 7])),
|
||||
DER.bitString(Array(key.publicKey.x963Representation)))
|
||||
var serial = [UInt8](repeating: 0, count: 8)
|
||||
_ = SecRandomCopyBytes(kSecRandomDefault, serial.count, &serial)
|
||||
serial[0] &= 0x7F
|
||||
let tbs = DER.sequence(
|
||||
DER.tlv(0xA0, DER.integer([2])),
|
||||
DER.integer(serial),
|
||||
ecdsaWithSHA256, name, validity, name, spki)
|
||||
let signature = try key.signature(for: tbs).derRepresentation
|
||||
self.privateKey = key
|
||||
self.der = DER.sequence(tbs, ecdsaWithSHA256, DER.bitString(Array(signature)))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
import CommonCrypto
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
/// AES-128 in ECB mode, used as the block function for counter mode (CryptoKit has no AES-CTR).
|
||||
final class AESBlock: @unchecked Sendable {
|
||||
private var cryptor: CCCryptorRef?
|
||||
|
||||
init(key: [UInt8]) {
|
||||
precondition(key.count == 16)
|
||||
CCCryptorCreate(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES), CCOptions(kCCOptionECBMode),
|
||||
key, key.count, nil, &cryptor)
|
||||
}
|
||||
|
||||
deinit { if let cryptor { CCCryptorRelease(cryptor) } }
|
||||
|
||||
/// Counter-mode keystream: AES(iv), AES(iv+1), … (counter in the last 16 bits, RFC 3711 4.1.1).
|
||||
func keystream(iv: [UInt8], count: Int) -> [UInt8] {
|
||||
let blocks = (count + 15) / 16
|
||||
var input = [UInt8](repeating: 0, count: blocks * 16)
|
||||
for b in 0..<blocks {
|
||||
var block = iv
|
||||
let counter = (UInt32(block[14]) << 8 | UInt32(block[15])) &+ UInt32(b)
|
||||
block[14] = UInt8((counter >> 8) & 0xFF)
|
||||
block[15] = UInt8(counter & 0xFF)
|
||||
input.replaceSubrange(b * 16..<(b + 1) * 16, with: block)
|
||||
}
|
||||
var output = [UInt8](repeating: 0, count: input.count)
|
||||
var moved = 0
|
||||
CCCryptorUpdate(cryptor, input, input.count, &output, output.count, &moved)
|
||||
return Array(output.prefix(count))
|
||||
}
|
||||
}
|
||||
|
||||
/// SRTP/SRTCP with the profile SRTP_AES128_CM_HMAC_SHA1_80 (RFC 3711, RFC 5764).
|
||||
/// One context per direction; this side only sends RTP and reads RTCP feedback.
|
||||
final class SRTPContext: @unchecked Sendable {
|
||||
struct SessionKeys {
|
||||
let cipher: AESBlock
|
||||
let salt: [UInt8]
|
||||
let auth: SymmetricKey
|
||||
}
|
||||
|
||||
static let tagLength = 10
|
||||
|
||||
let rtp: SessionKeys
|
||||
let rtcp: SessionKeys
|
||||
private var rolloverCounters: [UInt32: (roc: UInt32, lastSeq: UInt16)] = [:]
|
||||
private var srtcpIndex: UInt32 = 0
|
||||
|
||||
init(masterKey: [UInt8], masterSalt: [UInt8]) {
|
||||
rtp = Self.derive(masterKey: masterKey, masterSalt: masterSalt, labels: (0, 1, 2))
|
||||
rtcp = Self.derive(masterKey: masterKey, masterSalt: masterSalt, labels: (3, 4, 5))
|
||||
}
|
||||
|
||||
/// Key derivation (RFC 3711 4.3) with key_derivation_rate 0.
|
||||
static func deriveKey(masterKey: [UInt8], masterSalt: [UInt8], label: UInt8, length: Int) -> [UInt8] {
|
||||
var x = masterSalt + [0, 0]
|
||||
x[7] ^= label
|
||||
return AESBlock(key: masterKey).keystream(iv: x, count: length)
|
||||
}
|
||||
|
||||
private static func derive(masterKey: [UInt8], masterSalt: [UInt8], labels: (UInt8, UInt8, UInt8)) -> SessionKeys {
|
||||
SessionKeys(
|
||||
cipher: AESBlock(key: deriveKey(masterKey: masterKey, masterSalt: masterSalt, label: labels.0, length: 16)),
|
||||
salt: deriveKey(masterKey: masterKey, masterSalt: masterSalt, label: labels.2, length: 14),
|
||||
auth: SymmetricKey(data: deriveKey(masterKey: masterKey, masterSalt: masterSalt, label: labels.1, length: 20)))
|
||||
}
|
||||
|
||||
/// IV = salt·2^16 ⊕ SSRC·2^64 ⊕ index·2^16.
|
||||
private static func iv(salt: [UInt8], ssrc: UInt32, index: UInt64) -> [UInt8] {
|
||||
var iv = salt + [0, 0]
|
||||
for i in 0..<4 { iv[4 + i] ^= UInt8((ssrc >> UInt32(24 - 8 * i)) & 0xFF) }
|
||||
for i in 0..<6 { iv[8 + i] ^= UInt8((index >> UInt64(40 - 8 * i)) & 0xFF) }
|
||||
return iv
|
||||
}
|
||||
|
||||
/// Encrypts and authenticates one RTP packet. Packets per SSRC must be passed in sending order.
|
||||
func protectRTP(_ packet: [UInt8]) -> [UInt8] {
|
||||
guard packet.count >= 12 else { return packet }
|
||||
let headerLength = Self.rtpHeaderLength(packet)
|
||||
let seq = UInt16(packet[2]) << 8 | UInt16(packet[3])
|
||||
let ssrc = UInt32(packet[8]) << 24 | UInt32(packet[9]) << 16 | UInt32(packet[10]) << 8 | UInt32(packet[11])
|
||||
|
||||
var state = rolloverCounters[ssrc] ?? (0, seq)
|
||||
if seq < state.lastSeq && state.lastSeq - seq > 0x8000 { state.roc &+= 1 }
|
||||
state.lastSeq = seq
|
||||
rolloverCounters[ssrc] = state
|
||||
|
||||
let index = UInt64(state.roc) << 16 | UInt64(seq)
|
||||
var out = packet
|
||||
let payloadCount = packet.count - headerLength
|
||||
if payloadCount > 0 {
|
||||
let stream = rtp.cipher.keystream(iv: Self.iv(salt: rtp.salt, ssrc: ssrc, index: index), count: payloadCount)
|
||||
for i in 0..<payloadCount { out[headerLength + i] ^= stream[i] }
|
||||
}
|
||||
let roc = [UInt8(state.roc >> 24), UInt8((state.roc >> 16) & 0xFF), UInt8((state.roc >> 8) & 0xFF), UInt8(state.roc & 0xFF)]
|
||||
let tag = HMAC<Insecure.SHA1>.authenticationCode(for: out + roc, using: rtp.auth)
|
||||
return out + Array(tag).prefix(Self.tagLength)
|
||||
}
|
||||
|
||||
/// Encrypts and authenticates one (compound) RTCP packet.
|
||||
func protectRTCP(_ packet: [UInt8]) -> [UInt8] {
|
||||
guard packet.count >= 8 else { return packet }
|
||||
srtcpIndex = (srtcpIndex + 1) & 0x7FFF_FFFF
|
||||
let ssrc = UInt32(packet[4]) << 24 | UInt32(packet[5]) << 16 | UInt32(packet[6]) << 8 | UInt32(packet[7])
|
||||
var out = packet
|
||||
let stream = rtcp.cipher.keystream(iv: Self.iv(salt: rtcp.salt, ssrc: ssrc, index: UInt64(srtcpIndex)), count: packet.count - 8)
|
||||
for i in 0..<stream.count { out[8 + i] ^= stream[i] }
|
||||
let e = srtcpIndex | 0x8000_0000
|
||||
out += [UInt8(e >> 24), UInt8((e >> 16) & 0xFF), UInt8((e >> 8) & 0xFF), UInt8(e & 0xFF)]
|
||||
let tag = HMAC<Insecure.SHA1>.authenticationCode(for: out, using: rtcp.auth)
|
||||
return out + Array(tag).prefix(Self.tagLength)
|
||||
}
|
||||
|
||||
/// Verifies and decrypts one SRTCP packet; nil if the tag does not match.
|
||||
func unprotectRTCP(_ packet: [UInt8]) -> [UInt8]? {
|
||||
guard packet.count >= 8 + 4 + Self.tagLength else { return nil }
|
||||
let authEnd = packet.count - Self.tagLength
|
||||
let tag = Array(packet[authEnd...])
|
||||
let expected = Array(HMAC<Insecure.SHA1>.authenticationCode(for: packet[0..<authEnd], using: rtcp.auth)).prefix(Self.tagLength)
|
||||
guard constantTimeEqual(tag, Array(expected)) else { return nil }
|
||||
let e = UInt32(packet[authEnd - 4]) << 24 | UInt32(packet[authEnd - 3]) << 16 | UInt32(packet[authEnd - 2]) << 8 | UInt32(packet[authEnd - 1])
|
||||
var out = Array(packet[0..<authEnd - 4])
|
||||
guard e & 0x8000_0000 != 0 else { return out }
|
||||
let ssrc = UInt32(packet[4]) << 24 | UInt32(packet[5]) << 16 | UInt32(packet[6]) << 8 | UInt32(packet[7])
|
||||
let stream = rtcp.cipher.keystream(iv: Self.iv(salt: rtcp.salt, ssrc: ssrc, index: UInt64(e & 0x7FFF_FFFF)), count: out.count - 8)
|
||||
for i in 0..<stream.count { out[8 + i] ^= stream[i] }
|
||||
return out
|
||||
}
|
||||
|
||||
static func rtpHeaderLength(_ packet: [UInt8]) -> Int {
|
||||
var length = 12 + Int(packet[0] & 0x0F) * 4
|
||||
if packet[0] & 0x10 != 0, packet.count >= length + 4 {
|
||||
length += 4 + (Int(packet[length + 2]) << 8 | Int(packet[length + 3])) * 4
|
||||
}
|
||||
return min(length, packet.count)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Intercom
|
||||
|
||||
@Suite struct STUNTests {
|
||||
// RFC 5769 2.1: sample request, password "VOkJxbRl1RmTxUk/WvJxBt".
|
||||
let request = [UInt8](hex: """
|
||||
000100582112a442b7e7a701bc34d686fa87dfae
|
||||
802200105354554e207465737420636c69656e74
|
||||
002400046e0001ff
|
||||
80290008932ff9b151263b36
|
||||
000600096576746a3a68367659202020
|
||||
000800149aeaa70cbfd8cb56781ef2b5b2d3f249c1b571a2
|
||||
80280004e57a3bcf
|
||||
""")!
|
||||
let password = Array("VOkJxbRl1RmTxUk/WvJxBt".utf8)
|
||||
|
||||
@Test func parsesAndVerifiesRFC5769Request() throws {
|
||||
let message = try #require(STUN.parse(request))
|
||||
#expect(message.type == STUN.bindingRequest)
|
||||
#expect(message.username == "evtj:h6vY")
|
||||
#expect(STUN.verify(request, message: message, key: password))
|
||||
#expect(!STUN.verify(request, message: message, key: Array("wrong".utf8)))
|
||||
}
|
||||
|
||||
@Test func rejectsTamperedRequest() throws {
|
||||
var tampered = request
|
||||
tampered[45] ^= 1
|
||||
let message = try #require(STUN.parse(tampered))
|
||||
#expect(!STUN.verify(tampered, message: message, key: password))
|
||||
}
|
||||
|
||||
@Test func buildsVerifiableResponse() throws {
|
||||
let tid = Array(request[8..<20])
|
||||
let response = STUN.bindingResponse(transactionID: tid, address: [192, 0, 2, 1], port: 32853, key: password)
|
||||
let message = try #require(STUN.parse(response))
|
||||
#expect(message.type == STUN.bindingSuccess)
|
||||
#expect(message.transactionID == tid)
|
||||
#expect(STUN.verify(response, message: message, key: password))
|
||||
// RFC 5769 2.2: XOR-MAPPED-ADDRESS for 192.0.2.1:32853.
|
||||
#expect(message.value(.xorMappedAddress) == [UInt8](hex: "0001a147e112a643"))
|
||||
}
|
||||
|
||||
@Test func demultiplexesByFirstByte() {
|
||||
#expect(STUN.looksLikeSTUN(request))
|
||||
#expect(!STUN.looksLikeSTUN([22, 0xFE, 0xFD] + [UInt8](repeating: 0, count: 30)))
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct KeyDerivationTests {
|
||||
@Test func tlsPRFMatchesOpenSSL() {
|
||||
let out = TLSPRF.compute(secret: [UInt8](hex: "9bbe436ba940f017b17652849a71db35")!, label: "test label",
|
||||
seed: [UInt8](hex: "a0ba9f936cda311827a6f796ffd5198c")!, length: 100)
|
||||
#expect(out.hex == """
|
||||
e3f229ba727be17b8d122620557cd453c2aab21d07c3d495329b52d4e61edb5a6b301791e90d35c9c9a46b4e14baf9af\
|
||||
0fa022f7077def17abfd3797c0564bab4fbc91666e9def9b97fce34f796789baa48082d122ee42c5a72e5a5110fff701\
|
||||
87347b66
|
||||
""")
|
||||
}
|
||||
|
||||
// RFC 3711 B.2
|
||||
@Test func aesCounterKeystream() {
|
||||
let block = AESBlock(key: [UInt8](hex: "2B7E151628AED2A6ABF7158809CF4F3C")!)
|
||||
let stream = block.keystream(iv: [UInt8](hex: "F0F1F2F3F4F5F6F7F8F9FAFBFCFD0000")!, count: 48)
|
||||
#expect(stream.hex == "e03ead0935c95e80e166b16dd92b4eb4d23513162b02d0f72a43a2fe4a5f97ab41e95b3bb0a2e8dd477901e4fca894c0")
|
||||
}
|
||||
|
||||
// RFC 3711 B.3
|
||||
@Test func srtpKeyDerivation() {
|
||||
let key = [UInt8](hex: "E1F97A0D3E018BE0D64FA32C06DE4139")!
|
||||
let salt = [UInt8](hex: "0EC675AD498AFEEBB6960B3AABE6")!
|
||||
#expect(SRTPContext.deriveKey(masterKey: key, masterSalt: salt, label: 0, length: 16).hex == "c61e7a93744f39ee10734afe3ff7a087")
|
||||
#expect(SRTPContext.deriveKey(masterKey: key, masterSalt: salt, label: 2, length: 14).hex == "30cbbc08863d8c85d49db34a9ae1")
|
||||
#expect(SRTPContext.deriveKey(masterKey: key, masterSalt: salt, label: 1, length: 20).hex == "cebe321f6ff7716b6fd4ab49af256a156d38baa4")
|
||||
}
|
||||
|
||||
@Test func srtcpRoundTrips() throws {
|
||||
let sender = SRTPContext(masterKey: Array(repeating: 7, count: 16), masterSalt: Array(repeating: 9, count: 14))
|
||||
let receiver = SRTPContext(masterKey: Array(repeating: 7, count: 16), masterSalt: Array(repeating: 9, count: 14))
|
||||
// PLI: V=2, FMT=1, PT=206, length 2, sender SSRC, media SSRC
|
||||
let pli: [UInt8] = [0x81, 206, 0, 2, 1, 2, 3, 4, 5, 6, 7, 8]
|
||||
let protected = sender.protectRTCP(pli)
|
||||
#expect(protected != pli)
|
||||
#expect(receiver.unprotectRTCP(protected) == pli)
|
||||
var broken = protected
|
||||
broken[9] ^= 1
|
||||
#expect(receiver.unprotectRTCP(broken) == nil)
|
||||
}
|
||||
|
||||
@Test func srtpEncryptsPayloadOnlyAndAppendsTag() {
|
||||
let context = SRTPContext(masterKey: Array(repeating: 1, count: 16), masterSalt: Array(repeating: 2, count: 14))
|
||||
let packet: [UInt8] = [0x80, 111, 0, 1, 0, 0, 0, 0, 0xAA, 0xBB, 0xCC, 0xDD] + Array(repeating: 0x55, count: 20)
|
||||
let out = context.protectRTP(packet)
|
||||
#expect(out.count == packet.count + 10)
|
||||
#expect(Array(out.prefix(12)) == Array(packet.prefix(12)))
|
||||
#expect(Array(out[12..<32]) != Array(packet[12...]))
|
||||
}
|
||||
}
|
||||
|
||||
@Suite struct CertificateTests {
|
||||
@Test func selfSignedCertificateCarriesKeyAndValidSignature() throws {
|
||||
let cert = try DTLSCertificate()
|
||||
let spki = try #require(DER.subjectPublicKeyInfo(certificate: cert.der))
|
||||
let publicKey = try P256.Signing.PublicKey(derRepresentation: spki)
|
||||
#expect(publicKey.rawRepresentation == cert.privateKey.publicKey.rawRepresentation)
|
||||
|
||||
// Signature over TBSCertificate verifies with the embedded key.
|
||||
let outer = try #require(DER.elements(cert.der, in: 0..<cert.der.count)?.first)
|
||||
let parts = try #require(DER.elements(cert.der, in: outer.contentRange))
|
||||
#expect(parts.count == 3)
|
||||
let tbs = Array(cert.der[parts[0].range])
|
||||
let signatureBits = Array(cert.der[parts[2].contentRange].dropFirst())
|
||||
let signature = try P256.Signing.ECDSASignature(derRepresentation: signatureBits)
|
||||
#expect(publicKey.isValidSignature(signature, for: tbs))
|
||||
#expect(cert.fingerprint.count == 32 * 3 - 1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue