Skip to content

Commit

Permalink
Add API to create PKCS#12 (#486)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
gjcairo authored Oct 29, 2024
1 parent 8a6b89d commit c7e9542
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 0 deletions.
96 changes: 96 additions & 0 deletions Sources/NIOSSL/SSLPKCS12Bundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bytes: Collection>(
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<CChar>? = 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),
Expand Down
46 changes: 46 additions & 0 deletions Tests/NIOSSLTests/SSLPKCS12BundleTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

0 comments on commit c7e9542

Please sign in to comment.