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.
122 lines
4.5 KiB
Swift
122 lines
4.5 KiB
Swift
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])
|
|
}
|
|
}
|