From c7e95421334b1068490b5d41314a50e70bab23d1 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 29 Oct 2024 14:23:36 +0000 Subject: [PATCH] Add API to create PKCS#12 (#486) ## Motivation It would be handy to provide an API to create PKCS#12 files from a list of `NIOSSLCertificates` and a `NIOSSLPrivateKey`. This would be particularly useful when dealing with Network.framework/NIOTransportServices/Security.framework, which use `SecIdentity`s for SSL. Two particular use cases are https://github.com/apple/swift-nio-ssl/issues/484#issuecomment-2410375188 and `grpc-swift-nio-transport`, which would use this API for testing the NIOTS transport implementation. ## Modifications This PR adds a static method to `NIOSSLPKCS12Bundle` that creates a PKCS#12 file from the given array of certificates + private key, and returns it as an array of bytes. ## Result PKCS#12 files can be created using NIOSSL. --- Sources/NIOSSL/SSLPKCS12Bundle.swift | 96 +++++++++++++++++++++ Tests/NIOSSLTests/SSLPKCS12BundleTest.swift | 46 ++++++++++ 2 files changed, 142 insertions(+) diff --git a/Sources/NIOSSL/SSLPKCS12Bundle.swift b/Sources/NIOSSL/SSLPKCS12Bundle.swift index 524f9be8..f4eece87 100644 --- a/Sources/NIOSSL/SSLPKCS12Bundle.swift +++ b/Sources/NIOSSL/SSLPKCS12Bundle.swift @@ -157,6 +157,102 @@ public struct NIOSSLPKCS12Bundle: Hashable { extension NIOSSLPKCS12Bundle: Sendable {} +extension NIOSSLPKCS12Bundle { + /// Create a ``NIOSSLPKCS12Bundle`` from the given certificate chain and private key. + /// This constructor is particularly useful to create a new PKCS#12 file: + /// call ``serialize(passphrase:)`` to get the bytes making up the file. + /// + /// - parameters: + /// - certificateChain: The chain of ``NIOSSLCertificate`` objects in the PKCS#12 bundle. + /// - privateKey: The ``NIOSSLPrivateKey`` object for the leaf certificate in the PKCS#12 bundle. + public init( + certificateChain: [NIOSSLCertificate], + privateKey: NIOSSLPrivateKey + ) { + self.certificateChain = certificateChain + self.privateKey = privateKey + } + + /// Serialize this bundle into a PKCS#12 file. + /// + /// The first certificate of the `certificateChain` array will be considered the "primary" certificate for + /// this PKCS#12, and the bundle's`privateKey` must be its corresponding private key. + /// The other certificates included in `certificates`, if any, will be considered as additional + /// certificates in the certificate chain. + /// + /// - Parameters: + /// - passphrase: The password with which to protect this PKCS#12 file. + /// - Returns: An array of bytes making up the PKCS#12 file. + public func serialize( + passphrase: Bytes + ) throws -> [UInt8] where Bytes.Element == UInt8 { + guard let mainCertificate = self.certificateChain.first else { + preconditionFailure("At least one certificate must be provided") + } + + let certificateChainStack = CNIOBoringSSL_sk_X509_new(nil) + + defer { + CNIOBoringSSL_sk_X509_pop_free(certificateChainStack, CNIOBoringSSL_X509_free) + } + + for additionalCertificate in self.certificateChain.dropFirst() { + let result = additionalCertificate.withUnsafeMutableX509Pointer { certificate in + CNIOBoringSSL_X509_up_ref(certificate) + return CNIOBoringSSL_sk_X509_push(certificateChainStack, certificate) + } + if result == 0 { + fatalError("Failed to add certificate to chain") + } + } + + let pkcs12 = try passphrase.withSecureCString { passphrase in + privateKey.withUnsafeMutableEVPPKEYPointer { privateKey in + mainCertificate.withUnsafeMutableX509Pointer { certificate in + CNIOBoringSSL_PKCS12_create( + passphrase, + nil, + privateKey, + certificate, + certificateChainStack, + 0, + 0, + 0, + 0, + 0 + ) + } + } + } + + defer { + CNIOBoringSSL_PKCS12_free(pkcs12) + } + + guard let bio = CNIOBoringSSL_BIO_new(CNIOBoringSSL_BIO_s_mem()) else { + fatalError("Failed to malloc for a BIO handler") + } + + defer { + CNIOBoringSSL_BIO_free(bio) + } + + let rc = CNIOBoringSSL_i2d_PKCS12_bio(bio, pkcs12) + guard rc == 1 else { + let errorStack = BoringSSLError.buildErrorStack() + throw BoringSSLError.unknownError(errorStack) + } + + var dataPtr: UnsafeMutablePointer? = nil + let length = CNIOBoringSSL_BIO_get_mem_data(bio, &dataPtr) + guard let bytes = dataPtr.map({ UnsafeMutableRawBufferPointer(start: $0, count: length) }) else { + fatalError("Failed to get bytes from private key") + } + + return Array(bytes) + } +} + extension Collection where Element == UInt8 { /// Provides a contiguous copy of the bytes of this collection in a heap-allocated /// memory region that is locked into memory (that is, which can never be backed by a file), diff --git a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift index 609b7c3a..064efc4b 100644 --- a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift +++ b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift @@ -527,4 +527,50 @@ class SSLPKCS12BundleTest: XCTestCase { XCTAssertTrue(set.contains(bundle1_b)) XCTAssertTrue(set.contains(bundle2)) } + + func testMakePKCS12() throws { + let privateKey = try NIOSSLPrivateKey(bytes: .init(samplePemKey.utf8), format: .pem) + let mainCert = try NIOSSLCertificate(bytes: .init(samplePemCert.utf8), format: .pem) + let caOne = try NIOSSLCertificate(bytes: .init(multiSanCert.utf8), format: .pem) + let caTwo = try NIOSSLCertificate(bytes: .init(multiCNCert.utf8), format: .pem) + let caThree = try NIOSSLCertificate(bytes: .init(noCNCert.utf8), format: .pem) + let caFour = try NIOSSLCertificate(bytes: .init(unicodeCNCert.utf8), format: .pem) + let certificates = [mainCert, caOne, caTwo, caThree, caFour] + + // Create a PKCS#12... + let bundle = NIOSSLPKCS12Bundle( + certificateChain: certificates, + privateKey: privateKey + ) + let pkcs12 = try bundle.serialize(passphrase: "thisisagreatpassword".utf8) + + // And then decode it into a NIOSSLPKCS12Bundle + let decoded = try NIOSSLPKCS12Bundle(buffer: pkcs12, passphrase: "thisisagreatpassword".utf8) + + // Make sure everything is there + XCTAssertEqual(decoded.privateKey, privateKey) + XCTAssertEqual(decoded.certificateChain, certificates) + } + + func testMakePKCS12_IncorrectPassphrase() throws { + let privateKey = try NIOSSLPrivateKey(bytes: .init(samplePemKey.utf8), format: .pem) + let mainCert = try NIOSSLCertificate(bytes: .init(samplePemCert.utf8), format: .pem) + + // Create a PKCS#12... + let bundle = NIOSSLPKCS12Bundle( + certificateChain: [mainCert], + privateKey: privateKey + ) + let pkcs12 = try bundle.serialize(passphrase: "thisisagreatpassword".utf8) + + // And then try decoding it into a NIOSSLPKCS12Bundle, but with the wrong passphrase + XCTAssertThrowsError( + try NIOSSLPKCS12Bundle( + buffer: pkcs12, + passphrase: "thisisagreatpasswordbutnottherightone".utf8 + ) + ) { error in + XCTAssertNotNil(error as? BoringSSLError) + } + } }