test(ios): ClientTLS coverage 55.76% -> 89.49%, gate it, fix the three dead CI legs

ClientTLS was the most security-sensitive package in the tree and the least
covered, and it was not in the coverage gate at all (the gate's 4-package set
predates it). 48 -> 84 tests against the real macOS keychain, serialized with a
custom Testing trait after a @globalActor proved insufficient (actors yield at
await, so cross-await critical sections got interleaved by other cases' cleanup).

CI: the app/ipad/ios17 legs ran a bundle containing LiveServerSmokeTests, which
spawns tsx, with no npm ci -- a hard failure, not a skip, on a bare checkout.
Adds the missing iPad UI-test leg, and makes a missing iOS 17 runtime fail loudly
instead of silently reporting green.
This commit is contained in:
Yaojia Wang
2026-07-30 12:45:26 +02:00
parent 850531fd07
commit a5fa843f00
14 changed files with 1733 additions and 46 deletions

View File

@@ -0,0 +1,70 @@
import Foundation
import Security
@testable import ClientTLS
/// B4 · A KNOWN-GOOD PKCS#10 vector produced by OpenSSL, used to pin the
/// hand-written DER encoder in `CertificateSigningRequest` byte-for-byte.
///
/// The ECDSA signature is randomized, so the stable part of a CSR is its
/// `CertificationRequestInfo` for a fixed key + fixed subject it is fully
/// deterministic. That is what is pinned here: if our encoder ever drifts (a
/// non-minimal length, a PrintableString instead of UTF8String, a wrong curve
/// OID, an attributes SET that is not `A0 00`), the bytes stop matching
/// OpenSSL's and this test fails.
///
/// Regenerate (OpenSSL 3.0.18, 2026-07-30):
/// openssl ecparam -name prime256v1 -genkey -noout -out fixed.key.pem
/// openssl req -new -key fixed.key.pem -subj "/CN=web-terminal-device" \
/// -outform DER -out csr.der
/// # CertificationRequestInfo = the first child of the outer SEQUENCE:
/// openssl asn1parse -inform DER -in csr.der -i # offset 3, hl 3, l 128
/// dd if=csr.der bs=1 skip=3 count=131 | base64
/// # private key as X9.63 (0x04||X||Y||K) for SecKeyCreateWithData:
/// openssl ec -in fixed.key.pem -text -noout # pub(65) || priv(32)
///
/// SECURITY: `privateKeyX963Base64` is a THROWAWAY key generated solely for this
/// vector. It protects nothing, is never enrolled, and is never written to a
/// keychain (`SecKeyCreateWithData` keeps it in-process). Same convention as the
/// embedded `device.p12` + passphrase in `ClientTLSFixtures`.
enum KnownGoodCSR {
static let subjectCommonName = "web-terminal-device"
/// OpenSSL's `CertificationRequestInfo` DER for `privateKeyX963Base64` +
/// `CN=web-terminal-device` (131 bytes).
static let certificationRequestInfoBase64 = """
MIGAAgEAMB4xHDAaBgNVBAMME3dlYi10ZXJtaW5hbC1kZXZpY2UwWTATBgcqhkjOPQIBBggqhkjO\
PQMBBwNCAARiJ8iQGXzCgho1TRYPNNcnqtgK/mrsNf+CAvoPbSH+12v1Q2OfVRgVPVYeS2AO3y87\
Oel9Uxe2QsHluJnGoifSoAA=
"""
/// `0x04 || X(32) || Y(32) || K(32)` the format `SecKeyCreateWithData`
/// expects for an EC private key.
static let privateKeyX963Base64 = """
BGInyJAZfMKCGjVNFg801yeq2Ar+auw1/4IC+g9tIf7Xa/VDY59VGBU9Vh5LYA7fLzs56X1TF7ZC\
weW4mcaiJ9Ko07ifqUA6io//Czd0XRMztqg+nY0OJcA1Y1LwoBMBbA==
"""
static var certificationRequestInfoDER: Data {
Data(base64Encoded: certificationRequestInfoBase64, options: .ignoreUnknownCharacters)!
}
/// The fixed key as a `P256HardwareKey`, in-process only (no keychain item).
static func fixedKey() throws -> SecureEnclaveKey {
let blob = Data(
base64Encoded: privateKeyX963Base64, options: .ignoreUnknownCharacters
)!
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeyClass as String: kSecAttrKeyClassPrivate,
kSecAttrKeySizeInBits as String: 256,
]
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateWithData(blob as CFData, attributes as CFDictionary, &error)
else {
throw SecureEnclaveKeyError.keyGenerationFailed(
String(describing: error?.takeRetainedValue())
)
}
return SecureEnclaveKey(privateKey: key)
}
}