import Foundation import Security import Testing @testable import ClientTLS // C-iOS · Proves the manual PKCS#10 encoder produces a well-formed, self-signed // P-256 CSR that the control-plane `verifyCsrPoPEc` (id-ecPublicKey + prime256v1 // SPKI, ecdsa-with-SHA256 self-signature) would accept. Runs headless with a // SOFTWARE P-256 SecKey (SecKeyCreateRandomKey WITHOUT the Secure-Enclave token) // so no hardware/entitlement is needed — the signing path is byte-identical to // the on-device SE key. Real SE keygen + SecIdentity roundtrip are device-only. /// Software P-256 key via the SAME SecKey API used on-device (no SE token). private func makeSoftwareKey() throws -> SecureEnclaveKey { try SecureEnclaveKeyFactory.generateSoftware(tag: nil, permanent: false) } @Test("CSR is a canonical PKCS#10 SEQUENCE of exactly three elements") func csrOuterStructure() throws { // Arrange let signer = try makeSoftwareKey() // Act let der = try CertificateSigningRequest.der( subjectCommonName: "web-terminal-device", signer: signer ) // Assert — outer CertificationRequest ::= SEQUENCE { info, algId, sig }. let bytes = [UInt8](der) let outer = try #require(TestDER.read(bytes, at: 0)) #expect(outer.tag == 0x30) #expect(outer.end == bytes.count) // no trailing garbage let parts = TestDER.children(bytes, outer) #expect(parts.count == 3) #expect(parts[0].tag == 0x30) // certificationRequestInfo #expect(parts[1].tag == 0x30) // signatureAlgorithm #expect(parts[2].tag == 0x03) // signature BIT STRING } @Test("CSR self-signature verifies against the embedded P-256 public key") func csrSelfSignatureVerifies() throws { // Arrange let signer = try makeSoftwareKey() let expectedPoint = try signer.publicKeyX963() // Act let der = try CertificateSigningRequest.der( subjectCommonName: "web-terminal-device", signer: signer ) let bytes = [UInt8](der) // Extract the exact CertificationRequestInfo bytes that were signed and the // ECDSA signature (the same crypto check `verifyCsrPoPEc`'s req.verify() runs). let outer = try #require(TestDER.read(bytes, at: 0)) let parts = TestDER.children(bytes, outer) let infoBytes = Data(bytes[parts[0].start..? let ok = SecKeyVerifySignature( publicKey, .ecdsaSignatureMessageX962SHA256, infoBytes as CFData, signature as CFData, &error ) #expect(ok, "self-signature must verify: \(String(describing: error?.takeRetainedValue()))") } @Test("CSR embeds a P-256 SubjectPublicKeyInfo the server verifier accepts") func csrEmbedsP256Spki() throws { // Arrange let signer = try makeSoftwareKey() let point = [UInt8](try signer.publicKeyX963()) // Act let der = try CertificateSigningRequest.der( subjectCommonName: "web-terminal-device", signer: signer ) let bytes = [UInt8](der) // certificationRequestInfo → { version, subject, subjectPKInfo, [0] attrs } let outer = try #require(TestDER.read(bytes, at: 0)) let info = TestDER.children(bytes, outer)[0] let infoChildren = TestDER.children(bytes, info) #expect(infoChildren.count == 4) #expect(Array(bytes[infoChildren[0].start.. SecKey? { let attributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClass as String: kSecAttrKeyClassPublic, kSecAttrKeySizeInBits as String: 256, ] return SecKeyCreateWithData(point as CFData, attributes as CFDictionary, nil) } /// A throwaway DER reader for assertions (production parsing lives in /// CertificateSummary's X509 walk; this mirrors it for tests). enum TestDER { struct Element { let tag: UInt8 let start: Int // index of the tag byte let valueStart: Int let valueEnd: Int var end: Int { valueEnd } } static func read(_ bytes: [UInt8], at start: Int) -> Element? { guard start >= 0, start + 1 < bytes.count else { return nil } let tag = bytes[start] var index = start + 1 let first = bytes[index] index += 1 var length = 0 if first & 0x80 == 0 { length = Int(first) } else { let count = Int(first & 0x7F) guard count > 0, count <= 4, index + count <= bytes.count else { return nil } for _ in 0.. [Element] { var elements: [Element] = [] var index = parent.valueStart while index < parent.valueEnd, let element = read(bytes, at: index) { elements.append(element) index = element.valueEnd } return elements } }