-
Notifications
You must be signed in to change notification settings - Fork 139
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 support for PKCS#12 bundles. #23
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,222 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import CNIOOpenSSL | ||
import NIO | ||
|
||
|
||
/// A container of a single PKCS#12 bundle. | ||
/// | ||
/// PKCS#12 is a specification that defines an archive format for storing multiple | ||
/// cryptographic objects together in one file. Its most common usage, and the one | ||
/// that SwiftNIO is most interested in, is its use to bundle one or more X.509 | ||
/// certificates (`OpenSSLCertificate`) together with an associated private key | ||
/// (`OpenSSLPrivateKey`). | ||
/// | ||
/// ### Working with TLSConfiguration | ||
/// | ||
/// In many cases users will want to configure a `TLSConfiguration` with the data | ||
/// from a PKCS#12 bundle. This object assists in unpacking that bundle into its | ||
/// associated pieces. | ||
/// | ||
/// If you have a PKCS12 bundle, you configure a `TLSConfiguration` like this: | ||
/// | ||
/// let p12Bundle = OpenSSLPKCS12Bundle(file: pathToMyP12) | ||
/// let config = TLSConfiguration.forServer(certificateChain: p12Bundle.certificateChain, | ||
/// privateKey: p12Bundle.privateKey) | ||
/// | ||
/// The created `TLSConfiguration` can then be safely used for your endpoint. | ||
public struct OpenSSLPKCS12Bundle { | ||
public let certificateChain: [OpenSSLCertificate] | ||
public let privateKey: OpenSSLPrivateKey | ||
|
||
private init<Bytes: Collection>(ref: UnsafeMutablePointer<PKCS12>, passphrase: Bytes?) throws where Bytes.Element == UInt8 { | ||
var pkey: UnsafeMutablePointer<EVP_PKEY>? = nil | ||
var cert: UnsafeMutablePointer<X509>? = nil | ||
var caCerts: UnsafeMutablePointer<stack_st_X509>? = nil | ||
|
||
let rc = try passphrase.withSecureCString { passphrase in | ||
PKCS12_parse(ref, passphrase, &pkey, &cert, &caCerts) | ||
} | ||
guard rc == 1 else { | ||
throw OpenSSLError.unknownError(OpenSSLError.buildErrorStack()) | ||
} | ||
|
||
// Successfully parsed, let's unpack. The key and cert are mandatory, | ||
// the ca stack is not. | ||
guard let actualCert = cert, let actualKey = pkey else { | ||
// Free the pointers that we have. | ||
X509_free(cert) | ||
EVP_PKEY_free(pkey) | ||
CNIOOpenSSL_sk_X509_free(caCerts) | ||
throw NIOOpenSSLError.unableToAllocateOpenSSLObject | ||
} | ||
|
||
let certStackSize = caCerts.map(CNIOOpenSSL_sk_X509_num) ?? 0 | ||
var certs = [OpenSSLCertificate]() | ||
certs.reserveCapacity(Int(certStackSize) + 1) | ||
certs.append(OpenSSLCertificate.fromUnsafePointer(takingOwnership: actualCert)) | ||
|
||
for idx in 0..<certStackSize { | ||
guard let stackCertPtr = CNIOOpenSSL_sk_X509_value(caCerts, idx) else { | ||
preconditionFailure("Unable to get cert \(idx) from stack \(String(describing: caCerts))") | ||
} | ||
certs.append(OpenSSLCertificate.fromUnsafePointer(takingOwnership: stackCertPtr)) | ||
} | ||
|
||
self.certificateChain = certs | ||
self.privateKey = OpenSSLPrivateKey.fromUnsafePointer(takingOwnership: actualKey) | ||
|
||
} | ||
|
||
/// Create a `OpenSSLPKCS12Bundle` from the given bytes in memory, | ||
/// optionally decrypting the bundle with the given passphrase. | ||
/// | ||
/// - parameters: | ||
/// - buffer: The bytes of the PKCS#12 bundle. | ||
/// - passphrase: The passphrase used for the bundle, as a sequence of UTF-8 bytes. | ||
public init<Bytes: Collection>(buffer: [UInt8], passphrase: Bytes?) throws where Bytes.Element == UInt8 { | ||
guard openSSLIsInitialized else { fatalError("Failed to initialize OpenSSL") } | ||
|
||
let p12 = buffer.withUnsafeBytes { pointer -> UnsafeMutablePointer<PKCS12>? in | ||
let bio = BIO_new_mem_buf(UnsafeMutableRawPointer(mutating: pointer.baseAddress), CInt(pointer.count))! | ||
defer { | ||
BIO_free(bio) | ||
} | ||
return d2i_PKCS12_bio(bio, nil) | ||
} | ||
defer { | ||
p12.map(PKCS12_free) | ||
} | ||
|
||
if let p12 = p12 { | ||
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. similar issues here: the lifetime management of 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. If |
||
try self.init(ref: p12, passphrase: passphrase) | ||
} else { | ||
throw OpenSSLError.unknownError(OpenSSLError.buildErrorStack()) | ||
} | ||
} | ||
|
||
/// Create a `OpenSSLPKCS12Bundle` from the given bytes on disk, | ||
/// optionally decrypting the bundle with the given passphrase. | ||
/// | ||
/// - parameters: | ||
/// - file: The path to the PKCS#12 bundle on disk. | ||
/// - passphrase: The passphrase used for the bundle, as a sequence of UTF-8 bytes. | ||
public init<Bytes: Collection>(file: String, passphrase: Bytes?) throws where Bytes.Element == UInt8 { | ||
guard openSSLIsInitialized else { fatalError("Failed to initialize OpenSSL") } | ||
|
||
guard let fileObject = fopen(file, "rb") else { | ||
throw NIOOpenSSLError.noSuchFilesystemObject | ||
} | ||
defer { | ||
fclose(fileObject) | ||
} | ||
|
||
let p12 = d2i_PKCS12_fp(fileObject, nil) | ||
defer { | ||
p12.map(PKCS12_free) | ||
} | ||
|
||
if let p12 = p12 { | ||
try self.init(ref: p12, passphrase: passphrase) | ||
} else { | ||
throw OpenSSLError.unknownError(OpenSSLError.buildErrorStack()) | ||
} | ||
} | ||
|
||
/// Create a `OpenSSLPKCS12Bundle` from the given bytes on disk, | ||
/// assuming it has no passphrase. | ||
/// | ||
/// If the bundle does have a passphrase, call `init(file:passphrase:)` instead. | ||
/// | ||
/// - parameters: | ||
/// - file: The path to the PKCS#12 bundle on disk. | ||
public init(file: String) throws { | ||
try self.init(file: file, passphrase: Optional<[UInt8]>.none) | ||
} | ||
|
||
/// Create a `OpenSSLPKCS12Bundle` from the given bytes in memory, | ||
/// assuming it has no passphrase. | ||
/// | ||
/// If the bundle does have a passphrase, call `init(buffer:passphrase:)` instead. | ||
/// | ||
/// - parameters: | ||
/// - buffer: The bytes of the PKCS#12 bundle. | ||
public init(buffer: [UInt8]) throws { | ||
try self.init(buffer: buffer, passphrase: Optional<[UInt8]>.none) | ||
} | ||
} | ||
|
||
|
||
internal 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), | ||
/// and which will be scrubbed and freed after use, and which is null-terminated. | ||
/// | ||
/// This method should be used when it is necessary to take a secure copy of a collection of | ||
/// bytes. Its implementation relies on OpenSSL directly. | ||
internal func withSecureCString<T>(_ block: (UnsafePointer<Int8>) throws -> T) throws -> T { | ||
// We need to allocate some memory and prevent it being swapped to disk while we use it. | ||
// For that reason we use mlock. | ||
// Note that here we use UnsafePointer and UnsafeBufferPointer. Ideally we'd just use | ||
// the buffer pointer, but 4.0 doesn't have the APIs we want: specifically, .allocate | ||
// and .withMemoryRebound(to:) are only added to the buffer pointers in 4.1. | ||
let bufferSize = Int(self.count) + 1 | ||
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. why does it allocate one larger and put a 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, sorry, I see what you mean. Yeah, that's a better name. |
||
let ptr = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize) | ||
let bufferPtr = UnsafeMutableBufferPointer(start: ptr, count: bufferSize) | ||
defer { | ||
ptr.deallocate() | ||
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. the memory needs to be |
||
} | ||
|
||
if mlock(bufferPtr.baseAddress!, bufferPtr.count) != 0 { | ||
throw IOError(errnoCode: errno, function: "mlock") | ||
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 isn't guaranteed to give you the right 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. No, 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. ok, as of our chat a minute ago: this should then have its own 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. I agree, but are you open to me doing this as a follow-up PR? There are a few other sys calls in the package, I'd like to fix it all in one go. 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. @Lukasa yes! |
||
} | ||
defer { | ||
// If munlock fails take out the process. | ||
let rc = munlock(bufferPtr.baseAddress!, bufferPtr.count) | ||
precondition(rc == 0, "munlock failed with \(rc), errno \(errno)") | ||
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. also should go to 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. Same note. 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. here too, because as you pointed our: |
||
} | ||
|
||
let (_, nextIndex) = bufferPtr.initialize(from: self) | ||
assert(nextIndex == (bufferPtr.endIndex - 1)) | ||
|
||
// Add a null terminator. | ||
bufferPtr.baseAddress!.advanced(by: Int(self.count)).initialize(to: 0) | ||
|
||
defer { | ||
// We use OPENSSL_cleanse here because the compiler can't optimize this away. | ||
// .initialize(repeating: 0) can be, and empirically is, optimized away, bzero | ||
// is deprecated, memset_s is not well supported cross-platform, and memset-to-zero | ||
// is famously easily optimised away. This is our best bet. | ||
OPENSSL_cleanse(bufferPtr.baseAddress!, bufferPtr.count) | ||
ptr.deinitialize(count: bufferPtr.count) | ||
} | ||
|
||
// Ok, the memory is ready for use. Call the user. | ||
return try ptr.withMemoryRebound(to: Int8.self, capacity: bufferSize) { | ||
try block($0) | ||
} | ||
} | ||
} | ||
|
||
|
||
internal extension Optional where Wrapped: Collection, Wrapped.Element == UInt8 { | ||
internal func withSecureCString<T>(_ block: (UnsafePointer<Int8>?) throws -> T) throws -> T { | ||
if let `self` = self { | ||
return try self.withSecureCString(block) | ||
} else { | ||
return try block(nil) | ||
} | ||
} | ||
} |
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.
so
bio
can be freed before the returned value?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.
Yes. The BIO is used to read the data in, but that's it: the PKCS12 object holds a structured version of the data.