SSL Certificate
The HTTPS protocol is supported and you can bring your own certificate in PKCS#12 format.
With just a small change, the basic static example can serve HTTPS.
using System;
using System.Net;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using SimpleW;
using SimpleW.Observability;
namespace Sample {
class Program {
static async Task Main() {
// debug log
Log.SetSink(Log.ConsoleWriteLine, LogLevel.Debug);
// listen to all IPs port 2015
var server = new SimpleWServer(IPAddress.Any, 2015);
// create a certificate
#if NET9_0_OR_GREATER
X509Certificate2 cert = X509CertificateLoader.LoadPkcs12FromFile(@"C:\Users\SimpleW\ssl\domain.pfx", "password");
#else
X509Certificate2 cert = new(@"C:\Users\SimpleW\ssl\domain.pfx", "password");
#endif
// create a context with certificate, support for password protection
var context = new SslContext(SslProtocols.Tls12 | SslProtocols.Tls13, cert, clientCertificateRequired: false, checkCertificateRevocation: false);
// assign context
server.UseHttps(context);
// serve static content
server.UseStaticFilesModule(options => {
options.Path = @"C:\www\spa\"; // serve your files located here
options.Prefix = "/"; // to "/" endpoint
options.CacheTimeout = TimeSpan.FromDays(1); // cached for 24h
options.AutoIndex = true; // enable autoindex if no index.html exists in the directory
});
await server.RunAsync();
}
}
}NOTE
The clientCertificateRequired and checkCertificateRevocation allows better control over the SSL Context, and they are set to false for testing purposes. Take a look at the UseHttps for more information.
Example for local test
The example bellow will use a pregerenarated certificate. Use only locally for testing, do not use in production !!
using System;
using System.Net;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using SimpleW;
using SimpleW.Observability;
namespace Sample {
class Program {
static async Task Main() {
// debug log
Log.SetSink(Log.ConsoleWriteLine, LogLevel.Debug);
// listen to all IPs port 2015
var server = new SimpleWServer(IPAddress.Any, 2015);
// echo subjectAltName=DNS:localhost,IP:127.0.0.1 > san.cnf && openssl genrsa -out privkey.pem 2048 && openssl req -new -key privkey.pem -out cert.csr -subj "/C=FR/O=SimpleW/OU=Dev/CN=localhost/emailAddress=admin@localhost" && openssl x509 -req -days 1095 -in cert.csr -signkey privkey.pem -out cert.pem -extfile san.cnf && openssl pkcs12 -export -out simplew.pfx -inkey privkey.pem -in cert.pem -password pass:secret && del san.cnf
string SslCertificateBase64 = "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";
string certificateFilePath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) ?? "", "simplew.pfx");
Directory.CreateDirectory(Path.Combine(certificateFilePath, ".."));
File.WriteAllBytes(certificateFilePath, Convert.FromBase64String(SslCertificateBase64));
// create a certificate
#if NET9_0_OR_GREATER
X509Certificate2 cert = X509CertificateLoader.LoadPkcs12FromFile(certificateFilePath, "password");
#else
X509Certificate2 cert = new(certificateFilePath, "password");
#endif
// create a context with certificate, support for password protection
var context = new SslContext(SslProtocols.Tls12 | SslProtocols.Tls13, cert, clientCertificateRequired: false, checkCertificateRevocation: false);
// assign context
server.UseHttps(context);
// minimal api
server.MapGet("/", () => {
return new { message = "Hello World !" };
});
await server.RunAsync();
}
}
}Mutual Authentication
The server can also require the client to have a proper SSL Certificate.
Just add some check in the SslContext
// create a context with certificate, support for password protection
var context = new SslContext(
SslProtocols.Tls12 | SslProtocols.Tls13,
cert,
(sender, certificate, chain, sslPolicyErrors) => {
if (sslPolicyErrors != SslPolicyErrors.None) {
Console.WriteLine($"[MTLS] failed to validate client certificate : {sslPolicyErrors}");
return false; // deny connection when error
}
Console.WriteLine($"[MTLS] client certificate accepted : {certificate.Subject}");
return true; // accept
}
);Accessing the client certificate
After a successful TLS handshake, the client certificate is available on the session:
var cert = session.ClientCertificate;This property returns an X509Certificate2 or null if no client certificate was provided.
Mapping mTLS to HttpPrincipal
mTLS only validates the connection. To integrate with the SimpleW authentication model, you should map the client certificate to a HttpPrincipal. There are two approaches to resolve the principal at request time :
server.ConfigurePrincipalResolver(session => {
var cert = session.ClientCertificate;
if (cert == null) {
return null;
}
return new HttpPrincipal(new HttpIdentity(
isAuthenticated: true,
authenticationType: "MutualTLS",
identifier: cert.Thumbprint,
name: cert.GetNameInfo(X509NameType.SimpleName, false) ?? cert.Subject,
email: cert.GetNameInfo(X509NameType.EmailName, false),
roles: Array.Empty<string>(),
properties: [
new IdentityProperty("auth_scheme", "mTLS"),
new IdentityProperty("x509.subject", cert.Subject),
new IdentityProperty("x509.thumbprint", cert.Thumbprint ?? string.Empty)
]
));
});server.UseMiddleware(async (session, next) => {
var cert = session.ClientCertificate;
if (cert != null) {
session.Principal = new HttpPrincipal(new HttpIdentity(
isAuthenticated: true,
authenticationType: "MutualTLS",
identifier: cert.Thumbprint,
name: cert.GetNameInfo(X509NameType.SimpleName, false) ?? cert.Subject,
email: cert.GetNameInfo(X509NameType.EmailName, false),
roles: Array.Empty<string>(),
properties: [
new IdentityProperty("auth_scheme", "mTLS"),
new IdentityProperty("x509.subject", cert.Subject),
new IdentityProperty("x509.thumbprint", cert.Thumbprint ?? string.Empty),
new IdentityProperty("x509.serial_number", cert.SerialNumber ?? string.Empty)
]
));
}
await next();
});Disable Certificate
You can disable a certificate while the server is running. This uses the special method ReloadListenerAsync to allow such a configuration change.
await server.ReloadListenerAsync(s => {
s.DisableHttps();
s.UsePort(80);
});INFO
You can use the same logic to UseHttps() after you have already started your server without it.
Let's Encrypt
There is an SimpleW.Service.Letsencrypt addon that provides automatic TLS certificate management for SimpleW using Let's Encrypt and the ACME HTTP-01 challenge.
Just follow the documentation of the Let's Encrypt Module.