Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to create PKCS#12 #486

Merged
merged 7 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
44 changes: 44 additions & 0 deletions Tests/NIOSSLTests/SSLPKCS12BundleTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,48 @@ 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably try some non-matching passphrases.


// 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)
}
}
}
Loading