From b88bc5062d99aa739e4be02ff0c90c0d4e690f3b Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 24 Oct 2024 09:55:03 +0100 Subject: [PATCH 1/5] Add API to create PKCS#12 --- Sources/NIOSSL/SSLPKCS12Bundle.swift | 80 +++++++++++++++++++++ Tests/NIOSSLTests/SSLPKCS12BundleTest.swift | 25 +++++++ 2 files changed, 105 insertions(+) diff --git a/Sources/NIOSSL/SSLPKCS12Bundle.swift b/Sources/NIOSSL/SSLPKCS12Bundle.swift index af4859f10..174acb232 100644 --- a/Sources/NIOSSL/SSLPKCS12Bundle.swift +++ b/Sources/NIOSSL/SSLPKCS12Bundle.swift @@ -158,6 +158,86 @@ public struct NIOSSLPKCS12Bundle: Hashable { extension NIOSSLPKCS12Bundle: Sendable {} +extension NIOSSLPKCS12Bundle { + /// Create a PKCS#12 file containing the given certificates. + /// + /// The first certificate of the `certificates` array will be considered the "primary" certificate for + /// this PKCS#12, and `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: + /// - certificates: An array of certificates to include in this PKCS#12. Must contain at least one certificate. + /// - privateKey: The private key associated to the first certificate in `certificates`. + /// - passphrase: The password with which to protect this PKCS#12 file. + /// - name: The name to give this PKCS#12 file. + /// - Returns: An array of bytes making up the PKCS#12 file. + public static func makePKCS12( + certificates: [NIOSSLCertificate], + privateKey: NIOSSLPrivateKey, + passphrase: Bytes, + name: Bytes + ) throws -> [UInt8] where Bytes.Element == UInt8 { + guard let mainCertificate = certificates.first else { + preconditionFailure("At least one certificate must be provided") + } + + let certificateChainStack = CNIOBoringSSL_sk_X509_new(nil) + for additionalCertificate in certificates.dropFirst() { + let result = additionalCertificate.withUnsafeMutableX509Pointer { certificate in + CNIOBoringSSL_sk_X509_push(certificateChainStack, certificate) + } + if result == 0 { + fatalError("Failed to add certificate to chain") + } + } + + let pkcs12 = try passphrase.withSecureCString { passphrase in + try name.withSecureCString { name in + privateKey.withUnsafeMutableEVPPKEYPointer { privateKey in + mainCertificate.withUnsafeMutableX509Pointer { certificate in + CNIOBoringSSL_PKCS12_create( + passphrase, + name, + privateKey, + certificate, + certificateChainStack, + 0, + 0, + 0, + 0, + 0 + ) + } + } + } + } + + 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 12803f9ca..a40959e26 100644 --- a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift +++ b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift @@ -514,5 +514,30 @@ 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 pkcs12 = try NIOSSLPKCS12Bundle.makePKCS12( + certificates: certificates, + privateKey: privateKey, + passphrase: "thisisagreatpassword".utf8, + name: "thisisagreatname".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) + } } From c51a1d2296667561342ab4152f197ecea8a01496 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 28 Oct 2024 21:29:51 +0000 Subject: [PATCH 2/5] PR changes --- Sources/NIOSSL/SSLPKCS12Bundle.swift | 77 ++++++++++++--------- Tests/NIOSSLTests/SSLPKCS12BundleTest.swift | 9 ++- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/Sources/NIOSSL/SSLPKCS12Bundle.swift b/Sources/NIOSSL/SSLPKCS12Bundle.swift index 174acb232..c70e8c3db 100644 --- a/Sources/NIOSSL/SSLPKCS12Bundle.swift +++ b/Sources/NIOSSL/SSLPKCS12Bundle.swift @@ -159,60 +159,76 @@ public struct NIOSSLPKCS12Bundle: Hashable { extension NIOSSLPKCS12Bundle: Sendable {} extension NIOSSLPKCS12Bundle { - /// Create a PKCS#12 file containing the given certificates. + /// 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. /// - /// The first certificate of the `certificates` array will be considered the "primary" certificate for - /// this PKCS#12, and `privateKey` must be its corresponding private key. + /// - 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: - /// - certificates: An array of certificates to include in this PKCS#12. Must contain at least one certificate. - /// - privateKey: The private key associated to the first certificate in `certificates`. /// - passphrase: The password with which to protect this PKCS#12 file. - /// - name: The name to give this PKCS#12 file. /// - Returns: An array of bytes making up the PKCS#12 file. - public static func makePKCS12( - certificates: [NIOSSLCertificate], - privateKey: NIOSSLPrivateKey, - passphrase: Bytes, - name: Bytes + public func serialize( + passphrase: Bytes ) throws -> [UInt8] where Bytes.Element == UInt8 { - guard let mainCertificate = certificates.first else { + guard let mainCertificate = self.certificateChain.first else { preconditionFailure("At least one certificate must be provided") } let certificateChainStack = CNIOBoringSSL_sk_X509_new(nil) - for additionalCertificate in certificates.dropFirst() { + for additionalCertificate in self.certificateChain.dropFirst() { let result = additionalCertificate.withUnsafeMutableX509Pointer { certificate in - CNIOBoringSSL_sk_X509_push(certificateChainStack, certificate) + CNIOBoringSSL_X509_up_ref(certificate) + return CNIOBoringSSL_sk_X509_push(certificateChainStack, certificate) } if result == 0 { fatalError("Failed to add certificate to chain") } } + defer { + CNIOBoringSSL_sk_X509_pop_free(certificateChainStack, CNIOBoringSSL_X509_free) + } + let pkcs12 = try passphrase.withSecureCString { passphrase in - try name.withSecureCString { name in - privateKey.withUnsafeMutableEVPPKEYPointer { privateKey in - mainCertificate.withUnsafeMutableX509Pointer { certificate in - CNIOBoringSSL_PKCS12_create( - passphrase, - name, - privateKey, - certificate, - certificateChainStack, - 0, - 0, - 0, - 0, - 0 - ) - } + 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") } @@ -229,7 +245,6 @@ extension NIOSSLPKCS12Bundle { 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") } diff --git a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift index a40959e26..e0587ab09 100644 --- a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift +++ b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift @@ -525,12 +525,11 @@ class SSLPKCS12BundleTest: XCTestCase { let certificates = [mainCert, caOne, caTwo, caThree, caFour] // Create a PKCS#12... - let pkcs12 = try NIOSSLPKCS12Bundle.makePKCS12( - certificates: certificates, - privateKey: privateKey, - passphrase: "thisisagreatpassword".utf8, - name: "thisisagreatname".utf8 + 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) From 2ea858fa92efcb46e96b40a6ea0d4d3e637cfeb0 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 29 Oct 2024 10:02:14 +0000 Subject: [PATCH 3/5] Move defer block to free X509 stack right after allocating --- Sources/NIOSSL/SSLPKCS12Bundle.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/NIOSSL/SSLPKCS12Bundle.swift b/Sources/NIOSSL/SSLPKCS12Bundle.swift index c70e8c3db..14e1f4c30 100644 --- a/Sources/NIOSSL/SSLPKCS12Bundle.swift +++ b/Sources/NIOSSL/SSLPKCS12Bundle.swift @@ -192,6 +192,11 @@ extension NIOSSLPKCS12Bundle { } 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) @@ -202,10 +207,6 @@ extension NIOSSLPKCS12Bundle { } } - defer { - CNIOBoringSSL_sk_X509_pop_free(certificateChainStack, CNIOBoringSSL_X509_free) - } - let pkcs12 = try passphrase.withSecureCString { passphrase in privateKey.withUnsafeMutableEVPPKEYPointer { privateKey in mainCertificate.withUnsafeMutableX509Pointer { certificate in From b93c1c9edb1fe33a1cfc3668436c5a9881745d1a Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 29 Oct 2024 11:15:13 +0000 Subject: [PATCH 4/5] Add test --- Tests/NIOSSLTests/SSLPKCS12BundleTest.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift index cd52f604b..4571487e5 100644 --- a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift +++ b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift @@ -551,4 +551,24 @@ class SSLPKCS12BundleTest: XCTestCase { 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) + } + } } From b3aae21c00f6f973a996a724cbd37823af96e93e Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 29 Oct 2024 14:19:37 +0000 Subject: [PATCH 5/5] Formatting --- Tests/NIOSSLTests/SSLPKCS12BundleTest.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift index 4571487e5..064efc4be 100644 --- a/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift +++ b/Tests/NIOSSLTests/SSLPKCS12BundleTest.swift @@ -564,10 +564,12 @@ class SSLPKCS12BundleTest: XCTestCase { 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 + XCTAssertThrowsError( + try NIOSSLPKCS12Bundle( + buffer: pkcs12, + passphrase: "thisisagreatpasswordbutnottherightone".utf8 + ) + ) { error in XCTAssertNotNil(error as? BoringSSLError) } }