3
votes

Unable to connect to the greeter grpc service mentioned in this link - https://docs.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-3.0 from a greeter client which is written from a .net framework app using grpc.core libraries(Grpc.Core.2.24.0 and Grpc.Core.Api.2.24.0).

Below is my client code. It works with non SSL but not with SSL

Client code with non SSL(this works)

var channel = new Channel("localhost:5000", ChannelCredentials.Insecure);
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(new HelloRequest { Name = "GreeterClient" });
channel.ShutdownAsync().Wait();

Client code with SSL(this fails to connect )

SslCredentials secureChannel = new SslCredentials();
var channel = new Channel("localhost", 5001, secureChannel);
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(new HelloRequest { Name = "GreeterClient" });
channel.ShutdownAsync().Wait();

The error I get with SSL is:

Grpc.Core.RpcException: 'Status(StatusCode=Unavailable, Detail="failed to connect to all addresses")'

I tried with .net core app client mentioned in the same link(https://docs.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-3.0) that works with SSL and non SSL but not by using the grp libraries directly. My client is a .Net framework client that is the reason I can't use .net libraries for connecting to grpc service. .Net grpc libraries are supported only from .net core app.

SslCredentials secureChannel = new SslCredentials();
var channel = new Channel("localhost", 5001, secureChannel);
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(new HelloRequest { Name = "GreeterClient" });
channel.ShutdownAsync().Wait();

Expected result - response from the server

Actual result - Grpc.Core.RpcException: 'Status(StatusCode=Unavailable, Detail="failed to connect to all addresses")'

3
Because you're targetting "localhost", my first guess is that the SSL secure name check is failing. Note that unless you override the "authority" on your channel (there is a channel option for doing this but it's really designed for testing with localhost rather than production usage), your client will attempt to verify that the server's cert authenticates the name that your channel is targeting - in this case "localhost"apolcyn

3 Answers

8
votes

I made a working client on the .NET Framework c with a server on .NET Core on localhost:

static async Task Main(string[] args)
{    
    string s = GetRootCertificates();
    var channel_creds = new SslCredentials(s);
    var channel = new Channel("localhost",50051, channel_creds);
    var client = new Informer.InformerClient(channel);
    await GetPing(client);
}

public static string GetRootCertificates()
{
    StringBuilder builder = new StringBuilder();
    X509Store store = new X509Store(StoreName.Root);
    store.Open(OpenFlags.ReadOnly);
    foreach (X509Certificate2 mCert in store.Certificates)
    {
        builder.AppendLine(
            "# Issuer: " + mCert.Issuer.ToString() + "\n" +
            "# Subject: " + mCert.Subject.ToString() + "\n" +
            "# Label: " + mCert.FriendlyName.ToString() + "\n" +
            "# Serial: " + mCert.SerialNumber.ToString() + "\n" +
            "# SHA1 Fingerprint: " + mCert.GetCertHashString().ToString() + "\n" +
            ExportToPEM(mCert) + "\n");
    }
    return builder.ToString();
}

/// <summary>
/// Export a certificate to a PEM format string
/// </summary>
/// <param name="cert">The certificate to export</param>
/// <returns>A PEM encoded string</returns>
public static string ExportToPEM(X509Certificate cert)
{
    StringBuilder builder = new StringBuilder();            

    builder.AppendLine("-----BEGIN CERTIFICATE-----");
    builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
    builder.AppendLine("-----END CERTIFICATE-----");

    return builder.ToString();
}

private static async Task GetPing(Informer.InformerClient client)
{
    Console.WriteLine("Getting ping...");
    try
    {
        Metadata headers = null;
        var response = await client.GetServerPingAsync(new Empty(), headers);
        string result = "Nan";
        if (response.PingResponse_ == 1)
            result = "Ok!";
        Console.WriteLine($"Ping say: {result }");
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error get server ping." + Environment.NewLine + ex.ToString());
    }
}

But I have not yet succeeded in making this work on remote machines (for example, where ip 192.168.1.7 is the server address and the client address is 192.168.1.2)

2
votes

I got it working without saving the pem on the client side (event if the client and the server are on different machines).

First of all it's very important the the target/host name (the one you use to create the channel) must match the CN (Common Name) from the server certificate, the tricky part here is that it's case sensitive!

e.q: The CN of the certificate is SV-XXX-DEV-01 and you specify sv-xxx-dev-01 this will not work and you get the following error:

Grpc.Core.RpcException: 'Status(StatusCode=Unavailable, Detail="failed to connect to all addresses")'

So here is my solution (of course this can be optimized and should not be in one class (separation of concerns), but it's easier to get the point.

    public static async Task Main(string[] args)
            {
                await FullFrameworkSample();
            }
    
            private static async Task FullFrameworkSample()
            {
                Uri host = new Uri("https://sv-xxx-dev-cpu-01:44301");
                int port = host.Port;
    
                (string publicKeyInPemFormat, string commonName) = await GetCertificateInformationFromServer(host);
    
                //note: in the full framework implementation it's very important that the casing of the target is correct (the same casing as in the CN name of the certificate)
                string target = $"{commonName}:{port}";
    
                //note: thats only needed in our case, because we have a server side interceptor, that checks if the secureKey is valid.
                CallCredentials credentials = CallCredentials.FromInterceptor((context, metadata) =>
                                                                              {
                                                                                  metadata.Add("SecurityTokenId", "someSecureKey");
    
                                                                                  return Task.CompletedTask;
                                                                              });
    
                ChannelCredentials channelCredentials = ChannelCredentials.Create(new SslCredentials(publicKeyInPemFormat), credentials);
    
                Channel channel = new Channel(target, channelCredentials);
    
                ProjectInlayDataService.ProjectInlayDataServiceClient client = new ProjectInlayDataService.ProjectInlayDataServiceClient(channel);
    
                GetProjectInlayDataResponse result = await client.GetProjectInlayDataAsync(new GetProjectInlayDataRequest());                                                                                        
    
                await channel.ShutdownAsync();
    
                Console.WriteLine("Press any key to exit...");
                Console.ReadKey();
            }
    
            private static async Task<(string PublicKeyInPemFormat, string CommonName)> GetCertificateInformationFromServer(Uri host)
            {
                Regex commonNameRegex = new Regex("CN=([\\w\\-.]*),?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
    
                StringBuilder builder = new StringBuilder();
                const string newline = "\n";
    
                X509Certificate certFromServer;
    
                using (HttpClient client = new HttpClient())
                {
                    using (HttpResponseMessage _ = await client.GetAsync(host))
                    {
                       //get the certificate from the server, so we don't need to store the pem.
                        certFromServer = ServicePointManager.FindServicePoint(host).Certificate;
                        if (certFromServer == null)
                            throw new InvalidOperationException($"Could not get certificate from server ({host}).");
                    }
                }
    
                Match match = commonNameRegex.Match(certFromServer.Subject);
                if (!match.Success)
                    throw new InvalidOperationException($"Could not extract CN (Common Name) from server certificate ({certFromServer.Subject}).");
    
                string commonName = match.Groups[1].Captures[0].Value;
    
                X509Certificate2 certificate = new X509Certificate2(certFromServer);
                string pem = ExportToPem(certificate);
    
                builder.AppendLine(
                    "# Issuer: " + certificate.Issuer + newline +
                    "# Subject: " + certificate.Subject + newline +
                    "# Label: " + certificate.FriendlyName + newline +
                    "# Serial: " + certificate.SerialNumber + newline +
                    "# SHA1 Fingerprint: " + certificate.GetCertHashString() + newline +
                    pem + newline);
    
                return (builder.ToString(), commonName);
            }
    
            /// <summary>
            /// Export a certificate to a PEM format string
            /// </summary>
            /// <param name="cert">The certificate to export</param>
            /// <returns>A PEM encoded string</returns>
            private static string ExportToPem(X509Certificate cert)
            {
                StringBuilder builder = new StringBuilder();

                builder.AppendLine("-----BEGIN CERTIFICATE-----");           
        
 builder.AppendLine(Convert.ToBase64String(cert.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
            builder.AppendLine("-----END CERTIFICATE-----");

                return builder.ToString();
            }
    }
2
votes

I got it working with SSL port by using the Server's certificate in pem format in the client.

SslCredentials secureCredentials = new SslCredentials(File.ReadAllText("certificate.pem"));
var channel = new Channel("localhost", 5001, secureCredentials);

A bit of explanation, Asp.NETCore template in VS 2019 uses a development certificate with pfx file at %AppData%\ASP.NET\Https\ProjectName.pfx and password = %AppData%\Microsoft\UserSecrets\{UserSecretsId}\secrets.json {:Kestrel:Certificates:Development:Password} Value You can get the UserSecretsId id from the ProjectName.csproj. This will be different for each ASP.NET Core Project.

We just need the public key of the certificate as a certificate.pem file to communicate securely over gRPC. Use the command below to extract publickey from pfx

openssl pkcs12 -in "<DiskLocationOfPfx>\ProjectName.pfx" -nokeys -out "<TargetLocation>\certifcate.pem"

Copy this cerificate.pem for the gRPC .NET Framework client to use.

SslCredentials secureCredentials = new SslCredentials(File.ReadAllText("<DiskLocationTo the Folder>/certificate.pem"))
var channel = new Channel("localhost", 5001, secureCredentials);

Note that port 5001 I used is the SSL port of my ASP.NET Core application.

For Production Scenarios

Use a valid certificate from certificate signing authority and use same certificate in ASP.NET Core Server and .NET Framework client as pfx and pem respectively.

Or Using Self signed certificate

Using Self signed certificates are a valid option for most microservices that communicate between our own microservices. We may not need an authority signed certificate. One problem we may face with using self signed certificate is that the certificate may be issued to some target DNS name and our gRPC server may be running somewhere else and secure connection cannot be established.

Use gRPC Target Name override keys to override the ssl target name validation.

   List<ChannelOption> channelOptions = new List<ChannelOption>()
   {
       new ChannelOption("grpc.ssl_target_name_override", <DNS to which our certificate is issued to>),
   };
   SslCredentials secureCredentials = new SslCredentials(File.ReadAllText("certificate.pem"));

   var channel = new Channel("localhost", 5001, secureCredentials, channelOptions);