Forge.Next.Security 3.1.0

dotnet add package Forge.Next.Security --version 3.1.0
                    
NuGet\Install-Package Forge.Next.Security -Version 3.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Forge.Next.Security" Version="3.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Forge.Next.Security" Version="3.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Forge.Next.Security" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Forge.Next.Security --version 3.1.0
                    
#r "nuget: Forge.Next.Security, 3.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Forge.Next.Security@3.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Forge.Next.Security&version=3.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Forge.Next.Security&version=3.1.0
                    
Install as a Cake Tool

Forge.Next.Security

.NET License

A small, focused .NET library for X.509 certificate management and AES encryption — with first-class support for self-signed certificates (local development, internal services, TLS endpoints) and password-based symmetric encryption.

Every public method returns an ErrorOr<T> result instead of throwing, so failures are values you handle explicitly rather than exceptions you have to catch.

It is part of the Forge Patterns and Practices family.


Table of contents


Features

  • 🔐 Self-signed certificate generation with sensible, secure defaults (RSA 2048, SHA-256, TLS Server Authentication EKU) and a configurable key size.
  • 🌐 Automatic Subject Alternative Names (SAN) — loopback, "any", localhost and the machine name are always included, plus any custom DNS names / IP addresses you supply.
  • 💾 Export & import to/from PFX (PKCS#12) raw bytes or files, and export the public certificate as a .cer (DER) file.
  • 🗄️ Certificate store management — add, find (by thumbprint or by DNS name), enumerate and remove certificates from the Windows/OS certificate store.
  • 🧂 AES encryption — PBKDF2 key derivation plus AES-CBC encrypt/decrypt with a compact iv:ciphertext payload.
  • Exception-free API — every method returns ErrorOr<T>; nothing throws for expected failures.

All types live in the Forge.Next.Security namespace.


Requirements

Target frameworks net8.0, net9.0, net10.0
Language C# 12
Dependencies Forge.Next.Shared (brings in ErrorOr)
Platforms Cross-platform. Certificate-store and friendly-name features are richest on Windows (see platform notes).

Installation

dotnet add package Forge.Next.Security

or via the Package Manager Console:

Install-Package Forge.Next.Security

Then import the namespaces:

using Forge.Next.Security;                              // CertificateFactory, CertificateAccess, AesCryptography
using ErrorOr;                                          // ErrorOr<T>, Error, ErrorType
using System.Security.Cryptography.X509Certificates;    // X509Certificate2, StoreName, StoreLocation

Working with ErrorOr results

Because every method returns ErrorOr<T>, you never wrap calls in try/catch for expected failures. Inspect the result instead:

ErrorOr<X509Certificate2> result = CertificateFactory.GetForLocalhost("CN=localhost", "Dev HTTPS");

if (result.IsError)
{
    // result.FirstError.Type is an ErrorType (Validation, NotFound, Unexpected, …)
    Console.WriteLine($"Failed: {result.FirstError.Type} - {result.FirstError.Description}");
}
else
{
    X509Certificate2 certificate = result.Value;   // safe to read once IsError is false
    Console.WriteLine(certificate.Thumbprint);
}

Or use the functional helpers shipped with ErrorOr:

// Match: project both branches into a single value.
string message = result.Match(
    cert => $"Created {cert.Thumbprint}",
    errors => $"Failed: {errors[0].Description}");

// Then / ThenDo: chain further work that only runs on success.
ErrorOr<byte[]> pfx = CertificateFactory
    .GetForLocalhost("CN=localhost", "Dev HTTPS")
    .Then(cert => CertificateFactory.ExportToPfxRawData(cert, "p@ss"));

The error kinds this library produces are:

ErrorType When
Validation A required argument was null (e.g. a null password/salt). The Description names the offending parameter.
NotFound CertificateAccess.FindByThumbprint found no matching certificate.
Unexpected An underlying operation threw (wrong PFX password, missing file, …); the exception is caught and converted into this error.

In the examples below, code that is not specifically demonstrating error handling reads .Value directly for brevity. In production, guard with IsError (or Match) first.


API reference

Class Responsibility
CertificateFactory Create, export and import self-signed certificates.
CertificateAccess Read from and write to the OS certificate store; inspect SAN entries.
AesCryptography Derive AES keys (PBKDF2) and encrypt/decrypt strings (AES-CBC).

CertificateFactory

public static class CertificateFactory

Creates and manages self-signed certificates. Generated certificates use an RSA key (2048-bit by default, configurable), a SHA-256/PKCS#1 signature, the TLS Server Authentication enhanced key usage (1.3.6.1.5.5.7.3.1), the DigitalSignature | KeyEncipherment | DataEncipherment key usages, and are flagged as non-CA (end-entity) certificates.

Member Returns Summary
Localhost const string The constant "localhost".
CreateSelfSigned(string, string, IEnumerable<string>, DateTime, DateTime, int) ErrorOr<X509Certificate2> Create an in-memory self-signed certificate.
CreateSelfSigned(string, string, IEnumerable<string>, DateTime, DateTime, string, int) ErrorOr<X509Certificate2> Create a certificate and round-trip it through a password-protected PFX (machine key set).
ExportToPfxRawData(X509Certificate2, string) ErrorOr<byte[]> Export the certificate and private key as PFX bytes.
ExportToCerFile(X509Certificate2) ErrorOr<byte[]> Export the public certificate as DER (.cer) bytes.
GetForLocalhost(string, string, int) ErrorOr<X509Certificate2> Create a long-lived certificate for localhost.
GetFromRawData(byte[], string) ErrorOr<X509Certificate2> Load a certificate from PFX bytes.
GetFromFile(string, string?) ErrorOr<X509Certificate2> Load a certificate from a PFX file.

Localhost (constant)
public const string Localhost = "localhost";

The lower-case DNS label "localhost". Useful when you want to reference the same value the factory uses internally.

string name = CertificateFactory.Localhost; // "localhost"

CreateSelfSigned (without password)
public static ErrorOr<X509Certificate2> CreateSelfSigned(
    string x500,
    string friendlyName,
    IEnumerable<string> dnsNames,
    DateTime startTime,
    DateTime endTime,
    int keySize = 2048)

Creates a brand-new, in-memory (ephemeral) self-signed certificate that owns its freshly generated private key. Nothing is written to disk or to a certificate store.

Parameters

Name Type Description
x500 string The X.500 subject distinguished name (e.g. "CN=My Company"). null is treated as an empty subject.
friendlyName string The Windows friendly name. Applied only on Windows; ignored on other platforms.
dnsNames IEnumerable<string> Additional DNS names and/or IP addresses to add to the SAN. See how SANs are built.
startTime DateTime Validity start (NotBefore).
endTime DateTime Validity end (NotAfter).
keySize int RSA key size in bits. Defaults to 2048.

Example

ErrorOr<X509Certificate2> result = CertificateFactory.CreateSelfSigned(
    x500: "CN=My Company, O=My Company Ltd, C=US",
    friendlyName: "My Internal Service",
    dnsNames: new[] { "myapp.local", "api.myapp.local", "192.168.1.50" },
    startTime: DateTime.UtcNow.AddDays(-1),
    endTime: DateTime.UtcNow.AddYears(5),
    keySize: 4096);

if (!result.IsError)
{
    X509Certificate2 certificate = result.Value;
    Console.WriteLine(certificate.Subject);       // CN=My Company, O=My Company Ltd, C=US
    Console.WriteLine(certificate.HasPrivateKey); // True
}

💡 Use DateTime.UtcNow for the validity boundaries so the encoded values are unambiguous across time zones.


CreateSelfSigned (with password)
public static ErrorOr<X509Certificate2> CreateSelfSigned(
    string x500,
    string friendlyName,
    IEnumerable<string> dnsNames,
    DateTime startTime,
    DateTime endTime,
    string password,
    int keySize = 2048)

Same as the overload above, but after creation the certificate is exported to a password-protected PFX and re-imported with X509KeyStorageFlags.Exportable | PersistKeySet | MachineKeySet. Use this overload when you need the private key persisted in the machine key store (for example, a Windows service that must access the key across processes/sessions).

Parameters — identical to the overload above, plus a password (the password protecting the intermediate PFX round-trip), with keySize moving to the final position.

Example

ErrorOr<X509Certificate2> result = CertificateFactory.CreateSelfSigned(
    x500: "CN=My Windows Service",
    friendlyName: "My Service Certificate",
    dnsNames: new[] { "service.internal" },
    startTime: DateTime.UtcNow.AddDays(-1),
    endTime: DateTime.UtcNow.AddYears(2),
    password: "S3cr3t!Passw0rd");

Console.WriteLine(result.IsError ? result.FirstError.Description : result.Value.HasPrivateKey);

⚠️ Permissions: MachineKeySet writes to the machine-wide key container. On Windows this typically requires the process to run elevated (as Administrator). If you only need an in-memory key, prefer the password-less overload.


ExportToPfxRawData
public static ErrorOr<byte[]> ExportToPfxRawData(X509Certificate2 certificate, string password)

Exports the certificate including its private key as PFX/PKCS#12 raw bytes, protected by password. The result can later be re-imported with GetFromRawData or GetFromFile.

Example

X509Certificate2 certificate = CertificateFactory.GetForLocalhost("CN=localhost", "Dev").Value;

ErrorOr<byte[]> pfx = CertificateFactory.ExportToPfxRawData(certificate, "S3cr3t!Passw0rd");
if (!pfx.IsError)
    File.WriteAllBytes("mycert.pfx", pfx.Value);

ExportToCerFile
public static ErrorOr<byte[]> ExportToCerFile(X509Certificate2 certificate)

Exports the public certificate only (no private key) as DER-encoded .cer bytes. This is the file you distribute to clients that need to trust your certificate.

Example

ErrorOr<byte[]> cer = CertificateFactory.ExportToCerFile(certificate);
if (!cer.IsError)
{
    File.WriteAllBytes("mycert.cer", cer.Value);

    // Re-loading the .cer yields a certificate WITHOUT a private key:
    var publicOnly = X509CertificateLoader.LoadCertificate(cer.Value);
    Console.WriteLine(publicOnly.HasPrivateKey); // False
}

GetForLocalhost
public static ErrorOr<X509Certificate2> GetForLocalhost(string subject, string friendlyName, int keySize = 2048)

Convenience method that creates a self-signed certificate already valid (NotBefore = yesterday) and effectively non-expiring (NotAfter = DateTime.MaxValue, i.e. the year 9999), with the default localhost / loopback SAN entries. Ideal for local HTTPS during development.

Parameters

Name Type Description
subject string The subject name (e.g. "CN=localhost").
friendlyName string The Windows friendly name.
keySize int RSA key size in bits. Defaults to 2048.

Example

ErrorOr<X509Certificate2> result = CertificateFactory.GetForLocalhost(
    subject: "CN=localhost",
    friendlyName: "Local Dev HTTPS");

Console.WriteLine(result.Value.NotAfter.Year); // 9999

GetFromRawData
public static ErrorOr<X509Certificate2> GetFromRawData(byte[] certificateData, string password)

Loads a certificate (with private key) from PFX/PKCS#12 raw bytes using Exportable | PersistKeySet | MachineKeySet. The inverse of ExportToPfxRawData.

Example

byte[] pfxBytes = File.ReadAllBytes("mycert.pfx");

ErrorOr<X509Certificate2> result = CertificateFactory.GetFromRawData(pfxBytes, "S3cr3t!Passw0rd");

// A wrong password does not throw — it returns an ErrorType.Unexpected error:
if (result.IsError)
    Console.WriteLine(result.FirstError.Type); // Unexpected
else
    Console.WriteLine(result.Value.HasPrivateKey); // True

GetFromFile
public static ErrorOr<X509Certificate2> GetFromFile(string certificateFile, string? password)

Loads a certificate (with private key) directly from a PFX file on disk. The password may be null for a password-less PFX. A missing file or wrong password is returned as an ErrorType.Unexpected error rather than thrown.

Parameters

Name Type Description
certificateFile string Path to the .pfx file.
password string? The PFX password, or null if the file is not password-protected.

Example

ErrorOr<X509Certificate2> result = CertificateFactory.GetFromFile("mycert.pfx", "S3cr3t!Passw0rd");

result.Switch(
    cert  => Console.WriteLine($"Loaded {cert.Thumbprint}"),
    errs  => Console.WriteLine($"Could not load: {errs[0].Type}"));

CertificateAccess

public static class CertificateAccess

Accesses and manages certificates in the OS certificate store, and reads a certificate's Subject Alternative Names. All read methods open the store read-only; AddToStore/RemoveFromStore open it read-write.

Member Returns Summary
AddToStore(X509Certificate2, StoreName, StoreLocation) ErrorOr<Success> Add a certificate to a store.
FindByThumbprint(StoreName, StoreLocation, string) ErrorOr<X509Certificate2> Find a certificate by thumbprint (or NotFound).
GetAllFromStore(StoreName, StoreLocation) ErrorOr<IEnumerable<X509Certificate2>> Return every certificate in a store.
FindByDnsNames(StoreName, StoreLocation, IEnumerable<string>) ErrorOr<IEnumerable<X509Certificate2>> Find certificates whose SANs contain all of the given DNS names.
GetAlternateNames(X509Certificate2) ErrorOr<IEnumerable<string>> Read the IP-address and DNS-name SAN entries of a certificate.
RemoveFromStore(string, StoreName, StoreLocation) ErrorOr<Success> Remove a certificate by thumbprint (no-op if absent).

StoreName, StoreLocation come from System.Security.Cryptography.X509Certificates; Success comes from ErrorOr.


AddToStore
public static ErrorOr<Success> AddToStore(
    X509Certificate2 certificate,
    StoreName storeName,
    StoreLocation storeLocation)

Opens the specified store for read-write and adds the certificate.

Example

ErrorOr<Success> result = CertificateAccess.AddToStore(
    certificate,
    StoreName.My,                 // the "Personal" store
    StoreLocation.CurrentUser);   // per-user — no admin rights required

if (result.IsError) Console.WriteLine(result.FirstError.Description);

⚠️ Writing to StoreLocation.LocalMachine requires administrator privileges. StoreLocation.CurrentUser does not.


FindByThumbprint
public static ErrorOr<X509Certificate2> FindByThumbprint(
    StoreName storeName,
    StoreLocation storeLocation,
    string thumbprint)

Returns the certificate matching thumbprint, or an ErrorType.NotFound error if it is not present. The search is not restricted to valid certificates (expired/untrusted certificates are still returned).

Example

ErrorOr<X509Certificate2> result = CertificateAccess.FindByThumbprint(
    StoreName.My,
    StoreLocation.CurrentUser,
    certificate.Thumbprint);

if (!result.IsError)
    Console.WriteLine($"Found: {result.Value.Subject}");
else if (result.FirstError.Type == ErrorType.NotFound)
    Console.WriteLine("No certificate with that thumbprint.");

GetAllFromStore
public static ErrorOr<IEnumerable<X509Certificate2>> GetAllFromStore(
    StoreName storeName,
    StoreLocation storeLocation)

Returns all certificates currently in the store as a materialised list.

Example

ErrorOr<IEnumerable<X509Certificate2>> result = CertificateAccess.GetAllFromStore(
    StoreName.My,
    StoreLocation.CurrentUser);

foreach (X509Certificate2 cert in result.Value)
    Console.WriteLine($"{cert.Subject}  ({cert.Thumbprint})");

FindByDnsNames
public static ErrorOr<IEnumerable<X509Certificate2>> FindByDnsNames(
    StoreName storeName,
    StoreLocation storeLocation,
    IEnumerable<string> dnsEntries)

Returns every certificate in the store whose Subject Alternative Names contain all of the requested entries. Matching is case-insensitive. Because the comparison uses All, a certificate is returned only if it covers every name you pass.

Example

// Certificates that are valid for BOTH names:
ErrorOr<IEnumerable<X509Certificate2>> result = CertificateAccess.FindByDnsNames(
    StoreName.My,
    StoreLocation.CurrentUser,
    new[] { "myapp.local", "api.myapp.local" });

foreach (X509Certificate2 cert in result.Value)
    Console.WriteLine(cert.Thumbprint);

GetAlternateNames
public static ErrorOr<IEnumerable<string>> GetAlternateNames(X509Certificate2 certificate)

Reads the certificate's SAN extension and returns its IP-address entries (rendered via IPAddress.ToString()) followed by its DNS-name entries. A certificate with no SAN extension yields a successful, empty sequence.

Example

ErrorOr<IEnumerable<string>> result = CertificateAccess.GetAlternateNames(certificate);

foreach (string name in result.Value)
    Console.WriteLine(name);

// Typical output for a freshly created certificate:
// 127.0.0.1
// ::1
// 0.0.0.0
// ::
// 0.0.0.0
// localhost
// MACHINENAME
// myapp.local

RemoveFromStore
public static ErrorOr<Success> RemoveFromStore(
    string thumbprint,
    StoreName storeName,
    StoreLocation storeLocation)

Opens the store for read-write and removes the certificate identified by thumbprint. If no such certificate exists, the call is a harmless no-op that still reports success.

Example

ErrorOr<Success> result = CertificateAccess.RemoveFromStore(
    certificate.Thumbprint,
    StoreName.My,
    StoreLocation.CurrentUser);

AesCryptography

public static class AesCryptography

Derives AES keys with PBKDF2 and performs AES-CBC encryption/decryption of strings. Encrypted output is a compact "<base64 IV>:<base64 ciphertext>" string; a fresh random IV is generated for every AesEncrypt call.

Member Returns Summary
MakeAESKey(string, string, HashAlgorithmName?, int, int) ErrorOr<byte[]> Derive a key from a password + salt using PBKDF2.
AesEncrypt(string, string, HashAlgorithmName?, int, int) ErrorOr<string> Encrypt a string; returns "iv:ciphertext".
AesDecrypt(string, string, HashAlgorithmName?, int, int) ErrorOr<string> Decrypt an "iv:ciphertext" string.

Defaults: hashAlgorithmName = SHA-256, outputLength = 32 bytes (AES-256), iterations = 100 000.


MakeAESKey
public static ErrorOr<byte[]> MakeAESKey(
    string password,
    string salt,
    HashAlgorithmName? hashAlgorithmName = null,
    int outputLength = 32,
    int iterations = 100000)

Derives a symmetric key from password and salt using PBKDF2. Deterministic: the same inputs always produce the same key. A null password or salt returns an ErrorType.Validation error whose Description names the offending parameter.

Parameters

Name Type Description
password string The secret to derive the key from.
salt string The salt mixed into the derivation.
hashAlgorithmName HashAlgorithmName? The PBKDF2 PRF. Defaults to SHA-256 when null.
outputLength int Desired key length in bytes. Defaults to 32 (256-bit).
iterations int PBKDF2 iteration count. Defaults to 100000.

Example

ErrorOr<byte[]> key = AesCryptography.MakeAESKey("correct horse", "a-pinch-of-salt");

if (!key.IsError)
    Console.WriteLine(key.Value.Length); // 32

// Validation error for a null password:
ErrorOr<byte[]> bad = AesCryptography.MakeAESKey(null!, "salt");
Console.WriteLine(bad.FirstError.Type);        // Validation
Console.WriteLine(bad.FirstError.Description);  // password

AesEncrypt
public static ErrorOr<string> AesEncrypt(
    string password,
    string content,
    HashAlgorithmName? hashAlgorithmName = null,
    int outputLength = 32,
    int iterations = 100000)

Encrypts content with an AES key derived from password (via MakeAESKey) and a fresh random IV, returning "<base64 IV>:<base64 ciphertext>".

  • null password → ErrorType.Validation (Description = "password").
  • null/empty/whitespace content → a successful empty string (nothing to encrypt).
  • The same content encrypted twice yields different payloads (random IV).

When using AesEncrypt/AesDecrypt, keep outputLength at a valid AES key size — 16, 24 or 32 bytes — because the derived key is assigned to Aes.Key.

Example

ErrorOr<string> encrypted = AesCryptography.AesEncrypt("p@ssword", "Hello, world!");

Console.WriteLine(encrypted.Value); // e.g. "Q2hhbmdlSVZ...=:8f2bniH4...=="

AesDecrypt
public static ErrorOr<string> AesDecrypt(
    string password,
    string encryptedContent,
    HashAlgorithmName? hashAlgorithmName = null,
    int outputLength = 32,
    int iterations = 100000)

Decrypts a "<base64 IV>:<base64 ciphertext>" string produced by AesEncrypt, using the same password and key-derivation parameters.

Failure handling is deliberately lenient — the only error path is a null password (ErrorType.Validation). For any non-null password:

  • A correct password + valid payload → the original plaintext.
  • null/empty/whitespace input → a successful empty string.
  • Malformed input (not two colon-separated parts) → returned unchanged.
  • A wrong password or undecodable payload → the original input is returned unchanged (the plaintext is never disclosed), thanks to the internal .Else(...) fallback.

Example

string payload = AesCryptography.AesEncrypt("p@ssword", "Hello, world!").Value;

// Correct password round-trips back to the plaintext:
ErrorOr<string> ok = AesCryptography.AesDecrypt("p@ssword", payload);
Console.WriteLine(ok.Value); // Hello, world!

// Wrong password never reveals the plaintext (returns the input unchanged):
ErrorOr<string> wrong = AesCryptography.AesDecrypt("not-the-password", payload);
Console.WriteLine(wrong.Value == "Hello, world!"); // False

How Subject Alternative Names are built

When you call either CreateSelfSigned overload (and therefore GetForLocalhost), the SAN extension is assembled as follows:

  1. Your custom entries (dnsNames) are processed one by one:
    • A value that parses as an IP address is added as an IP SAN — unless it is a loopback address (127.0.0.1 / ::1), which is skipped because it is added by default below.
    • The reserved names localhost, *, and the current machine name are skipped (also added by default below).
    • Anything else is added as a DNS name.
  2. A fixed set of default entries is always appended:
    • IP addresses: 127.0.0.1, ::1, 0.0.0.0, ::, and 0.0.0.0 (any).
    • DNS names: localhost and the current machine name.

The net effect: every generated certificate is automatically valid for loopback, "any" addresses, localhost and the local machine — and the skip rules ensure those defaults are never duplicated even if you pass them explicitly.


End-to-end example

Create a certificate, persist it to a store, retrieve it, then clean up:

using System.Security.Cryptography.X509Certificates;
using ErrorOr;
using Forge.Next.Security;

// 1. Create a self-signed certificate valid for an internal host name.
ErrorOr<X509Certificate2> created = CertificateFactory.CreateSelfSigned(
    x500: "CN=Orders Service",
    friendlyName: "Orders Service TLS",
    dnsNames: new[] { "orders.internal", "10.0.0.20" },
    startTime: DateTime.UtcNow.AddDays(-1),
    endTime: DateTime.UtcNow.AddYears(3));

if (created.IsError)
    return;

X509Certificate2 certificate = created.Value;

// 2. Persist it to the current user's personal store (no admin rights needed).
CertificateAccess.AddToStore(certificate, StoreName.My, StoreLocation.CurrentUser);

try
{
    // 3. Later, retrieve it by thumbprint...
    ErrorOr<X509Certificate2> loaded = CertificateAccess.FindByThumbprint(
        StoreName.My, StoreLocation.CurrentUser, certificate.Thumbprint);

    // ...or by the names it must cover.
    ErrorOr<IEnumerable<X509Certificate2>> byDns = CertificateAccess.FindByDnsNames(
        StoreName.My, StoreLocation.CurrentUser, new[] { "orders.internal" });

    // 4. Inspect what it is valid for.
    foreach (string san in CertificateAccess.GetAlternateNames(loaded.Value).Value)
        Console.WriteLine(san);

    // 5. Back it up to a password-protected PFX for safe keeping.
    ErrorOr<byte[]> pfx = CertificateFactory.ExportToPfxRawData(loaded.Value, "Backup#2024");
    if (!pfx.IsError)
        File.WriteAllBytes("orders-service.pfx", pfx.Value);
}
finally
{
    // 6. Clean up the store entry when you are done.
    CertificateAccess.RemoveFromStore(
        certificate.Thumbprint, StoreName.My, StoreLocation.CurrentUser);
}

Using a certificate to host HTTPS with Kestrel:

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listen =>
    {
        ErrorOr<X509Certificate2> cert = CertificateFactory.GetForLocalhost("CN=localhost", "Dev HTTPS");
        listen.UseHttps(cert.Value);
    });
});

var app = builder.Build();
app.MapGet("/", () => "Hello over HTTPS!");
app.Run();

Encrypting and decrypting a secret:

string token = AesCryptography.AesEncrypt("master-password", "api-key-12345").Value;
string back  = AesCryptography.AesDecrypt("master-password", token).Value;
Console.WriteLine(back); // api-key-12345

Notes, gotchas & platform behaviour

  • Exception-free: expected failures are returned as ErrorOr errors, not thrown. Always check IsError (or use Match/Switch) before reading .Value.
  • Friendly names are Windows-only. friendlyName is applied only when running on Windows; on Linux/macOS the property is left empty (the certificate is otherwise identical).
  • MachineKeySet may require elevation. The password-based CreateSelfSigned, GetFromRawData and GetFromFile import keys with MachineKeySet. On Windows this writes to the machine key container and usually needs administrator rights; failures surface as ErrorType.Unexpected.
  • Store write permissions. AddToStore/RemoveFromStore against StoreLocation.CurrentUser work without elevation; StoreLocation.LocalMachine requires administrator rights.
  • Validity precision. X.509 stores NotBefore/NotAfter with whole-second precision, so sub-second parts of your DateTime arguments are truncated.
  • X509Certificate2.NotBefore / NotAfter are returned in local time. Convert with .ToUniversalTime() before comparing against UTC values.
  • AES key size. Through AesEncrypt/AesDecrypt, outputLength must be 16, 24 or 32 bytes (the derived key becomes Aes.Key). MakeAESKey on its own accepts any length.
  • AesDecrypt is lenient by design. With a non-null password it never returns an error: a wrong password or malformed payload yields the input unchanged rather than an exception, so it never leaks the plaintext but also will not tell you decryption "failed" — compare the result against the input if you need to detect that.
  • Disposal. X509Certificate2 is IDisposable. Dispose certificates you no longer need (e.g. with a using statement) to release key material promptly.

Testing

The repository contains an xUnit test suite under tests/Forge.Next.Security.Tests (xUnit + Shouldly + NSubstitute) that exercises the full public surface of all three classes. Run it with:

dotnet test

License

Licensed under the Apache-2.0 License. See the license for details.

Authored by Zoltan Juhasz — part of the Forge Patterns and Practices family.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.1.0 107 6/30/2026