We had several issues when setting up a Net TCP server in WCF with transport security. I'm sharing our findings to help anyone encountering any of the same later, even though some of this has been mentioned in other posts:
The first issue was that the key was not accessible, because when generating a self-signed certificate, the certificate "knows" about the key but cannot access it.
This is fixed by exporting the certificate as bytes, and re-importing it, creating a new object that knows about the secret key.
var temp = GenerateCertificate(subjectName ?? DEFAULT_SUBJECT, keySize, validDays);
var completeCert = new X509Certificate2(temp.Export(X509ContentType.Pfx, password), password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
temp.Dispose();
return completeCert;
private static X509Certificate2 GenerateCertificate(string subjectName, int keySize, int validDays)
{
var cngKey = new CngKeyCreationParameters
{
ExportPolicy = CngExportPolicies.AllowPlaintextExport,
KeyUsage = CngKeyUsages.AllUsages,
Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider
};
rsa = new RSACng(CngKey.Create(CngAlgorithm.Rsa, null, cngKey));
var request = new CertificateRequest(subjectName, rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.KeyEncipherment, false));
var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(10));
return certificate;
}
Note that this generates a key file in C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys, (OR in some cases C:\ProgramData\Microsoft\Crypto\Keys) - More on that later...
Then, creating a self signed certificate at runtime and setting it to service like this:
ServiceHost host = new ServiceHost(service);
X509Certificate2 certificate = MyNamespace.GenerateCertificate();
host.Credentials.ServiceCertificate.Certificate = certificate;
This worked like a charm while running from IDE, but not when running the program in a real environment. What now?
The mentioned keys are generated and saved as files on the computer. This is due to the inner workings of the Microsoft cryptographic service providers and therefore you don't really have a choice.
However this is a problem when the program is running under a user that don't have access to these folders. The solution is to add these permissions!
Do this however you like, we added a powershell script to be run at install time that grants the "Everyone" user group read, write, and delete access to this folder (NSIS calls this .ps1 script). This can look like:
$FolderPath = "$env:ALLUSERSPROFILE\Microsoft\Crypto\RSA\MachineKeys"
$Acl = Get-ACL $FolderPath
# protect folder from inherited rules
$Acl.SetAccessRuleProtection($True, $False)
#Everyone group regardless of localization
$Everyone = New-Object System.Security.Principal.SecurityIdentifier('S-1-1-0')
#add user permission to folder
$AccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule($Everyone,"Read,Write,Delete","ContainerInherit,ObjectInherit","none","Allow")
$Acl.SetAccessRule($AccessRule)
#apply changes
Set-Acl $FolderPath $Acl
Voilá! Now it works when a normal user runs the program on Windows 11.
BUT wait, it still throws when running the program as a service on a windows 10 PC. AND the keys aren't being generated in Crypto/RSA/MachineKeys folder, but in Crypto/Keys.
This one tripped us up for a long time. It seemed no matter what we did on windows 10 it did not work.
The process run as a service, so we troubleshooted all the possible implications this could have, with no luck. The process ran under LOCAL SYSTEM, which should mean the file access shouldn't be an issue as SYSTEM has access to everything.
A deep dive into the stack trace and the "magical" workings of the "old" CryptoAPI and the newer Crypto Next Generation and some decompiled Microsoft package code, revealed that the validation of the certificate fails because of a check performed in System.ServiceModel.Security.SecurityUtils.CanKeyDoKeyExchange(X509Certificate2 certificate).
This method has a flag to ensure it doesn't try to access the obsolete X509Certificate2.PrivateKey property, WHICH IS STILL IN USE because the transition from CAPI to CNG hasn't gone entirely flawless. (No judgement here Microsoft, we all make mistakes.)
So the final piece of the puzzle that took us weeks to troubleshoot is to set the System.ServiceModel.LocalAppContextSwitches.DisableCngCertificates flag to false for out application.
For our WPF application this was done by adding this to the app.config:
<configuration>
<runtime>
<AppContextSwitchOverrides value="Switch.System.ServiceModel.DisableCngCertificates=false" />
</runtime>
</configuration>
And lo and behold it works!