-
Notifications
You must be signed in to change notification settings - Fork 143
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
Changes from 1 commit
b88bc50
c51a1d2
2ea858f
185875a
b93c1c9
ef709ac
b3aae21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
) throws -> [UInt8] where Bytes.Element == UInt8 { | ||
Lukasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, the joys of manual memory management. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we're also missing the free of the |
||
} | ||
if result == 0 { | ||
fatalError("Failed to add certificate to chain") | ||
} | ||
} | ||
|
||
let pkcs12 = try passphrase.withSecureCString { passphrase in | ||
Lukasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.