Skip to content

javaChip56/dotnetmtlslayer

Repository files navigation

DotNetMTlsLayer

DotNetMTlsLayer is a .NET 8 library for mutual TLS between two APIs. It handles both sides of the exchange:

  • Kestrel HTTPS configuration for inbound client certificates
  • ASP.NET Core certificate authentication and authorization
  • HttpClient registration for outbound mTLS calls
  • shared certificate trust validation logic

Contents

What the library gives you

The library does not issue certificates for you. It assumes you already have:

  • a server certificate for each API
  • a client certificate for each API when it calls the other API
  • either a shared/internal CA or explicit thumbprint pinning

What it does provide:

  • Kestrel helpers to require or allow client certificates during the TLS handshake
  • certificate-auth registration so a trusted client certificate becomes an authenticated ASP.NET Core user
  • outbound HttpClient registration that automatically sends a client certificate
  • shared trust evaluation for inbound and outbound peer validation
  • certificate loading helpers for .pfx, .pem, Base64 PFX, and Windows certificate store lookup

Project layout

  • src/DotNetMTlsLayer: reusable library code
  • tests/DotNetMTlsLayer.Tests: unit tests
  • samples/ApiA: sample API A
  • samples/ApiB: sample API B
  • samples/certs: generated sample certificates
  • tools/SampleCertificatesGenerator: local certificate generator for the samples

Certificate model

In a typical two-API setup:

  1. API A hosts HTTPS with its server certificate.
  2. API B hosts HTTPS with its server certificate.
  3. API A presents its client certificate when calling API B.
  4. API B presents its client certificate when calling API A.
  5. Both APIs trust either:
    • the same internal root CA, or
    • each other's exact certificate thumbprints.

The library supports both CA-based trust and direct pinning.

Flow diagram

sequenceDiagram
    participant ApiA as API A
    participant ApiB as API B

    ApiA->>ApiB: HTTPS request with client certificate
    ApiB->>ApiA: Server certificate during TLS handshake
    ApiA->>ApiA: Validate API B server certificate
    ApiB->>ApiB: Validate API A client certificate
    ApiB->>ApiB: Build ClaimsPrincipal from certificate
    ApiB-->>ApiA: Protected response
Loading

Using the library in your own APIs

1. Reference the library

If the library is in the same solution:

dotnet add YourApi.csproj reference src/DotNetMTlsLayer/DotNetMTlsLayer.csproj

2. Load certificates at startup

Most API-to-API mTLS setups need:

  • one server certificate for the current API
  • one client certificate for outbound calls from the current API
  • one trusted root certificate, or explicit thumbprints/subject names for peers

Example:

var serverCertificate = CertificateLoader.LoadFromPfxFile("certs/api-a-server.pfx", "password");
var clientCertificate = CertificateLoader.LoadFromPfxFile("certs/api-a-client.pfx", "password");
var trustedRoot = CertificateLoader.LoadPublicCertificate("certs/internal-root-ca.cer");

3. Configure inbound mTLS on Kestrel

Use UseMutualTls on the listener. This configures HTTPS and validates incoming client certificates.

builder.WebHost.ConfigureKestrel(kestrel =>
{
    kestrel.ListenAnyIP(5001, listen =>
    {
        listen.UseMutualTls(options =>
        {
            options.ServerCertificate = serverCertificate;
            options.ClientCertificateTrust.TrustedRoots.Add(trustedRoot);
            options.ClientCertificateTrust.AllowSubjectName("api-b-client");
        });
    });
});

4. Configure ASP.NET Core certificate authentication

This turns a validated client certificate into an authenticated user that can be used by authorization.

builder.Services.AddMutualTlsAuthentication(options =>
{
    options.ClientCertificateTrust.TrustedRoots.Add(trustedRoot);
    options.ClientCertificateTrust.AllowSubjectName("api-b-client");
});

builder.Services.AddAuthorization();

5. Protect endpoints

The library creates a default policy named MutualTlsDefaults.AuthorizationPolicyName.

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

app.MapGet("/secure/data", () => Results.Ok("secure"))
   .RequireAuthorization(MutualTlsDefaults.AuthorizationPolicyName);

6. Configure outbound mTLS for peer API calls

Use AddMutualTlsHttpClient so the outgoing HttpClient sends your client certificate and validates the peer server certificate.

builder.Services.AddMutualTlsHttpClient("ApiB", options =>
{
    options.BaseAddress = new Uri("https://api-b.internal:5002");
    options.ClientCertificate = clientCertificate;
    options.ServerCertificateTrust.TrustedRoots.Add(trustedRoot);
    options.ServerCertificateTrust.AllowSubjectName("api-b-server");
});

7. Call the peer API

app.MapGet("/call-peer", async (IHttpClientFactory httpClientFactory) =>
{
    var client = httpClientFactory.CreateClient("ApiB");
    var response = await client.GetStringAsync("/secure/data");
    return Results.Ok(response);
});

Configuration walkthrough

These are the main types you will use:

  • CertificateLoader Loads certificates from .pfx, .pem, Base64 PFX data, or the Windows certificate store.
  • MutualTlsTrustOptions Defines what you trust:
    • TrustedRoots
    • IntermediateCertificates
    • allowed thumbprints
    • allowed subject names
    • revocation and chain policy options
  • MutualTlsServerTlsOptions Controls the HTTPS listener:
    • ServerCertificate
    • ClientCertificateMode
    • inbound trust rules
  • MutualTlsServerAuthenticationOptions Controls certificate authentication and the default authorization policy.
  • MutualTlsHttpClientOptions Controls outbound mTLS:
    • BaseAddress
    • ClientCertificate
    • peer server trust options

Common trust setups:

  • Internal CA: Add the CA certificate to TrustedRoots and optionally restrict accepted peers by subject name.
  • Direct pinning: Call AllowThumbprint(...) and set AllowThumbprintPinningWithoutChainValidation = true.
  • Hybrid: Trust a CA but still pin a subject or thumbprint for a tighter allow-list.

Example server setup

using DotNetMTlsLayer;

var builder = WebApplication.CreateBuilder(args);

var serverCertificate = CertificateLoader.LoadFromPfxFile("certs/api-a-server.pfx", "password");
var trustedClientRoot = CertificateLoader.LoadPublicCertificate("certs/internal-root-ca.cer");

builder.WebHost.ConfigureKestrel(kestrel =>
{
    kestrel.ListenAnyIP(5001, listen =>
    {
        listen.UseMutualTls(options =>
        {
            options.ServerCertificate = serverCertificate;
            options.ClientCertificateTrust.TrustedRoots.Add(trustedClientRoot);
        });
    });
});

builder.Services.AddMutualTlsAuthentication(options =>
{
    options.ClientCertificateTrust.TrustedRoots.Add(trustedClientRoot);
    options.ClientCertificateTrust.AllowSubjectName("api-b-client");
});

builder.Services.AddAuthorization();

var app = builder.Build();

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

app.MapGet("/secure", () => Results.Ok("mTLS OK"))
   .RequireAuthorization(MutualTlsDefaults.AuthorizationPolicyName);

app.Run();

Example client setup

using DotNetMTlsLayer;

var builder = WebApplication.CreateBuilder(args);

var clientCertificate = CertificateLoader.LoadFromPfxFile("certs/api-b-client.pfx", "password");
var trustedServerRoot = CertificateLoader.LoadPublicCertificate("certs/internal-root-ca.cer");

builder.Services.AddMutualTlsHttpClient("ApiA", options =>
{
    options.BaseAddress = new Uri("https://api-a.internal:5001");
    options.ClientCertificate = clientCertificate;
    options.ServerCertificateTrust.TrustedRoots.Add(trustedServerRoot);
    options.ServerCertificateTrust.AllowSubjectName("api-a-server");
});

Self-signed or pinned peers

If both APIs pin each other by thumbprint instead of using a CA, enable thumbprint pinning explicitly:

options.ServerCertificateTrust.AllowThumbprint("7A3C...");
options.ServerCertificateTrust.AllowThumbprintPinningWithoutChainValidation = true;

The same option exists on ClientCertificateTrust for the server side. Thumbprint pinning works, but it shifts trust management to certificate distribution and rotation.

Runnable sample APIs

The repo includes two runnable sample APIs in samples/ApiA and samples/ApiB, plus a certificate generator in tools/SampleCertificatesGenerator.

1. Generate sample certificates

dotnet run --project tools/SampleCertificatesGenerator

This creates:

  • samples/certs/mtls-root.cer
  • samples/certs/api-a-server.pfx
  • samples/certs/api-a-client.pfx
  • samples/certs/api-b-server.pfx
  • samples/certs/api-b-client.pfx
  • samples/certs/operator-client.pfx

All generated PFX files use the password sample-password.

2. Run the sample APIs

Open two terminals from the repository root:

dotnet run --project samples/ApiA
dotnet run --project samples/ApiB

API addresses:

  • ApiA: https://localhost:5001
  • ApiB: https://localhost:5002

3. Trigger API-to-API mTLS

Once both APIs are running, you can trigger an API-to-API mTLS call without presenting a browser certificate yourself:

curl -k https://localhost:5001/demo/call-peer
curl -k https://localhost:5002/demo/call-peer

Each API will:

  1. create an outbound HttpClient configured with its own client certificate
  2. call the peer API's /secure/whoami endpoint
  3. validate the peer server certificate against the generated root CA
  4. present its client certificate to the peer
  5. return the authenticated identity details from the peer

4. Call a protected endpoint directly

Use the generated operator certificate to call /secure/whoami yourself.

PowerShell example:

$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
    "samples/certs/operator-client.pfx",
    "sample-password")

Invoke-RestMethod `
    -Uri "https://localhost:5001/secure/whoami" `
    -Certificate $cert `
    -SkipCertificateCheck

Without a client certificate, the /secure/* endpoints should return unauthorized or forbidden depending on the request path and auth state.

More sample-specific detail is in samples/README.md.

GitHub CI/CD

The repository now includes two GitHub Actions workflows:

ci.yml behavior:

  • runs on every push
  • runs on every pull request
  • can also be started manually
  • restores, builds, and runs the solution tests

release.yml behavior:

  • runs when a git tag matching v* is pushed
  • can also be started manually with:
    • version
    • publish_to_nuget
  • restores, builds, and tests the solution
  • packs src/DotNetMTlsLayer
  • uploads the .nupkg and .snupkg as workflow artifacts
  • creates a GitHub release
  • optionally pushes the package and symbol package to NuGet.org

Release versioning:

  • tag-driven releases use the tag version, for example v1.2.3 becomes package version 1.2.3
  • manual releases use the version input exactly as provided

Required GitHub repository secret:

  • NUGET_API_KEY: NuGet.org API key with push permission for the package ID

The release workflow automatically injects the active GitHub repository URL into the package metadata during CI publishing.

If you also plan to pack locally outside GitHub Actions, replace these placeholder values in src/DotNetMTlsLayer/DotNetMTlsLayer.csproj:

  • RepositoryUrl
  • PackageProjectUrl

Example release commands:

git tag v1.0.0
git push origin v1.0.0

Or run release.yml manually from GitHub Actions and provide the version in the workflow form.

Troubleshooting

  • The certificate did not match any configured thumbprint or subject name. Your allow-list does not match the presented certificate. Check the configured subject names or thumbprints.
  • TLS policy errors: RemoteCertificateNameMismatch The hostname you are calling does not match the server certificate. Fix DNS/URL usage or issue the server certificate with the correct subject alternative name.
  • UntrustedRoot The presented certificate chain does not build to a trusted root. Add the root CA to TrustedRoots or switch to explicit pinning.
  • Protected endpoints return 401 or 403 Make sure the client presented a certificate and that UseAuthentication() and UseAuthorization() are in the middleware pipeline.
  • Sample APIs fail at startup because certificate files are missing Run dotnet run --project tools/SampleCertificatesGenerator first.

Running the tests

dotnet test DotNetMTlsLayer.sln

About

Abstraction library providing mTLS authentication between .Net Web APIs.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages