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 support for PKCS#12 bundles. #23

Merged
merged 1 commit into from
Jul 12, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions Sources/CNIOOpenSSL/include/c_nio_openssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ static inline const GENERAL_NAME *CNIOOpenSSL_sk_GENERAL_NAME_value(STACK_OF(GEN
return sk_GENERAL_NAME_value(x, idx);
}

static inline int CNIOOpenSSL_sk_X509_num(STACK_OF(X509) *x) {
return sk_X509_num(x);
}

static inline const X509 *CNIOOpenSSL_sk_X509_value(STACK_OF(X509) *x, int idx) {
return sk_X509_value(x, idx);
}

static inline void CNIOOpenSSL_sk_X509_free(STACK_OF(X509) *x) {
return sk_X509_free(x);
}

static inline int CNIOOpenSSL_SSL_CTX_set_app_data(SSL_CTX *ctx, void *arg) {
return SSL_CTX_set_app_data(ctx, arg);
}
Expand Down
22 changes: 20 additions & 2 deletions Sources/NIOOpenSSL/SSLCertificate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public class OpenSSLCertificate {
///
/// Note that this method will only ever load the first certificate from a given file.
public convenience init (file: String, format: OpenSSLSerializationFormats) throws {
let fileObject = file.withCString { filePtr in
return fopen(filePtr, "rb")
guard let fileObject = fopen(file, "rb") else {
throw NIOOpenSSLError.noSuchFilesystemObject
}
defer {
fclose(fileObject)
Expand Down Expand Up @@ -103,7 +103,25 @@ public class OpenSSLCertificate {
///
/// In general, however, this function should be avoided in favour of one of the convenience
/// initializers, which ensure that the lifetime of the `X509` object is better-managed.
///
/// This function is deprecated in favor of `fromUnsafePointer(takingOwnership:)`, an identical function
/// that more accurately communicates its behaviour.
@available(*, deprecated, renamed: "fromUnsafePointer(takingOwnership:)")
static public func fromUnsafePointer(pointer: UnsafePointer<X509>) -> OpenSSLCertificate {
return OpenSSLCertificate.fromUnsafePointer(takingOwnership: pointer)
}

/// Create an OpenSSLCertificate wrapping a pointer into OpenSSL.
///
/// This is a function that should be avoided as much as possible because it plays poorly with
/// OpenSSL's reference-counted memory. This function does not increment the reference count for the `X509`
/// object here, nor does it duplicate it: it just takes ownership of the copy here. This object
/// **will** deallocate the underlying `X509` object when deinited, and so if you need to keep that
/// `X509` object alive you should call `X509_dup` before passing the pointer here.
///
/// In general, however, this function should be avoided in favour of one of the convenience
/// initializers, which ensure that the lifetime of the `X509` object is better-managed.
static public func fromUnsafePointer(takingOwnership pointer: UnsafePointer<X509>) -> OpenSSLCertificate {
return OpenSSLCertificate(withReference: UnsafeMutablePointer(mutating: pointer))
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/NIOOpenSSL/SSLConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,6 @@ internal final class SSLConnection {
return nil
}

return OpenSSLCertificate.fromUnsafePointer(pointer: certPtr)
return OpenSSLCertificate.fromUnsafePointer(takingOwnership: certPtr)
}
}
222 changes: 222 additions & 0 deletions Sources/NIOOpenSSL/SSLPKCS12Bundle.swift
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)
Copy link
Member

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?

Copy link
Contributor Author

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.

}
return d2i_PKCS12_bio(bio, nil)
}
defer {
p12.map(PKCS12_free)
}

if let p12 = p12 {
Copy link
Member

Choose a reason for hiding this comment

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

similar issues here: the lifetime management of p12 is unclear to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If p12 is nil then we have no lifecycle to manage: there is no object here to free, only the BIO, which was freed once we exited the block above.

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
Copy link
Member

Choose a reason for hiding this comment

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

why does it allocate one larger and put a NULL in there? If that's necessary it should read withSecureCString or something maybe?

Copy link
Contributor Author

@Lukasa Lukasa Jul 10, 2018

Choose a reason for hiding this comment

The 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()
Copy link
Member

Choose a reason for hiding this comment

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

the memory needs to be deinitialized too before deallocate

}

if mlock(bufferPtr.baseAddress!, bufferPtr.count) != 0 {
throw IOError(errnoCode: errno, function: "mlock")
Copy link
Member

Choose a reason for hiding this comment

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

this isn't guaranteed to give you the right errno. Can we move into the Posix module?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, Posix is internal to NIO. Why isn't it guaranteed to give me the right errno?

Copy link
Member

Choose a reason for hiding this comment

The 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 Posix wrapper following NIO's pattern

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 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.

Copy link
Member

Choose a reason for hiding this comment

The 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)")
Copy link
Member

Choose a reason for hiding this comment

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

also should go to Posix

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same note.

Copy link
Member

Choose a reason for hiding this comment

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

here too, because as you pointed our: EINTR could be a real issue here ...

}

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)
}
}
}
26 changes: 22 additions & 4 deletions Sources/NIOOpenSSL/SSLPrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ public class OpenSSLPrivateKey {

/// A delegating initializer for `init(file:format:passphraseCallback)` and `init(file:format:)`.
private convenience init(file: String, format: OpenSSLSerializationFormats, callbackManager: CallbackManagerProtocol?) throws {
let fileObject = file.withCString { filePtr in
return fopen(filePtr, "rb")
guard let fileObject = fopen(file, "rb") else {
throw NIOOpenSSLError.noSuchFilesystemObject
}
defer {
fclose(fileObject)
Expand Down Expand Up @@ -235,11 +235,29 @@ public class OpenSSLPrivateKey {
/// OpenSSL's reference-counted memory. This function does not increment the reference count for the EVP_PKEY
/// object here, nor does it duplicate it: it just takes ownership of the copy here. This object
/// **will** deallocate the underlying EVP_PKEY object when deinited, and so if you need to keep that
/// EVP_PKEY object alive you should call X509_dup before passing the pointer here.
/// EVP_PKEY object alive you create a new EVP_PKEY before passing that object here.
///
/// In general, however, this function should be avoided in favour of one of the convenience
/// initializers, which ensure that the lifetime of the X509 object is better-managed.
/// initializers, which ensure that the lifetime of the EVP_PKEY object is better-managed.
///
/// This function is deprecated in favor of `fromUnsafePointer(takingOwnership:)`, an identical function
/// that more accurately communicates its behaviour.
@available(*, deprecated, renamed: "fromUnsafePointer(takingOwnership:)")
static public func fromUnsafePointer(pointer: UnsafePointer<EVP_PKEY>) -> OpenSSLPrivateKey {
return OpenSSLPrivateKey.fromUnsafePointer(takingOwnership: pointer)
}

/// Create an OpenSSLPrivateKey wrapping a pointer into OpenSSL.
///
/// This is a function that should be avoided as much as possible because it plays poorly with
/// OpenSSL's reference-counted memory. This function does not increment the reference count for the EVP_PKEY
/// object here, nor does it duplicate it: it just takes ownership of the copy here. This object
/// **will** deallocate the underlying EVP_PKEY object when deinited, and so if you need to keep that
/// EVP_PKEY object alive you create a new EVP_PKEY before passing that object here.
///
/// In general, however, this function should be avoided in favour of one of the convenience
/// initializers, which ensure that the lifetime of the EVP_PKEY object is better-managed.
static public func fromUnsafePointer(takingOwnership pointer: UnsafePointer<EVP_PKEY>) -> OpenSSLPrivateKey {
return OpenSSLPrivateKey(withReference: UnsafeMutablePointer(mutating: pointer))
}

Expand Down
1 change: 1 addition & 0 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import XCTest
testCase(OpenSSLALPNTest.allTests),
testCase(OpenSSLIntegrationTest.allTests),
testCase(SSLCertificateTest.allTests),
testCase(SSLPKCS12BundleTest.allTests),
testCase(SSLPrivateKeyTest.allTests),
testCase(TLSConfigurationTest.allTests),
])
Expand Down
2 changes: 1 addition & 1 deletion Tests/NIOOpenSSLTests/OpenSSLTestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,5 +325,5 @@ func generateSelfSignedCert() -> (OpenSSLCertificate, OpenSSLPrivateKey) {

X509_sign(x, pkey, EVP_sha256())

return (OpenSSLCertificate.fromUnsafePointer(pointer: x), OpenSSLPrivateKey.fromUnsafePointer(pointer: pkey))
return (OpenSSLCertificate.fromUnsafePointer(takingOwnership: x), OpenSSLPrivateKey.fromUnsafePointer(takingOwnership: pkey))
}
2 changes: 2 additions & 0 deletions Tests/NIOOpenSSLTests/SSLCertificateTest+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ extension SSLCertificateTest {
("testLoadingGibberishFromMemoryAsDerFails", testLoadingGibberishFromMemoryAsDerFails),
("testLoadingGibberishFromFileAsPemFails", testLoadingGibberishFromFileAsPemFails),
("testLoadingGibberishFromFileAsDerFails", testLoadingGibberishFromFileAsDerFails),
("testLoadingNonexistentFileAsPem", testLoadingNonexistentFileAsPem),
("testLoadingNonexistentFileAsDer", testLoadingNonexistentFileAsDer),
("testEnumeratingSanFields", testEnumeratingSanFields),
("testNonexistentSan", testNonexistentSan),
("testCommonName", testCommonName),
Expand Down
22 changes: 22 additions & 0 deletions Tests/NIOOpenSSLTests/SSLCertificateTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,28 @@ class SSLCertificateTest: XCTestCase {
}
}

func testLoadingNonexistentFileAsPem() throws {
do {
_ = try OpenSSLCertificate(file: "/nonexistent/path", format: .pem)
XCTFail("Did not throw")
} catch NIOOpenSSLError.noSuchFilesystemObject {
// ok
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testLoadingNonexistentFileAsDer() throws {
do {
_ = try OpenSSLCertificate(file: "/nonexistent/path", format: .der)
XCTFail("Did not throw")
} catch NIOOpenSSLError.noSuchFilesystemObject {
// ok
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func testEnumeratingSanFields() throws {
var v4addr = in_addr()
var v6addr = in6_addr()
Expand Down
Loading