Home > Back-end >  C# Certificate Authentication with gRPC
C# Certificate Authentication with gRPC

Time:12-12

I am trying my hand at gRPC in C# and wanted to do authentication integration with aspnet core .Net 6. I am following the tutorial on microsoft for setting up a gRPC application.

The application works with no issues until I attempt to add in the authentication. I am working with a self-signed cert, that I have added to my local user store as well as my trusted root store just in case the cert was rejected due to revocation. I've tried a lot of things to help troubleshoot the issue, but with no luck. I am not sure where to go now and would appreciate any pointers/tips.

Kestrel Web Server gRPC File

using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using grpcServerTest.Services;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;

namespace grpcServerTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Additional configuration is required to successfully run gRPC on macOS.
            // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

            // Add services to the container.
            builder.Services.AddGrpc();
            //builder.Services.AddAuthorization();
            builder.Services.AddAuthentication(
                CertificateAuthenticationDefaults.AuthenticationScheme)
                .AddCertificate(options =>
                {
                    options.AllowedCertificateTypes = CertificateTypes.All;
                    options.ValidateCertificateUse = false;
                    options.RevocationFlag = System.Security.Cryptography.X509Certificates.X509RevocationFlag.ExcludeRoot;
                    options.RevocationMode = System.Security.Cryptography.X509Certificates.X509RevocationMode.NoCheck;
                    options.Events = new CertificateAuthenticationEvents
                    {
                        OnChallenge = context =>
                        {
                            return Task.CompletedTask;
                        },
                        OnAuthenticationFailed = context =>
                        {

                            return Task.CompletedTask;
                        },
                        OnCertificateValidated = context =>
                        {

                            if (true)
                            {
                                var claims = new[]
                                {
                                    new Claim(
                                        ClaimTypes.NameIdentifier,
                                        context.ClientCertificate.Subject,
                                        ClaimValueTypes.String, context.Options.ClaimsIssuer),
                                    new Claim(
                                        ClaimTypes.Name,
                                        context.ClientCertificate.Subject,
                                        ClaimValueTypes.String, context.Options.ClaimsIssuer)
                                };

                                context.Principal = new ClaimsPrincipal(
                                    new ClaimsIdentity(claims, context.Scheme.Name));
                                context.Success();
                            }

                            return Task.CompletedTask;
                        }
                    };
                });

            builder.Services.Configure<KestrelServerOptions>(options =>
            {
                options.ConfigureHttpsDefaults(options =>
                {
                    options.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
                    options.CheckCertificateRevocation = false;
                    //options.ServerCertificate = GetClientCertificate();
                    options.ClientCertificateValidation = (cert, chain, errors) =>
                    {
                        Console.WriteLine("Client Validation Called");
                        errors = System.Net.Security.SslPolicyErrors.None;
                        return true;
                    };
                });
            });

            var app = builder.Build();

            app.UseCertificateForwarding();

            app.UseAuthentication();
            //app.UseAuthorization();

            // Configure the HTTP request pipeline.
            app.MapGrpcService<GreeterService>();
            app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
        
            app.Run();
        }
    }
}

gRPC Client Code

// See https://aka.ms/new-console-template for more information
using Grpc.Core;
using Grpc.Net.Client;
using GrpcTest;
using System.Security.Cryptography.X509Certificates;

internal class Program
{
    private static async Task Main(string[] args)
    {
        Console.WriteLine("Hello, World!");

        var x509 = GetClientCertificate();

        var handler = new HttpClientHandler();

        handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls;
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        handler.ClientCertificates.Add(x509);
        handler.UseProxy = false;

        Console.ReadKey();
        using var channel = GrpcChannel.ForAddress("https://localhost:7283", new GrpcChannelOptions()
        {
            HttpHandler = handler,
            DisposeHttpClient = true
        });

        var client = new Greeter.GreeterClient(channel);
        var reply = await client.SayHelloAsync(
                          new HelloRequest { Name = "GreeterClient" });
        Console.WriteLine("Greeting: "   reply.Message);
        Console.WriteLine("Press any key to exit...");
        Console.ReadKey();
    }

    private static X509Certificate2 GetClientCertificate()
    {
        X509Store userCaStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        try
        {
            userCaStore.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
            X509Certificate2Collection findResult = certificatesInStore.Find(X509FindType.FindBySubjectName, "grpctest", true);
            X509Certificate2 clientCertificate = null!;
            if (findResult.Count == 1)
            {
                clientCertificate = findResult[0];
            }
            else
            {
                throw new Exception("Unable to locate the correct client certificate.");
            }
            return clientCertificate;
        }
        catch
        {
            throw;
        }
        finally
        {
            userCaStore.Close();
        }
    }
}

CodePudding user response:

Is this .NET 6 or 7?

See the parts relevant to gRPC and full chain support in Kestrel at https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-7.0?view=aspnetcore-7.0

With .NET 6 you might need to use something like https://github.com/MarkCiliaVincenti/TlsCertificateLoader

CodePudding user response:

I am a dingus. I was passing the public cert to the HttpHandler in the request instead of a public and private key to actually perform authentication.

I am not sure if this is the appropriate answer or if I have a weird configuration value somewhere that I do not see, but I was able to get certificates to be passed through.

The issue, Kestrel seems to have been blocking the certificate, despite my attempt to bypass it by configuring the default HTTPS connection. Once I manually created my own listener in builder and did not rely on the default configuration, it seem that it worked without an issue.

This is the code I used for my builder, albeit maybe overboard and I do not recommend it for production use without removing my security bypass.

builder.Services.Configure<KestrelServerOptions>(options =>
            {
                options.Listen(IPAddress.Loopback, 8080, listenOptions =>
                {
                    listenOptions.UseConnectionLogging();
                    listenOptions.UseHttps(options =>
                    {
                        options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
                        options.CheckCertificateRevocation = false;
                        options.AllowAnyClientCertificate();
                        options.ClientCertificateValidation = (cert, chain, errors) =>
                        {
                            Console.WriteLine("Client Validation Called");
                            errors = System.Net.Security.SslPolicyErrors.None;
                            return true;
                        };
                    });
                });
            });
  • Related