Browse Source

Add tests to verify mTLS behaviour (#1307)

This adds some tests to check that configuring for mTLS behaves as expected.

The tests aim to show the following:
* Mutually trusted certs succeed
* Untrusted certs from either side results in failure
* Plaintext peer on either side results in failure

Other changes included in this PR:
* Not all certs in the sample data are actually generated in the `makecert` script (e.g. the `localhost` server cert).
* The `makecert` script and sample data were extended to include some certs signed by a different CA for mTLS failure mode tests.
Si Beaumont 4 years ago
parent
commit
b8fd052944

+ 244 - 158
Sources/GRPCSampleData/GRPCSwiftCertificate.swift

@@ -24,37 +24,58 @@ public struct SampleCertificate {
 
   public static let ca = SampleCertificate(
     certificate: try! NIOSSLCertificate(bytes: .init(caCert.utf8), format: .pem),
-    commonName: "foo",
-    // 22/07/2024 16:32:23
-    notAfter: Date(timeIntervalSince1970: 1_721_662_343.0)
+    commonName: "some-ca",
+    // Not After : Nov 12 13:06:40 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_400.0)
+  )
+
+  public static let otherCA = SampleCertificate(
+    certificate: try! NIOSSLCertificate(bytes: .init(otherCACert.utf8), format: .pem),
+    commonName: "some-other-ca",
+    // Not After : Nov 12 13:06:41 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_401.0)
   )
 
   public static let server = SampleCertificate(
     certificate: try! NIOSSLCertificate(bytes: .init(serverCert.utf8), format: .pem),
     commonName: "localhost",
-    // 22/07/2024 16:32:23
-    notAfter: Date(timeIntervalSince1970: 1_721_662_343.0)
+    // Not After : Nov 12 13:06:41 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_401.0)
   )
 
   public static let exampleServer = SampleCertificate(
     certificate: try! NIOSSLCertificate(bytes: .init(exampleServerCert.utf8), format: .pem),
     commonName: "example.com",
-    // 22/07/2024 16:43:12
-    notAfter: Date(timeIntervalSince1970: 1_721_662_992.0)
+    // Not After : Nov 12 13:06:41 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_401.0)
+  )
+
+  public static let serverSignedByOtherCA = SampleCertificate(
+    certificate: try! NIOSSLCertificate(bytes: .init(serverSignedByOtherCACert.utf8), format: .pem),
+    commonName: "localhost",
+    // Not After : Nov 12 13:06:41 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_401.0)
   )
 
   public static let client = SampleCertificate(
     certificate: try! NIOSSLCertificate(bytes: .init(clientCert.utf8), format: .pem),
     commonName: "localhost",
-    // 22/07/2024 16:32:23
-    notAfter: Date(timeIntervalSince1970: 1_721_662_343.0)
+    // Not After : Nov 12 13:06:41 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_401.0)
+  )
+
+  public static let clientSignedByOtherCA = SampleCertificate(
+    certificate: try! NIOSSLCertificate(bytes: .init(clientSignedByOtherCACert.utf8), format: .pem),
+    commonName: "localhost",
+    // Not After : Nov 12 13:06:41 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_401.0)
   )
 
   public static let exampleServerWithExplicitCurve = SampleCertificate(
     certificate: try! NIOSSLCertificate(bytes: .init(serverExplicitCurveCert.utf8), format: .pem),
     commonName: "localhost",
-    // 13/05/2021 12:32:03
-    notAfter: Date(timeIntervalSince1970: 1_620_909_123.0)
+    // Not After : Nov 12 13:06:41 2022 GMT
+    notAfter: Date(timeIntervalSince1970: 1_668_258_401.0)
   )
 }
 
@@ -83,201 +104,266 @@ public struct SamplePrivateKey {
 
 // MARK: - Certificates and private keys
 
-// NOTE: use the "makecerts" script in the scripts directory to generate new
+// NOTE: use the "makecert" script in the scripts directory to generate new
 // certificates and private keys when these expire.
 
 private let caCert = """
 -----BEGIN CERTIFICATE-----
-MIICmDCCAYACCQDdfOxq8GY7uzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANm
-b28wHhcNMTkwNzI0MTYzMjIzWhcNMjQwNzIyMTYzMjIzWjAOMQwwCgYDVQQDDANm
-b28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6sq2mHj6HhX9kaMEB
-39JT3QQoRJne/jELnLG7Z2tlKn1L4aSf5dYdBYK0OoPvko3VJtYIMK/7zl6LeEkB
-vJjVmDI/t/g4EjW1IaN369L3xnUh+1CeT63pgQ2WAMIFCQ6Sg0cK9Yma0QmvIzp7
-iPrYM4V7xKZMxSa+tNY2visaUFxsjY03ZAp8IrmmKvnfwGH4AjLTbmmJqR9Cx/0z
-QASravOvwKLFlor1v1ngK5HCnkgi+mZjHE161rbt/mR6KjgBxP4/xCZxc4RaiyUa
-DoTIOQ67wwkOd9SuBjLZ0snFTehoVPenlWlB6QfxglK/AMlaFwceKlAWH+AarGhZ
-7SZVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFXzkgWBmF5U98VdeC9oyH2hRSiu
-+SyKJ1dzxJaSRMxu/v0pnWXjlMiFbGOe5ioIR6uiF1ZI9nZIg7RZODrnA+KNLN5l
-jaDVQ1iHXE8yEjljgkxBaUbgiHHNVMNLNpBOZGvix/dIhgIEyVzNOHzQZbN9uYA7
-zBI9G9eZedZxCNBwBDJKcYGuFZ34wmEP5zZRlTgrbCWbpIMAp11TtJ/M0bJAME6l
-0c6uF6AJOvJ/ocB98FMNwVDaKo4rFYJIF+WNebi/6kV3KhafUnToOrUcQIBK7kX4
-rKSPSUzGCU9/oeLdKa6xXdrBa3ZhX7QEnkFme1OewSiD7VJYFWvOPrQXeDc=
+MIIC9zCCAd+gAwIBAgIJAMfc2gvFlVceMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV
+BAMMB3NvbWUtY2EwHhcNMjExMTEyMTMwNjQwWhcNMjIxMTEyMTMwNjQwWjASMRAw
+DgYDVQQDDAdzb21lLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+8Ko4QgNV2oiM2h/zQcgyQHF4eFpgU8h3XQuev7Hwa8ZkMFOgVDQANZJEhURNtLV0
+rFjft23uMet1tXrEnFBW2Lj05gIdkbsrZXVSyhRAZ+LKUyY5hCvXSy66JXQpkbQi
+UhpKyqni3GhIMTyYb7HMo8dA/0XNWl+I+DrsivBXiE2hD2Fq/0b+G4gOxGHujYRJ
+WGMm8uHA/B67sPej9V6yn7OKsqb9OEI/VE5IsfHumAR6HNV4KBHnlIqR+REzXcT1
+eqra8koM9A7un1SRkMoU4HyVdk4VAdXzAxZ6IiFtXzmQR4uwfVpQfiQ1bZEpVKFV
+kZusdmmoTvgfU1MQNZLj9QIDAQABo1AwTjAdBgNVHQ4EFgQU0qP3HeQn4dJyjUwN
+EKLs4fZY/8QwHwYDVR0jBBgwFoAU0qP3HeQn4dJyjUwNEKLs4fZY/8QwDAYDVR0T
+BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAlPbE2MXzO06GcRuTj8z6P7FMRSEV
+UrUvCkhHzSkJPYA2t3gsShbksNrj815LxQGu66QtuwqkL9Ey/K5pO/8XH00oR58H
+QkDcPuAoVac/8ezEc2z1aJ6FzvAwiKBJDkS6q3EYllGmLHFRBFbg0oewtppHZuv5
+6dVdra/4XH3KNMdSsdv8rKc/mAG34eRsT5UPNTuW0CBm2whfob4nq3sVwedh4/IU
+aWeKsutFYsrVC/ppA1H3ZUS/L9bEcpj3CmEdjRtX1wXN6yC2WesjwFOvYBZ9ENWL
+6p6Dk6yoQWwmoM9Y72MoWC5PMHc/4zkHNl4g6Fcbhv82prR5e2AzOaWB5w==
+-----END CERTIFICATE-----
+"""
+
+private let otherCACert = """
+-----BEGIN CERTIFICATE-----
+MIIDAzCCAeugAwIBAgIJAKXWyMK52taJMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
+BAMMDXNvbWUtb3RoZXItY2EwHhcNMjExMTEyMTMwNjQxWhcNMjIxMTEyMTMwNjQx
+WjAYMRYwFAYDVQQDDA1zb21lLW90aGVyLWNhMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAnwzOvLDX6wsyZRbX8LaVGgtmVEEft2BWF8V+/2gHo428g1ba
+YyHvMS8CJTOneIKB6HYlBEamB+wjnCFvWMz0eynzaT0HVJwhK5qhRYNZDr4ZtGFx
+ov0Xau+rS/YW7NMNKLgLDgHYLMBDWLnyfDy+VWBUlhwV1lzlk9xeZdHvFodNbjpC
+Mrw0gukfoFXvdvOqQSP4J/9TITv3jCY4gs4wZMFY/+XzpxQNEP5deEJGsH13PdXV
+hHxx11VP+ippkkcx7d11UP8UR9Y/RJM319tgOl91H7hTfLE/dn+N18yFGdW7fGW8
++MGV9tnk8K9JavnGOL9pYfjCMuZav3dMd1ObaQIDAQABo1AwTjAdBgNVHQ4EFgQU
+GJjjNxqJOPKpPAiKVIYT1TRhJYEwHwYDVR0jBBgwFoAUGJjjNxqJOPKpPAiKVIYT
+1TRhJYEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAA+zbg+pxa3lI
+W1lZcBb6fEiaK9sazyJfZ0vXBQmX3GuCQnWhTHmccvYQVAxqzQAwTfTEXfIu8nPr
+76j60zr/rTjDHsc0i/xMoaCAsWX7h/UcMYOsfgbKbFR7kvbf5n/2RdKbIbd4A2Og
+2sD8k7gqhBBsDgGbNsrIgzKoQYSrJOjTxTFlDAkG6gypRKqSzgiUvh+6wP1h4Jj7
+GpS82LHl1x3oXH/RJR3mWBy61VMbOHDc54lmbezs43WLOvnfimAvr7LfsfYSmp3O
+AlNpK9RvHP1Xfjx43l+Bb2EEJWAf/eBjWGpCcWz3EAl8k9W+lousBh9/wacdeLCV
+lNtTzgbeYA==
 -----END CERTIFICATE-----
 """
 
 private let serverCert = """
 -----BEGIN CERTIFICATE-----
-MIICljCCAX4CAQEwDQYJKoZIhvcNAQEFBQAwDjEMMAoGA1UEAwwDZm9vMB4XDTE5
-MDcyNDE2MzIyM1oXDTI0MDcyMjE2MzIyM1owFDESMBAGA1UEAwwJbG9jYWxob3N0
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2kGt8X2cNAmJTcPRfrL
-ksCrV3UvN04P9A7VPQeINC+f7IY+cZd3EMcuTleRcnl14pW2CCeDltihi4EQvkxp
-UxTj528mXh8P3nJ0wwIS8Dkrhdm8Ya1/UFHigLEaYaPJlUOzTPJqL5gFo8hQpB9N
-80bXbz0NPtN7yyIbrCCek4XoWxUM/Hjhr6pgt2nl5y5JYKwKlWq5lCIfyPWwM+fG
-Mo+RrUxDmKaCiorMJc5GOaF/X2BlR1TcRBz3N8Er9wP5kcmwsaXDar6YuNgLQ1Tb
-ZYRUanV+C3f2/ndwduzQtlNLmFq2Vr4lekjyCNuaLD+WIC7S+mtEUEp+S5qcPMpt
-rQIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAUXmufkCiMxXSQRl1lz21G+1mhnxM0
-fxEil43Eby5UYjyoAqTtrLmN3USKFVSpDvhzLbkAGEDeiM8GUdYF/nRVYuGnrpVX
-+AW29oNhgxbEg75P0AGu7TbM9nX2Ojm65ZIncKatopuqJbR9JgyMFgjc/H3HCGQT
-CcYNw8xzokuv0uHzQXtYok7AQ9JrUzIqzeuoJjOMiv2maIR0xKdS7nxyXpikgrMy
-IPk+M4Aat92k/5PIXZxTE1Zy8C2eFqwyDtITR7tVHCb1HtcOcNj6elxSPHlwR5wS
-vKLuveCELG7WZ8j8xOXLyPIAJ6Y0c7+5a/TPFxvkt1hGnqkbmsKZT3tv
+MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe
+Fw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMMCWxvY2Fs
+aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZbykEoZj1mdTRW
+ofPPmEmBNlPJcepamcKtBqsqjv4OBbWVFgMIlmNgRef7ZWlz/YHE3cu7dISYEZ2/
+C5zKgNGPiJFtiPC+GHKGPAbAHoxQa1kT204r5Hxpjr5iKAPVrHKilJO3wpF8Ins9
+G6AcR63lyppO/bpW6RxcF4fivcjiVvcq+TtBRrA3JROduQjjD6rYXuRHcrIq9Hc6
+CnUxQ1t1DCU7xqUGV8mdpJlI41NGqz0zoxFw/OKT2/3MR7mHGzAD/vHjIgKE0B5T
+9xPa75ur7Gi70T5jT/3jwFaVKCt4bQCIAREuUEtzbwxGYoXDkSodUS/KLfbycCgy
+u0wxOsECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAONUYolaHs2I3CVB9rCspr25I
+jxQfW2xzGuLsm91l9GxvtDS89wEu15zhFDsQPR6u9br6qENfMVDiOa36wCmtvjxJ
+2vYzWiWBVoSEBvwIkWMg/qlmdNB2lRxj/WgjcGNtUJwbe5Ex9ldquspoQNfEvwjz
+KeKnDCJ8oit6IJvkdG0crowReX9w3fOFiARp87J/NoQlmm1hiY3FD0nQ3wvBhtby
+0svSQSwG244B9TlzLBwEzRvC8+qLIP1LSjBXg3nXZCfkV6or/B2+gOl9uxB76O2h
+KYJqo2GeZKYQHF5Lco7DYfzJcL7IeDNV2yeLk/3i7e6OoLcMr8G4aJ645em+WQ==
+-----END CERTIFICATE-----
+"""
+
+private let serverSignedByOtherCACert = """
+-----BEGIN CERTIFICATE-----
+MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl
+ci1jYTAeFw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMM
+CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZbykEo
+Zj1mdTRWofPPmEmBNlPJcepamcKtBqsqjv4OBbWVFgMIlmNgRef7ZWlz/YHE3cu7
+dISYEZ2/C5zKgNGPiJFtiPC+GHKGPAbAHoxQa1kT204r5Hxpjr5iKAPVrHKilJO3
+wpF8Ins9G6AcR63lyppO/bpW6RxcF4fivcjiVvcq+TtBRrA3JROduQjjD6rYXuRH
+crIq9Hc6CnUxQ1t1DCU7xqUGV8mdpJlI41NGqz0zoxFw/OKT2/3MR7mHGzAD/vHj
+IgKE0B5T9xPa75ur7Gi70T5jT/3jwFaVKCt4bQCIAREuUEtzbwxGYoXDkSodUS/K
+LfbycCgyu0wxOsECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAb195ecB8D8P20X3j
+Sc1dHF5s5845aTOB+wYeFVFKVWFbRJwx7x2qpUXv4KqYrHNNruQKukmFTIA1pBHC
+Ejdr5PDudUoxLwZE43PrpjxqhdV8bgXogB13xTEJwCkpjj3b9BNsiL67n4B3BAzy
+aXOiZJ7tPYmB9Fpxom4W6Iq0uc0n1UShbxZerAuBet0pYkmsoMPVupnIH8TqZbIL
+6Jht6iodCjf+WP7hgK4nXEXVd0SFj9mjpWTaz/PPfv/hTd53K1kZE13VrEifi3C7
+HrCPXcACcUohrXZJW1764yuODuQKpleBjBt+QvlhO54pBBXdP3F+h/FirQIrymA7
+BBG4rg==
 -----END CERTIFICATE-----
 """
 
 private let serverKey = """
 -----BEGIN RSA PRIVATE KEY-----
-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
+MIIEogIBAAKCAQEAplvKQShmPWZ1NFah88+YSYE2U8lx6lqZwq0GqyqO/g4FtZUW
+AwiWY2BF5/tlaXP9gcTdy7t0hJgRnb8LnMqA0Y+IkW2I8L4YcoY8BsAejFBrWRPb
+TivkfGmOvmIoA9WscqKUk7fCkXwiez0boBxHreXKmk79ulbpHFwXh+K9yOJW9yr5
+O0FGsDclE525COMPqthe5Edysir0dzoKdTFDW3UMJTvGpQZXyZ2kmUjjU0arPTOj
+EXD84pPb/cxHuYcbMAP+8eMiAoTQHlP3E9rvm6vsaLvRPmNP/ePAVpUoK3htAIgB
+ES5QS3NvDEZihcORKh1RL8ot9vJwKDK7TDE6wQIDAQABAoIBABWH17trISBdPFoT
+xE4r1gfdY0ygy8+K/k+F2VEZ5vvWkMKZkwm9eMlP0nxduxhU3MCI3DPcBQ6MJ+uE
+qFoYk2eL7h70UD7oO33HBcnR36JFXj9fJIkPgTjg6IqXZZppczI6/IPJyrLNoCDX
+HdYxEs3c6cXi50/Qo8b53EnH/Mwc2bS8HSAkL7sYd5+AKjfe2XlIt395nA6Vbpex
+SRnGmWnfkyo/PdHyNd4WNjhDn4zW3rlJtFO6z/Rf7DZgU+utETf4CEDH3Bfm0hqV
+bAl8zyuytpGWv/2e0eyvQ6dEQZLNI2RAuid5M+FjT5u5fBOpggaqGe1L7SfI5KiO
+E1SiQAECgYEA1d5g2if0ZOLg3dG+tzYXscss62o5dhW4ULa5VzVZ9B0AX+CPBJCj
+2uSpVbhibrCljzgoYdL4+sZw6955pieXBFrPMgzfR4sjfK5z9O9GerMFecUr3+i4
+XLgYCzrmoSytNem64mV/H5WauR6Ob0UlDYrLDWfer1OMHClplDsVdoECgYEAxyFt
+SYDCabN+4Yvh+TC/X80h65f2GNrwZqFrXjnfyOv4SyY9U2XvMy6YTjM9gTy5RJQ2
+XaG7Upk2auRU6n0VL311/GA2eL7iBRqJL/3NHibT5KfeXfoiAAYyjlbHrGN1QJWe
+3o8XdPaUh9ccGT8fZhl+Eg9VeorUmSCwC4UUJEECgYA8/CyiCMKoAgodNrIrjEE1
+cbpdZuz7vzXPzksLkysTcTGqJV6i7pvKz2l6CBoJdlW/gUQCoSZeXDfXCpmlx6RI
+mZx7qTACNqrn4tcuAQ0X7/SfxJm+P55S0iwJB8K8MwExXnTsGgUl/IMiRpRXJmBq
+fClqqTPWyvwpC6YPnsmAAQKBgHLt3wa6Uvrwxz1kH9NUCFBBs98nALnNu0xww+hJ
+XNi5IMA23NRCk/ElZnBT8J6jroZfSJV34AbHOPouuLfx44VaUvuLiETeXtL1QtK5
+GGbboBZrsNLqqC79ZLZ0baAYczcIY/4t9irimk1goO4NWZDzC6lewkYM1LFghVrQ
+vxRBAoGAEDEh/IUmsYjyPVVDm+nV4/6ODtnhqBpBpAGo0JL3FJ7XEOR3byH4DXQG
+Rdz6haHMfQQqBfiPOk9HZ+Y2WO+PHJ3OrM6wyzZUXKZr7r4im5hkisITVbU/cIdl
+FqOsrd5SuS98XIo05JKlWbPFLgzgYtnx2zLiU8ATyJHIF6LrbwY=
 -----END RSA PRIVATE KEY-----
 """
 
 private let exampleServerCert = """
 -----BEGIN CERTIFICATE-----
-MIICmDCCAYACAQEwDQYJKoZIhvcNAQEFBQAwDjEMMAoGA1UEAwwDZm9vMB4XDTE5
-MDcyNDE2NDMxMloXDTI0MDcyMjE2NDMxMlowFjEUMBIGA1UEAwwLZXhhbXBsZS5j
-b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC85TeHIfnz8XeWVAYQ
-NNPNZt1BeWfYSe90PbYhBEVjtQPLWTlDGjVtcWdVcFO1uIaZPrKmtDIrgi6vWIhq
-VsW+LHKZW5lZgVzD/pIKOOAkgurxubGIR3E5O9f7qwHTM0Dv2jxYCtIujhK+K6C3
-o9nD5GsQBLE5qn/K5DkPKhCIgvnmR1C+Mvaz4IxbkgPBRT73bCr48qqgOaQ0I4Tv
-eOzu6uRKf/nwTpPGX9fjTwOb2Nu/oh2w0juFdEO3ZXMAN1F5Nn6w7zre2qR03rw1
-Mm+q/8aXDzwlzjb3Q1TGoJx2Bgrj8Q1vUcWq8/NMoGVyTHCK4qhBk123xVRSYjmv
-ANyxAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBADpw4EOj4JNn8ltTTlJfuJKh7Gor
-9R7xuvDC0M81814g0LOKTOegqtPV7ezYobQ+QGvfmBzLKke7boyYDTPeUo9Wx4g4
-auRFXPWF3QKSvJFF4tPxZe0LXQ0nJFnnnYqqfT+3tro8BktQLeo7TUGLsQpPf5By
-4Zx9NIjOCfOrLxg/zX+P8QgnSl3/k/X84LOZMV/oydajxFclE7h1YXTE8AKMRMff
-iagsSobYwSoKTitC5EoJgdOB1UHMIR6PHNSgA6K+JWJkAoN+1EA12SeyWGJIt6wU
-SroyIgFyKQBEzxlpBcoENKtPsn1jdrV+Qi8nfGea4ddsYnLdZHwX5NslBkI=
+MIICnDCCAYQCAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe
+Fw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBYxFDASBgNVBAMMC2V4YW1w
+bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5eeBEXc8K/Tg
+YkfueXOlX3tZU45ooudYeBMyHPLG/2j1bRxLEHTPQ/1SrSKaTZH+s+KdKTtD7i7t
+6SEatnMZi01NzWMA4etARx3cOMtDtZIoA8w6pgyxFVY8iOlehrQrPoOvZrZc1LJl
+7xK6xZQR3v1t/68fhQDU9uOU6dNJgvyjMsrhOuJ6DPHmOAe1pUUVu+/7SCwCd4of
+Vsq7pfahQhMQK5w8IUtOlqd4xg5tgTGWIPFmppVACYSOpDVDQM30KH8I1W7idhH8
+EAyyTEbJx9B1wwi7PXd4u3cCeZRrSAgwEpRAYS/ZznCnXuZ5fafcvUO3X3gB7jAS
+XRPrm7/EMQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA12dbCAmQBefUQ5PkjDu02
+pO+ajryeivexwM4mpn8u9IIANQKu7E1oJYfj3zflpIxly3DebuoHV7zdFzfDzdu4
+LIQIEqqqDLsBA2gvk25INMgXx9rtA1fmL3pB4CDUoBGHkprJHdni6Z397sZIe0Bl
+vOca55RbpZ4kj8qEUxI2WrSVtotRuQLS6KuqGEFeZS4+31kP6RVD7H3ZlO0gevNM
+TgIHZknzu5Lvhdz5jx6PO/3RRwC3V5UB/mHnY6f8QS0M2c2gpS8nZUNpuN6dkRHX
+bcEVVDiWHZnLwA8Ugts2m3eNXyPgmXSgFW6Be9FGYwuTCh6mvXR2Zf5pS+wAndBg
 -----END CERTIFICATE-----
 """
 
 private let exampleServerKey = """
 -----BEGIN RSA PRIVATE KEY-----
-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
+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
 -----END RSA PRIVATE KEY-----
 """
 
 private let clientCert = """
 -----BEGIN CERTIFICATE-----
-MIICljCCAX4CAQEwDQYJKoZIhvcNAQEFBQAwDjEMMAoGA1UEAwwDZm9vMB4XDTE5
-MDcyNDE2MzIyM1oXDTI0MDcyMjE2MzIyM1owFDESMBAGA1UEAwwJbG9jYWxob3N0
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvZguzd7SpvleMWDBXS8Q
-Ugc/uE9nyCKDUHG6OgTZOeM6W16CfoKE2UI1lXAyL5V/FjOctzSVMXfMhbYT/Mw/
-8RvrMyKGdJqf1j6OP6ziJpbWT/hAlFK143nB5zR/RxVTlUcE+Cq5IMkvsjL0QD2p
-vQZ/eSRic3SWAfS2OnJI3xqhNipf3sIuDR7xVeUUVKxWAVSXGnjB6CBkUnLX4doh
-PTtU8QKToCrWfMLTon4XOPTq++IPifRZ7Ct8gR8munE266Hz5dmVuFAPMuqt/LmE
-UUcNv/sZXNyVjhx9AfKatstH6i7n4opBMyq1JBFsIpJHlzp1tR7rTRKlyfUwpVk9
-HwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQCoRe9bTYTgz+NduY64rmuvSCjvUvr+
-2OlNFBp/6ZJzKR1vk2ALrbvPDBF+L4zoKNodlKyy3ejaeNPij/XsZzvReh+kyzXu
-Xo0a6koUxrrYRW8YKgOCEnsGKc6zXVe4bpT7sAf5+dLPIEI5qIImeQGDfkkwkgWz
-pM2/9HyNC+pahmM2+IOZOuCemo5cpZeruH3HVjoY4dsNnqO1QKKlk8LYhU+CY0mK
-m01QXLslXNMYx7sZr3IMl5A9EUQfUUE1y+b4nD9sj1bL2hosP2TXwBnlaPM4O8cS
-oyQpD9JXYI7yAoYLziq0aE0BGAJ8++bqaIoj7nVc1/HGPX/LnHc/VyTV
+MIICmjCCAYICAQEwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHc29tZS1jYTAe
+Fw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMMCWxvY2Fs
+aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+3lfFuI8LRc31C
+qzBJw60JEJMohIXRdoNYVmNnTSd1LQHaCd/qM5J2QU7LtMpAfJ29DyT0Q3Z67VMG
+Bgz2kcx5lKGJpNfT7BJcB2F+Em1wgPj/N/wrHzQS+hBrnRokt0buyrqAaEsRRU6O
+8g6trnda9+h7/5ktQpHkNpitJXWYyykyAP97NjYS1wDsSORbAYIbFIBuGoPQeiDW
+AQVja9VTTCqEdYsShsUjXRGUPgBEk9i+IIqZg3eLZ26dEfVx0f5vGrmWnVDKCSNK
+TzHL7p6mO8NL0k8CrTZwHYvfgUjVgZu43L6GIYiG3PTZhsHCZZgXZ4hUuRWrwTeJ
+6wx6u8sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJfnPkN+JURx7aYnyBisPFD0s
+wdF9XC6/Oxxe9GZ3aaLcNsf6Cwshd6sI/7FKOdP3EVtixcrCMupH16cN1sVw4UC8
+o8iv7G8h9XeY3DrzpOaAm1xxnRAox4AjY9Hkz2FzKyywCFNfj7I/vuPivdi1xk4v
+tWGPXQvMmVhl9bP5FVEbK4w0MFIMkV0XnXuKffh+6lKS2hZhVejQ56S4hRMYT7Qp
+wLDLwn5b2fYUaPKwjeoj1RA6tuagPnzVbvwGMrViQiwSqDgv2X7BxMP/5IxsWxlr
+Ar/Yo+UQbilfT7pJWKvuRomZ+h+lg63AW1YmoLMkGV0rQL3qTpVypIRiJTdPvQ==
+-----END CERTIFICATE-----
+"""
+
+private let clientSignedByOtherCACert = """
+-----BEGIN CERTIFICATE-----
+MIICoDCCAYgCAQEwDQYJKoZIhvcNAQELBQAwGDEWMBQGA1UEAwwNc29tZS1vdGhl
+ci1jYTAeFw0yMTExMTIxMzA2NDFaFw0yMjExMTIxMzA2NDFaMBQxEjAQBgNVBAMM
+CWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+3lfFu
+I8LRc31CqzBJw60JEJMohIXRdoNYVmNnTSd1LQHaCd/qM5J2QU7LtMpAfJ29DyT0
+Q3Z67VMGBgz2kcx5lKGJpNfT7BJcB2F+Em1wgPj/N/wrHzQS+hBrnRokt0buyrqA
+aEsRRU6O8g6trnda9+h7/5ktQpHkNpitJXWYyykyAP97NjYS1wDsSORbAYIbFIBu
+GoPQeiDWAQVja9VTTCqEdYsShsUjXRGUPgBEk9i+IIqZg3eLZ26dEfVx0f5vGrmW
+nVDKCSNKTzHL7p6mO8NL0k8CrTZwHYvfgUjVgZu43L6GIYiG3PTZhsHCZZgXZ4hU
+uRWrwTeJ6wx6u8sCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAlbFNTPHLbJABMLy2
+DwfznwxAwkCfyAdZWq7A5gLP3GhY5xafc3HdeJ6gOfeXH9v1y3DEI6jeAXxmXAAJ
+4hOIQuVfLL76PIkAwdHRXYrzLdTiN+pGNGVBRiOZoXIzGcdC4k+2KgeHGyVu5bb4
+EDFeeiuHMaeNlBEeIrUH38AsTm8hruVPuqnm0WAZVJ5RvWPOJgv667P6B/le+U5O
+z5F11zPr47l69Zlh5Ud+WlG3yymj2ZIibuqdcQC9iuiLFcig6PhBheJzxr7MCvcA
+T8T/DZusQBdcHqVrMbBfSnL426Kunqd8AXWEz09o5oTkeyK/CNWFyMfPfJqEOTWs
+tHgfiw==
 -----END CERTIFICATE-----
 """
 
 private let clientKey = """
 -----BEGIN RSA PRIVATE KEY-----
-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
+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
 -----END RSA PRIVATE KEY-----
 """
 
 private let serverExplicitCurveCert = """
 -----BEGIN CERTIFICATE-----
-MIICEDCCAbYCCQDOr0V8CUAs8TAKBggqhkjOPQQDAjAWMRQwEgYDVQQDDAtleGFt
-cGxlLmNvbTAeFw0yMDA1MTMxMjMyMDNaFw0yMTA1MTMxMjMyMDNaMBYxFDASBgNV
-BAMMC2V4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0B
-AQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAAB
-AAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxT
-sPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vObl
-Y6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBo
-N79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABChr
-XwTLM3T1C0aA+8pJMVJOyVDP0Scd38OdqBISYvHLaNPRuIaMFA2KTE25pMqsqNe9
-YNfgimABp6HUG7xKTMwwCgYIKoZIzj0EAwIDSAAwRQIhAM6ihMqgQ3Rr/w7oBhG6
-uuA2+wn2KhZgSqgqTTtyo/ImAiBLrG/b76/7eaZ4t6xWHtKWH4y2e1zrxLDDpcjD
-0zglag==
+MIICZzCCAg2gAwIBAgIJANBDrZOOttwWMAoGCCqGSM49BAMCMBYxFDASBgNVBAMM
+C2V4YW1wbGUuY29tMB4XDTIxMTExMjEzMDY0MVoXDTIyMTExMjEzMDY0MVowFjEU
+MBIGA1UEAwwLZXhhbXBsZS5jb20wggFLMIIBAwYHKoZIzj0CATCB9wIBATAsBgcq
+hkjOPQEBAiEA/////wAAAAEAAAAAAAAAAAAAAAD///////////////8wWwQg////
+/wAAAAEAAAAAAAAAAAAAAAD///////////////wEIFrGNdiqOpPns+u9VXaYhrxl
+HQawzFOw9jvOPD4n0mBLAxUAxJ02CIbnBJNqZnjhE50mt4GffpAEQQRrF9Hy4SxC
+R/i85uVjpEDydwN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFe
+zsu2QGg3v1H1AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQED
+QgAE+YMhPzGDLJuuMxB+ICQKWvPjbTEsRXgbq3Hmrds6x/1QY14e/xh4TqpAEW6M
+0R5xhqg7ZU7O8NHtuiCyxPzCoqNQME4wHQYDVR0OBBYEFD22/486iAai5jKBCttn
+JstzA2u8MB8GA1UdIwQYMBaAFD22/486iAai5jKBCttnJstzA2u8MAwGA1UdEwQF
+MAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgHwZi/Vk9odNqrae9LBxmt/ve4twT8JT0
++CvvSVkNtnkCIQDcKZBIlsR1OVVxZDWGtcvz9oW+MMOrrbFYAIaf0akhWQ==
 -----END CERTIFICATE-----
 """
 
 private let serverExplicitCurveKey = """
 -----BEGIN EC PRIVATE KEY-----
-MIIBaAIBAQQgZeJYnJVaOdltFsUs6KatYy9XFmX6ujfUSkOR69RoyRWggfowgfcC
+MIIBaAIBAQQgxqYoMA6z4uJGgk/XIg1R2j8qF/Sv/g38YJG81dGGq4SggfowgfcC
 AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
 MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
 vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
 axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
 K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
-YyVRAgEBoUQDQgAEKGtfBMszdPULRoD7ykkxUk7JUM/RJx3fw52oEhJi8cto09G4
-howUDYpMTbmkyqyo171g1+CKYAGnodQbvEpMzA==
+YyVRAgEBoUQDQgAE+YMhPzGDLJuuMxB+ICQKWvPjbTEsRXgbq3Hmrds6x/1QY14e
+/xh4TqpAEW6M0R5xhqg7ZU7O8NHtuiCyxPzCog==
 -----END EC PRIVATE KEY-----
 """

+ 10 - 1
Tests/GRPCTests/BasicEchoTestCase.swift

@@ -87,12 +87,21 @@ class EchoTestCaseBase: GRPCTestCase {
     case .none:
       return Server.insecure(group: self.serverEventLoopGroup)
 
-    case .anonymousClient, .mutualAuthentication:
+    case .anonymousClient:
       return Server.usingTLSBackedByNIOSSL(
         on: self.serverEventLoopGroup,
         certificateChain: [SampleCertificate.server.certificate],
         privateKey: SamplePrivateKey.server
       ).withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
+
+    case .mutualAuthentication:
+      return Server.usingTLSBackedByNIOSSL(
+        on: self.serverEventLoopGroup,
+        certificateChain: [SampleCertificate.server.certificate],
+        privateKey: SamplePrivateKey.server
+      )
+      .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
+      .withTLS(certificateVerification: .noHostnameVerification)
     }
   }
 

+ 269 - 0
Tests/GRPCTests/MutualTLSTests.swift

@@ -0,0 +1,269 @@
+/*
+ * Copyright 2021, 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 EchoImplementation
+import EchoModel
+@testable import GRPC
+import GRPCSampleData
+import NIOCore
+import NIOPosix
+import NIOSSL
+import XCTest
+
+class MutualTLSTests: GRPCTestCase {
+  enum ExpectedClientError {
+    case handshakeError
+    case alertCertRequired
+    case dropped
+  }
+
+  var clientEventLoopGroup: EventLoopGroup!
+  var serverEventLoopGroup: EventLoopGroup!
+  var channel: GRPCChannel?
+  var server: Server?
+
+  override func setUp() {
+    super.setUp()
+    self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+    self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+  }
+
+  override func tearDown() {
+    XCTAssertNoThrow(try self.channel?.close().wait())
+    XCTAssertNoThrow(try self.server?.close().wait())
+    XCTAssertNoThrow(try self.clientEventLoopGroup.syncShutdownGracefully())
+    XCTAssertNoThrow(try self.serverEventLoopGroup.syncShutdownGracefully())
+    super.tearDown()
+  }
+
+  func performTestWith(
+    _ serverTLSConfiguration: GRPCTLSConfiguration?,
+    _ clientTLSConfiguration: GRPCTLSConfiguration?,
+    expectServerHandshakeError: Bool,
+    expectedClientError: ExpectedClientError?
+  ) throws {
+    // Setup the server.
+    var serverConfiguration = Server.Configuration.default(
+      target: .hostAndPort("localhost", 0),
+      eventLoopGroup: self.serverEventLoopGroup,
+      serviceProviders: [EchoProvider()]
+    )
+    serverConfiguration.tlsConfiguration = serverTLSConfiguration
+    serverConfiguration.logger = self.serverLogger
+    let serverErrorExpectation = self.expectation(description: "server error")
+    serverErrorExpectation.isInverted = !expectServerHandshakeError
+    serverErrorExpectation.assertForOverFulfill = false
+    let serverErrorDelegate = ServerErrorRecordingDelegate(expectation: serverErrorExpectation)
+    serverConfiguration.errorDelegate = serverErrorDelegate
+
+    self.server = try! Server.start(configuration: serverConfiguration).wait()
+
+    let port = self.server!.channel.localAddress!.port!
+
+    // Setup the client.
+    var clientConfiguration = ClientConnection.Configuration.default(
+      target: .hostAndPort("localhost", port),
+      eventLoopGroup: self.clientEventLoopGroup
+    )
+    clientConfiguration.tlsConfiguration = clientTLSConfiguration
+    clientConfiguration.connectionBackoff = nil
+    clientConfiguration.backgroundActivityLogger = self.clientLogger
+    let clientErrorExpectation = self.expectation(description: "client error")
+    switch expectedClientError {
+    case .none:
+      clientErrorExpectation.isInverted = true
+    case .handshakeError, .alertCertRequired:
+      // After the SSL error, the connection being closed also presents as an error.
+      clientErrorExpectation.expectedFulfillmentCount = 2
+    case .dropped:
+      clientErrorExpectation.expectedFulfillmentCount = 1
+    }
+    let clientErrorDelegate = ErrorRecordingDelegate(expectation: clientErrorExpectation)
+    clientConfiguration.errorDelegate = clientErrorDelegate
+
+    self.channel = ClientConnection(configuration: clientConfiguration)
+    let client = Echo_EchoClient(channel: channel!)
+
+    // Make the call.
+    let call = client.get(.with { $0.text = "mumble" })
+
+    // Wait for side effects.
+    self.wait(for: [clientErrorExpectation, serverErrorExpectation], timeout: 10)
+
+    if !expectServerHandshakeError {
+      XCTAssert(
+        serverErrorDelegate.errors.isEmpty,
+        "Unexpected server errors: \(serverErrorDelegate.errors)"
+      )
+    } else if case .handshakeFailed = serverErrorDelegate.errors.first as? NIOSSLError {
+      // This is the expected error.
+    } else {
+      XCTFail(
+        "Expected NIOSSLError.handshakeFailed, actual error(s): \(serverErrorDelegate.errors)"
+      )
+    }
+
+    switch expectedClientError {
+    case .none:
+      XCTAssert(
+        clientErrorDelegate.errors.isEmpty,
+        "Unexpected client errors: \(clientErrorDelegate.errors)"
+      )
+    case .some(.handshakeError):
+      if case .handshakeFailed = clientErrorDelegate.errors.first as? NIOSSLError {
+        // This is the expected error.
+      } else {
+        XCTFail(
+          "Expected NIOSSLError.handshakeFailed, actual error(s): \(clientErrorDelegate.errors)"
+        )
+      }
+    case .some(.alertCertRequired):
+      if let error = clientErrorDelegate.errors.first, error is BoringSSLError {
+        // This is the expected error when client receives TLSV1_ALERT_CERTIFICATE_REQUIRED.
+      } else {
+        XCTFail("Expected BoringSSLError, actual error(s): \(clientErrorDelegate.errors)")
+      }
+    case .some(.dropped):
+      if let error = clientErrorDelegate.errors.first as? GRPCStatus, error.code == .unavailable {
+        // This is the expected error when client closes the connection.
+      } else {
+        XCTFail("Expected BoringSSLError, actual error(s): \(clientErrorDelegate.errors)")
+      }
+    }
+
+    if !expectServerHandshakeError, expectedClientError == nil {
+      // Verify response.
+      let response = try call.response.wait()
+      XCTAssertEqual(response.text, "Swift echo get: mumble")
+      let status = try call.status.wait()
+      XCTAssertEqual(status.code, .ok)
+    }
+  }
+
+  func test_trustedClientAndServerCerts_success() throws {
+    let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.server.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.server),
+      trustRoots: .certificates([
+        SampleCertificate.ca.certificate,
+        SampleCertificate.otherCA.certificate,
+      ]),
+      certificateVerification: .noHostnameVerification
+    )
+    let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.client),
+      trustRoots: .certificates([
+        SampleCertificate.ca.certificate,
+        SampleCertificate.otherCA.certificate,
+      ]),
+      certificateVerification: .fullVerification
+    )
+    try self.performTestWith(
+      serverTLSConfiguration,
+      clientTLSConfiguration,
+      expectServerHandshakeError: false,
+      expectedClientError: nil
+    )
+  }
+
+  func test_untrustedServerCert_clientError() throws {
+    let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.server.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.server),
+      trustRoots: .certificates([
+        SampleCertificate.ca.certificate,
+        SampleCertificate.otherCA.certificate,
+      ]),
+      certificateVerification: .noHostnameVerification
+    )
+    let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.client),
+      trustRoots: .certificates([
+        SampleCertificate.otherCA.certificate,
+      ]),
+      certificateVerification: .fullVerification
+    )
+    try self.performTestWith(
+      serverTLSConfiguration,
+      clientTLSConfiguration,
+      expectServerHandshakeError: true,
+      expectedClientError: .handshakeError
+    )
+  }
+
+  func test_untrustedClientCert_serverError() throws {
+    let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.server.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.server),
+      trustRoots: .certificates([
+        SampleCertificate.ca.certificate,
+      ]),
+      certificateVerification: .noHostnameVerification
+    )
+    let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.client),
+      trustRoots: .certificates([
+        SampleCertificate.ca.certificate,
+        SampleCertificate.otherCA.certificate,
+      ]),
+      certificateVerification: .fullVerification
+    )
+    try self.performTestWith(
+      serverTLSConfiguration,
+      clientTLSConfiguration,
+      expectServerHandshakeError: true,
+      expectedClientError: .alertCertRequired
+    )
+  }
+
+  func test_plaintextServer_clientError() throws {
+    let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.clientSignedByOtherCA.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.client),
+      trustRoots: .certificates([
+        SampleCertificate.ca.certificate,
+        SampleCertificate.otherCA.certificate,
+      ]),
+      certificateVerification: .fullVerification
+    )
+    try self.performTestWith(
+      nil,
+      clientTLSConfiguration,
+      expectServerHandshakeError: false,
+      expectedClientError: .handshakeError
+    )
+  }
+
+  func test_plaintextClient_serverError() throws {
+    let serverTLSConfiguration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL(
+      certificateChain: [.certificate(SampleCertificate.server.certificate)],
+      privateKey: .privateKey(SamplePrivateKey.server),
+      trustRoots: .certificates([
+        SampleCertificate.ca.certificate,
+        SampleCertificate.otherCA.certificate,
+      ]),
+      certificateVerification: .noHostnameVerification
+    )
+    try self.performTestWith(
+      serverTLSConfiguration,
+      nil,
+      expectServerHandshakeError: true,
+      expectedClientError: .dropped
+    )
+  }
+}

+ 24 - 14
scripts/makecert

@@ -7,31 +7,41 @@
 #
 # https://github.com/grpc/grpc-java/tree/master/examples
 #
-SIZE=2048
 
-CN_CA=foo
-CN_SERVER=example.com
-CN_CLIENT=localhost
+set -euo pipefail
+
+SIZE=2048
 
 # CA
 openssl genrsa -out ca.key $SIZE
-openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=${CN_CA}"
+openssl req -new -x509 -days 365 -key ca.key -out ca.crt -subj "/CN=some-ca"
+
+# Other CA
+openssl genrsa -out other-ca.key $SIZE
+openssl req -new -x509 -days 365 -key other-ca.key -out other-ca.crt -subj "/CN=some-other-ca"
+
+# Server certs (localhost)
+openssl genrsa -out server-localhost.key $SIZE
+openssl req -new -key server-localhost.key -out server-localhost.csr -subj "/CN=localhost"
+openssl x509 -req -days 365 -in server-localhost.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server-localhost.crt
+openssl x509 -req -days 365 -in server-localhost.csr -CA other-ca.crt -CAkey other-ca.key -set_serial 01 -out server-localhost-other-ca.crt
 
-# Server
-openssl genrsa -out server.key $SIZE
-openssl req -new -key server.key -out server.csr -subj "/CN=${CN_SERVER}"
-openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt
+# Server certs (example.com)
+openssl genrsa -out server-example.com.key $SIZE
+openssl req -new -key server-example.com.key -out server-example.com.csr -subj "/CN=example.com"
+openssl x509 -req -days 365 -in server-example.com.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server-example.com.crt
 
-# Client
+# Client certs (localhost)
 openssl genrsa -out client.key $SIZE
-openssl req -new -key client.key -out client.csr -subj "/CN=${CN_CLIENT}"
+openssl req -new -key client.key -out client.csr -subj "/CN=localhost"
 openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt
+openssl x509 -req -days 365 -in client.csr -CA other-ca.crt -CAkey other-ca.key -set_serial 01 -out client-other-ca.crt
 
 # netty only supports PKCS8 keys. openssl is used to convert from PKCS1 to PKCS8
 # http://netty.io/wiki/sslcontextbuilder-and-private-key.html
 openssl pkcs8 -topk8 -nocrypt -in client.key -out client.pem
-openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pem
+openssl pkcs8 -topk8 -nocrypt -in server-example.com.key -out server.pem
 
 # Server cert with explicit EC parameters (not supported)
-openssl ecparam -name prime256v1 -genkey -param_enc explicit -out server-explicit.key
-openssl req -new -x509 -days 365 -key server-explicit.key -out server-explicit.crt -subj "/CN=${CN_SERVER}"
+openssl ecparam -name prime256v1 -genkey -param_enc explicit -out server-explicit-ec.key
+openssl req -new -x509 -days 365 -key server-explicit-ec.key -out server-explicit-ec.crt -subj "/CN=example.com"