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.authenticationCode(for: fullSeed, using: key)) var out: [UInt8] = [] out.reserveCapacity(length + 32) while out.count < length { out += HMAC.authenticationCode(for: a + fullSeed, using: key) a = Array(HMAC.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 var contentRange: Range { range.lowerBound + header..) -> [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.. [UInt8]? { guard let cert = elements(certificate, in: 0.. 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))) } }