Parcourir la source

Add NIOTS transport to E2E TLS-enabled tests (#20)

Gus Cairo il y a 1 an
Parent
commit
7b9b6f301e

+ 1 - 1
Package.swift

@@ -51,7 +51,7 @@ let dependencies: [Package.Dependency] = [
   ),
   .package(
     url: "https://github.com/apple/swift-nio-ssl.git",
-    from: "2.27.2"
+    from: "2.29.0"
   ),
   .package(
     url: "https://github.com/apple/swift-nio-extras.git",

+ 32 - 49
Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOTransportServicesTests.swift

@@ -19,57 +19,9 @@ import GRPCCore
 import GRPCNIOTransportCore
 import GRPCNIOTransportHTTP2TransportServices
 import XCTest
+import NIOSSL
 
 final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
-  private static let p12bundleURL = URL(fileURLWithPath: #filePath)
-    .deletingLastPathComponent()  // (this file)
-    .deletingLastPathComponent()  // GRPCHTTP2TransportTests
-    .deletingLastPathComponent()  // Tests
-    .appendingPathComponent("Sources")
-    .appendingPathComponent("GRPCSampleData")
-    .appendingPathComponent("bundle")
-    .appendingPathExtension("p12")
-
-  @Sendable private static func loadIdentity() throws -> SecIdentity {
-    let data = try Data(contentsOf: Self.p12bundleURL)
-
-    var externalFormat = SecExternalFormat.formatUnknown
-    var externalItemType = SecExternalItemType.itemTypeUnknown
-    let passphrase = "password" as CFTypeRef
-    var exportKeyParams = SecItemImportExportKeyParameters()
-    exportKeyParams.passphrase = Unmanaged.passUnretained(passphrase)
-    var items: CFArray?
-
-    let status = SecItemImport(
-      data as CFData,
-      "bundle.p12" as CFString,
-      &externalFormat,
-      &externalItemType,
-      SecItemImportExportFlags(rawValue: 0),
-      &exportKeyParams,
-      nil,
-      &items
-    )
-
-    if status != errSecSuccess {
-      XCTFail(
-        """
-        Unable to load identity from '\(Self.p12bundleURL)'. \
-        SecItemImport failed with status \(status)
-        """
-      )
-    } else if items == nil {
-      XCTFail(
-        """
-        Unable to load identity from '\(Self.p12bundleURL)'. \
-        SecItemImport failed.
-        """
-      )
-    }
-
-    return ((items! as NSArray)[0] as! SecIdentity)
-  }
-
   func testGetListeningAddress_IPv4() async throws {
     let transport = GRPCNIOTransportCore.HTTP2ServerTransport.TransportServices(
       address: .ipv4(host: "0.0.0.0", port: 0),
@@ -198,6 +150,33 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
     }
   }
 
+  @Sendable private static func loadIdentity() throws -> SecIdentity {
+    let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
+    let password = "somepassword"
+    let bundle = NIOSSLPKCS12Bundle(
+      certificateChain: [
+        try NIOSSLCertificate(bytes: certificateKeyPairs.server.certificate, format: .der)
+      ],
+      privateKey: try NIOSSLPrivateKey(bytes: certificateKeyPairs.server.key, format: .der)
+    )
+    let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8)
+    let options = [kSecImportExportPassphrase as String: password]
+    var rawItems: CFArray?
+    let status = SecPKCS12Import(
+      Data(pkcs12Bytes) as CFData,
+      options as CFDictionary,
+      &rawItems
+    )
+    guard status == errSecSuccess else {
+      XCTFail("Failed to import PKCS12 bundle: status \(status).")
+      throw HTTP2TransportNIOTransportServicesTestsError.failedToImportPKCS12
+    }
+    let items = rawItems! as! [[String: Any]]
+    let firstItem = items[0]
+    let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity
+    return identity
+  }
+
   func testServerConfig_Defaults() throws {
     let grpcTLSConfig = HTTP2ServerTransport.TransportServices.TLS.defaults(
       identityProvider: Self.loadIdentity
@@ -229,4 +208,8 @@ final class HTTP2TransportNIOTransportServicesTests: XCTestCase {
     XCTAssertEqual(grpcTLSConfig.trustRoots, .systemDefault)
   }
 }
+
+enum HTTP2TransportNIOTransportServicesTestsError: Error {
+  case failedToImportPKCS12
+}
 #endif

+ 252 - 122
Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift

@@ -14,14 +14,16 @@
  * limitations under the License.
  */
 
-import Crypto
 import Foundation
 import GRPCCore
 import GRPCNIOTransportHTTP2Posix
+import GRPCNIOTransportHTTP2TransportServices
 import NIOSSL
-import SwiftASN1
 import Testing
-import X509
+
+#if canImport(Network)
+import Network
+#endif
 
 @Suite("HTTP/2 transport E2E tests with TLS enabled")
 struct HTTP2TransportTLSEnabledTests {
@@ -29,8 +31,8 @@ struct HTTP2TransportTLSEnabledTests {
 
   @Test(
     "When using defaults, server does not perform client verification",
-    arguments: [TransportKind.posix],
-    [TransportKind.posix]
+    arguments: TransportKind.supported,
+    TransportKind.supported
   )
   func testRPC_Defaults_OK(
     clientTransport: TransportKind,
@@ -58,8 +60,8 @@ struct HTTP2TransportTLSEnabledTests {
 
   @Test(
     "When using mTLS defaults, both client and server verify each others' certificates",
-    arguments: [TransportKind.posix],
-    [TransportKind.posix]
+    arguments: TransportKind.supported,
+    TransportKind.supported
   )
   func testRPC_mTLS_OK(
     clientTransport: TransportKind,
@@ -89,8 +91,8 @@ struct HTTP2TransportTLSEnabledTests {
 
   @Test(
     "Error is surfaced when client fails server verification",
-    arguments: [TransportKind.posix],
-    [TransportKind.posix]
+    arguments: TransportKind.supported,
+    TransportKind.supported
   )
   // Verification should fail because the custom hostname is missing on the client.
   func testClientFailsServerValidation(
@@ -98,43 +100,51 @@ struct HTTP2TransportTLSEnabledTests {
     serverTransport: TransportKind
   ) async throws {
     let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
-    let clientConfig = self.makeMTLSClientConfig(
+    let clientTransportConfig = self.makeDefaultTLSClientConfig(
       for: clientTransport,
       certificateKeyPairs: certificateKeyPairs,
-      serverHostname: "the-wrong-hostname"
+      authority: nil
     )
-
-    let serverConfig = self.makeMTLSServerConfig(
+    let serverTransportConfig = self.makeDefaultTLSServerConfig(
       for: serverTransport,
-      certificateKeyPairs: certificateKeyPairs,
-      includeClientCertificateInTrustRoots: true
+      certificateKeyPairs: certificateKeyPairs
     )
 
     try await self.withClientAndServer(
-      clientConfig: clientConfig,
-      serverConfig: serverConfig
+      clientConfig: clientTransportConfig,
+      serverConfig: serverTransportConfig
     ) { control in
       await #expect {
         try await self.executeUnaryRPC(control: control)
       } throws: { error in
-        guard let rootError = error as? RPCError else {
-          Issue.record("Should be an RPC error")
-          return false
-        }
+        let rootError = try #require(error as? RPCError)
         #expect(rootError.code == .unavailable)
-        #expect(
-          rootError.message
-            == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface."
-        )
 
-        guard
-          let sslError = rootError.cause as? NIOSSLExtraError,
-          case .failedToValidateHostname = sslError
-        else {
-          Issue.record(
-            "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))"
+        switch clientTransport {
+        case .posix:
+          #expect(
+            rootError.message
+              == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface."
           )
-          return false
+          let sslError = try #require(rootError.cause as? NIOSSLExtraError)
+          guard sslError == .failedToValidateHostname else {
+            Issue.record(
+              "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))"
+            )
+            return false
+          }
+
+        #if canImport(Network)
+        case .transportServices:
+          #expect(rootError.message.starts(with: "Could not establish a connection to"))
+          let nwError = try #require(rootError.cause as? NWError)
+          guard case .tls(Security.errSSLBadCert) = nwError else {
+            Issue.record(
+              "Should be a NWError.tls(-9808/errSSLBadCert) error, but was: \(String(describing: rootError.cause))"
+            )
+            return false
+          }
+        #endif
         }
 
         return true
@@ -144,51 +154,60 @@ struct HTTP2TransportTLSEnabledTests {
 
   @Test(
     "Error is surfaced when server fails client verification",
-    arguments: [TransportKind.posix],
-    [TransportKind.posix]
+    arguments: TransportKind.supported,
+    TransportKind.supported
   )
-  // Verification should fail because the server does not have trust roots containing the client cert.
+  // Verification should fail because the client does not offer a cert that
+  // the server can use for mutual verification.
   func testServerFailsClientValidation(
     clientTransport: TransportKind,
     serverTransport: TransportKind
   ) async throws {
     let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
-    let clientConfig = self.makeMTLSClientConfig(
+    let clientTransportConfig = self.makeDefaultTLSClientConfig(
       for: clientTransport,
-      certificateKeyPairs: certificateKeyPairs,
-      serverHostname: "localhost"
+      certificateKeyPairs: certificateKeyPairs
     )
-    let serverConfig = self.makeMTLSServerConfig(
+    let serverTransportConfig = self.makeMTLSServerConfig(
       for: serverTransport,
       certificateKeyPairs: certificateKeyPairs,
-      includeClientCertificateInTrustRoots: false
+      includeClientCertificateInTrustRoots: true
     )
 
     try await self.withClientAndServer(
-      clientConfig: clientConfig,
-      serverConfig: serverConfig
+      clientConfig: clientTransportConfig,
+      serverConfig: serverTransportConfig
     ) { control in
       await #expect {
         try await self.executeUnaryRPC(control: control)
       } throws: { error in
-        guard let rootError = error as? RPCError else {
-          Issue.record("Should be an RPC error")
-          return false
-        }
+        let rootError = try #require(error as? RPCError)
         #expect(rootError.code == .unavailable)
         #expect(
           rootError.message
             == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface."
         )
 
-        guard
-          let sslError = rootError.cause as? NIOSSL.BoringSSLError,
-          case .sslError = sslError
-        else {
-          Issue.record(
-            "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))"
-          )
-          return false
+        switch clientTransport {
+        case .posix:
+          let sslError = try #require(rootError.cause as? NIOSSL.BoringSSLError)
+          guard case .sslError = sslError else {
+            Issue.record(
+              "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))"
+            )
+            return false
+          }
+
+        #if canImport(Network)
+        case .transportServices:
+          let nwError = try #require(rootError.cause as? NWError)
+          guard case .tls(Security.errSSLPeerCertUnknown) = nwError else {
+            Issue.record(
+              "Should be a NWError.tls(-9829/errSSLPeerCertUnknown) error, but was: \(String(describing: rootError.cause))"
+            )
+            return false
+          }
+        #endif
         }
 
         return true
@@ -198,8 +217,26 @@ struct HTTP2TransportTLSEnabledTests {
 
   // - MARK: Test Utilities
 
+  enum TLSEnabledTestsError: Error {
+    case failedToImportPKCS12
+    case unexpectedListeningAddress
+    case serverError(cause: any Error)
+    case clientError(cause: any Error)
+  }
+
   enum TransportKind: Sendable {
     case posix
+    #if canImport(Network)
+    case transportServices
+    #endif
+
+    static var supported: [TransportKind] {
+      #if canImport(Network)
+      return [.posix, .transportServices]
+      #else
+      return [.posix]
+      #endif
+    }
   }
 
   struct Config<Transport, Security> {
@@ -213,6 +250,14 @@ struct HTTP2TransportTLSEnabledTests {
       HTTP2ClientTransport.Posix.TransportSecurity
     >
     case posix(Posix)
+
+    #if canImport(Network)
+    typealias TransportServices = Config<
+      HTTP2ClientTransport.TransportServices.Config,
+      HTTP2ClientTransport.TransportServices.TransportSecurity
+    >
+    case transportServices(TransportServices)
+    #endif
   }
 
   enum ServerConfig {
@@ -221,6 +266,14 @@ struct HTTP2TransportTLSEnabledTests {
       HTTP2ServerTransport.Posix.TransportSecurity
     >
     case posix(Posix)
+
+    #if canImport(Network)
+    typealias TransportServices = Config<
+      HTTP2ServerTransport.TransportServices.Config,
+      HTTP2ServerTransport.TransportServices.TransportSecurity
+    >
+    case transportServices(TransportServices)
+    #endif
   }
 
   private func makeDefaultPlaintextPosixClientConfig() -> ClientConfig.Posix {
@@ -234,9 +287,23 @@ struct HTTP2TransportTLSEnabledTests {
     )
   }
 
+  #if canImport(Network)
+  private func makeDefaultPlaintextTSClientConfig() -> ClientConfig.TransportServices {
+    ClientConfig.TransportServices(
+      security: .plaintext,
+      transport: .defaults { config in
+        config.backoff.initial = .milliseconds(100)
+        config.backoff.multiplier = 1
+        config.backoff.jitter = 0
+      }
+    )
+  }
+  #endif
+
   private func makeDefaultTLSClientConfig(
     for transportSecurity: TransportKind,
-    certificateKeyPairs: SelfSignedCertificateKeyPairs
+    certificateKeyPairs: SelfSignedCertificateKeyPairs,
+    authority: String? = "localhost"
   ) -> ClientConfig {
     switch transportSecurity {
     case .posix:
@@ -246,11 +313,52 @@ struct HTTP2TransportTLSEnabledTests {
           .bytes(certificateKeyPairs.server.certificate, format: .der)
         ])
       }
-      config.transport.http2.authority = "localhost"
+      config.transport.http2.authority = authority
       return .posix(config)
+
+    #if canImport(Network)
+    case .transportServices:
+      var config = self.makeDefaultPlaintextTSClientConfig()
+      config.security = .tls {
+        $0.trustRoots = .certificates([
+          .bytes(certificateKeyPairs.server.certificate, format: .der)
+        ])
+      }
+      config.transport.http2.authority = authority
+      return .transportServices(config)
+    #endif
     }
   }
 
+  #if canImport(Network)
+  private func makeSecIdentityProvider(
+    certificateBytes: [UInt8],
+    privateKeyBytes: [UInt8]
+  ) throws -> SecIdentity {
+    let password = "somepassword"
+    let bundle = NIOSSLPKCS12Bundle(
+      certificateChain: [try NIOSSLCertificate(bytes: certificateBytes, format: .der)],
+      privateKey: try NIOSSLPrivateKey(bytes: privateKeyBytes, format: .der)
+    )
+    let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8)
+    let options = [kSecImportExportPassphrase as String: password]
+    var rawItems: CFArray?
+    let status = SecPKCS12Import(
+      Data(pkcs12Bytes) as CFData,
+      options as CFDictionary,
+      &rawItems
+    )
+    guard status == errSecSuccess else {
+      Issue.record("Failed to import PKCS12 bundle: status \(status).")
+      throw TLSEnabledTestsError.failedToImportPKCS12
+    }
+    let items = rawItems! as! [[String: Any]]
+    let firstItem = items[0]
+    let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity
+    return identity
+  }
+  #endif
+
   private func makeMTLSClientConfig(
     for transportKind: TransportKind,
     certificateKeyPairs: SelfSignedCertificateKeyPairs,
@@ -269,6 +377,23 @@ struct HTTP2TransportTLSEnabledTests {
       }
       config.transport.http2.authority = serverHostname
       return .posix(config)
+
+    #if canImport(Network)
+    case .transportServices:
+      var config = self.makeDefaultPlaintextTSClientConfig()
+      config.security = .mTLS {
+        try self.makeSecIdentityProvider(
+          certificateBytes: certificateKeyPairs.client.certificate,
+          privateKeyBytes: certificateKeyPairs.client.key
+        )
+      } configure: {
+        $0.trustRoots = .certificates([
+          .bytes(certificateKeyPairs.server.certificate, format: .der)
+        ])
+      }
+      config.transport.http2.authority = serverHostname
+      return .transportServices(config)
+    #endif
     }
   }
 
@@ -276,6 +401,12 @@ struct HTTP2TransportTLSEnabledTests {
     ServerConfig.Posix(security: .plaintext, transport: .defaults)
   }
 
+  #if canImport(Network)
+  private func makeDefaultPlaintextTSServerConfig() -> ServerConfig.TransportServices {
+    ServerConfig.TransportServices(security: .plaintext, transport: .defaults)
+  }
+  #endif
+
   private func makeDefaultTLSServerConfig(
     for transportKind: TransportKind,
     certificateKeyPairs: SelfSignedCertificateKeyPairs
@@ -288,6 +419,18 @@ struct HTTP2TransportTLSEnabledTests {
         privateKey: .bytes(certificateKeyPairs.server.key, format: .der)
       )
       return .posix(config)
+
+    #if canImport(Network)
+    case .transportServices:
+      var config = self.makeDefaultPlaintextTSServerConfig()
+      config.security = .tls {
+        try self.makeSecIdentityProvider(
+          certificateBytes: certificateKeyPairs.server.certificate,
+          privateKeyBytes: certificateKeyPairs.server.key
+        )
+      }
+      return .transportServices(config)
+    #endif
     }
   }
 
@@ -310,6 +453,24 @@ struct HTTP2TransportTLSEnabledTests {
         }
       }
       return .posix(config)
+
+    #if canImport(Network)
+    case .transportServices:
+      var config = self.makeDefaultPlaintextTSServerConfig()
+      config.security = .mTLS {
+        try self.makeSecIdentityProvider(
+          certificateBytes: certificateKeyPairs.server.certificate,
+          privateKeyBytes: certificateKeyPairs.server.key
+        )
+      } configure: {
+        if includeClientCertificateInTrustRoots {
+          $0.trustRoots = .certificates([
+            .bytes(certificateKeyPairs.client.certificate, format: .der)
+          ])
+        }
+      }
+      return .transportServices(config)
+    #endif
     }
   }
 
@@ -322,25 +483,33 @@ struct HTTP2TransportTLSEnabledTests {
       let server = self.makeServer(config: serverConfig)
 
       group.addTask {
-        try await server.serve()
+        do {
+          try await server.serve()
+        } catch {
+          throw TLSEnabledTestsError.serverError(cause: error)
+        }
       }
 
       guard let address = try await server.listeningAddress?.ipv4 else {
-        Issue.record("Unexpected address to connect to")
-        return
+        throw TLSEnabledTestsError.unexpectedListeningAddress
       }
+
       let target: any ResolvableTarget = .ipv4(host: address.host, port: address.port)
       let client = try self.makeClient(config: clientConfig, target: target)
 
       group.addTask {
-        try await client.run()
+        do {
+          try await client.run()
+        } catch {
+          throw TLSEnabledTestsError.clientError(cause: error)
+        }
       }
 
       let control = ControlClient(wrapping: client)
       try await test(control)
 
-      server.beginGracefulShutdown()
       client.beginGracefulShutdown()
+      server.beginGracefulShutdown()
     }
   }
 
@@ -349,7 +518,7 @@ struct HTTP2TransportTLSEnabledTests {
 
     switch config {
     case .posix(let config):
-      let server = GRPCServer(
+      return GRPCServer(
         transport: .http2NIOPosix(
           address: .ipv4(host: "127.0.0.1", port: 0),
           transportSecurity: config.security,
@@ -358,7 +527,17 @@ struct HTTP2TransportTLSEnabledTests {
         services: services
       )
 
-      return server
+    #if canImport(Network)
+    case .transportServices(let config):
+      return GRPCServer(
+        transport: .http2NIOTS(
+          address: .ipv4(host: "127.0.0.1", port: 0),
+          transportSecurity: config.security,
+          config: config.transport
+        ),
+        services: services
+      )
+    #endif
     }
   }
 
@@ -376,6 +555,16 @@ struct HTTP2TransportTLSEnabledTests {
         config: config.transport,
         serviceConfig: ServiceConfig()
       )
+
+    #if canImport(Network)
+    case .transportServices(let config):
+      transport = try HTTP2ClientTransport.TransportServices(
+        target: target,
+        transportSecurity: config.security,
+        config: config.transport,
+        serviceConfig: ServiceConfig()
+      )
+    #endif
     }
 
     return GRPCClient(transport: transport)
@@ -389,62 +578,3 @@ struct HTTP2TransportTLSEnabledTests {
     }
   }
 }
-
-struct SelfSignedCertificateKeyPairs {
-  struct CertificateKeyPair {
-    let certificate: [UInt8]
-    let key: [UInt8]
-  }
-
-  let server: CertificateKeyPair
-  let client: CertificateKeyPair
-
-  init() throws {
-    let server = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate")
-    let client = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate")
-
-    self.server = CertificateKeyPair(certificate: server.cert, key: server.key)
-    self.client = CertificateKeyPair(certificate: client.cert, key: client.key)
-  }
-
-  private static func makeSelfSignedDERCertificateAndPrivateKey(
-    name: String
-  ) throws -> (cert: [UInt8], key: [UInt8]) {
-    let swiftCryptoKey = P256.Signing.PrivateKey()
-    let key = Certificate.PrivateKey(swiftCryptoKey)
-    let subjectName = try DistinguishedName { CommonName(name) }
-    let issuerName = subjectName
-    let now = Date()
-    let extensions = try Certificate.Extensions {
-      Critical(
-        BasicConstraints.isCertificateAuthority(maxPathLength: nil)
-      )
-      Critical(
-        KeyUsage(digitalSignature: true, keyCertSign: true)
-      )
-      Critical(
-        try ExtendedKeyUsage([.serverAuth, .clientAuth])
-      )
-      SubjectAlternativeNames([.dnsName("localhost")])
-    }
-    let certificate = try Certificate(
-      version: .v3,
-      serialNumber: Certificate.SerialNumber(),
-      publicKey: key.publicKey,
-      notValidBefore: now.addingTimeInterval(-60 * 60),
-      notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365),
-      issuer: issuerName,
-      subject: subjectName,
-      signatureAlgorithm: .ecdsaWithSHA256,
-      extensions: extensions,
-      issuerPrivateKey: key
-    )
-
-    var serializer = DER.Serializer()
-    try serializer.serialize(certificate)
-
-    let certBytes = serializer.serializedBytes
-    let keyBytes = try key.serializeAsPEM().derBytes
-    return (certBytes, keyBytes)
-  }
-}

+ 79 - 0
Tests/GRPCNIOTransportHTTP2Tests/Test Utilities/SelfSignedCertificateKeyPairs.swift

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Crypto
+import Foundation
+import SwiftASN1
+import X509
+
+struct SelfSignedCertificateKeyPairs {
+  struct CertificateKeyPair {
+    let certificate: [UInt8]
+    let key: [UInt8]
+  }
+
+  let server: CertificateKeyPair
+  let client: CertificateKeyPair
+
+  init() throws {
+    let server = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate")
+    let client = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate")
+
+    self.server = CertificateKeyPair(certificate: server.cert, key: server.key)
+    self.client = CertificateKeyPair(certificate: client.cert, key: client.key)
+  }
+
+  private static func makeSelfSignedDERCertificateAndPrivateKey(
+    name: String
+  ) throws -> (cert: [UInt8], key: [UInt8]) {
+    let swiftCryptoKey = P256.Signing.PrivateKey()
+    let key = Certificate.PrivateKey(swiftCryptoKey)
+    let subjectName = try DistinguishedName { CommonName(name) }
+    let issuerName = subjectName
+    let now = Date()
+    let extensions = try Certificate.Extensions {
+      Critical(
+        BasicConstraints.isCertificateAuthority(maxPathLength: nil)
+      )
+      Critical(
+        KeyUsage(digitalSignature: true, keyCertSign: true)
+      )
+      Critical(
+        try ExtendedKeyUsage([.serverAuth, .clientAuth])
+      )
+      SubjectAlternativeNames([.dnsName("localhost")])
+    }
+    let certificate = try Certificate(
+      version: .v3,
+      serialNumber: Certificate.SerialNumber(),
+      publicKey: key.publicKey,
+      notValidBefore: now.addingTimeInterval(-60 * 60),
+      notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365),
+      issuer: issuerName,
+      subject: subjectName,
+      signatureAlgorithm: .ecdsaWithSHA256,
+      extensions: extensions,
+      issuerPrivateKey: key
+    )
+
+    var serializer = DER.Serializer()
+    try serializer.serialize(certificate)
+
+    let certBytes = serializer.serializedBytes
+    let keyBytes = try key.serializeAsPEM().derBytes
+    return (certBytes, keyBytes)
+  }
+}