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.
71 lines
3.2 KiB
Swift
71 lines
3.2 KiB
Swift
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)
|
|
}
|
|
}
|