HTTP2TransportTLSEnabledTests.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. /*
  2. * Copyright 2024, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import Foundation
  17. import GRPCCore
  18. import GRPCNIOTransportHTTP2Posix
  19. import GRPCNIOTransportHTTP2TransportServices
  20. import NIOSSL
  21. import Testing
  22. #if canImport(Network)
  23. import Network
  24. #endif
  25. @Suite("HTTP/2 transport E2E tests with TLS enabled")
  26. struct HTTP2TransportTLSEnabledTests {
  27. // - MARK: Tests
  28. @Test(
  29. "When using defaults, server does not perform client verification",
  30. arguments: TransportKind.supported,
  31. TransportKind.supported
  32. )
  33. func testRPC_Defaults_OK(
  34. clientTransport: TransportKind,
  35. serverTransport: TransportKind
  36. ) async throws {
  37. let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
  38. let clientConfig = self.makeDefaultTLSClientConfig(
  39. for: clientTransport,
  40. certificateKeyPairs: certificateKeyPairs
  41. )
  42. let serverConfig = self.makeDefaultTLSServerConfig(
  43. for: serverTransport,
  44. certificateKeyPairs: certificateKeyPairs
  45. )
  46. try await self.withClientAndServer(
  47. clientConfig: clientConfig,
  48. serverConfig: serverConfig
  49. ) { control in
  50. await #expect(throws: Never.self) {
  51. try await self.executeUnaryRPC(control: control)
  52. }
  53. }
  54. }
  55. @Test(
  56. "When using mTLS defaults, both client and server verify each others' certificates",
  57. arguments: TransportKind.supported,
  58. TransportKind.supported
  59. )
  60. func testRPC_mTLS_OK(
  61. clientTransport: TransportKind,
  62. serverTransport: TransportKind
  63. ) async throws {
  64. let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
  65. let clientConfig = self.makeMTLSClientConfig(
  66. for: clientTransport,
  67. certificateKeyPairs: certificateKeyPairs,
  68. serverHostname: "localhost"
  69. )
  70. let serverConfig = self.makeMTLSServerConfig(
  71. for: serverTransport,
  72. certificateKeyPairs: certificateKeyPairs,
  73. includeClientCertificateInTrustRoots: true
  74. )
  75. try await self.withClientAndServer(
  76. clientConfig: clientConfig,
  77. serverConfig: serverConfig
  78. ) { control in
  79. await #expect(throws: Never.self) {
  80. try await self.executeUnaryRPC(control: control)
  81. }
  82. }
  83. }
  84. @Test(
  85. "Error is surfaced when client fails server verification",
  86. arguments: TransportKind.supported,
  87. TransportKind.supported
  88. )
  89. // Verification should fail because the custom hostname is missing on the client.
  90. func testClientFailsServerValidation(
  91. clientTransport: TransportKind,
  92. serverTransport: TransportKind
  93. ) async throws {
  94. let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
  95. let clientTransportConfig = self.makeDefaultTLSClientConfig(
  96. for: clientTransport,
  97. certificateKeyPairs: certificateKeyPairs,
  98. authority: "wrong-hostname"
  99. )
  100. let serverTransportConfig = self.makeDefaultTLSServerConfig(
  101. for: serverTransport,
  102. certificateKeyPairs: certificateKeyPairs
  103. )
  104. await #expect {
  105. try await self.withClientAndServer(
  106. clientConfig: clientTransportConfig,
  107. serverConfig: serverTransportConfig
  108. ) { control in
  109. try await self.executeUnaryRPC(control: control)
  110. }
  111. } throws: { error in
  112. let rootError = try #require(error as? RPCError)
  113. #expect(rootError.code == .unavailable)
  114. switch clientTransport {
  115. case .posix:
  116. #expect(
  117. rootError.message
  118. == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface."
  119. )
  120. let sslError = try #require(rootError.cause as? NIOSSLExtraError)
  121. guard sslError == .failedToValidateHostname else {
  122. Issue.record(
  123. "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))"
  124. )
  125. return false
  126. }
  127. #if canImport(Network)
  128. case .transportServices:
  129. #expect(rootError.message.starts(with: "Could not establish a connection to"))
  130. let nwError = try #require(rootError.cause as? NWError)
  131. guard case .tls(Security.errSSLBadCert) = nwError else {
  132. Issue.record(
  133. "Should be a NWError.tls(-9808/errSSLBadCert) error, but was: \(String(describing: rootError.cause))"
  134. )
  135. return false
  136. }
  137. #endif
  138. }
  139. return true
  140. }
  141. }
  142. @Test(
  143. "Error is surfaced when server fails client verification",
  144. arguments: TransportKind.supported,
  145. TransportKind.supported
  146. )
  147. // Verification should fail because the client does not offer a cert that
  148. // the server can use for mutual verification.
  149. func testServerFailsClientValidation(
  150. clientTransport: TransportKind,
  151. serverTransport: TransportKind
  152. ) async throws {
  153. let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
  154. let clientTransportConfig = self.makeDefaultTLSClientConfig(
  155. for: clientTransport,
  156. certificateKeyPairs: certificateKeyPairs
  157. )
  158. let serverTransportConfig = self.makeMTLSServerConfig(
  159. for: serverTransport,
  160. certificateKeyPairs: certificateKeyPairs,
  161. includeClientCertificateInTrustRoots: true
  162. )
  163. await #expect {
  164. try await self.withClientAndServer(
  165. clientConfig: clientTransportConfig,
  166. serverConfig: serverTransportConfig
  167. ) { control in
  168. try await self.executeUnaryRPC(control: control)
  169. }
  170. } throws: { error in
  171. let rootError = try #require(error as? RPCError)
  172. #expect(rootError.code == .unavailable)
  173. #expect(
  174. rootError.message
  175. == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface."
  176. )
  177. switch clientTransport {
  178. case .posix:
  179. let sslError = try #require(rootError.cause as? NIOSSL.BoringSSLError)
  180. guard case .sslError = sslError else {
  181. Issue.record(
  182. "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))"
  183. )
  184. return false
  185. }
  186. #if canImport(Network)
  187. case .transportServices:
  188. let nwError = try #require(rootError.cause as? NWError)
  189. guard case .tls(Security.errSSLPeerCertUnknown) = nwError else {
  190. // When the TLS handshake fails, the connection will be closed from the client.
  191. // Network.framework will generally surface the right SSL error (in this case, an "unknown
  192. // certificate" from the server), but it will sometimes instead return the broken pipe
  193. // error caused by the underlying TLS handshake handler closing the connection:
  194. // we should tolerate this.
  195. if case .posix(POSIXErrorCode.EPIPE) = nwError {
  196. return true
  197. }
  198. Issue.record(
  199. "Should be a NWError.tls(-9829/errSSLPeerCertUnknown) error, but was: \(String(describing: rootError.cause))"
  200. )
  201. return false
  202. }
  203. #endif
  204. }
  205. return true
  206. }
  207. }
  208. // - MARK: Test Utilities
  209. enum TLSEnabledTestsError: Error {
  210. case failedToImportPKCS12
  211. case unexpectedListeningAddress
  212. }
  213. struct Config<Transport, Security> {
  214. var security: Security
  215. var transport: Transport
  216. }
  217. enum ClientConfig {
  218. typealias Posix = Config<
  219. HTTP2ClientTransport.Posix.Config,
  220. HTTP2ClientTransport.Posix.TransportSecurity
  221. >
  222. case posix(Posix)
  223. #if canImport(Network)
  224. typealias TransportServices = Config<
  225. HTTP2ClientTransport.TransportServices.Config,
  226. HTTP2ClientTransport.TransportServices.TransportSecurity
  227. >
  228. case transportServices(TransportServices)
  229. #endif
  230. }
  231. enum ServerConfig {
  232. typealias Posix = Config<
  233. HTTP2ServerTransport.Posix.Config,
  234. HTTP2ServerTransport.Posix.TransportSecurity
  235. >
  236. case posix(Posix)
  237. #if canImport(Network)
  238. typealias TransportServices = Config<
  239. HTTP2ServerTransport.TransportServices.Config,
  240. HTTP2ServerTransport.TransportServices.TransportSecurity
  241. >
  242. case transportServices(TransportServices)
  243. #endif
  244. }
  245. private func makeDefaultPlaintextPosixClientConfig() -> ClientConfig.Posix {
  246. ClientConfig.Posix(
  247. security: .plaintext,
  248. transport: .defaults { config in
  249. config.backoff.initial = .milliseconds(100)
  250. config.backoff.multiplier = 1
  251. config.backoff.jitter = 0
  252. }
  253. )
  254. }
  255. #if canImport(Network)
  256. private func makeDefaultPlaintextTSClientConfig() -> ClientConfig.TransportServices {
  257. ClientConfig.TransportServices(
  258. security: .plaintext,
  259. transport: .defaults { config in
  260. config.backoff.initial = .milliseconds(100)
  261. config.backoff.multiplier = 1
  262. config.backoff.jitter = 0
  263. }
  264. )
  265. }
  266. #endif
  267. private func makeDefaultTLSClientConfig(
  268. for transportSecurity: TransportKind,
  269. certificateKeyPairs: SelfSignedCertificateKeyPairs,
  270. authority: String? = "localhost"
  271. ) -> ClientConfig {
  272. switch transportSecurity {
  273. case .posix:
  274. var config = self.makeDefaultPlaintextPosixClientConfig()
  275. config.security = .tls {
  276. $0.trustRoots = .certificates([
  277. .bytes(certificateKeyPairs.server.certificate, format: .der)
  278. ])
  279. }
  280. config.transport.http2.authority = authority
  281. return .posix(config)
  282. #if canImport(Network)
  283. case .transportServices:
  284. var config = self.makeDefaultPlaintextTSClientConfig()
  285. config.security = .tls {
  286. $0.trustRoots = .certificates([
  287. .bytes(certificateKeyPairs.server.certificate, format: .der)
  288. ])
  289. }
  290. config.transport.http2.authority = authority
  291. return .transportServices(config)
  292. #endif
  293. }
  294. }
  295. #if canImport(Network)
  296. private func makeSecIdentityProvider(
  297. certificateBytes: [UInt8],
  298. privateKeyBytes: [UInt8]
  299. ) throws -> SecIdentity {
  300. let password = "somepassword"
  301. let bundle = NIOSSLPKCS12Bundle(
  302. certificateChain: [try NIOSSLCertificate(bytes: certificateBytes, format: .der)],
  303. privateKey: try NIOSSLPrivateKey(bytes: privateKeyBytes, format: .der)
  304. )
  305. let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8)
  306. let options =
  307. [
  308. kSecImportExportPassphrase as String: password,
  309. kSecImportToMemoryOnly: kCFBooleanTrue!,
  310. ] as [AnyHashable: Any]
  311. var rawItems: CFArray?
  312. let status = SecPKCS12Import(
  313. Data(pkcs12Bytes) as CFData,
  314. options as CFDictionary,
  315. &rawItems
  316. )
  317. guard status == errSecSuccess else {
  318. Issue.record("Failed to import PKCS12 bundle: status \(status).")
  319. throw TLSEnabledTestsError.failedToImportPKCS12
  320. }
  321. let items = rawItems! as! [[String: Any]]
  322. let firstItem = items[0]
  323. let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity
  324. return identity
  325. }
  326. #endif
  327. private func makeMTLSClientConfig(
  328. for transportKind: TransportKind,
  329. certificateKeyPairs: SelfSignedCertificateKeyPairs,
  330. serverHostname: String?
  331. ) -> ClientConfig {
  332. switch transportKind {
  333. case .posix:
  334. var config = self.makeDefaultPlaintextPosixClientConfig()
  335. config.security = .mTLS(
  336. certificateChain: [.bytes(certificateKeyPairs.client.certificate, format: .der)],
  337. privateKey: .bytes(certificateKeyPairs.client.key, format: .der)
  338. ) {
  339. $0.trustRoots = .certificates([
  340. .bytes(certificateKeyPairs.server.certificate, format: .der)
  341. ])
  342. }
  343. config.transport.http2.authority = serverHostname
  344. return .posix(config)
  345. #if canImport(Network)
  346. case .transportServices:
  347. var config = self.makeDefaultPlaintextTSClientConfig()
  348. config.security = .mTLS {
  349. try self.makeSecIdentityProvider(
  350. certificateBytes: certificateKeyPairs.client.certificate,
  351. privateKeyBytes: certificateKeyPairs.client.key
  352. )
  353. } configure: {
  354. $0.trustRoots = .certificates([
  355. .bytes(certificateKeyPairs.server.certificate, format: .der)
  356. ])
  357. }
  358. config.transport.http2.authority = serverHostname
  359. return .transportServices(config)
  360. #endif
  361. }
  362. }
  363. private func makeDefaultPlaintextPosixServerConfig() -> ServerConfig.Posix {
  364. ServerConfig.Posix(security: .plaintext, transport: .defaults)
  365. }
  366. #if canImport(Network)
  367. private func makeDefaultPlaintextTSServerConfig() -> ServerConfig.TransportServices {
  368. ServerConfig.TransportServices(security: .plaintext, transport: .defaults)
  369. }
  370. #endif
  371. private func makeDefaultTLSServerConfig(
  372. for transportKind: TransportKind,
  373. certificateKeyPairs: SelfSignedCertificateKeyPairs
  374. ) -> ServerConfig {
  375. switch transportKind {
  376. case .posix:
  377. var config = self.makeDefaultPlaintextPosixServerConfig()
  378. config.security = .tls(
  379. certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)],
  380. privateKey: .bytes(certificateKeyPairs.server.key, format: .der)
  381. )
  382. return .posix(config)
  383. #if canImport(Network)
  384. case .transportServices:
  385. var config = self.makeDefaultPlaintextTSServerConfig()
  386. config.security = .tls {
  387. try self.makeSecIdentityProvider(
  388. certificateBytes: certificateKeyPairs.server.certificate,
  389. privateKeyBytes: certificateKeyPairs.server.key
  390. )
  391. }
  392. return .transportServices(config)
  393. #endif
  394. }
  395. }
  396. private func makeMTLSServerConfig(
  397. for transportKind: TransportKind,
  398. certificateKeyPairs: SelfSignedCertificateKeyPairs,
  399. includeClientCertificateInTrustRoots: Bool
  400. ) -> ServerConfig {
  401. switch transportKind {
  402. case .posix:
  403. var config = self.makeDefaultPlaintextPosixServerConfig()
  404. config.security = .mTLS(
  405. certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)],
  406. privateKey: .bytes(certificateKeyPairs.server.key, format: .der)
  407. ) {
  408. if includeClientCertificateInTrustRoots {
  409. $0.trustRoots = .certificates([
  410. .bytes(certificateKeyPairs.client.certificate, format: .der)
  411. ])
  412. }
  413. }
  414. return .posix(config)
  415. #if canImport(Network)
  416. case .transportServices:
  417. var config = self.makeDefaultPlaintextTSServerConfig()
  418. config.security = .mTLS {
  419. try self.makeSecIdentityProvider(
  420. certificateBytes: certificateKeyPairs.server.certificate,
  421. privateKeyBytes: certificateKeyPairs.server.key
  422. )
  423. } configure: {
  424. if includeClientCertificateInTrustRoots {
  425. $0.trustRoots = .certificates([
  426. .bytes(certificateKeyPairs.client.certificate, format: .der)
  427. ])
  428. }
  429. }
  430. return .transportServices(config)
  431. #endif
  432. }
  433. }
  434. func withClientAndServer(
  435. clientConfig: ClientConfig,
  436. serverConfig: ServerConfig,
  437. _ test: (ControlClient<NIOClientTransport>) async throws -> Void
  438. ) async throws {
  439. let serverTransport: NIOServerTransport
  440. switch serverConfig {
  441. case .posix(let posix):
  442. serverTransport = NIOServerTransport(
  443. .http2NIOPosix(
  444. address: .ipv4(host: "127.0.0.1", port: 0),
  445. transportSecurity: posix.security,
  446. config: posix.transport
  447. )
  448. )
  449. #if canImport(Network)
  450. case .transportServices(let config):
  451. serverTransport = NIOServerTransport(
  452. .http2NIOTS(
  453. address: .ipv4(host: "127.0.0.1", port: 0),
  454. transportSecurity: config.security,
  455. config: config.transport
  456. )
  457. )
  458. #endif
  459. }
  460. try await withGRPCServer(transport: serverTransport, services: [ControlService()]) { server in
  461. guard let address = try await server.listeningAddress?.ipv4 else {
  462. throw TLSEnabledTestsError.unexpectedListeningAddress
  463. }
  464. let target: any ResolvableTarget = .ipv4(host: address.host, port: address.port)
  465. let clientTransport: NIOClientTransport
  466. switch clientConfig {
  467. case .posix(let config):
  468. clientTransport = try NIOClientTransport(
  469. .http2NIOPosix(
  470. target: target,
  471. transportSecurity: config.security,
  472. config: config.transport
  473. )
  474. )
  475. #if canImport(Network)
  476. case .transportServices(let config):
  477. clientTransport = try NIOClientTransport(
  478. .http2NIOTS(target: target, transportSecurity: config.security, config: config.transport)
  479. )
  480. #endif
  481. }
  482. try await withGRPCClient(transport: clientTransport) { client in
  483. let control = ControlClient(wrapping: client)
  484. try await test(control)
  485. }
  486. }
  487. }
  488. private func executeUnaryRPC(control: ControlClient<NIOClientTransport>) async throws {
  489. let input = ControlInput.with { $0.numberOfMessages = 1 }
  490. let request = ClientRequest(message: input)
  491. try await control.unary(request: request) { response in
  492. #expect(throws: Never.self) { try response.message }
  493. }
  494. }
  495. }