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:
@@ -0,0 +1,121 @@
|
||||
import Foundation
|
||||
import Security
|
||||
import Testing
|
||||
@testable import ClientTLS
|
||||
|
||||
// B4 · Pins the hand-written PKCS#10 encoder against an EXTERNAL known-good
|
||||
// vector (OpenSSL) plus the two boundary behaviours the structural tests in
|
||||
// CertificateSigningRequestTests don't reach: a signer that hands back a
|
||||
// non-X9.63 public key, and a subject long enough to need a long-form DER
|
||||
// length. `verifyCsrPoPEc` re-serializes `CertificationRequestInfo` server-side
|
||||
// before checking the PoP, so a single byte of drift breaks enrollment.
|
||||
|
||||
@Test("CertificationRequestInfo is byte-identical to OpenSSL's for the same key + subject")
|
||||
func csrRequestInfoMatchesOpenSSLVector() throws {
|
||||
// Arrange — the exact key OpenSSL used for the embedded vector.
|
||||
let signer = try KnownGoodCSR.fixedKey()
|
||||
|
||||
// Act
|
||||
let der = try CertificateSigningRequest.der(
|
||||
subjectCommonName: KnownGoodCSR.subjectCommonName, signer: signer
|
||||
)
|
||||
|
||||
// Assert — the signed half must match OpenSSL byte for byte.
|
||||
let parsed = try #require(parseCSR(der))
|
||||
#expect(parsed.certificationRequestInfoDER == KnownGoodCSR.certificationRequestInfoDER)
|
||||
}
|
||||
|
||||
@Test("the vector's self-signature verifies over the exact CertificationRequestInfo bytes")
|
||||
func csrVectorSignatureVerifies() throws {
|
||||
// Arrange
|
||||
let signer = try KnownGoodCSR.fixedKey()
|
||||
let der = try CertificateSigningRequest.der(
|
||||
subjectCommonName: KnownGoodCSR.subjectCommonName, signer: signer
|
||||
)
|
||||
let bytes = [UInt8](der)
|
||||
let outer = try #require(TestDER.read(bytes, at: 0))
|
||||
let parts = TestDER.children(bytes, outer)
|
||||
let signature = Data(bytes[(parts[2].valueStart + 1)..<parts[2].valueEnd])
|
||||
|
||||
// Act — verify against the public point embedded in the CSR itself.
|
||||
let parsed = try #require(parseCSR(der))
|
||||
let publicKey = try #require(
|
||||
SecKeyCreateWithData(
|
||||
parsed.publicPointX963 as CFData,
|
||||
[
|
||||
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
||||
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
|
||||
kSecAttrKeySizeInBits as String: 256,
|
||||
] as CFDictionary,
|
||||
nil
|
||||
)
|
||||
)
|
||||
var error: Unmanaged<CFError>?
|
||||
let verified = SecKeyVerifySignature(
|
||||
publicKey,
|
||||
.ecdsaSignatureMessageX962SHA256,
|
||||
KnownGoodCSR.certificationRequestInfoDER as CFData,
|
||||
signature as CFData,
|
||||
&error
|
||||
)
|
||||
|
||||
// Assert
|
||||
#expect(verified, "\(String(describing: error?.takeRetainedValue()))")
|
||||
}
|
||||
|
||||
@Test("a signer whose public key is not a 65-byte uncompressed point is rejected")
|
||||
func csrRejectsNonX963PublicKey() {
|
||||
// Arrange — a compressed (33-byte) point: valid EC, wrong encoding for SPKI.
|
||||
let compressed = Data([0x02] + [UInt8](repeating: 0x11, count: 32))
|
||||
let signer = StubSigner(publicKey: compressed)
|
||||
|
||||
// Act / Assert — must fail BEFORE signing (no PoP over a bogus SPKI).
|
||||
#expect(throws: CertificateSigningRequest.CSRError.invalidPublicKey) {
|
||||
_ = try CertificateSigningRequest.der(subjectCommonName: "device", signer: signer)
|
||||
}
|
||||
#expect(signer.signCallCount == 0)
|
||||
}
|
||||
|
||||
@Test("a subject long enough to need a 2-byte DER length still round-trips")
|
||||
func csrLongSubjectUsesLongFormLength() throws {
|
||||
// Arrange — 300 chars pushes CertificationRequestInfo past 255 bytes, so its
|
||||
// length must be encoded long-form as 0x82 <hi> <lo>.
|
||||
let longCommonName = String(repeating: "d", count: 300)
|
||||
let signer = try KnownGoodCSR.fixedKey()
|
||||
|
||||
// Act
|
||||
let der = try CertificateSigningRequest.der(
|
||||
subjectCommonName: longCommonName, signer: signer
|
||||
)
|
||||
|
||||
// Assert
|
||||
let parsed = try #require(parseCSR(der))
|
||||
#expect(parsed.subjectCommonName == longCommonName)
|
||||
let infoBytes = [UInt8](parsed.certificationRequestInfoDER)
|
||||
#expect(infoBytes[1] == 0x82) // long form, two length bytes
|
||||
let declaredLength = Int(infoBytes[2]) << 8 | Int(infoBytes[3])
|
||||
#expect(declaredLength == infoBytes.count - 4) // minimal + accurate
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
/// A `P256HardwareKey` that returns a caller-chosen public key and records
|
||||
/// whether it was ever asked to sign.
|
||||
private final class StubSigner: P256HardwareKey, @unchecked Sendable {
|
||||
private let publicKey: Data
|
||||
private let lock = NSLock()
|
||||
private var signCalls = 0
|
||||
|
||||
init(publicKey: Data) {
|
||||
self.publicKey = publicKey
|
||||
}
|
||||
|
||||
var signCallCount: Int { lock.withLock { signCalls } }
|
||||
|
||||
func publicKeyX963() throws -> Data { publicKey }
|
||||
|
||||
func sign(_ message: Data) throws -> Data {
|
||||
lock.withLock { signCalls += 1 }
|
||||
return Data([0x30, 0x00])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user