1// TLSCertManager.swift2// SophaxChatCore3//4// Generates an ephemeral per-session P-256 self-signed TLS certificate for5// use in the TCP transport layer.6//7// The certificate is NOT used for application identity — that role belongs to8// the Ed25519/X25519 keypairs managed by IdentityManager and verified via9// X3DH + Ed25519 signatures. The TLS layer closes the transport-metadata gap:10// without it, a network-path observer (ISP, router) can see IP-level social11// graph, message sizes, and timing even though application content is encrypted.12//13// A fresh P-256 key and certificate are generated on every app session.14// The private key is added to the Keychain temporarily (required to create15// a SecIdentity) and deleted on deinit. The certificate lives in memory only.16//17// The SHA-256 fingerprint of the DER certificate is included in PreKeyBundle18// (tlsCertFingerprint). When a peer connects outbound, they extract this19// fingerprint and verify the server cert against it — mutual pinning with20// no CA and no trust chain.2122import Foundation23import Security24import CryptoKit2526public final class TLSCertManager:@unchecked Sendable {2728// Keychain application tag for the ephemeral TLS private key.29// One tag per install — cleaned up on every start() and deinit.30 private static let keychainTag ="com.sophax.tls.ephemeral"3132/// The session's SecIdentity (ephemeral key + self-signed cert).33/// Assign to TCPTransport.tlsIdentity before calling TCPTransport.start().34 public private(set) var secIdentity: SecIdentity?3536/// SHA-256 fingerprint of the DER-encoded session certificate (32 bytes).37/// Assign to PreKeyManager.tlsCertFingerprint so it propagates in PreKeyBundles.38 public private(set) var certFingerprint: Data?3940 public init(){}4142 deinit { deleteKeychainKey()}4344// MARK: - Public4546/// Generate a fresh P-256 key + self-signed certificate for this session.47/// Call once at app start, before TCPTransport.start().48///49/// Returns true on success. Failure is non-fatal — the transport falls back50/// to plain TCP (still protected by application-layer ChaCha20-Poly1305).51@discardableResult52 public func start()-> Bool {53// Remove any stale key from a previous session (e.g. after a crash).54 deleteKeychainKey()5556 let tag = Self.keychainTag.data(using:.utf8)!5758// 1. Generate P-256 private key directly in the Keychain.59// kSecAttrIsPermanent: true is required so SecIdentityCreateWithCertificate60// can locate the key via its public key when we present the certificate.61 let keyParams:[String: Any]=[62 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,63 kSecAttrKeySizeInBits as String: 256,64 kSecAttrIsPermanent as String: true,65 kSecAttrApplicationTag as String: tag,66 kSecAttrLabel as String:"sophax-tls-session",67 kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly68]69 var cfErr: Unmanaged<CFError>?70 guard let privKey = SecKeyCreateRandomKey(keyParams as CFDictionary,&cfErr),71 let pubKey = SecKeyCopyPublicKey(privKey) else { return false }7273// Export public key as uncompressed X9.63 EC point: 0x04 ‖ x ‖ y (65 bytes).74 var exportErr: Unmanaged<CFError>?75 guard let x963Data = SecKeyCopyExternalRepresentation(pubKey,&exportErr) as Data?76 else { return false }7778// 2. Encode a minimal self-signed X.509v3 DER certificate and sign it.79 guard let certDER = buildCert(publicKeyX963: Array(x963Data), signingKey: privKey),80 let secCert = SecCertificateCreateWithData(nil, certDER as CFData)81 else { return false }8283// 3. Pair the in-memory cert with the Keychain key to form a SecIdentity.84// The Keychain locates the private key by matching its public key against85// the public key embedded in secCert.86 var identityRef: SecIdentity?87 guard SecIdentityCreateWithCertificate(nil, secCert,&identityRef)== errSecSuccess,88 let id = identityRef else { return false }8990 secIdentity = id91 certFingerprint = Data(SHA256.hash(data: certDER))92 return true93}9495// MARK: - Private: minimal X.509v3 DER certificate9697 private func buildCert(publicKeyX963:[UInt8], signingKey: SecKey)-> Data?{98// OID byte sequences (value only; tag + length added by DERWriter.writeOID).99 let oidECDSASHA256:[UInt8]=[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x04, 0x03, 0x02]100 let oidECPublicKey:[UInt8]=[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]101 let oidPrime256v1:[UInt8]=[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]102 let oidCommonName:[UInt8]=[0x55, 0x04, 0x03]103104 var serial =[UInt8](repeating: 0, count: 8)105 _ = SecRandomCopyBytes(kSecRandomDefault, 8,&serial)106107 let now = Date()108 let expire = Date(timeIntervalSinceNow: 365 * 24 * 3600)109110// Helper: Name ::= SEQUENCE { SET { SEQUENCE { OID(CN) UTF8String("sx") } } }111 let buildName:(inout DERWriter)-> Void ={ w in112 w.writeSequence { seq in113 seq.writeSet { set in114 set.writeSequence { atv in115 atv.writeOID(oidCommonName)116 atv.writeUTF8String("sx")117}118}119}120}121122// TBSCertificate123 var tbs = DERWriter()124125// version [0] EXPLICIT INTEGER(2) → v3126 tbs.writeExplicit(tag: 0xA0){ v in v.writeInteger([0x02])}127128// serialNumber129 tbs.writeInteger(serial)130131// signature AlgorithmIdentifier: ecdsa-with-SHA256 (no parameters — RFC 5758)132 tbs.writeSequence { $0.writeOID(oidECDSASHA256)}133134// issuer135 buildName(&tbs)136137// validity138 tbs.writeSequence { v in v.writeUTCTime(now); v.writeUTCTime(expire)}139140// subject (self-signed → same as issuer)141 buildName(&tbs)142143// subjectPublicKeyInfo144 tbs.writeSequence { spki in145 spki.writeSequence { algId in146 algId.writeOID(oidECPublicKey)147 algId.writeOID(oidPrime256v1)148}149// BIT STRING: 0x00 (zero unused bits) ‖ uncompressed EC point150 spki.writeBitString([0x00]+ publicKeyX963)151}152153 let tbsBytes = tbs.bytes154155// Sign TBSCertificate with ECDSA-SHA256156 var signErr: Unmanaged<CFError>?157 guard let sigCF = SecKeyCreateSignature(158 signingKey,159.ecdsaSignatureMessageX962SHA256,160 Data(tbsBytes) as CFData,161&signErr162) else { return nil }163164// Certificate = SEQUENCE { TBSCertificate, signatureAlgorithm, signatureValue }165 var cert = DERWriter()166 cert.writeSequence { c in167 c.writeRaw(tbsBytes)168 c.writeSequence { $0.writeOID(oidECDSASHA256)}169 c.writeBitString([0x00]+ Array(sigCF as Data))170}171 return Data(cert.bytes)172}173174// MARK: - Private: Keychain cleanup175176 private func deleteKeychainKey(){177 let tag = Self.keychainTag.data(using:.utf8)!178 let query:[String: Any]=[179 kSecClass as String: kSecClassKey,180 kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,181 kSecAttrApplicationTag as String: tag182]183 SecItemDelete(query as CFDictionary)184 secIdentity = nil185 certFingerprint = nil186}187}188189// MARK: - Minimal ASN.1 / DER writer (private to this file)190191private struct DERWriter {192 private(set) var bytes:[UInt8]=[]193194 mutating func writeRaw(_ raw:[UInt8]){195 bytes.append(contentsOf: raw)196}197198 mutating func writeTLV(tag: UInt8, content:[UInt8]){199 bytes.append(tag)200 appendLength(content.count)201 bytes.append(contentsOf: content)202}203204 private mutating func appendLength(_ len: Int){205 if len < 0x80 {206 bytes.append(UInt8(len))207} else if len < 0x100 {208 bytes.append(0x81)209 bytes.append(UInt8(len))210} else {211 bytes.append(0x82)212 bytes.append(UInt8(len >> 8))213 bytes.append(UInt8(len & 0xFF))214}215}216217 mutating func writeSequence(_ build:(inout DERWriter)-> Void){218 var inner = DERWriter(); build(&inner)219 writeTLV(tag: 0x30, content: inner.bytes)220}221222 mutating func writeSet(_ build:(inout DERWriter)-> Void){223 var inner = DERWriter(); build(&inner)224 writeTLV(tag: 0x31, content: inner.bytes)225}226227/// Write a context-specific EXPLICIT wrapper tag (e.g. 0xA0 for [0]).228 mutating func writeExplicit(tag: UInt8, _ build:(inout DERWriter)-> Void){229 var inner = DERWriter(); build(&inner)230 writeTLV(tag: tag, content: inner.bytes)231}232233/// DER INTEGER: prepends 0x00 if high bit is set to keep value positive.234 mutating func writeInteger(_ value:[UInt8]){235 var content = value236 if let first = content.first, first & 0x80 != 0 { content.insert(0x00, at: 0)}237 writeTLV(tag: 0x02, content: content)238}239240 mutating func writeOID(_ oid:[UInt8]){ writeTLV(tag: 0x06, content: oid)}241242 mutating func writeUTF8String(_ s: String){ writeTLV(tag: 0x0C, content: Array(s.utf8))}243244/// BIT STRING: caller must include the "unused bits" prefix byte (0x00 = none).245 mutating func writeBitString(_ bytes:[UInt8]){ writeTLV(tag: 0x03, content: bytes)}246247/// UTCTime: "YYMMDDHHMMSSZ" (13 bytes, tag 0x17).248 mutating func writeUTCTime(_ date: Date){249 let fmt = DateFormatter()250 fmt.dateFormat ="yyMMddHHmmss"251 fmt.timeZone = TimeZone(abbreviation:"UTC")252 writeTLV(tag: 0x17, content: Array((fmt.string(from: date)+"Z").utf8))253}254}