Skip to content

Commit 9173d85

Browse files
authored
Add toDerBytes to SSLPrivateKey (#540)
This PR adds new API (`SSLPrivateKey/toDerBytes()`) to get the DER bytes for a private key. This is only supported for native (EVP) keys; if using a custom key, an empty array of bytes will be returned instead.
1 parent 43e4c3c commit 9173d85

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

Sources/NIOSSL/CustomPrivateKey.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public protocol NIOSSLCustomPrivateKey: _NIOPreconcurrencySendable {
4141
/// The signature algorithms supported by this key.
4242
var signatureAlgorithms: [SignatureAlgorithm] { get }
4343

44+
/// The DER bytes for this private key.
45+
///
46+
/// Custom key implementations should return an appropriate value, but by default, an empty array will be returned.
47+
var derBytes: [UInt8] { get }
48+
4449
/// Called to perform a signing operation.
4550
///
4651
/// The data being passed to the call has not been hashed, and it is the responsibility of the implementer
@@ -75,12 +80,14 @@ public protocol NIOSSLCustomPrivateKey: _NIOPreconcurrencySendable {
7580
func decrypt(channel: Channel, data: ByteBuffer) -> EventLoopFuture<ByteBuffer>
7681
}
7782

83+
extension NIOSSLCustomPrivateKey {
84+
@inlinable public var derBytes: [UInt8] { [] }
85+
}
86+
7887
/// This is a type-erased wrapper that can be used to encapsulate a NIOSSLCustomPrivateKey and provide it with
7988
/// hashability and equatability.
8089
///
81-
/// While generally speaking type-erasure has some nasty performance problems, we only need the type-erasure for
82-
/// Hashable conformance, which we don't use in any production code: only the tests use it. To that end, we don't
83-
/// mind too much that we need to do this.
90+
/// While generally speaking type-erasure has some nasty performance problems, we need the type-erasure for Hashable conformance.
8491
@usableFromInline
8592
internal struct AnyNIOSSLCustomPrivateKey: NIOSSLCustomPrivateKey, Hashable {
8693
@usableFromInline let _value: NIOSSLCustomPrivateKey
@@ -100,6 +107,10 @@ internal struct AnyNIOSSLCustomPrivateKey: NIOSSLCustomPrivateKey, Hashable {
100107
self._value.signatureAlgorithms
101108
}
102109

110+
@inlinable var derBytes: [UInt8] {
111+
self._value.derBytes
112+
}
113+
103114
// This method does not need to be @inlinable for performance, but it needs to be _at least_
104115
// @usableFromInline as it's a protocol requirement on a @usableFromInline type.
105116
@inlinable func sign(

Sources/NIOSSL/SSLPrivateKey.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,19 @@ extension NIOSSLPrivateKey {
404404
return customKey.signatureAlgorithms
405405
}
406406
}
407+
408+
/// Extracts the bytes of this private key in DER format.
409+
/// - Returns: The DER-encoded bytes for this private key.
410+
public var derBytes: [UInt8] {
411+
get throws {
412+
switch self.representation {
413+
case .native(let evpKey):
414+
return try Self.withUnsafeDERBuffer(of: evpKey) { Array($0) }
415+
case .custom(let custom):
416+
return custom.derBytes
417+
}
418+
}
419+
}
407420
}
408421

409422
extension NIOSSLPrivateKey: Equatable {

Tests/NIOSSLTests/CustomPrivateKeyTests.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,51 @@ private final class CustomKeyDelayedCompletion: NIOSSLCustomPrivateKey, Hashable
257257
}
258258
}
259259

260+
private final class CustomKeyWithoutDERBytes: NIOSSLCustomPrivateKey, Hashable {
261+
var signatureAlgorithms: [SignatureAlgorithm] { [] }
262+
263+
func sign(channel: Channel, algorithm: SignatureAlgorithm, data: ByteBuffer) -> EventLoopFuture<ByteBuffer> {
264+
fatalError("Not implemented")
265+
}
266+
267+
func decrypt(channel: Channel, data: ByteBuffer) -> EventLoopFuture<ByteBuffer> {
268+
fatalError("Not implemented")
269+
}
270+
271+
static func == (lhs: CustomKeyWithoutDERBytes, rhs: CustomKeyWithoutDERBytes) -> Bool {
272+
lhs.signatureAlgorithms == rhs.signatureAlgorithms
273+
}
274+
275+
func hash(into hasher: inout Hasher) {
276+
hasher.combine(ObjectIdentifier(self))
277+
hasher.combine(signatureAlgorithms)
278+
}
279+
}
280+
281+
private final class CustomKeyWithDERBytes: NIOSSLCustomPrivateKey, Hashable {
282+
var signatureAlgorithms: [NIOSSL.SignatureAlgorithm] { [] }
283+
284+
var derBytes: [UInt8] { [42] }
285+
286+
func sign(channel: Channel, algorithm: SignatureAlgorithm, data: ByteBuffer) -> EventLoopFuture<ByteBuffer> {
287+
fatalError("Not implemented")
288+
}
289+
290+
func decrypt(channel: Channel, data: ByteBuffer) -> EventLoopFuture<ByteBuffer> {
291+
fatalError("Not implemented")
292+
}
293+
294+
static func == (lhs: CustomKeyWithDERBytes, rhs: CustomKeyWithDERBytes) -> Bool {
295+
lhs.signatureAlgorithms == rhs.signatureAlgorithms && lhs.derBytes == rhs.derBytes
296+
}
297+
298+
func hash(into hasher: inout Hasher) {
299+
hasher.combine(ObjectIdentifier(self))
300+
hasher.combine(signatureAlgorithms)
301+
hasher.combine(derBytes)
302+
}
303+
}
304+
260305
final class CustomPrivateKeyTests: XCTestCase {
261306
fileprivate static let customECDSACertAndKey: (certificate: NIOSSLCertificate, key: CustomPKEY) = {
262307
let (cert, originalKey) = generateSelfSignedCert(keygenFunction: {
@@ -686,6 +731,20 @@ final class CustomPrivateKeyTests: XCTestCase {
686731
])
687732
)
688733
}
734+
735+
func testDERBytes_DefaultImplementation_ReturnsEmptyArray() throws {
736+
let customKey = CustomKeyWithoutDERBytes()
737+
let key = NIOSSLPrivateKey(customPrivateKey: customKey)
738+
let derBytes = try key.derBytes
739+
XCTAssertEqual(derBytes, [])
740+
}
741+
742+
func testDERBytes_ReturnsBytes() throws {
743+
let customKey = CustomKeyWithDERBytes()
744+
let key = NIOSSLPrivateKey(customPrivateKey: customKey)
745+
let derBytes = try key.derBytes
746+
XCTAssertEqual(derBytes, [42])
747+
}
689748
}
690749

691750
extension SignatureAlgorithm {

Tests/NIOSSLTests/SSLPrivateKeyTests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,4 +338,10 @@ class SSLPrivateKeyTest: XCTestCase {
338338
XCTAssertNotEqual(key1, key2)
339339
XCTAssertNotEqual(key1.hashValue, key2.hashValue)
340340
}
341+
342+
func testDERBytes() throws {
343+
let key = try NIOSSLPrivateKey(bytes: .init(samplePemKey.utf8), format: .pem)
344+
let derBytes = try key.derBytes
345+
XCTAssertEqual(Data(derBytes), pemToDer(samplePemKey))
346+
}
341347
}

0 commit comments

Comments
 (0)