File: SSLPrivateKey.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (345 lines) | stat: -rw-r--r-- 17,059 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#if compiler(>=5.1)
@_implementationOnly import CNIOBoringSSL
#else
import CNIOBoringSSL
#endif

/// An `NIOSSLPassphraseCallback` is a callback that will be invoked by NIOSSL when it needs to
/// get access to a private key that is stored in encrypted form.
///
/// This callback will be invoked with one argument, a non-escaping closure that must be called with the
/// passphrase. Failing to call the closure will cause decryption to fail.
///
/// The reason this design has been used is to allow you to secure any memory storing the passphrase after
/// use. We guarantee that after the `NIOSSLPassphraseSetter` closure has been invoked the `Collection`
/// you have passed in will no longer be needed by BoringSSL, and so you can safely destroy any memory it
/// may be using if you need to.
public typealias NIOSSLPassphraseCallback<Bytes: Collection> = (NIOSSLPassphraseSetter<Bytes>) throws -> Void where Bytes.Element == UInt8


/// An `NIOSSLPassphraseSetter` is a closure that you must invoke to provide a passphrase to BoringSSL.
/// It will be provided to you when your `NIOSSLPassphraseCallback` is invoked.
public typealias NIOSSLPassphraseSetter<Bytes: Collection> = (Bytes) -> Void where Bytes.Element == UInt8


/// An internal protocol that exists to let us avoid problems with generic types.
///
/// The issue we have here is that we want to allow users to use whatever collection type suits them best to set
/// the passphrase. For this reason, `NIOSSLPassphraseSetter` is a generic function, generic over the `Collection`
/// protocol. However, that causes us an issue, because we need to stuff that callback into an
/// `BoringSSLPassphraseCallbackManager` in order to create an `Unmanaged` and round-trip the pointer through C code.
///
/// That makes `BoringSSLPassphraseCallbackManager` a generic object, and now we're in *real* trouble, becuase
/// `Unmanaged` requires us to specify the *complete* type of the object we want to unwrap. In this case, we
/// don't know it, because it's generic!
///
/// Our way out is to note that while the class itself is generic, the only function we want to call in the
/// `globalBoringSSLPassphraseCallback` is not. Thus, rather than try to hold the actual specific `BoringSSLPassphraseManager`,
/// we can hold it inside a protocol existential instead, so long as that protocol existential gives us the correct
/// function to call. Hence: `CallbackManagerProtocol`, a private protocol with a single conforming type.
internal protocol CallbackManagerProtocol: AnyObject {
    func invoke(buffer: UnsafeMutableBufferPointer<CChar>) -> CInt
}


/// This class exists primarily to work around the fact that Swift does not let us stuff
/// a closure into an `Unmanaged`. Instead, we use this object to keep hold of it.
final class BoringSSLPassphraseCallbackManager<Bytes: Collection>: CallbackManagerProtocol where Bytes.Element == UInt8 {
    private let userCallback: NIOSSLPassphraseCallback<Bytes>

    init(userCallback: @escaping NIOSSLPassphraseCallback<Bytes>){
        // We have to type-erase this.
        self.userCallback = userCallback
    }

    func invoke(buffer: UnsafeMutableBufferPointer<CChar>) -> CInt {
        var count: CInt = 0

        do {
            try self.userCallback { passphraseBytes in
                // If we don't have enough space for the passphrase plus NUL, bail out.
                guard passphraseBytes.count < buffer.count else { return }
                _ = buffer.initialize(from: passphraseBytes.lazy.map { CChar($0) })
                count = CInt(passphraseBytes.count)

                // We need to add a NUL terminator, in case the user did not.
                buffer[Int(passphraseBytes.count)] = 0
            }
        } catch {
            // If we hit an error here, we just need to tolerate it. We'll return zero-length.
            count = 0
        }

        return count
    }
}


/// Our global static BoringSSL passphrase callback. This is used as a thunk to dispatch out to
/// the user-provided callback.
func globalBoringSSLPassphraseCallback(buf: UnsafeMutablePointer<CChar>?,
                                       size: CInt,
                                       rwflag: CInt,
                                       u: UnsafeMutableRawPointer?) -> CInt {
    guard let buffer = buf, let userData = u else {
        preconditionFailure("Invalid pointers passed to passphrase callback, buf: \(String(describing: buf)) u: \(String(describing: u))")
    }
    let bufferPointer = UnsafeMutableBufferPointer(start: buffer, count: Int(size))
    guard let cbManager = Unmanaged<AnyObject>.fromOpaque(userData).takeUnretainedValue() as? CallbackManagerProtocol else {
        preconditionFailure("Failed to pass object that can handle callback")
    }
    return cbManager.invoke(buffer: bufferPointer)
}


/// A reference to an BoringSSL private key object in the form of an `EVP_PKEY *`.
///
/// This thin wrapper class allows us to use ARC to automatically manage
/// the memory associated with this key. That ensures that BoringSSL
/// will not free the underlying buffer until we are done with the key.
///
/// This class also provides several convenience constructors that allow users
/// to obtain an in-memory representation of a key from a buffer of
/// bytes or from a file path.
public class NIOSSLPrivateKey {
    internal let _ref: UnsafeMutableRawPointer /*<EVP_PKEY>*/

    internal var ref: UnsafeMutablePointer<EVP_PKEY> {
        return self._ref.assumingMemoryBound(to: EVP_PKEY.self)
    }

    private init(withReference ref: UnsafeMutablePointer<EVP_PKEY>) {
        self._ref = UnsafeMutableRawPointer(ref) // erasing the type for @_implementationOnly import CNIOBoringSSL
    }

    /// A delegating initializer for `init(file:format:passphraseCallback)` and `init(file:format:)`.
    private convenience init(file: String, format: NIOSSLSerializationFormats, callbackManager: CallbackManagerProtocol?) throws {
        let fileObject = try Posix.fopen(file: file, mode: "rb")
        defer {
            // If fclose fails there is nothing we can do about it.
            _ = try? Posix.fclose(file: fileObject)
        }

        let key = withExtendedLifetime(callbackManager) { callbackManager -> UnsafeMutablePointer<EVP_PKEY>? in
            guard let bio = CNIOBoringSSL_BIO_new_fp(fileObject, BIO_NOCLOSE) else {
                return nil
            }
            defer {
                CNIOBoringSSL_BIO_free(bio)
            }

            switch format {
            case .pem:
                // This annoying conditional binding is used to work around the fact that I cannot pass
                // a variable to a function pointer argument.
                if let callbackManager = callbackManager {
                    return CNIOBoringSSL_PEM_read_PrivateKey(fileObject, nil, { globalBoringSSLPassphraseCallback(buf: $0, size: $1, rwflag: $2, u: $3) }, Unmanaged.passUnretained(callbackManager as AnyObject).toOpaque())
                } else {
                    return CNIOBoringSSL_PEM_read_PrivateKey(fileObject, nil, nil, nil)
                }
            case .der:
                return CNIOBoringSSL_d2i_PrivateKey_fp(fileObject, nil)
            }
        }

        if key == nil {
            throw NIOSSLError.failedToLoadPrivateKey
        }

        self.init(withReference: key!)
    }

    /// A delegating initializer for `init(buffer:format:passphraseCallback)` and `init(buffer:format:)`.
    private convenience init(bytes: [UInt8], format: NIOSSLSerializationFormats, callbackManager: CallbackManagerProtocol?) throws {
        let ref = bytes.withUnsafeBytes { (ptr) -> UnsafeMutablePointer<EVP_PKEY>? in
            let bio = CNIOBoringSSL_BIO_new_mem_buf(ptr.baseAddress!, CInt(ptr.count))!
            defer {
                CNIOBoringSSL_BIO_free(bio)
            }

            return withExtendedLifetime(callbackManager) { callbackManager -> UnsafeMutablePointer<EVP_PKEY>? in
                switch format {
                case .pem:
                    if let callbackManager = callbackManager {
                        // This annoying conditional binding is used to work around the fact that I cannot pass
                        // a variable to a function pointer argument.
                        return CNIOBoringSSL_PEM_read_bio_PrivateKey(bio, nil, { globalBoringSSLPassphraseCallback(buf: $0, size: $1, rwflag: $2, u: $3) }, Unmanaged.passUnretained(callbackManager as AnyObject).toOpaque())
                    } else {
                        return CNIOBoringSSL_PEM_read_bio_PrivateKey(bio, nil, nil, nil)
                    }
                case .der:
                    return CNIOBoringSSL_d2i_PrivateKey_bio(bio, nil)
                }
            }
        }

        if ref == nil {
            throw NIOSSLError.failedToLoadPrivateKey
        }

        self.init(withReference: ref!)
    }

    /// Create an NIOSSLPrivateKey from a file at a given path in either PEM or
    /// DER format, providing a passphrase callback.
    ///
    /// - parameters:
    ///     - file: The path to the file to load.
    ///     - format: The format of the key to load, either DER or PEM.
    public convenience init(file: String, format: NIOSSLSerializationFormats) throws {
        try self.init(file: file, format: format, callbackManager: nil)
    }

    /// Create an NIOSSLPrivateKey from a file at a given path in either PEM or
    /// DER format, providing a passphrase callback.
    ///
    /// - parameters:
    ///     - file: The path to the file to load.
    ///     - format: The format of the key to load, either DER or PEM.
    ///     - passphraseCallback: A callback to invoke to obtain the passphrase for
    ///         encrypted keys.
    public convenience init<T: Collection>(file: String, format: NIOSSLSerializationFormats, passphraseCallback: @escaping NIOSSLPassphraseCallback<T>) throws where T.Element == UInt8 {
        let manager = BoringSSLPassphraseCallbackManager(userCallback: passphraseCallback)
        try self.init(file: file, format: format, callbackManager: manager)
    }

    /// Create an NIOSSLPrivateKey from a buffer of bytes in either PEM or
    /// DER format.
    ///
    /// - parameters:
    ///     - buffer: The key bytes.
    ///     - format: The format of the key to load, either DER or PEM.
    /// - SeeAlso: `NIOSSLPrivateKey.init(bytes:format:)`
    @available(*, deprecated, renamed: "NIOSSLPrivateKey.init(bytes:format:)")
    public convenience init(buffer: [Int8], format: NIOSSLSerializationFormats) throws {
        try self.init(bytes: buffer.map(UInt8.init), format: format)
    }

    /// Create an NIOSSLPrivateKey from a buffer of bytes in either PEM or
    /// DER format.
    ///
    /// - parameters:
    ///     - bytes: The key bytes.
    ///     - format: The format of the key to load, either DER or PEM.
    public convenience init(bytes: [UInt8], format: NIOSSLSerializationFormats) throws {
        try self.init(bytes: bytes, format: format, callbackManager: nil)
    }

    /// Create an NIOSSLPrivateKey from a buffer of bytes in either PEM or
    /// DER format.
    ///
    /// - parameters:
    ///     - buffer: The key bytes.
    ///     - format: The format of the key to load, either DER or PEM.
    ///     - passphraseCallback: Optionally a callback to invoke to obtain the passphrase for
    ///         encrypted keys. If not provided, or set to `nil`, the default BoringSSL
    ///         behaviour will be used, which prints a prompt and requests the passphrase from
    ///         stdin.
    /// - SeeAlso: `NIOSSLPrivateKey.init(bytes:format:passphraseCallback:)`
    @available(*, deprecated, renamed: "NIOSSLPrivateKey.init(bytes:format:passphraseCallback:)")
    public convenience init<T: Collection>(buffer: [Int8], format: NIOSSLSerializationFormats, passphraseCallback: @escaping NIOSSLPassphraseCallback<T>) throws where T.Element == UInt8  {
        try self.init(bytes: buffer.map(UInt8.init), format: format, passphraseCallback: passphraseCallback)
    }

    /// Create an NIOSSLPrivateKey from a buffer of bytes in either PEM or
    /// DER format.
    ///
    /// - parameters:
    ///     - bytes: The key bytes.
    ///     - format: The format of the key to load, either DER or PEM.
    ///     - passphraseCallback: Optionally a callback to invoke to obtain the passphrase for
    ///         encrypted keys. If not provided, or set to `nil`, the default BoringSSL
    ///         behaviour will be used, which prints a prompt and requests the passphrase from
    ///         stdin.
    public convenience init<T: Collection>(bytes: [UInt8], format: NIOSSLSerializationFormats, passphraseCallback: @escaping NIOSSLPassphraseCallback<T>) throws where T.Element == UInt8  {
        let manager = BoringSSLPassphraseCallbackManager(userCallback: passphraseCallback)
        try self.init(bytes: bytes, format: format, callbackManager: manager)
    }

    /// Create an NIOSSLPrivateKey wrapping a pointer into BoringSSL.
    ///
    /// This is a function that should be avoided as much as possible because it plays poorly with
    /// BoringSSL'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 internal func fromUnsafePointer(takingOwnership pointer: UnsafeMutablePointer<EVP_PKEY>) -> NIOSSLPrivateKey {
        return NIOSSLPrivateKey(withReference: pointer)
    }

    deinit {
        CNIOBoringSSL_EVP_PKEY_free(self.ref)
    }
}


// MARK:- Utilities
extension NIOSSLPrivateKey {
    /// Calls the given body function with a temporary buffer containing the DER-encoded bytes of this
    /// private key. This function does allocate for these bytes, but there is no way to avoid doing so with the
    /// X509 API in BoringSSL.
    ///
    /// The pointer provided to the closure is not valid beyond the lifetime of this method call.
    private func withUnsafeDERBuffer<T>(_ body: (UnsafeRawBufferPointer) throws -> T) throws -> T {
        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_PrivateKey_bio(bio, self.ref)
        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({ UnsafeRawBufferPointer(start: $0, count: length) }) else {
            fatalError("Failed to map bytes from a private key")
        }

        return try body(bytes)
    }
}


extension NIOSSLPrivateKey: Equatable {
    public static func ==(lhs: NIOSSLPrivateKey, rhs: NIOSSLPrivateKey) -> Bool {
        // Annoyingly, EVP_PKEY_cmp does not have a traditional return value pattern. 1 means equal, 0 means non-equal,
        // negative means error. Here we treat "error" as "not equal", because we have no error reporting mechanism from this call site,
        // and anyway, BoringSSL considers "these keys aren't of the same type" to be an error, which is in my mind pretty ludicrous.
        return CNIOBoringSSL_EVP_PKEY_cmp(lhs.ref, rhs.ref) == 1
    }
}


extension NIOSSLPrivateKey: Hashable {
    public func hash(into hasher: inout Hasher) {
        // Sadly, BoringSSL doesn't provide us with a nice key hashing function. We therefore have only two options:
        // we can either serialize the key into DER and feed that into the hasher, or we can attempt to hash the key parameters directly.
        // We could attempt the latter, but frankly it causes a lot of pain for minimal gain, so we don't bother. This incurs an allocation,
        // but that's ok. We crash if we hit an error here, as there is no way to recover.
        try! self.withUnsafeDERBuffer { hasher.combine(bytes: $0) }
    }
}