Description
I have been using for a while GRPC with c# to learn and test it’s capabilities. I recently installed on a secondary computer Kubuntu and docker and tried to make use of GRPC service by calling it from my laptop. This has failed because it requires a valid certificate for that (well..at leas a certificate because without a certificate authority it will still complain).
To solve my problems I used 2 amazing source of inspiration
- https://devblogs.microsoft.com/aspnet/configuring-https-in-asp-net-core-across-different-platforms/
- https://docs.servicestack.net/grpc-ssl
I will present the steps in a simple manner below
1. Install choco
(https://jcutrer.com/windows/install-chocolatey-choco-windows10)
2. Install openssl
3. Generate Certificates
From https://docs.servicestack.net/grpc-ssl – we have the 2 .sh files which we use to create certificates
From the above link I extracted 2 shell scripts but you have to chose one of them. For this example I used gen-prod.https.sh
The first script
gen-dev.https.sh
PASSWORD=grpc if [ $# -ge 1 ] then PASSWORD=$1 fi cat <>dev.config [ req ] default_bits = 2048 default_md = sha256 default_keyfile = dev.key prompt = no encrypt_key = no distinguished_name = dn req_extensions = v3_req x509_extensions = x509_req string_mask = utf8only [ dn ] commonName = localhost dev cert emailAddress = test@localtest.me countryName = US stateOrProvinceName = DE localityName = Wilmington organizationName = Todo World [ x509_req ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = critical, CA:false keyUsage = critical, keyEncipherment subjectAltName = @alt_names # extendedKeyUsage = serverAuth, clientAuth nsComment = "OpenSSL Generated Certificate" [ v3_req ] subjectKeyIdentifier = hash basicConstraints = critical, CA:false subjectAltName = @alt_names # extendedKeyUsage = serverAuth, clientAuth nsComment = "OpenSSL Generated Certificate" [ alt_names ] DNS.1 = localhost EOT openssl req -config dev.config -new -out dev.csr.pem openssl x509 -req -days 365 -extfile dev.config -extensions v3_req -in dev.csr.pem -signkey dev.key -out dev.crt openssl pkcs12 -export -out dev.pfx -inkey dev.key -in dev.crt -password pass:$PASSWORD rm dev.config dev.csr.pem # cp dev.pfx ../MyApp
To use this script open power shell, navigate to the folder and type
C:\folder> ./gen-dev.https.sh <domain>
The second shell script is
gen-prod.https.sh
DOMAIN=todoworld.servicestack.net if [ $# -ge 1 ] then DOMAIN=$1 fi PASSWORD=grpc if [ $# -ge 2 ] then PASSWORD=$2 fi cat <>prod.config [ req ] default_bits = 2048 default_md = sha256 default_keyfile = prod.key prompt = no encrypt_key = no distinguished_name = dn req_extensions = v3_req x509_extensions = x509_req string_mask = utf8only [ dn ] commonName = TodoWorld prod cert emailAddress = todoworld@servicestack.net countryName = US stateOrProvinceName = DE localityName = Wilmington organizationName = Todo World [ x509_req ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = critical, CA:false keyUsage = critical, keyEncipherment subjectAltName = @alt_names # extendedKeyUsage = serverAuth, clientAuth nsComment = "OpenSSL Generated Certificate" [ v3_req ] subjectKeyIdentifier = hash basicConstraints = critical, CA:false subjectAltName = @alt_names # extendedKeyUsage = serverAuth, clientAuth nsComment = "OpenSSL Generated Certificate" [ alt_names ] DNS.1 = $DOMAIN EOT openssl req -config prod.config -new -out prod.csr.pem openssl x509 -req -days 365 -extfile prod.config -extensions v3_req -in prod.csr.pem -signkey prod.key -out prod.crt openssl pkcs12 -export -out prod.pfx -inkey prod.key -in prod.crt -password pass:$PASSWORD rm prod.config prod.csr.pem # cp prod.pfx ../MyApp # cp prod.crt ../MyApp/wwwroot/grpc.crt
To use this script open power shell, navigate to the folder and type
C:\folder> ./gen-prod.https.sh <domain> <password>
I will use the prod one in the example, meaning I will provide a password for the certificate.
The generated certificates will look like:
4. The Grpc Server
We now need to load the grpc server. For this I am using Daniel’s example (see the link from the beginning of the post). I made 2 changes to this method:
- I am account for both relative/absolute path
- If the asp environment is “LinuxDevelopment” I am using the localhost “0.0.0.0”
Creating the kestrel options
namespace ComX.Integrations.SendEmailService { public static class KestrelServerOptionsExtensions { public static void ConfigureEndpoints(this KestrelServerOptions options) { var configuration = options.ApplicationServices.GetRequiredService<IConfiguration>(); var environment = options.ApplicationServices.GetRequiredService<IWebHostEnvironment>(); var endpoints = configuration.GetSection("HttpServer:Endpoints") .GetChildren() .ToDictionary(section => section.Key, section => { var endpoint = new EndpointConfiguration(); section.Bind(endpoint); return endpoint; }); foreach (var endpoint in endpoints) { var config = endpoint.Value; var port = config.Port ?? (config.Scheme == "https" ? 443 : 80); var ipAddresses = new List<IPAddress>(); if (config.Host == "localhost") { if (environment.EnvironmentName == "LinuxDevelopment") { ipAddresses.Add(IPAddress.Parse("0.0.0.0")); } else { ipAddresses.Add(IPAddress.IPv6Loopback); ipAddresses.Add(IPAddress.Loopback); } } else if (IPAddress.TryParse(config.Host, out var address)) { ipAddresses.Add(address); } else { ipAddresses.Add(IPAddress.IPv6Any); } foreach (var address in ipAddresses) { options.Listen(address, port, listenOptions => { if (config.Scheme == "https") { var certPass = Environment.GetEnvironmentVariable("mycertpass"); if (certPass is null) { throw new Exception("Could not find the environment variable \'mycertpass\'"); } config.Password = certPass; var certificate = LoadCertificate(config, environment); listenOptions.UseHttps(certificate); } }); } } } private static X509Certificate2 LoadCertificate(EndpointConfiguration config, IWebHostEnvironment environment) { //store not used. I kept it as an example if (config.StoreName != null && config.StoreLocation != null) { using (var store = new X509Store(config.StoreName, Enum.Parse<StoreLocation>(config.StoreLocation))) { store.Open(OpenFlags.ReadOnly); var certificate = store.Certificates.Find( X509FindType.FindBySubjectName, config.Host, validOnly: !environment.IsDevelopment()); if (certificate.Count == 0) { throw new InvalidOperationException($"Certificate not found for {config.Host}."); } return certificate[0]; } } if (config.FilePath != null && config.Password != null) { string[] splitPath = config.FilePath.Split('/', '\\').Where(x => !(x.ContainsOnly('/') || x.ContainsOnly('\\'))).ToArray(); string fixedPath = string.Join(Path.DirectorySeparatorChar, splitPath); if (File.Exists(fixedPath)) { fixedPath = Path.Combine(AppContext.BaseDirectory, fixedPath); if (!File.Exists(fixedPath)) { throw new Exception("Could not find the certificate. Please provide a correct absolute or relative path"); } } return new X509Certificate2(fixedPath, config.Password); } throw new InvalidOperationException("No valid certificate configuration found for the current endpoint."); } } public class EndpointConfiguration { public string Host { get; set; } public int? Port { get; set; } public string Scheme { get; set; } public string StoreName { get; set; } public string StoreLocation { get; set; } public string FilePath { get; set; } public string Password { get; set; } } }
Pay attention: In these options I am looking for the password in the environment variables. “mycertpass”
appsettings
Replace kestrel entry with
Add the certificate (.pfx) under the certificates folder and make sure it’s “copy if newer” on build
Environment variable
Add the environment variable for the certificate password (restart visual studio after)
Configuring Program.cs
Now we have to tell Kestrel to use our options
Docker-Compose
If you are using docker under linux like I am, you can use a docker-compose file and pass the environment variable there. A secret should not be exposed like this and there are other ways to manage secrets but for development it suffice
5. The Grpc Client
Let’s create a console application that uses the client generated by the proto file
In the console application i created the certificates folder and added the certificate (also with “copy if newer” on build)
And now you can use the Grpc client.
Note that you still have to define HttpClientHandler.DangerousAcceptAnyServerCertificateValidator. This is because the certificate cannot be valiated against a certificate authority