Huh wut? Yes, let me explain.

Suppose you want to host a C# WebAPI application in a docker container, and the hostname used is changing all the time because you deploy this image for testing every branch you make.

You could make a wildcard certificate for that, but that would involve changing it whenever the domain changes, so I wanted a more robust solution.

I started googling and found this: AspNet Core starting Kestrel with Generated SelfSigned certificate - // ITNiels | Tech-Blog //. While this works fine, it also generates a new self-signed certificate all the time, and to make this trusted would involve extra steps.

The solution for this is to issue certificates from a CA (certificate authority) that you already trust. While this sounds easy it took me a while to get right, so I'm sharing it here.

In my case I'm getting the hostname from the configuration, so this could be appsettings.json, but also an environment variable:

builder.WebHost.ConfigureKestrel(options =>
{
    var hostname = builder.Configuration.GetValue<string>("hostname");
    options.ListenAnyIP(7001, configure =>   	         configure.UseHttps(getCert(hostname))); 
});

This call to getCert does serveral things:

  • Creates a rootCA.pfx file if it does not exist
  • Loads the rootCA.pfx file if it does exist
  • Spits out the public key part of the CA in the console, so you can trust this one as a CA
  • Creates a CA signed certificate with the desired hostname

So, while you keep the generated rootCA.pfx around in your solution(s), you only have to trust this one once, and all certs (dynamically) generated of it will automatically be trusted.

Full solution here:

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.WebHost.ConfigureKestrel(options =>
{
    var hostname = builder.Configuration.GetValue<string>("hostname");
    options.ListenAnyIP(7001, configure => configure.UseHttps(getCert(hostname))); 
});

var app = builder.Build();


// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();


static X509Certificate2 getCert(string hostname)
{
    var pwd = "passwwwordzz";

    X509Certificate2 rootCA;
    if (!File.Exists("rootca.pfx"))
    {
        Console.WriteLine("Creating new rootCA");
        rootCA = createCA();
        var byteCert = rootCA.Export(X509ContentType.Pfx, pwd);
        File.WriteAllBytes("rootCA.pfx", byteCert);
    }
    else 
    {
        Console.WriteLine("Loading existing rootCA");
        var byteCert = File.ReadAllBytes("rootCA.pfx");
        rootCA = new X509Certificate2(byteCert, pwd);
    }


    var rootRaw = new string(PemEncoding.Write("CERTIFICATE", rootCA.RawData));
    Console.WriteLine(rootRaw);


    var cert = GetSignedCertificate(rootCA, hostname);
    return cert;
}


static X509Certificate2 GetSignedCertificate(X509Certificate2 rootCA, string commonName)
{
    var password = "my awesome cert pw";

    var sanBuilder = new SubjectAlternativeNameBuilder();
    sanBuilder.AddDnsName(commonName);

    using (var rsa = RSA.Create(2048))
    {
        var request = new CertificateRequest($"CN={commonName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

        request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DigitalSignature, false));
        request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
        request.CertificateExtensions.Add(sanBuilder.Build());

        var certificate = request.Create(rootCA, DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(2000), new byte[] { 1, 2, 3, 4 });

        return new X509Certificate2(certificate.CopyWithPrivateKey(rsa).Export(X509ContentType.Pfx, password), password, X509KeyStorageFlags.MachineKeySet);
    }
}

static X509Certificate2 createCA()
{
    var password = "My password123";

    using (RSA parent = RSA.Create(2048))
    {
        var caRequest = new CertificateRequest("CN=Issuing Authority", parent, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

        caRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
        caRequest.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(caRequest.PublicKey, false));

        var caCert = caRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-45), DateTimeOffset.UtcNow.AddDays(3650));
        return new X509Certificate2(caCert.Export(X509ContentType.Pfx, password), password, X509KeyStorageFlags.Exportable);

    }
}

Hope this helps.