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 1 commit
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
80 changes: 80 additions & 0 deletions Sources/NIOSSL/SSLPKCS12Bundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bytes: Collection>(
certificates: [NIOSSLCertificate],
privateKey: NIOSSLPrivateKey,
passphrase: Bytes,
name: Bytes
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need to provide a name, and I'd argue we shouldn't bother. If we're going to enable this, it should be optional, and needs a different generic type parameter to the passphrase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll just get rid of it, don't think it serves any purpose really.

) 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

This gets the refcount wrong. We need to increase the refcount of the certificate pointer here, using X509_up_ref

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, the joys of manual memory management.
Should we decrease the refcount after we've created the PKCS12?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we're also missing the free of the sk_509. Done correctly, the free call is sk_X509_pop_free with the second argument as X509_free.

}
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<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
25 changes: 25 additions & 0 deletions Tests/NIOSSLTests/SSLPKCS12BundleTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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)
}
}