Lokad.Auth
1.0.0-rc2
Prefix Reserved
dotnet add package Lokad.Auth --version 1.0.0-rc2
NuGet\Install-Package Lokad.Auth -Version 1.0.0-rc2
<PackageReference Include="Lokad.Auth" Version="1.0.0-rc2" />
<PackageVersion Include="Lokad.Auth" Version="1.0.0-rc2" />
<PackageReference Include="Lokad.Auth" />
paket add Lokad.Auth --version 1.0.0-rc2
#r "nuget: Lokad.Auth, 1.0.0-rc2"
#:package Lokad.Auth@1.0.0-rc2
#addin nuget:?package=Lokad.Auth&version=1.0.0-rc2&prerelease
#tool nuget:?package=Lokad.Auth&version=1.0.0-rc2&prerelease
Lokad.Auth
A .NET library for establishing QUIC, HTTPS or TLS connections to a remote server using a TPM-backed private key for client authentication.
Features
- Client (
Lokad.Auth): use the TPM or a PFX file to sign outgoing connections via ephemeral client certificates. - Server (
Lokad.Auth.Server): verify incoming connections are signed correctly and manage a list of allowed clients. - SecOps (
Lokad.Auth.SecOps): set up the TPM and extract public keys to register on the server. - TrustMap (
Lokad.Auth.Trust): manage groups of principals and composePrincipalDirectoryfiles from JSON and PEM sources. - Blob (
Lokad.Auth.Blob): persist a private key or certificate on disk, encrypted under a TPM-resident unwrap key, so it survives a reboot without re-issuance and without ever storing the key in the clear.
How it works
On Linux, each client holds a long-lived CA certificate (the IssuerCertificate) whose private key is stored in the TPM. For every outgoing connection a short-lived EphemeralClientCertificate is signed by the issuer and used for TLS client authentication. On Windows the conventional path loads a direct client certificate from the machine certificate store instead.
The server holds a PrincipalDirectory — a GZ-compressed concatenation of trusted issuer certificates. When a connection arrives, the server verifies the ephemeral certificate signature against the directory and identifies the caller by the issuer's PrincipalIdentifier (the certificate CN).
Quick start
// Client side
var credentials = new TpmClientCredentials(
issuerHandle: 0x81660000,
authValue: CredentialsUnlocker.FromFile("/etc/lokad/auth.key"),
lifetime: 300,
clientIp: null);
var options = credentials.ToSslClientAuthenticationOptions("my-server.example.com");
// Server side
var directory = PrincipalDirectory.Load(File.ReadAllBytes("/etc/lokad/principals.bin"));
var validator = new ClientCredentialsValidator(directory, serverName: "my-server.example.com");
var serverOptions = validator.ToSslServerAuthenticationOptions(
clientIp: remoteEndPoint.Address,
onAuthenticated: principal => Console.WriteLine($"Authenticated: {principal}"));
Signing certificates using the TPM
In addition to client authentication, the TPM-backed issuer key can be used to sign arbitrary X.509 certificates — for example, to implement a TLS CA that issues leaf certificates from CSRs.
Call OpenSigningContext() on a TpmClientCredentials instance to obtain an IssuerSigningContext. The context provides:
IssuerCertificate— the long-lived CA certificate stored in the TPM NV index.SignatureGenerator— anX509SignatureGeneratorthat delegates signing to the TPM private key.
Pass both to CertificateRequest.Create() to produce a certificate signed by the TPM-resident issuer key. Dispose the context when done to release the TPM connection.
using var ctx = credentials.OpenSigningContext();
var cert = request.Create(
ctx.IssuerCertificate,
ctx.SignatureGenerator,
notBefore, notAfter,
serialNumber);
// Build PEM chain: leaf certificate followed by the issuer certificate.
var pemChain = cert.ExportCertificatePem() + ctx.IssuerCertificate.ExportCertificatePem();
The signing operation uses the signature algorithm associated with the TPM-resident issuer key, such as RSA-PKCS1v1.5 or ECDSA with SHA-256.
Persisting TPM-protected secrets across reboots (Lokad.Auth.Blob)
This is intended for server certificates rather than client certificates.
The signing keys described above never leave the TPM, which means every use of the key (e.g. a TLS handshake) is a TPM operation. That is fine for occasional issuer signing, but too slow to use directly as a Kestrel server certificate's private key: each handshake would serialize on the chip.
Lokad.Auth.Blob addresses this with envelope encryption instead of live signing: the TPM is only used once, at process startup, to unwrap a symmetric key that decrypts an on-disk blob into an ordinary in-memory certificate. All subsequent TLS operations run entirely in the .NET crypto stack, at normal speed, with no TPM in the hot path — while the on-disk blob remains meaningless without the specific TPM that sealed it (e.g. lifted from a stolen disk image).
This is a different key shape than the signing keys above: it requires a decrypt-capable TPM key (RSA-OAEP TPM2_RSA_Decrypt, or an ECC key used for TPM2_ECDH_ZGen), not a Sign-restricted one, so it is a distinct persistent handle from any IssuerCertificate key.
Key allocation
A BlobStore is constructed from a key path — a small file holding the TPM persistent handle and AuthValue used for all sealing done by that store instance.
- If the key path doesn't exist yet, the first
Seal()call provisions a fresh decrypt-capable TPM key: it scans the0x816A_xxxxrange (the prefix reserved forLokad.Auth.Blob) for a free slot, and claims it viaEvictControldirectly rather than check-then-create —EvictControlfails if the slot is already occupied, so a slot is never silently double-claimed even if twoBlobStores happened to scan concurrently. - The claimed handle is tagged with an owner label equal to the key path itself, stored alongside the wrap key (the same paired-NV-index mechanism the CA code uses to store a certificate next to its key). This makes an occupied handle self-describing: if a key path is later deleted without evicting its handle first, the orphaned TPM slot can still be traced back to the key path that created it, rather than showing up as an anonymous occupied handle.
- Once a key path exists, every subsequent
Seal()from aBlobStoreopened on that path reuses the same handle andAuthValue.
Blob format
Each sealed blob is fully self-contained: it carries its own TPM handle and AuthValue alongside the encrypted payload, so unsealing needs only the blob itself (plus the machine's TPM) — not the key path that created it. The key path is purely a seal-time convenience for handle reuse; if a blob and the key path ever disagree (e.g. the key path was deleted and reprovisioned after the blob was sealed), the handle and AuthValue embedded in the blob are what's used to unseal it.
Sequence:
- Provisioning — generate a random AES-256 data-encryption key (DEK); encrypt the private key/certificate with it (AES-GCM); wrap the DEK itself with the TPM decrypt key; write
handle || AuthValue || wrapped-DEK || IV || AES-GCM(cert+key)to disk as a small versioned blob, using write-to-temp-then-rename so a crash never leaves a partial file. - Unlock at boot — read the blob; call the TPM once (using the handle/
AuthValueembedded in the blob) to unwrap the DEK; AES-decrypt the payload in memory; materialize anX509Certificate2by combining the certificate with an ephemeral in-memory RSA or ECDSA private key (the recovered private key is never written back to disk). - Serve — hand the resulting certificate to Kestrel (e.g. via
HttpsConnectionAdapterOptions.ServerCertificateSelector) like any other in-memory certificate. - Refresh — when a new certificate is issued (e.g. by Flash), re-run step 1 to replace the blob, so the next reboot unlocks the latest certificate even if the issuer is unreachable at that time.
API:
// Provisioning (whenever a new certificate/key is issued)
var store = new BlobStore(keyPath: "/etc/example/blobs.tpm");
byte[] blob = store.Seal(leafCertWithKey);
File.WriteAllBytes("/etc/example/server-tls.blob", blob);
// Unlock at boot — no key path needed, the blob is self-contained.
// No TPM involved after this call returns.
X509Certificate2 cert = BlobStore.Unseal(File.ReadAllBytes("/etc/example/server-tls.blob"));
The same store can also seal arbitrary bytes instead of a certificate (e.g. a symmetric key, a token), using the identical envelope scheme:
byte[] blob = store.Seal((ReadOnlyMemory<byte>)someSecretBytes);
byte[] recovered = BlobStore.UnsealBytes(blob);
A sealed blob does not record what kind of payload it holds, so the caller is responsible for remembering which of Unseal (certificate) or UnsealBytes (raw bytes) matches how a given blob was sealed.
As with the other TPM handles, the AuthValue is not a secret that must be kept from the machine's own TPM-equipped owner — it is useless without the physical TPM that generated the wrap key. But it is a capability: any local process that can read the key file or a sealed blob, and can reach that TPM, can invoke the wrap key. Both the key file and every sealed blob should therefore get the same file-permission treatment as any other private-key material (e.g. 0600, owned by the service account that needs it, not group- or world-readable, excluded from casual backups/snapshots of the containing directory).
Platform notes
- Linux: requires a TPM 2.0 device accessible via the Microsoft TSS stack.
- Windows: uses the Windows certificate store with an optional TPM-backed key via the Microsoft Platform Crypto Provider.
- PFX credentials are available on Linux as a fallback (private key stored on disk — use with caution).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Microsoft.TSS (>= 2.1.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.