Remotely push PFX Cert and Private Key with .NET and Powershell - c#

The task at hand is to push a certificate from a central server to recipient servers. I'm able to leverage the x509certificate2 methods to accomplish the certificate install. Even tho the storage flags direct the add() method to install the private key, it doesn't install it on the remote machine. In the code below, please trust that $CertObj is a x509certificate2 object created with the storage flags Exportable, MachineKeySet, and PersistKeySet.
Function Import-CertificateObject
{
Param
(
[parameter(mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$Computer
)
$CertStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList "\\$($Computer)\$Location",$Store
$CertStore.Open("ReadWrite")
$CertStore.Add($CertObj)
$CertStore.Close()
}
Using powershell to isolate the installed certificate object I can see that the HasPrivateKey property has been set to true. This is the case when the installed certificate is inspected locally on the server and from the remote server which installed it. Next, if you inspect the PrivateKey property from server which had the certificate installed, it's blank. However, when inspected from the server which installed it, powershell returns object data for the private key.
Using ProcMon I could see that when the certificate is installing the regkey's for the private key it's doing so on the server which is performing the install on the remote server. I need .Net to install the certificate private key on the remote machine. I've read over the x509certificate2 docs but it doesn't touch on remote installs at all, nor is there an in depth explanation as to what these methods do.
I'm hoping it's as simple as changing an environment variable before the add() method, or maybe I've just totally approached this from the wrong angle. So how do I get it to install the private key on the remote server, not the server pushing the cert?

You can't move/copy certificates with associated private keys over a network. With your code, you are just copying the public part of the certificate. Private key remains on a source server and is not moved/copied anywhere.
HasPrivateKey property is a store-attached property and has a little relation to the fact of existence of the private key and is not a reliable way to determine whether the private key is installed for that certificate.
The only correct way to copy certificate with private key across machines is to:
Export certificate and assiciated private key by using this: Export(X509ContentType, SecureString) or this: Export(X509ContentType, String) overload.
copy PFX file to destination servers and use one of these overloads to import certificate with private key to X509Certificate2 object and use X509Store object to install it to the store.
However, be aware, that if private key was not marked as exportable during key generation or installation, your task will be impossible, because private key is protected by a CSP/KSP and you will be unable to export the key from provider.

The solution here was to leverage Invoke-Command -ScriptBlock{} to execute the code on the machine locally.

Related

Difference between MakeCert and OpenSSL wrt C# SslStream

I am attempting to create a TCP connection on TLS encryption in C#, using an OpenSSL generated cert.
The SslStream sample code given here seems to work using a cert created with windows makecert.exe, which comes out as a .cer file. It does not work with a cert created through OpenSSL, which is a .pem file, throwing excpetion: "The server mode SSL must use a certificate with the associated private key."
Both cert files are being parsed successfully into X509Certificate objects as I can see their issuers and subjects and other properties, and note that the "HasPrivateKey" property is false "PrivateKey" property is null on both of them.
I found this which says that you need to combine the (OpenSSL) certificate and private key into one PKCS12 package. Yet a PKCS12 package is the file format .pfx -- why does an OpenSSL cert have to be combined with a private key into a pfx, but a makecert-generated .cer file does not need to be a pfx (and is not PKCS12)?
Also, what is different about the OpenSSL certificate in the first place that makes it throw this exception about a missing private key, when the MakeCert certificate also does not have a private key? Or to put another way, why does the MakeCert certificate not seem to require a private key, like the OpenSSL cert?
Someone on IRC suggested to me that it might be that makecert is putting credentials in the windows Certificate Store. I didn't expect that would be the case since we are exporting files, but I moved my SslStream prototype (with makecert certificates) to a different machine (ie not the one where makecert was run) and I got the same error "The server mode SSL must use a certificate with the associated private key."
So officially, the answer is "makecert puts the key into your cert store, and when you run AuthenticateAsClient, in the background it is pulling the private key out of there. This is why the makecert certificate works and the OpenSSL cert doesn't, even though they are almost identical.
Taking it a step further, to get the thing working with OpenSSL certs, I'm sure the advice I linked in the question about combining the public and private keys into a PKCS12 (.pfx) file would have worked, however I wanted to load/combine them on the fly. I found this codeproject article which contains sample code that does all the dirty work of decoding the RSA private key, making it into an RSACryptoProvider, and attaching that object to the X509Certificate2.PrivateKey property. After doing this and providing paths to the seperate cert and private key files generated by OpenSSL, everything works great!

How to create a certificate to use with SslStream AuthenticateAsServer without importing

I'm lost in a twisty maze of certificates and private keys.
I am writing a server in C#. I want it to accept SSL connections. For test purposes, I want to generate a certificate for "localhost", and use that certificate for the server. Ideally I don't want to pollute my certificate store, so I just want a file, or files, on disk that the C# code can load to provide the certificate.
Everything I have tried (using cookbook approaches suggested by web searches) either gives me "The credentials supplied to the package were not recognized" or "The server mode SSL must use a certificate with the associated private key."
Is there a way to generate a certificate and private key, and subsequently to load them into an X509Certificate object without also loading them into my machine certificate store?
In the end, I ran the following to create a server.pfx file:
makecert.exe -r -pe -n "CN=localhost" -sky exchange -sv server.pvk server.cer
pvk2pfx -pvk server.pvk -spc server.cer -pfx server.pfx
Then I loaded it in code with:
certificate = new X509Certificate2("server.pfx", "password");
(I didn't actually hard code the password like that :-)
The trick was to know that I needed a pfx file, and that I needed to load it using the X509Certificate2 class, rather than X509Certificate.
Niki Loche method works.
If you get The specified network password is not correct., then you should try it without password in C#. It doesn't matter what your input password was in makecert.
certificate = new X509Certificate2("Server.pfx", "");
But if you want to use password (there is a reason, it's there :)), try changing pvk2pfx.exe command to:
pvk2pfx.exe" -pi password -pvk Server.pvk -spc Server.cer -pfx Server.pfx
and in C# enter:
certificate = new X509Certificate2("Server.pfx", "password");
Password must be the same as it is in creating cer file.
That did the trick for me. I hope it will help someone.
There are a number of tools that should let you act as your own CA and generate a certificate. XCA is one of them. There are also a number of methods using OpenSSL commands, for example.
Generating a self-signed certificate only may seem like the easiest option, but using a test CA (and a separate server certificate) may be worth it. This would allow you to import the test CA into the browser's store if needed to make the tests more realistic. It's not much more difficult with the right tools (e.g. XCA).
Once you have generated your server certificate and its private key, turn it into a PKCS#12 file (.p12/.pfx).
You should then be able to load it using X509Certificate2.import(...). (See example in this answer.)
I recently developed a SMTP server with TLS. I needed to install the server as a Windows Service on a Windows Server 2012 box. I used a Let's Encrypt SSL Certificate (* wildcard) for my domain. While developing "on the server" I had to run Visual Studio as Administrator for my code to work using "new X509Certificate("cert.pfx", "{password}") and it ran perfectly. However, once installed as a service, that scheme did not work. Turns out the simpler/safer way is to use X509Store. Here's the code that solved the problem...
private X509Certificate GetSslCertificate()
{
X509Certificate cert = null;
string certname = "*.mydomain.com";
try
{
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly);
foreach (X509Certificate certificate in store.Certificates)
{
if (certificate.Subject.ToLower().Contains(certname))
{
cert = certificate;
break;
}
}
}
catch (Exception) { /* Handle Exception */ }
return cert;
}
Also notice I used "LocalMachine" as the StoreLocation. You'll need to change to CurrentUser if that's where you installed your SSL Certificate.
If you have IIS, you can do it through the IIS Manager as detailed here:
http://blogs.msdn.com/b/ericnel/archive/2009/09/22/using-iis-to-generate-a-x509-certificate-for-use-with-the-windows-azure-service-management-api-step-by-step.aspx
I had the same issue when importing a machine certificate from the machine store (not the user store). I solved it by running Visual Studio as an administrator, as administrator privileges are required to access the private key of a machine store certificate.

"The credentials supplied to the package were not recognized" error when authenticating as server with certificate generated using BouncyCastle

I'm trying to create a certificate using the BouncyCastle.Crypto dll, which is then used to authenticate a SslStream as the server in a Windows Service process, which runs under the Local System account.
However when I get to the SslStream.AuthenticateAsServer(certificate) call, it throws a Win32 exception with the error message "The credentials supplied to the package were not recognized".
There are several questions on here about this error message, but none of them seem to describe, or solve, my particular problem.
In the hope that someone may be able to offer some help, I include the code I am using to create and install the certificate:
// First create a certificate using the BouncyCastle classes
BigInteger serialNumber = BigInteger.ProbablePrime(120, new Random());
AsymmetricCipherKeyPair keyPair = GenerateKeyPair();
X509V1CertificateGenerator generator = new X509V1CertificateGenerator();
generator.SetSerialNumber(serialNumber);
generator.SetIssuerDN(new X509Name("CN=My Issuer"));
generator.SetNotBefore(DateTime.Today);
generator.SetNotAfter(DateTime.Today.AddYears(100));
generator.SetSubjectDN(new X509Name("CN=My Issuer"));
generator.SetPublicKey(keyPair.Public);
generator.SetSignatureAlgorithm("SHA1WITHRSA");
Org.BouncyCastle.X509.X509Certificate cert = generator.Generate(
keyPair.Private, SecureRandom.GetInstance("SHA1PRNG"));
// Ok, now we have a BouncyCastle certificate, we need to convert it to the
// System.Security.Cryptography class, by writing it out to disk and reloading
X509Certificate2 dotNetCert;
string tempStorePassword = "Password01"; // In real life I'd use a random password
FileInfo tempStoreFile = new FileInfo(Path.GetTempFileName());
try
{
Pkcs12Store newStore = new Pkcs12Store();
X509CertificateEntry entry = new X509CertificateEntry(cert);
newStore.SetCertificateEntry(Environment.MachineName, entry);
newStore.SetKeyEntry(
Environment.MachineName,
new AsymmetricKeyEntry(keyPair.Private),
new [] { entry });
using (FileStream s = tempStoreFile.Create())
{
newStore.Save(s,
tempStorePassword.ToCharArray(),
new SecureRandom(new CryptoApiRandomGenerator()));
}
// Reload the certificate from disk
dotNetCert = new X509Certificate2(tempStoreFile.FullName, tempStorePassword);
}
finally
{
tempStoreFile.Delete();
}
// Now install it into the required certificate stores
X509Store targetStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
targetStore.Open(OpenFlags.ReadWrite);
targetStore.Add(dotNetCert);
targetStore.Close();
Ok, now I have created and installed the certificate. I then configure my Windows Service to use this certificate by supplying it with the generated certificate's thumbprint. I then use the certificate like this:
// First load the certificate
X509Certificate2 certificate = null;
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 certInStore in store.Certificates)
{
if (certInStore.Thumbprint == "...value not shown...")
{
certificate = certInStore;
break;
}
}
SslStream sslStream = new SslStream(new NetworkStream(socket, false), false);
// Now this line throws a Win32Exception
// "The credentials supplied to the package were not recognized"
sslStream.AuthenticateAsServer(certificate);
Does anyone have any idea what the problem could be here?
I don't get the problem if I install a certificate created with 'makecert', but that isn't suitable for production certificates.
I've also tried creating a separate x509v1 CA certificate and then x509v3 certificate for server authentication, but I get the same error, so I removed this in the example code for simplicity.
That particular error message rings a bell. I'll guess that either you did not store the private key with the certificate, or, the Windows service does not have access to the private key. To check this, open the Certificates MMC snap-in:
Run mmc (e.g. from the Start menu)
File menu > Add/Remove Snap-in
Select "Certificates" in left pane and then click Add
Select "Computer Account" (for LocalMachine) then click Next,
and then Finish
Navigate to the certificate and double-click in the right pane. On the General tab that comes up, you should see a little key icon at the bottom, along with the text, "You have a private key that corresponds to this certificate." If not, that's the problem. The private key was not saved.
If the private key is present, click Ok to dismiss this dialog, and then right-click on the certificate in the right pane and select on the pop-up menu: All Tasks > Manage Private Keys. In that dialog, make sure that the Windows account that the service runs under has read access to the private key. If it doesn't, that's the problem.
Edit: Oops, you wrote that the service runs as Local System, so it must be a missing private key, if it is one of these two problems. I'll leave the key access check in my answer anyway, for anybody else that hits this and is not running as Local System.
Sometime the problem happens when the application try to reach the certificate doesn't have enough privilege to access the certificate, the issue may resolve by running the application as administrator.
I've the same issue, tried everything from many posts, and google researching.
But looks like I found fix.
When I changed Identify from ApplicationPoolIdentity to LocalSystem everything start working perfectly.
May be will be helpful for someone.
For me works on Windows Server 2012 R2 (.net 4.6.1) - "All Tasks > Manage Private Keys" and set access to Everyone (setting to IS_IUSRS was not enough)
Found this solution online but I can't find the source to give the credit.
Since I ran into the "The credentials supplied to the package were not recognized" problem with AuthenticateAsClient() (for client verification), I'd like to document how I solved it. It's a different method with the same end goal. Since it might be useful for AuthenticateAsServer(), figured why not.
Here I convert a BC Certificate to a .NET certificate. Add an extra step in converting it to a .NET X509Certificate2 to store it's PrivateKey property.
Org.BouncyCastle.X509.X509Certificate bcCert;
X509Certificate dotNetCert = DotNetUtilities.ToX509Certificate(bcCert);
X509Certificate2 dotNetCert2 = new X509Certificate2(dotNetCert);
Problem showed up when adding a BouncyCastle private key to a .NET private key. The X509 certificates converted fine but not the private keys. I converted the BC private key to RSACryptoServiceProvider using the provided DotNetUtilities. Unfortunately it looks like the conversion isn't complete. So I created another RSACryptoServiceProvider which I then initialized. Then I imported the private key into the one I created.
// Apparently, using DotNetUtilities to convert the private key is a little iffy. Have to do some init up front.
RSACryptoServiceProvider tempRcsp = (RSACryptoServiceProvider)DotNetUtilities.ToRSA((RsaPrivateCrtKeyParameters)ackp.Private);
RSACryptoServiceProvider rcsp = new RSACryptoServiceProvider(new CspParameters(1, "Microsoft Strong Cryptographic Provider",
new Guid().ToString(),
new CryptoKeySecurity(), null));
rcsp.ImportCspBlob(tempRcsp.ExportCspBlob(true));
dotNetCert2.PrivateKey = rcsp;
After that, I was able to save the X509Certificate2 object directly to the key store. I didn't need the actual file so I skipped that step.
Previously, every time I have run into this issue, I have had to delete the cert out of my local machine cert store and re-import it. Then it all seems happy. I can't see how it could be a global permissions issue or invalid cert if simply re-importing it fixes the issue.
How I finally fixed it was using the winhttpcertcfg tool from the Windows Resource Kit to grant permission to the specific user that was using the cert.
The syntax would be:
"C:\Program Files (x86)\Windows Resource Kits\Tools\winhttpcertcfg" -i cert.p12 -c LOCAL_MACHINE\My -a UserWhoUsesTheCert -p passwordforp12
I had the similar issue when calling a WCF REST service from .NET application where I need to attach the client certificate; All I had to do was provide access to the certificate in cert store[mmc console] to the "NETWORKSERVICE] off course my IIS Pool was default pool which indicates its using NETWORKService user account.
the mistake that I did was, I copied the cert from another store to Local
Machine -> Personnel store where the certificate was protected with password. should import the certificate explicitly in required store.
If you running from IIS, ensure that the Application Pool has 'Load User Profile' set to true.
This was the only solution for me.
I don't recall this error but the certificate you're creating is not a valid to be used for SSL/TLS, including:
v1 (not v3) certificate;
missing extensions;
invalid CN;
...
There are several RFC that talks about this, including RFC5246 on TLS (1.2).
Finally making your own certificates is not more suitable than using the ones made by makecert (but the last one can generate the minimum set to be usable for an SSL/TLS server certificate).
I strongly suggest you to buy, from a good known Certificate Authority (CA), a SSL/TLS certificate for production. That will get you a working certificate recognized by the most browsers and tools.
Another reason for this error is that you ran the application / server under an account which password has changed, breaking its capability of accessing the certificate it wants to use in the certificate store.
This especially may not be as obvious if you use a NuGet package like LettuceEncrypt which automatically stores the LetsEncrypt in your store.
Delete the certificate from your store and reimport it.

X509Certificate.CreateFromCertFile - the specified network password is not correct

I have a .NET application that I want to use as a client to call an SSL SOAP web service. I have been supplied with a valid client certificate called foo.pfx. There is a password on the certificate itself.
I've located the certificate at the following location: C:\certs\foo.pfx
To call the web service, I need to attach the client certificate. Here's the code:
public X509Certificate GetCertificateFromDisk(){
try{
string certPath = ConfigurationManager.AppSettings["MyCertPath"].ToString();
//this evaluates to "c:\\certs\\foo.pfx". So far so good.
X509Certificate myCert = X509Certificate.CreateFromCertFile(certPath);
// exception is raised here! "The specified network password is not correct"
return cert;
}
catch (Exception ex){
throw;
}
}
It sounds like the exception is around the .NET application trying to read the disk. The method CreateFromCertFile is a static method that should create a new instance of X509Certificate. The method isn't overridden, and has only one argument: the path.
When I inspect the Exception, I find this:
_COMPlusExceptionCode = -532459699
Source=mscorlib
Question: does anyone know what the cause of the exception "The specified network password is not correct" ?
Turns out that I was trying to create a certificate from the .pfx instead of the .cer file.
Lesson learned...
.cer files are an X.509 certificate in binary form. They are DER encoded.
.pfx files are container files. Also DER encoded. They contain not only certificates, but also private keys in encrypted form.
You might need to user X509Certificate2() with a parameter of X509KeyStorageFlags.MachineKeySet instead. This fixed a similar issue we had. Credit to the original website that suggested this: http://vdachev.net/2012/03/07/c-sharp-error-creating-x509certificate2-from-a-pfx-or-p12-file-in-production/
Quoting:
Cause
The cause of the problem doesn’t seem to have much to do with the
error messages. For some reason the constructor is trying to get
access to the private key store although the private key is in stored
in the file being opened. By default the user key store is used but
ASP.NET (and probably non-interactive Windows services in general) are
not allowed to open it. Chances are the user key store for the
selected account doesn’t even exist.
Solution
One thing you could try is creating a user key store by logging into
the account and importing a certificate in its Personal store (and
then remove it again).
Another solution is to pass an additional parameter to the constructor
– a flag indicating the private keys are (supposed to be) stored in
the local computer – X509KeyStorageFlags.MachineKeySet, like this: var
certificate = new X509Certificate2(fileName, password,
X509KeyStorageFlags.MachineKeySet);
For a PFX with no password, then password can be specified as string.Empty.
See also https://stackoverflow.com/a/8291956/130352
Depending on your situation you probably need to install the certificate on the server first to get the trust level up before you export the .cer file.
I had to do this for a similar project and here were my notes on how it was accomplished.
Replace the Foo.cer with an export of the certificate installed on the server. (Install the cert from the pfx file and then export it to a cer file.)
Command for IIS6 to allow IIS_WPG group access to the cert key. Need to install winhttpcertcfg (You can follow the link below to grab your own copy).
C:\Program Files\Windows Resource Kits\Tools>winhttpcertcfg -i (Path to pfx file, eg. e:\Certs\Foo.pfx) -c LOCAL_MACHINE\My -a IIS_WPG -p (Password for pfx file)
Spits out key info and grants privilege
Granting private key access for account:
(SERVERNAME)\IIS_WPG
Download WinHttpCertCfg.msi here that installs the exe.
More info on how to use the cert config can be found here.
Then it just goes back to how you are doing your cert push.
//Cert Challenge URL
Uri requestURI = new Uri("https://someurl");
//Create the Request Object
HttpWebRequest pageRequest = (HttpWebRequest)WebRequest.Create(requestURI);
//After installing the cert on the server export a client cert to the working directory as Foo.cer
string certFile = MapPath("Foo.cer");
X509Certificate cert = X509Certificate.CreateFromCertFile(certFile);
//Set the Request Object parameters
pageRequest.ContentType = "application/x-www-form-urlencoded";
pageRequest.Method = "POST";
pageRequest.AllowWriteStreamBuffering = false;
pageRequest.AllowAutoRedirect = false;
pageRequest.ClientCertificates.Add(cert);
This how I passed the cert but not sure exactly what you are needing to do with your cert so this might not be the same for you.
The 'the specified network password is not correct' error message is also returned when the certificate you are trying to import in one of the OS stores is already present in that store.
In my case I was trying to run in the Private Application mode and I got the same error.
The specified network password is not correct
The PrivateAuthenticator constructor (in Xero.Api.Example.Applications.Private) was trying to import the certificate assuming there is no password defined during the creation of the certificate.
_certificate = new X509Certificate2();
_certificate.Import(certificatePath);
Then I changed the import to use an overload method which uses the password,
_certificate.Import(certificatePath, "mypasswordusedtocreatethecertificate", X509KeyStorageFlags.MachineKeySet);
You might need to X509KeyStorageFlags.MachineKeySet.
I am using certificate from web job.
In my case changing Identity to NetworkService in Application Pool solved this problem.

Import certificate with private key programmatically

I'm trying to use the HttpListener class in a C# application to have a mini webserver serve content over SSL. In order to do this I need to use the httpcfg tool. I have a .pfx file with my public and private key pair. If I import this key pair manually using mmc into the local machine store, everything works fine. However, if I import this key pair programmatically using the X509Store class, I am not able to connect to my mini webserver. Note that in both methods the cert is getting imported to the MY store in LocalMachine. Oddly, I am able to view the certificate in mmc once I programmatically import it and when I view it, the UI indicates that a private key is also available for this certificate.
Digging a little deeper, I notice that when I manually import the key pair, I can see a new file appear in C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys, but one does not appear when I import programmatically. On a related note, when I delete a manually imported certificate, it does not remove the corresponding private key file from the previously mentioned directory.
Ultimately, my question is this: When I programmatically add the certificate to the store, where is the private key being stored and why isn't it accessible to the HttpListener class (HttpApi)?
Note that this question is slightly related but I don't think permissioning is the problem since this is all being done as the same Windows user:
How to set read permission on the private key file of X.509 certificate from .NET
Ok, I figured it out. It had to do with the key storage parameters for the certificate object. For anyone else that runs into this problem, make sure you construct your X509Certificate2 objects that you are adding to the store using the X509KeyStorageFlags.PersistKeySet and X509KeyStorageFlags.MachineKeySet flags. This will force the private key to persist in the machine key set location which is required by HttpApi (HttpListener wraps this).
Is this a 2 way SSL? If it is then did you send over a SSL Certificate Request file generated on your machine? This Certificate Request file will be used to create the SSL and they together form a public private key pair.
Also did you try assigning the cert permission for the user account that is being used to run the web app? You can do this by using the Microsoft WSE 3.0 tool.
Not exactly the answer to your question, but here for reference of others going down this path:
Here is a link to a MS chat that gives sample C# code to do what httpcfg does, thus eliminating the need for the tool on deployment.

Categories