【问题标题】:Grpc .Net client fails to connect to server with SSLGrpc .Net 客户端无法使用 SSL 连接到服务器
【发布时间】:2020-01-27 05:20:28
【问题描述】:

无法从使用 grpc.core 库(Grpc.Core.2.24.0Grpc.Core.Api.2.24.0)的 .net 框架应用程序编写的 greeter 客户端连接到此链接中提到的 greeter grpc 服务 - https://docs.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-3.0

下面是我的客户端代码。它适用于非 SSL 但不适用于 SSL

非 SSL 的客户端代码(可行)

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();

带有 SSL 的客户端代码(连接失败)

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();

我在使用 SSL 时遇到的错误是:

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

我尝试使用在同一个链接 (https://docs.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-3.0) 中提到的 .net 核心应用程序客户端,它适用于 SSL 和非 SSL,但不是直接使用 grp 库。我的客户端是一个 .Net 框架客户端,这就是我无法使用 .net 库连接到 grpc 服务的原因。 .Net grpc 库仅受 .net 核心应用支持。

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();

预期结果 - 来自服务器的响应

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

【问题讨论】:

  • 因为您的目标是“localhost”,所以我的第一个猜测是 SSL 安全名称检查失败。请注意,除非您覆盖频道上的“权限”(有一个频道选项可以执行此操作,但它实际上是为使用 localhost 进行测试而不是生产使用而设计的),您的客户端将尝试验证服务器的证书是否验证了您的名称频道正在定位 - 在本例中为“localhost”

标签: c# grpc


【解决方案1】:

我在 .NET Framework c 上创建了一个工作客户端,并在 localhost 上的 .NET Core 上创建了一个服务器:

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());
    }
}

但我还没有成功地在远程机器上完成这项工作(例如,ip 192.168.1.7 是服务器地址,客户端地址是 192.168.1.2)

【讨论】:

  • 我相信传递.pem文件的内容就足够了。而不是GetRootCertificates() 只是做System.IO.File.ReadAllText(@"c:\some-folder\my-cert.pem")
  • 我遇到了同样的问题,你的示例对我来说非常适合本地主机,你知道在连接到远程 gRPC 服务时如何解决这个问题吗?非常感谢
  • 有人知道这个问题的答案吗?:stackoverflow.com/questions/65057450/…
【解决方案2】:

我通过在客户端中使用 pem 格式的服务器证书使其与 SSL 端口一起工作。

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

稍微解释一下,VS 2019 中的 Asp.NETCore 模板使用了带有 pfx 文件的开发证书%AppData%\ASP.NET\Https\ProjectName.pfx 和 密码 = %AppData%\Microsoft\UserSecrets\{UserSecretsId}\secrets.json {:Kestrel:Certificates:Development:Password} Value 您可以从ProjectName.csproj 获取UserSecretsId id。这对于每个 ASP.NET Core 项目都是不同的。

我们只需要将证书的公钥作为 certificate.pem 文件即可通过 gRPC 进行安全通信。使用以下命令从 pfx 中提取公钥

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

复制此 cerificate.pem 以供 gRPC .NET Framework 客户端使用。

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

请注意,我使用的端口 5001 是我的 ASP.NET Core 应用程序的 SSL 端口。

适用于生产场景

使用来自证书签名机构的有效证书,并在 ASP.NET Core Server 和 .NET Framework 客户端中分别使用与 pfx 和 pem 相同的证书。

或使用自签名证书

对于在我们自己的微服务之间进行通信的大多数微服务,使用自签名证书是一种有效的选择。我们可能不需要权威签署的证书。我们在使用自签名证书时可能面临的一个问题是,证书可能会颁发给某个目标 DNS 名称,而我们的 gRPC 服务器可能正在其他地方运行,并且无法建立安全连接。

使用 gRPC 目标名称覆盖键覆盖 ssl 目标名称验证。

   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);

【讨论】:

  • 我在我的电脑上找不到这些。我正在使用 VS 2019 16.9.0。
  • 你找不到什么?
  • 没关系,我错误地生成了证书。
【解决方案3】:

我没有在客户端保存 pem 就可以正常工作(如果客户端和服务器在不同的机器上)。

首先,目标/主机名(用于创建频道的名称)必须与服务器证书中的 CN(公用名)匹配,这一点非常重要,这里的棘手部分是它区分大小写

e.q: 证书的 CN 是 SV-XXX-DEV-01 并且您指定 sv-xxx-dev-01 这将不起作用并且您收到以下错误:

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

所以这是我的解决方案(当然这可以优化,不应该在一个类中(关注点分离),但更容易理解。

    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();
            }
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-20
    • 2012-10-15
    相关资源
    最近更新 更多