Start Debugging

Fix: System.Security.Cryptography.CryptographicException: Keyset does not exist

Der private Schlüssel des Zertifikats liegt in einer separaten Windows-Schlüsseldatei, die die Prozessidentität nicht lesen kann. Setzen Sie die ACL, laden Sie das PFX mit MachineKeySet oder verwenden Sie EphemeralKeySet.

Die Lösung: Es handelt sich um NTE_BAD_KEYSET (HRESULT 0x80090016), das aus einem Win32-Krypto-Aufruf hochsprudelt. Das Zertifikatsobjekt in Ihrer Hand enthält einen öffentlichen Schlüssel, aber die private Schlüsseldatei auf der Festplatte liegt an einem separaten Ort, und die aktuelle Prozessidentität hat keine Leseberechtigung darauf, oder diese Datei existiert nicht mehr für das Benutzerprofil, unter dem Sie laufen. Drei Lösungen decken nahezu jeden Fall ab: Gewähren Sie der Prozessidentität Lesezugriff auf den privaten Schlüssel (Zertifikat-Snap-In oder icacls auf der Schlüsselcontainerdatei), laden Sie das PFX mit X509KeyStorageFlags.MachineKeySet | PersistKeySet, damit der Schlüssel im Maschinenspeicher landet, oder laden Sie das PFX mit EphemeralKeySet, sodass gar keine Datei auf der Festplatte nötig ist.

System.Security.Cryptography.CryptographicException: Keyset does not exist
   at System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32 hr)
   at System.Security.Cryptography.X509Certificates.CertificatePal.GetPrivateKey[T](Func`2 createCsp, Func`2 createCng)
   at System.Security.Cryptography.X509Certificates.X509Certificate2.GetRSAPrivateKey()
   at MyApp.SigningService.Sign(Byte[] payload)

Dieser Leitfaden wurde gegen .NET 11 preview 4 auf Windows Server 2025 / Windows 11 24H2 geschrieben. Der Fehler ist seit .NET Framework 1.1 identisch, denn die Meldung kommt wörtlich aus NTE_BAD_KEYSET in winerror.h. Was sich im modernen .NET geändert hat, ist das Standardverhalten der Schlüsselspeicherung beim Laden eines PFX: .NET 9 hat X509CertificateLoader eingeführt, die Konstruktoren X509Certificate2(byte[]) und new X509Certificate2(path, password) für PFX-Inhalte als veraltet markiert (SYSLIB0057) und den empfohlenen Ladepfad verschoben. Die neue API persistiert Schlüssel nicht still auf der Festplatte, wie es die alten Konstruktoren taten. Wenn Sie das eine mechanisch durch das andere ersetzt haben, ist der Fehler, den Sie lesen, die Konsequenz.

Zwei Dinge sind vor jeder Lösung unten zu prüfen. Erstens liefert certificate.HasPrivateKey auch dann true, wenn die Schlüsseldatei fehlt oder unzugänglich ist. Das Flag verfolgt, ob die Zertifikatsmetadaten einen privaten Schlüssel anzeigen, nicht ob Sie ihn tatsächlich öffnen können. Zweitens ist der Fehler identisch bei einem fehlenden Schlüsselcontainer, einem Schlüsselcontainer auf einem anderen Benutzerprofil und einem Schlüsselcontainer, dessen ACL Ihren Prozess verweigert. Die Abhilfe hängt davon ab, welcher der drei Fälle vorliegt; die folgenden Abschnitte gehen sie der Reihe nach durch.

Warum ein Zertifikat überhaupt eine Schlüsseldatei hat

Unter Windows ist ein X.509-Zertifikat ein öffentliches Dokument. Der zugehörige private Schlüssel liegt nie in der Zertifikatsdatei; er lebt in einem Schlüsselcontainer, der vom Kryptoanbieter verwaltet wird. Es gibt zwei Anbieter und zwei Geltungsbereiche, die Sie auseinanderhalten müssen:

Beim Import eines PFX schreibt der Loader das Zertifikat in den Eigene Zertifikate-Speicher (Cert:\CurrentUser\My oder Cert:\LocalMachine\My) und das Schlüsselmaterial in eines dieser vier Verzeichnisse, je nach X509KeyStorageFlags. Das Herausziehen des Zertifikats aus dem Speicher in ein X509Certificate2 ruft nur den öffentlichen Teil plus einen Zeiger auf den Schlüsselcontainer ab. Beim ersten Aufruf von GetRSAPrivateKey(), GetECDsaPrivateKey() oder der veralteten PrivateKey-Eigenschaft öffnet die Laufzeit diesen Container über die entsprechende Win32-API. Wenn die Datei verschwunden ist, die SID keine Lese-ACE besitzt oder der Pfad auf ein Profil verweist, das in der aktuellen Anmeldesitzung nicht existiert, erhalten Sie NTE_BAD_KEYSET.

Die Erkenntnis: Zertifikat und Schlüssel sind entkoppelt, und die Frage “hat dieser Prozess den privaten Schlüssel?” ist eigentlich “hat diese Prozessidentität Lesezugriff auf die Datei, auf die dieses Zertifikat verweist?”. Jede Lösung unten beantwortet diese Frage auf andere Weise.

Minimaler Repro

Das kürzeste Programm, das den Fehler nach dem Wechsel auf den .NET-9+-Loader reproduziert:

// .NET 11 preview 4, Windows 11 24H2
using System.Security.Cryptography.X509Certificates;

var bytes = File.ReadAllBytes("signing.pfx");
var cert = X509CertificateLoader.LoadPkcs12(bytes, password: "test");

using var rsa = cert.GetRSAPrivateKey();
var signature = rsa!.SignData("hello"u8.ToArray(), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

Aus einer interaktiven Desktop-Sitzung funktioniert das. Führen Sie dieselbe Binärdatei als Windows-Dienst unter NT SERVICE\MyService oder aus einem IIS-Anwendungspool mit LoadUserProfile = false aus, und GetRSAPrivateKey() wirft “Keyset does not exist”. Das PFX wurde sauber geladen. Das Zertifikatsobjekt wurde konstruiert. Der Absturz passiert in dem Moment, in dem Sie nach dem privaten Schlüssel greifen.

Der Grund: X509CertificateLoader.LoadPkcs12 verwendet standardmäßig X509KeyStorageFlags.DefaultKeySet, was unter Windows den Benutzer-Schlüsselspeicher bedeutet. Eine Dienst-Identität ohne geladenes Benutzerprofil hat kein %APPDATA%\Microsoft\Crypto\..., in das geschrieben werden könnte, also wird die Schlüsseldatei an einer temporären, beim nächsten Aufruf unerreichbaren Stelle erzeugt, oder gar nicht erzeugt.

Lösung 1: der aktuellen Identität Lesezugriff auf den Schlüssel gewähren

Das ist die richtige Lösung, wenn das Zertifikat bereits im Maschinenspeicher installiert ist und Sie den Importcode nicht kontrollieren (typisch für ops-verwaltete Server). Öffnen Sie certlm.msc (LocalMachine) oder certmgr.msc (CurrentUser), suchen Sie das Zertifikat unter Eigene Zertifikate > Zertifikate, Rechtsklick, Alle Aufgaben > Private Schlüssel verwalten, und fügen Sie die Prozessidentität mit Berechtigung Lesen hinzu.

Dieselbe Operation aus PowerShell, automatisiert für einen IIS-Anwendungspool:

# Windows 11 24H2 / Windows Server 2025, PowerShell 7.5
$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object Thumbprint -eq 'AABBCC...'
$keyFile = $cert.PrivateKey.Key.UniqueName  # CNG

$keyPath = Join-Path $env:ProgramData "Microsoft\Crypto\Keys\$keyFile"
$acl = Get-Acl $keyPath
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    'IIS APPPOOL\MyAppPool', 'Read', 'Allow')
$acl.AddAccessRule($rule)
Set-Acl $keyPath $acl

Für CAPI-Zertifikate tauschen Sie Crypto\Keys gegen Crypto\RSA\MachineKeys und lesen stattdessen cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName. Das Cmdlet Get-PfxCertificate wirkt hier verlockend, gibt aber nicht das lebende Zertifikat aus dem Speicher zurück; verwenden Sie Get-ChildItem Cert:\..., damit die PrivateKey-Eigenschaft auf den Container auf der Festplatte zeigt.

Für einen Windows-Dienst unter NT SERVICE\<DienstName> ist die SID virtuell, und Sie gewähren sie mit icacls:

icacls $keyPath /grant "NT SERVICE\MyService:R"

Falls unklar ist, unter welcher SID Ihr Prozess läuft, druckt whoami /user aus einer als diese Identität gestarteten Eingabeaufforderung sie aus. Für Windows-Dienste in Containern sind nt authority\system und nt authority\network service die üblichen Verdächtigen.

Lösung 2: das PFX mit den richtigen Schlüsselspeicher-Flags laden

Das ist die richtige Lösung, wenn Ihre Anwendung die Zertifikatsdatei besitzt und beim Start lädt. Verlassen Sie sich nicht mehr auf DefaultKeySet, sondern werden Sie explizit:

// .NET 11 preview 4, long-running service that needs the key to survive restarts
var cert = X509CertificateLoader.LoadPkcs12FromFile(
    "signing.pfx",
    password: "test",
    keyStorageFlags: X509KeyStorageFlags.MachineKeySet
                   | X509KeyStorageFlags.PersistKeySet
                   | X509KeyStorageFlags.Exportable);

MachineKeySet schreibt den Schlüssel nach %ProgramData%\Microsoft\Crypto\Keys (CNG) unter eine ACL, die die Dienst-Identität lesen kann. PersistKeySet behält die Datei auf der Festplatte, nachdem das X509Certificate2 verworfen wurde; ohne dieses Flag wird der Schlüssel seit .NET Framework 4.7.2 standardmäßig in dem Moment gelöscht, in dem das Zertifikatsobjekt aus dem Gültigkeitsbereich fällt, was beim zweiten Aufruf desselben Codepfads “Keyset does not exist” auslöst. Exportable ist optional und nur erforderlich, wenn Sie später ExportPkcs8PrivateKey aufrufen oder den Schlüssel in einen anderen Speicher schreiben. Hinweis: PersistKeySet und EphemeralKeySet schließen sich gegenseitig aus; beide zu übergeben wirft CryptographicException aus dem X509CertificateLoader.

Für eine gehostete .NET-11-App, in der Sie den Schlüssel nur in-process brauchen und niemals eine Datei auf der Festplatte wollen, verwenden Sie EphemeralKeySet und überspringen die Persistenzfrage ganz:

// .NET 11 preview 4, ASP.NET Core minimal API loading a cert from configuration
var cert = X509CertificateLoader.LoadPkcs12FromFile(
    path,
    password,
    keyStorageFlags: X509KeyStorageFlags.EphemeralKeySet);

EphemeralKeySet ist die robusteste Wahl für Container, Scale-to-Zero-Workloads und jede Umgebung, in der das Benutzerprofil fehlen oder recycelt werden kann. Der Schlüssel lebt nur innerhalb der X509Certificate2-Instanz; sobald Sie sie verwerfen, ist der Schlüssel weg. Der Kompromiss: Das Zertifikat ist danach nicht mehr in den Windows-Speicher importierbar, und im .NET Framework vor 4.7.2 / .NET Core vor 2.0 existierte das Flag noch nicht. Unter Linux und macOS ist es ein No-op, weil dort gar keine Windows-Schlüsseldatei vorliegt.

Lösung 3: Eigenheiten von IIS-Anwendungspool- und Windows-Dienst-Identitäten

IIS-Anwendungspools sind die häufigste einzelne Quelle dieses Fehlers. Zwei Einstellungen entscheiden, ob Ihre Suche nach dem privaten Schlüssel funktioniert:

Für Windows-Dienste, die mit der .NET-11-Vorlage für gehostete Dienste plus Microsoft.Extensions.Hosting.WindowsServices geschrieben sind, ist das Pendant, den Dienst mit seinem eigenen virtuellen Konto zu registrieren:

sc.exe create MyService binPath= "C:\svc\MyService.exe" obj= "NT SERVICE\MyService" start= auto
icacls "C:\ProgramData\Microsoft\Crypto\Keys\<keyfile>" /grant "NT SERVICE\MyService:R"

Vermeiden Sie es, Dienste unter LocalSystem zu betreiben, aus demselben Grund wie bei Anwendungspools: Sie gewähren versehentlich auch Lesezugriff auf jeden anderen Maschinen-Schlüssel auf dem Host. Das Prinzip der minimalen Berechtigung gilt für Krypto-Container ebenso wie für Dateipfade.

Lösung 4: Azure App Service, Azure Functions, Container

Cloud-Hosts treffen eine schärfere Variante dieses Problems, weil das zugrunde liegende Dateisystem nicht Ihres ist. Drei plattformspezifische Stellschrauben:

Für .NET-11-AWS-Lambda-Funktionen gilt dieselbe Logik wie für Linux-Container: Es gibt keinen Windows-Schlüsselspeicher, laden Sie das PFX mit EphemeralKeySet aus einer Lambda-Layer oder dem Secrets Manager, niemals aus /tmp zwischen Aufrufen, denn die Sandbox garantiert nicht, dass /tmp Kaltstarts überdauert.

Lösung 5: den Schlüssel mit der modernen API lesen, nicht mit PrivateKey

Die Eigenschaft X509Certificate2.PrivateKey existiert in .NET 11 noch, ist aber veraltet (SYSLIB0028 für den Setter, SYSLIB0058 für den Getter bei EC-Schlüsseln). Sie liefert ein AsymmetricAlgorithm, das intern CAPI aufruft, selbst wenn das Zertifikat als CNG importiert wurde, und die CAPI-Shim-Schicht ist diejenige, die am zuverlässigsten “Keyset does not exist” wirft, wenn der Schlüsselcontainer fehlt. Die modernen Zugriffsmethoden prüfen vorab und legen klarere Ausnahmen offen:

// .NET 11 preview 4
using var rsa = cert.GetRSAPrivateKey();
using var ecdsa = cert.GetECDsaPrivateKey();
using var dsa = cert.GetDSAPrivateKey();

Wenn HasPrivateKey true ist, aber GetRSAPrivateKey() null zurückgibt, verwendet das Zertifikat einen Nicht-RSA-Algorithmus; versuchen Sie stattdessen GetECDsaPrivateKey(). Wenn GetRSAPrivateKey() “Keyset does not exist” wirft, behaupten die Metadaten, dass ein Schlüssel existiert, die Datei tut es nicht, und eine der Lösungen 1 bis 4 trifft zu.

Fallstricke und Verwechslungsfehler

Verwandt

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück