Skip to content

Commit c7e9542

Browse files
authored
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 #484 (comment) 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.
1 parent 8a6b89d commit c7e9542

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

Sources/NIOSSL/SSLPKCS12Bundle.swift

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,102 @@ public struct NIOSSLPKCS12Bundle: Hashable {
157157

158158
extension NIOSSLPKCS12Bundle: Sendable {}
159159

160+
extension NIOSSLPKCS12Bundle {
161+
/// Create a ``NIOSSLPKCS12Bundle`` from the given certificate chain and private key.
162+
/// This constructor is particularly useful to create a new PKCS#12 file:
163+
/// call ``serialize(passphrase:)`` to get the bytes making up the file.
164+
///
165+
/// - parameters:
166+
/// - certificateChain: The chain of ``NIOSSLCertificate`` objects in the PKCS#12 bundle.
167+
/// - privateKey: The ``NIOSSLPrivateKey`` object for the leaf certificate in the PKCS#12 bundle.
168+
public init(
169+
certificateChain: [NIOSSLCertificate],
170+
privateKey: NIOSSLPrivateKey
171+
) {
172+
self.certificateChain = certificateChain
173+
self.privateKey = privateKey
174+
}
175+
176+
/// Serialize this bundle into a PKCS#12 file.
177+
///
178+
/// The first certificate of the `certificateChain` array will be considered the "primary" certificate for
179+
/// this PKCS#12, and the bundle's`privateKey` must be its corresponding private key.
180+
/// The other certificates included in `certificates`, if any, will be considered as additional
181+
/// certificates in the certificate chain.
182+
///
183+
/// - Parameters:
184+
/// - passphrase: The password with which to protect this PKCS#12 file.
185+
/// - Returns: An array of bytes making up the PKCS#12 file.
186+
public func serialize<Bytes: Collection>(
187+
passphrase: Bytes
188+
) throws -> [UInt8] where Bytes.Element == UInt8 {
189+
guard let mainCertificate = self.certificateChain.first else {
190+
preconditionFailure("At least one certificate must be provided")
191+
}
192+
193+
let certificateChainStack = CNIOBoringSSL_sk_X509_new(nil)
194+
195+
defer {
196+
CNIOBoringSSL_sk_X509_pop_free(certificateChainStack, CNIOBoringSSL_X509_free)
197+
}
198+
199+
for additionalCertificate in self.certificateChain.dropFirst() {
200+
let result = additionalCertificate.withUnsafeMutableX509Pointer { certificate in
201+
CNIOBoringSSL_X509_up_ref(certificate)
202+
return CNIOBoringSSL_sk_X509_push(certificateChainStack, certificate)
203+
}
204+
if result == 0 {
205+
fatalError("Failed to add certificate to chain")
206+
}
207+
}
208+
209+
let pkcs12 = try passphrase.withSecureCString { passphrase in
210+
privateKey.withUnsafeMutableEVPPKEYPointer { privateKey in
211+
mainCertificate.withUnsafeMutableX509Pointer { certificate in
212+
CNIOBoringSSL_PKCS12_create(
213+
passphrase,
214+
nil,
215+
privateKey,
216+
certificate,
217+
certificateChainStack,
218+
0,
219+
0,
220+
0,
221+
0,
222+
0
223+
)
224+
}
225+
}
226+
}
227+
228+
defer {
229+
CNIOBoringSSL_PKCS12_free(pkcs12)
230+
}
231+
232+
guard let bio = CNIOBoringSSL_BIO_new(CNIOBoringSSL_BIO_s_mem()) else {
233+
fatalError("Failed to malloc for a BIO handler")
234+
}
235+
236+
defer {
237+
CNIOBoringSSL_BIO_free(bio)
238+
}
239+
240+
let rc = CNIOBoringSSL_i2d_PKCS12_bio(bio, pkcs12)
241+
guard rc == 1 else {
242+
let errorStack = BoringSSLError.buildErrorStack()
243+
throw BoringSSLError.unknownError(errorStack)
244+
}
245+
246+
var dataPtr: UnsafeMutablePointer<CChar>? = nil
247+
let length = CNIOBoringSSL_BIO_get_mem_data(bio, &dataPtr)
248+
guard let bytes = dataPtr.map({ UnsafeMutableRawBufferPointer(start: $0, count: length) }) else {
249+
fatalError("Failed to get bytes from private key")
250+
}
251+
252+
return Array(bytes)
253+
}
254+
}
255+
160256
extension Collection where Element == UInt8 {
161257
/// Provides a contiguous copy of the bytes of this collection in a heap-allocated
162258
/// memory region that is locked into memory (that is, which can never be backed by a file),

Tests/NIOSSLTests/SSLPKCS12BundleTest.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,4 +527,50 @@ class SSLPKCS12BundleTest: XCTestCase {
527527
XCTAssertTrue(set.contains(bundle1_b))
528528
XCTAssertTrue(set.contains(bundle2))
529529
}
530+
531+
func testMakePKCS12() throws {
532+
let privateKey = try NIOSSLPrivateKey(bytes: .init(samplePemKey.utf8), format: .pem)
533+
let mainCert = try NIOSSLCertificate(bytes: .init(samplePemCert.utf8), format: .pem)
534+
let caOne = try NIOSSLCertificate(bytes: .init(multiSanCert.utf8), format: .pem)
535+
let caTwo = try NIOSSLCertificate(bytes: .init(multiCNCert.utf8), format: .pem)
536+
let caThree = try NIOSSLCertificate(bytes: .init(noCNCert.utf8), format: .pem)
537+
let caFour = try NIOSSLCertificate(bytes: .init(unicodeCNCert.utf8), format: .pem)
538+
let certificates = [mainCert, caOne, caTwo, caThree, caFour]
539+
540+
// Create a PKCS#12...
541+
let bundle = NIOSSLPKCS12Bundle(
542+
certificateChain: certificates,
543+
privateKey: privateKey
544+
)
545+
let pkcs12 = try bundle.serialize(passphrase: "thisisagreatpassword".utf8)
546+
547+
// And then decode it into a NIOSSLPKCS12Bundle
548+
let decoded = try NIOSSLPKCS12Bundle(buffer: pkcs12, passphrase: "thisisagreatpassword".utf8)
549+
550+
// Make sure everything is there
551+
XCTAssertEqual(decoded.privateKey, privateKey)
552+
XCTAssertEqual(decoded.certificateChain, certificates)
553+
}
554+
555+
func testMakePKCS12_IncorrectPassphrase() throws {
556+
let privateKey = try NIOSSLPrivateKey(bytes: .init(samplePemKey.utf8), format: .pem)
557+
let mainCert = try NIOSSLCertificate(bytes: .init(samplePemCert.utf8), format: .pem)
558+
559+
// Create a PKCS#12...
560+
let bundle = NIOSSLPKCS12Bundle(
561+
certificateChain: [mainCert],
562+
privateKey: privateKey
563+
)
564+
let pkcs12 = try bundle.serialize(passphrase: "thisisagreatpassword".utf8)
565+
566+
// And then try decoding it into a NIOSSLPKCS12Bundle, but with the wrong passphrase
567+
XCTAssertThrowsError(
568+
try NIOSSLPKCS12Bundle(
569+
buffer: pkcs12,
570+
passphrase: "thisisagreatpasswordbutnottherightone".utf8
571+
)
572+
) { error in
573+
XCTAssertNotNil(error as? BoringSSLError)
574+
}
575+
}
530576
}

0 commit comments

Comments
 (0)