1 #region Copyright notice and license 2 3 // Copyright 2015-2016 gRPC authors. 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 #endregion 18 19 using System; 20 using System.Collections.Generic; 21 using System.IO; 22 using System.Linq; 23 using System.Threading; 24 using System.Threading.Tasks; 25 using Google.Protobuf; 26 using Grpc.Core; 27 using Grpc.Core.Utils; 28 using Grpc.Testing; 29 using NUnit.Framework; 30 31 namespace Grpc.IntegrationTesting 32 { 33 /// <summary> 34 /// Test SSL credentials where server authenticates client 35 /// and client authenticates the server. 36 /// </summary> 37 public class SslCredentialsTest 38 { 39 const string Host = "localhost"; 40 const string IsPeerAuthenticatedMetadataKey = "test_only_is_peer_authenticated"; 41 Server server; 42 Channel channel; 43 TestService.TestServiceClient client; 44 45 string rootCert; 46 KeyCertificatePair keyCertPair; 47 InitClientAndServer(bool clientAddKeyCertPair, SslClientCertificateRequestType clientCertRequestType, VerifyPeerCallback verifyPeerCallback = null)48 public void InitClientAndServer(bool clientAddKeyCertPair, 49 SslClientCertificateRequestType clientCertRequestType, 50 VerifyPeerCallback verifyPeerCallback = null) 51 { 52 rootCert = File.ReadAllText(TestCredentials.ClientCertAuthorityPath); 53 keyCertPair = new KeyCertificatePair( 54 File.ReadAllText(TestCredentials.ServerCertChainPath), 55 File.ReadAllText(TestCredentials.ServerPrivateKeyPath)); 56 57 var serverCredentials = new SslServerCredentials(new[] { keyCertPair }, rootCert, clientCertRequestType); 58 var clientCredentials = new SslCredentials(rootCert, clientAddKeyCertPair ? keyCertPair : null, verifyPeerCallback); 59 60 // Disable SO_REUSEPORT to prevent https://github.com/grpc/grpc/issues/10755 61 server = new Server(new[] { new ChannelOption(ChannelOptions.SoReuseport, 0) }) 62 { 63 Services = { TestService.BindService(new SslCredentialsTestServiceImpl()) }, 64 Ports = { { Host, ServerPort.PickUnused, serverCredentials } } 65 }; 66 server.Start(); 67 68 var options = new List<ChannelOption> 69 { 70 new ChannelOption(ChannelOptions.SslTargetNameOverride, TestCredentials.DefaultHostOverride) 71 }; 72 73 channel = new Channel(Host, server.Ports.Single().BoundPort, clientCredentials, options); 74 client = new TestService.TestServiceClient(channel); 75 } 76 77 [OneTimeTearDown] Cleanup()78 public void Cleanup() 79 { 80 if (channel != null) 81 { 82 channel.ShutdownAsync().Wait(); 83 } 84 if (server != null) 85 { 86 server.ShutdownAsync().Wait(); 87 } 88 } 89 90 [Test] NoClientCert_DontRequestClientCertificate_Accepted()91 public async Task NoClientCert_DontRequestClientCertificate_Accepted() 92 { 93 InitClientAndServer( 94 clientAddKeyCertPair: false, 95 clientCertRequestType: SslClientCertificateRequestType.DontRequest); 96 97 await CheckAccepted(expectPeerAuthenticated: false); 98 } 99 100 [Test] ClientWithCert_DontRequestClientCertificate_AcceptedButPeerNotAuthenticated()101 public async Task ClientWithCert_DontRequestClientCertificate_AcceptedButPeerNotAuthenticated() 102 { 103 InitClientAndServer( 104 clientAddKeyCertPair: true, 105 clientCertRequestType: SslClientCertificateRequestType.DontRequest); 106 107 await CheckAccepted(expectPeerAuthenticated: false); 108 } 109 110 [Test] NoClientCert_RequestClientCertificateButDontVerify_Accepted()111 public async Task NoClientCert_RequestClientCertificateButDontVerify_Accepted() 112 { 113 InitClientAndServer( 114 clientAddKeyCertPair: false, 115 clientCertRequestType: SslClientCertificateRequestType.RequestButDontVerify); 116 117 await CheckAccepted(expectPeerAuthenticated: false); 118 } 119 120 [Test] NoClientCert_RequestClientCertificateAndVerify_Accepted()121 public async Task NoClientCert_RequestClientCertificateAndVerify_Accepted() 122 { 123 InitClientAndServer( 124 clientAddKeyCertPair: false, 125 clientCertRequestType: SslClientCertificateRequestType.RequestAndVerify); 126 127 await CheckAccepted(expectPeerAuthenticated: false); 128 } 129 130 [Test] ClientWithCert_RequestAndRequireClientCertificateButDontVerify_Accepted()131 public async Task ClientWithCert_RequestAndRequireClientCertificateButDontVerify_Accepted() 132 { 133 InitClientAndServer( 134 clientAddKeyCertPair: true, 135 clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireButDontVerify); 136 137 await CheckAccepted(expectPeerAuthenticated: true); 138 await CheckAuthContextIsPopulated(); 139 } 140 141 [Test] ClientWithCert_RequestAndRequireClientCertificateAndVerify_Accepted()142 public async Task ClientWithCert_RequestAndRequireClientCertificateAndVerify_Accepted() 143 { 144 InitClientAndServer( 145 clientAddKeyCertPair: true, 146 clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireAndVerify); 147 148 await CheckAccepted(expectPeerAuthenticated: true); 149 await CheckAuthContextIsPopulated(); 150 } 151 152 [Test] NoClientCert_RequestAndRequireClientCertificateButDontVerify_Rejected()153 public void NoClientCert_RequestAndRequireClientCertificateButDontVerify_Rejected() 154 { 155 InitClientAndServer( 156 clientAddKeyCertPair: false, 157 clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireButDontVerify); 158 159 CheckRejected(); 160 } 161 162 [Test] NoClientCert_RequestAndRequireClientCertificateAndVerify_Rejected()163 public void NoClientCert_RequestAndRequireClientCertificateAndVerify_Rejected() 164 { 165 InitClientAndServer( 166 clientAddKeyCertPair: false, 167 clientCertRequestType: SslClientCertificateRequestType.RequestAndRequireAndVerify); 168 169 CheckRejected(); 170 } 171 172 [Test] Constructor_LegacyForceClientAuth()173 public void Constructor_LegacyForceClientAuth() 174 { 175 var creds = new SslServerCredentials(new[] { keyCertPair }, rootCert, true); 176 Assert.AreEqual(SslClientCertificateRequestType.RequestAndRequireAndVerify, creds.ClientCertificateRequest); 177 178 var creds2 = new SslServerCredentials(new[] { keyCertPair }, rootCert, false); 179 Assert.AreEqual(SslClientCertificateRequestType.DontRequest, creds2.ClientCertificateRequest); 180 } 181 182 [Test] Constructor_NullRootCerts()183 public void Constructor_NullRootCerts() 184 { 185 var keyCertPairs = new[] { keyCertPair }; 186 Assert.DoesNotThrow(() => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.DontRequest)); 187 Assert.DoesNotThrow(() => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.RequestAndVerify)); 188 Assert.DoesNotThrow(() => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.RequestAndRequireButDontVerify)); 189 Assert.Throws(typeof(ArgumentNullException), () => new SslServerCredentials(keyCertPairs, null, SslClientCertificateRequestType.RequestAndRequireAndVerify)); 190 } 191 192 [Test] VerifyPeerCallback_Accepted()193 public async Task VerifyPeerCallback_Accepted() 194 { 195 string targetNameFromCallback = null; 196 string peerPemFromCallback = null; 197 InitClientAndServer( 198 clientAddKeyCertPair: false, 199 clientCertRequestType: SslClientCertificateRequestType.DontRequest, 200 verifyPeerCallback: (ctx) => 201 { 202 targetNameFromCallback = ctx.TargetName; 203 peerPemFromCallback = ctx.PeerPem; 204 return true; 205 }); 206 await CheckAccepted(expectPeerAuthenticated: false); 207 Assert.AreEqual(TestCredentials.DefaultHostOverride, targetNameFromCallback); 208 var expectedServerPem = File.ReadAllText(TestCredentials.ServerCertChainPath).Replace("\r", ""); 209 Assert.AreEqual(expectedServerPem, peerPemFromCallback); 210 } 211 212 [Test] VerifyPeerCallback_CallbackThrows_Rejected()213 public void VerifyPeerCallback_CallbackThrows_Rejected() 214 { 215 InitClientAndServer( 216 clientAddKeyCertPair: false, 217 clientCertRequestType: SslClientCertificateRequestType.DontRequest, 218 verifyPeerCallback: (ctx) => 219 { 220 throw new Exception("VerifyPeerCallback has thrown on purpose."); 221 }); 222 CheckRejected(); 223 } 224 225 [Test] VerifyPeerCallback_Rejected()226 public void VerifyPeerCallback_Rejected() 227 { 228 InitClientAndServer( 229 clientAddKeyCertPair: false, 230 clientCertRequestType: SslClientCertificateRequestType.DontRequest, 231 verifyPeerCallback: (ctx) => 232 { 233 return false; 234 }); 235 CheckRejected(); 236 } 237 CheckAccepted(bool expectPeerAuthenticated)238 private async Task CheckAccepted(bool expectPeerAuthenticated) 239 { 240 var call = client.UnaryCallAsync(new SimpleRequest { ResponseSize = 10 }); 241 var response = await call; 242 Assert.AreEqual(10, response.Payload.Body.Length); 243 Assert.AreEqual(expectPeerAuthenticated.ToString(), call.GetTrailers().First((entry) => entry.Key == IsPeerAuthenticatedMetadataKey).Value); 244 } 245 CheckRejected()246 private void CheckRejected() 247 { 248 var ex = Assert.Throws<RpcException>(() => client.UnaryCall(new SimpleRequest { ResponseSize = 10 })); 249 if (ex.Status.StatusCode != StatusCode.Unavailable & ex.Status.StatusCode != StatusCode.Unknown) { 250 Assert.Fail("Expect status to be either Unavailable or Unknown"); 251 } 252 } 253 CheckAuthContextIsPopulated()254 private async Task CheckAuthContextIsPopulated() 255 { 256 var call = client.StreamingInputCall(); 257 await call.RequestStream.CompleteAsync(); 258 var response = await call.ResponseAsync; 259 Assert.AreEqual(12345, response.AggregatedPayloadSize); 260 } 261 262 private class SslCredentialsTestServiceImpl : TestService.TestServiceBase 263 { UnaryCall(SimpleRequest request, ServerCallContext context)264 public override Task<SimpleResponse> UnaryCall(SimpleRequest request, ServerCallContext context) 265 { 266 context.ResponseTrailers.Add(IsPeerAuthenticatedMetadataKey, context.AuthContext.IsPeerAuthenticated.ToString()); 267 return Task.FromResult(new SimpleResponse { Payload = CreateZerosPayload(request.ResponseSize) }); 268 } 269 StreamingInputCall(IAsyncStreamReader<StreamingInputCallRequest> requestStream, ServerCallContext context)270 public override async Task<StreamingInputCallResponse> StreamingInputCall(IAsyncStreamReader<StreamingInputCallRequest> requestStream, ServerCallContext context) 271 { 272 var authContext = context.AuthContext; 273 await requestStream.ForEachAsync(request => TaskUtils.CompletedTask); 274 275 Assert.IsTrue(authContext.IsPeerAuthenticated); 276 Assert.AreEqual("x509_subject_alternative_name", authContext.PeerIdentityPropertyName); 277 Assert.IsTrue(authContext.PeerIdentity.Count() > 0); 278 Assert.AreEqual("ssl", authContext.FindPropertiesByName("transport_security_type").First().Value); 279 280 return new StreamingInputCallResponse { AggregatedPayloadSize = 12345 }; 281 } 282 CreateZerosPayload(int size)283 private static Payload CreateZerosPayload(int size) 284 { 285 return new Payload { Body = ByteString.CopyFrom(new byte[size]) }; 286 } 287 } 288 } 289 } 290