Create X509Certificate2 from Cert and Key, without making a PFX file - c#

In the past I have been making secure TcpListener by exporting a PFX certificate with a password, but would like to know if this step could be skipped.
I'm not using commercial SSL certificates, and have a Root CA, that I use to issue server certificates. These server certificates require additional steps when hosting a TcpListener in C# (I guess because the CSR wasn't used)... but what if I do have the Private Key, and the Certificate that OpenSSL generates/uses.
sslCertificate = new X509Certificate2("myExportedCert.pfx", "1234");
So this is great, however I have to issue an openssl command to make a pfx file from the Certificate and the Private Key, then make up some password. Then include this password in my code.
I was wondering if this step was quite necessary. Is there a way to make up a X509Certificate2 from the Cert, and then apply the Private Key. The constructor arguments allow the Cert only part, but encrypting fails then because there is no private key.
Also, I don't want to rely on OpenSSL or IIS to export the pfx.... seems clumsy.
Ideally i would like:
sslCertificate = new X509Certificate2("myCert.crt");
sslCertificate.ApplyPrivateKey(keyBytes) // <= or "private.key" or whatever
sslStream.AuthenticateAsServer(sslCertificate, false, SslProtocols.Default, false);

There are a couple of different things you're asking for, with different levels of ease.
Attaching a private key to a certificate
Starting in .NET Framework 4.7.2 or .NET Core 2.0 you can combine a cert and a key. It doesn't modify the certificate object, but rather produces a new cert object which knows about the key.
using (X509Certificate2 pubOnly = new X509Certificate2("myCert.crt"))
using (X509Certificate2 pubPrivEphemeral = pubOnly.CopyWithPrivateKey(privateKey))
{
// Export as PFX and re-import if you want "normal PFX private key lifetime"
// (this step is currently required for SslStream, but not for most other things
// using certificates)
return new X509Certificate2(pubPrivEphemeral.Export(X509ContentType.Pfx));
}
on .NET Framework (but not .NET Core) if your private key is RSACryptoServiceProvider or DSACryptoServiceProvider you can use cert.PrivateKey = key, but that has complex side-effects and is discouraged.
Loading the private key
This one is harder, unless you've already solved it.
For the most part the answer for this is in Digital signature in c# without using BouncyCastle, but if you can move to .NET Core 3.0 things get a lot easier.
PKCS#8 PrivateKeyInfo
Starting in .NET Core 3.0 you can do this relatively simply:
using (RSA rsa = RSA.Create())
{
rsa.ImportPkcs8PrivateKey(binaryEncoding, out _);
// do stuff with the key now
}
(of course, if you had a PEM you need to "de-PEM" it, by extracting the contents between the BEGIN and END delimiters and running it through Convert.FromBase64String in order to get binaryEncoding).
PKCS#8 EncryptedPrivateKeyInfo
Starting in .NET Core 3.0 you can do this relatively simply:
using (RSA rsa = RSA.Create())
{
rsa.ImportEncryptedPkcs8PrivateKey(password, binaryEncoding, out _);
// do stuff with the key now
}
(as above, you need to "de-PEM" it first, if it was PEM).
PKCS#1 RSAPrivateKey
Starting in .NET Core 3.0 you can do this relatively simply:
using (RSA rsa = RSA.Create())
{
rsa.ImportRSAPrivateKey(binaryEncoding, out _);
// do stuff with the key now
}
(same "de-PEM" if PEM).

In the end i did this, and it works fine:
...
if (!File.Exists(pfx)) {
// Generate PFX
string arguments = "openssl pkcs12 -export -in " + certPath + "" + certFile + ".crt -inkey " + certPath + "" + certFile + ".key -out " + certPath + "" + certFile + ".pfx -passout pass:" + pfxPassword;
ProcessStartInfo opensslPsi = new ProcessStartInfo("sudo", arguments);
opensslPsi.UseShellExecute = false;
opensslPsi.RedirectStandardOutput = true;
using (Process p = Process.Start(opensslPsi)) {
p.WaitForExit();
}
// Set Permission
ProcessStartInfo chmodPsi = new ProcessStartInfo("sudo", "chmod 644 " + certPath + "" + certFile + ".pfx");
chmodPsi.UseShellExecute = false;
chmodPsi.RedirectStandardOutput = true;
using (Process p = Process.Start(chmodPsi)) {
p.WaitForExit();
}
}
sslCertificate = new X509Certificate2(pfx, pfxPassword);
...

Related

Combining Public Certificate with Private RSA Key in C#

I received from a client public certificate and private RSA key, as follows (only partially displayed here):
-----BEGIN CERTIFICATE-----
MIIFKDCCAxACAQEwDQYJKoZIhvcNAQELBQAwbjELMAkGA1UEBhMCVVMxETAPBgNV
BAgMCElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRMwEQYDVQQKDApDbG91ZFF1
YW50MSUwIwYJKoZIhvcNAQkBFhZjcWFpb3BzQGNsb3VkcXVhbnQuY29tMB4XDTIw
MDkyMzIwMTgyN1oXDTQwMDkxODIwMTgyN1owRjELMAkGA1UEBhMCVVMxETAPBgNV
iESEKmYvylUwce7TcOuVnLtufyXxr8egu43jPvWDHsK0QvhMbx0q2KvyxGneQJ5E
...........
U0oIrq7M0qZTAf1BXEw9wgfQlIKfLzWDbIYKIg==
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAuxHk8lBZaqRTzhi/jvlj59pehTkK/u2fVHLuHWZPGOvPiAjq
HqrAgM1urPpmQPC2QuWObmhvQ1uluo/tq6V56WZNUEYALZnXhGvVNrvnYfoFooI0
F+1dJI2xQ8AC11Khinj+yAPtKymMhZFOHzQaFnGvjhKWTn/I0MaoJc+TQ8JKbDQ0
ycvopM2finujmIb62cxxhkhkEC5T+yMUJ8MbCnfmWpYux/oU2CXSHrnPympCbx7x
cReH0BeZEZPMI7+1Yi7U+XKGc7RP+zt9AoIBAQDE0BZb3WfertNqmyj9TF73VAwq
SDtSa6MTC8bOdtQewCz9/zC13MSI+jZGRDKSh7nxNL+bgM0OgmM6n/5B/61SIRjM
hswgOU9AHewIFSB/5C/ZcxWqM+PrgYXXYfOl9ZeWs1x+YRKuqk/CW/Z2rHJXykNx
.............
czG95J+2TWdGAjPuFLA596PRXT5KN2ITOWXUym3UksHmonbJ9om+k0ckPr4J
-----END RSA PRIVATE KEY-----
Initially, I scanned many postings here and realized that the easiest way to combine the puplic PEM certificate with private RSA key (PEM format) is by using .NET 5:
// Needs .NET 5, that is .NET Core and VS2019 16.8 or higher
X509Certificate2 X509Cert = X509Certificate2.CreateFromPemFile("cert.cer", "cert.key");
X509Certificate2 cert = new X509Certificate2(mX509Cert.Export(X509ContentType.Pfx, "12345678"), "12345678");
This worked great.
However, later on I realized that the type of project I am targeting (VSTO Excel Add-In) is not supporting .NET 5 yet...
Therefore, I changed the above code to the following, (which also worked) not realizing that it also requires .NET 5...
X509Certificate2 pubOnly = new X509Certificate2("cert.cer");
RSA rsa = RSA.Create();
rsa.ImportRSAPrivateKey(keyBuffer, out _);
X509Certificate2 pubPrivEphemeral = pubOnly.CopyWithPrivateKey(rsa);
X509Certificate2 cert = new X509Certificate2(pubPrivEphemeral.Export(X509ContentType.Pfx, "12345678"), "12345678");
Then when I finally switched to .NET 4.7.2 and now 4.8 I was looking for compatible way to do the same, and tired several methods offered here, but in vain, none did not work - they had complex routines that threw exceptions...
So I am trying now BouncyCastle that helped a lot in the past and came up with this code:
string cerFilePath = Path.Combine(InstallDir, "cert.cer");
X509Certificate2 dotnetCertificate2 = new X509Certificate2(cerFilePath);
string keyFilePath = Path.Combine(InstallDir, "cert.key");
var pemReader = new Org.BouncyCastle.OpenSsl.PemReader(File.OpenText(keyFilePath));
var pemObject = pemReader.ReadObject();
var rsa = DotNetUtilities.ToRSA((RsaPrivateCrtKeyParameters)pemObject);
Unfortunately, the last line throws exception - there is something incompatible here and I do not know what it is... The exception message states:
"Unable to cast object of type 'Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair' to type 'Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters'."
So if anyone can either correct me with the above BouncyCastle code or show me pure .NET 4.6 - 4.8 code - I'd really appreciate.
EDIT:
So I did find another code from here
private X509Certificate2 CreateCertWithKey()
{
using (Log.VerboseCall())
{
X509Certificate2 x509Cert = null;
try
{
string cerFilePath = Path.Combine(InstallDir, "cert.cer");
using (TextReader tr = new StreamReader(cerFilePath))
{
publicPemCert = tr.ReadToEnd();
}
string keyFilePath = Path.Combine(InstallDir, "cert.key");
using (TextReader tr = new StreamReader(keyFilePath))
{
privatePemKey = tr.ReadToEnd();
}
var keyPair = (AsymmetricCipherKeyPair)new PemReader(new StringReader(privatePemKey)).ReadObject();
var cert = (Org.BouncyCastle.X509.X509Certificate)new PemReader(new StringReader(publicPemCert)).ReadObject();
var builder = new Pkcs12StoreBuilder();
builder.SetUseDerEncoding(true);
var store = builder.Build();
var certEntry = new X509CertificateEntry(cert);
store.SetCertificateEntry("", certEntry);
store.SetKeyEntry("", new AsymmetricKeyEntry(keyPair.Private), new[] { certEntry });
byte[] data;
using (var ms = new MemoryStream())
{
//store.Save(ms, Array.Empty<char>(), new SecureRandom());
data = ms.ToArray();
x509Cert = new X509Certificate2(data);
}
}
catch(Exception ex)
{
Log.Verbose(ex);
}
return x509Cert;
}
}
It runs without problem but with the generated certificate my HttpWebRequest does not work, claiming that TSL/SSL connection cannot be created (and I tries 1.1, 1.2, and 1.3, although I recall that just Tls negotiates the version by itself...
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;
The .NET 5 code ran without a problem with exactly the same code for HttpWebRequest...
Just came across this while working through similar issues and I found a way to create the X509Certificate2 object that includes the private key (which I use for XML Signing). It requires that you create a PFX record using the public/private keys, I used OpenSSL:
openssl pkcs12 -export -inkey private-key.pem -in cert.pem -out cert.pfx
You can either open just instantiate the instance directly with the file name:
var cert = new X509Certificate2(filePath);
Or if you have already opened the file somewhere and stored it in base64 format, you can use the bytes:
var bytes = Convert.FromBase64String(pfx_base64);
var cert = new X509Certificate2(bytes);
A lot of credit for this goes to reading Scott Brady's blog.

How to get private key as Byte[] of a password protected pfx fetched from azure key vault

I am fetching my certificate from Azure Key Vault using GetSecretAsync() method and then I am expecting to get the byte[] of the private key and the certificate eventually.
I have my application in .netcore3.1
This is how my code looks like :
var certWithPrivateKey = Client.GetSecretAsync(ConfigurationSettings.AppSettings["AKVEndpoint"], ConfigurationSettings.AppSettings["CertName"]).GetAwaiter().GetResult();
var privateKeyBytes = Convert.FromBase64String(certWithPrivateKey.Value);
X509Certificate2 x509Certificate = new X509Certificate2(privateKeyBytes);
var privateKey = x509Certificate.GetRSAPrivateKey() as RSA;
I get a valid privateKey of type RSACng, but any operation (tried ExportRSAPrivateKey()) on that throws an error of "'privateKey.ExportRSAPrivateKey()' threw an exception of type 'Internal.Cryptography.CryptoThrowHelper.WindowsCryptographicException'" and "The requested operation is not supported."
I am not sure how to proceed next here to get the byte[] of the private key and certificate.
Since you do actually seem to need to export: Your current code doesn't load the private key as exportable, so it can't be exported. The fix is to assert exportability:
X509Certificate2 x509Certificate =
new X509Certificate2(privateKeyBytes, "", X509KeyStorageFlags.Exportable);
If that's not enough, then you're encountering the difference between CAPI exportability and CNG exportability (Windows older, and newer, crypto libraries). If the private key from a PFX/PKCS#12 gets loaded into CNG it's only "encrypted exportable", but ExportParameters is plaintext-export.
There's a workaround, though... export it encrypted, then import that somewhere else with a more flexible export policy, then export again.
This snippet uses the .NET Core 3.0+ ExportPkcs8PrivateKey() method, since that's the format you want your data in, and new .NET 5 PemEncoding class to simplify turning the DER encoded output into PEM+DER output. If your exporter is on .NET Framework, this is a more complex problem. For .NET Standard 2.0 there's not really a clean solution (reflect call the methods for .NET Core/.NET 5, otherwise use the Windows-specific version for .NET Framework?).
byte[] pkcs8PrivateKey;
using (RSA privateKey = x509Certificate.GetRSAPrivateKey())
{
pkcs8PrivateKey = ExportPrivateKey(privateKey);
}
File.WriteAllText(
"tls.cer",
new string(PemEncoding.Write("CERTIFICATE", x509Certificate.RawData));
File.WriteAllText(
"tls.key",
new string(PemEncoding.Write("PRIVATE KEY", pkcs8PrivateKey));
...
private static byte[] ExportPrivateKey(RSA privateKey)
{
try
{
// If it's plaintext exportable, just do the easy thing.
return privateKey.ExportPkcs8PrivateKey();
}
catch (CryptographicException)
{
}
using (RSA exportRewriter = RSA.Create())
{
// Only one KDF iteration is being used here since it's immediately being
// imported again. Use more if you're actually exporting encrypted keys.
exportRewriter.ImportEncryptedPkcs8PrivateKey(
"password",
privateKey.ExportEncryptedPkcs8PrivateKey(
"password",
new PbeParameters(
PbeEncryptionAlgorithm.Aes128Cbc,
HashAlgorithmName.SHA256,
1)),
out _);
return exportRewriter.ExportPkcs8PrivateKey();
}
}

Certificate signing produces different signature when on server

I am trying to sign some data using a certificate private key. The issue I'm finding is that the signature is different depending on if I'm executing it locally or on a server.
I'm using the following code as a test, running under the same user both locally and on the server:
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace TestSignature
{
class Program
{
static void Main(string[] args)
{
var key = SigningKeyFromCertificate(StoreName.My, StoreLocation.LocalMachine, X509FindType.FindByThumbprint, "thumbprint");
var alg = CryptoConfig.MapNameToOID("SHA256");
var data = Encoding.UTF8.GetBytes("test");
var sig = key.SignData(data, alg);
Console.WriteLine(Convert.ToBase64String(sig));
}
private static RSACryptoServiceProvider SigningKeyFromCertificate(StoreName storeName, StoreLocation storeLocation, X509FindType findType, string findValue)
{
X509Store store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates.Find(findType, findValue, false);
if (certs?.Count > 0)
{
var cert = certs[0];
if (cert.HasPrivateKey)
{
// Force use of Enhanced RSA and AES Cryptographic Provider to allow use of SHA256.
var key = cert.PrivateKey as RSACryptoServiceProvider;
var enhanced = new RSACryptoServiceProvider().CspKeyContainerInfo;
var parameters = new CspParameters(enhanced.ProviderType, enhanced.ProviderName, key.CspKeyContainerInfo.UniqueKeyContainerName);
return new RSACryptoServiceProvider(parameters);
}
else
{
throw new Exception($"No private key access to cert '{findValue}.'");
}
}
else
{
throw new Exception($"Cert '{findValue}' not found!");
}
}
}
}
Locally, I get the following signature:
YUjspKhLl7v3u5VQkh1PfHytMTpEtbAftxOA5v4lmph3B4ssVlZp7KedO5NW9K5L222Kz9Ik9/55NirS0cNCz/cDhEFRtD4daJ9qLRuM8oD5hCj6Jt9Vc6WeS2he+Cqfoylnv4V9plfi1xw8y7EyAf4C77BGkXOdyP5wyz2Xubo=
On the server, I get this one instead:
u1RUDwbBlUpOgNNkAjXhYEWfVLGpMOa0vEfm6PUkB4y9PYBk1lDmCAp+488ta+ipbTdSDLM9btRqsQfZ7JlIn/dIBw9t5K63Y7dcDcc7gDLE1+umLJ7EincMcdwUv3YQ0zCvzc9RrP0jKJManV1ptQNnODpMktGYAq1KmJb9aTY=
Any idea of what could be different? I would think, with the same certificate, the same code, and the same data, the signature should be the same.
(The example is written in C# 4.5.2.)
You have some code to reopen the CAPI key handle under PROV_RSA_AES:
// Force use of Enhanced RSA and AES Cryptographic Provider to allow use of SHA256.
var key = cert.PrivateKey as RSACryptoServiceProvider;
var enhanced = new RSACryptoServiceProvider().CspKeyContainerInfo;
var parameters = new CspParameters(
enhanced.ProviderType,
enhanced.ProviderName,
key.CspKeyContainerInfo.UniqueKeyContainerName);
return new RSACryptoServiceProvider(parameters);
But key.CspKeyContainerInfo.UniqueKeyContainerName isn't the name of the key (it's the name of the file on disk where the key lives), so you're opening a brand new key (you're also generating a new ephemeral key just to ask what the default provider is). Since it's a named key it persists, and subsequent application executions resolve to the same key -- but a different "same" key on each computer.
A more stable way of reopening the key is
var cspParameters = new CspParameters
{
KeyContainerName = foo.CspKeyContainerInfo.KeyContainerName,
Flags = CspProviderFlags.UseExistingKey,
};
(since the provider type and name aren't specified they will use the defaults, and by saying UseExistingKey you get an exception if you reference a key that doesn't exist).
That said, the easiest fix is to stop using RSACryptoServiceProvider. .NET Framework 4.6 (and .NET Core 1.0) have a(n extension) method on X509Certificate2, GetRSAPrivateKey(), it returns an RSA (which you should avoid casting) which is usually RSACng (on Windows), but may be RSACryptoServiceProvider if only CAPI had a driver required for a HSM, and may be some other RSA in the future. Since RSACng handles SHA-2 better there's almost never a need to "reopen" the return object (even if it's RSACryptoServiceProvider, and even if the type isn't PROV_RSA_AES (24), that doesn't mean the HSM will fail to do SHA-2).

Creating of ECDSA certficate signed by other ECDSA certificate

I need to create "Client" ECDSA certificate signed by "Root" certificate (self-signed, ECDSA).
"Root" certificate was created as described in Translating Elliptic Curve parameters (BC to MS).
To create "Client" certificate (signed by "Root") slightly modified algorithm can be used.
The difference is that the private key (used to sign public key from keypair generated for "Client" certificate) must be supplied from the "outside" - it is a private-key of "Root" certificate.
But this is the issue. I cannot find a way how to get and translate private key to type Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters that could be passed to signature-factory.
// 1. get private-key of "Root" certificate from existing certificate:
byte[] msRootCertData = File.ReadAllBytes(#"c:\root_ecdsa_cert.pfx");
X509Certificate2 msRootCert = new X509Certificate2(msRootCertData);
ECDsaCng msRootPrivateKey = msRootCert.GetECDsaPrivateKey() as ECDsaCng;
ECParameters msRootPrivateKeyParameters = msRootPrivateKey.ExportParameters(true);
// here comes the issue:
ECPrivateKeyParameters bcRootPrivateKeysParameters = TranslateMSKeysToBouncy(msRootPrivateKeyParameters);
// 2. generate "Client" key-pair:
AsymmetricCipherKeyPair bcClientKeyPair = bcKeyGen.GenerateKeyPair();
ECPrivateKeyParameters bcClientPrivKey = (ECPrivateKeyParameters)bcClientKeyPair.Private;
ECPrivateKeyParameters bcClientPublKey = (ECPublicKeyParameters)bcClientKeyPair.Public;
// 3. create X509 certificate:
X509V3CertificateGenerator bcCertGen = new X509V3CertificateGenerator();
bcCertGen.SetPublicKey(bcClientPublKey);
// .. set subject, validity period etc
ISignatureFactory sigFac = new Asn1SignatureFactory("Sha256WithECDSA", bcRootPrivateKeysParameters);
Org.BouncyCastle.X509.X509Certificate bcClientX509Cert = bcCertGen.Generate(sigFac);
byte[] x509CertEncoded = bcClientX509Cert.GetEncoded();
// the rest is the same as in the mentioned example.
Any hints?
Or is there other way? (for example: passing instance of X509Certificate2 directly to BouncyCastle library (avoid to translate private-keys to Cng), or generating "Client" certificate without BouncyCastle)
Thanks.
If you can take a dependency on .NET Framework 4.7.2 (or .NET Core 2.0) you can do it without BouncyCastle, via the new CertificateRequest class:
X509Certificate2 publicPrivate;
using (ECDsa clientPrivateKey = ECDsa.Create())
{
var request = new CertificateRequest(
"CN=Et. Cetera",
clientPrivateKey,
HashAlgorithmName.SHA256);
// Assuming this isn't another CA cert:
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
// other CertificateExtensions as you desire.
// Assign, or derive, a serial number.
// RFC 3280 recommends that it have no more than 20 bytes encoded.
// 12 random bytes seems long enough.
byte[] serial = new byte[12];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetBytes(serial);
}
DateTimeOffset notBefore = DateTimeOffset.UtcNow;
DateTimeOffset notAfter = notBefore.AddMonths(15);
using (X509Certificate2 publicOnly = request.Create(
msRootCert,
notBefore,
notAfter,
serial))
{
publicPrivate = publicOnly.CopyWithPrivateKey(clientPrivateKey);
}
}
// The original key object was disposed,
// but publicPrivate.GetECDsaPrivateKey() still works.
If you want to add publicPrivate to an X509Store you need to either 1) export it to a PFX and re-import it, or 2) change the key creation to use a named key. Otherwise, only the public portion will be saved (on Windows).

CMS signing in .NET with certificate chain not in local trusted certificate store

I have X509 certificates that are stored on the network. I can read the chain from remote windows certificate store. I need to sign some data and include chain to the signature to make it possible to validate it later.
The problem is that I can't find a way to put certificate chain to the CsmSigner. I have read that it takes certificate from constructor parameter and tries to build a chain with X509Chain.Build. It ignores Certificates list values and fails (obviously) because no certificate can be found in the local Windows cert store.
Please find below my test code (that works only if certificates were saved locally to the windows cert store)
protected byte[] SignWithSystem(byte[] data, X509Certificate2 cert, X509Certificate[] chain)
{
ContentInfo contentInfo = new ContentInfo(data);
SignedCms signedCms = new SignedCms(contentInfo, true);
CmsSigner cmsSigner = new CmsSigner(cert);
cmsSigner.DigestAlgorithm = new Oid("2.16.840.1.101.3.4.2.1"); //sha256
cmsSigner.IncludeOption = X509IncludeOption.WholeChain;
if (chain != null)
{
//adding cert chain to signer
cmsSigner.Certificates.AddRange(chain);
signedCms.Certificates.AddRange(chain);
}
signedCms.ComputeSignature(cmsSigner); //fails here with System.Security.Cryptography.CryptographicException : A certificate chain could not be built to a trusted root authority.
byte[] signedPkcs = signedCms.Encode();
return signedPkcs;
}
Is there any way to make it work without uploading certificates to the local store? Should I use any alternative signer?
I can try to upload certificates to the store but the problems are that
I have to add and remove certificates (permissions have to be granted)
There are several processes that applies signature so cross-process synchronization have to be added.
This is not that I'd like to do.
Example CMS Signing with BouncyCastle for .NET
You could use the BouncyCastle crypto library for .NET, which contains its own X509 certificate and CMS signing machinery. A lot of the examples and documentation on the web are for Java, as BouncyCastle was a Java library first. I've used the answer to this Stackoverflow question as a starting point for the certificate and key loading, and added the CMS signing. You may have to tweak parameters to produce the results you want for your use case.
I've made the signing function look approximately like yours, but note the private key is a separate parameter now.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Org.BouncyCastle.Cms;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.X509.Store;
class Program
{
protected static byte[] SignWithSystem(byte[] data, AsymmetricKeyParameter key, X509Certificate cert, X509Certificate[] chain)
{
var generator = new CmsSignedDataGenerator();
// Add signing key
generator.AddSigner(
key,
cert,
"2.16.840.1.101.3.4.2.1"); // SHA256 digest ID
var storeCerts = new List<X509Certificate>();
storeCerts.Add(cert); // NOTE: Adding end certificate too
storeCerts.AddRange(chain); // I'm assuming the chain collection doesn't contain the end certificate already
// Construct a store from the collection of certificates and add to generator
var storeParams = new X509CollectionStoreParameters(storeCerts);
var certStore = X509StoreFactory.Create("CERTIFICATE/COLLECTION", storeParams);
generator.AddCertificates(certStore);
// Generate the signature
var signedData = generator.Generate(
new CmsProcessableByteArray(data),
false); // encapsulate = false for detached signature
return signedData.GetEncoded();
}
static void Main(string[] args)
{
try
{
// Load end certificate and signing key
AsymmetricKeyParameter key;
var signerCert = ReadCertFromFile(#"C:\Temp\David.p12", "pin", out key);
// Read CA cert
var caCert = ReadCertFromFile(#"C:\Temp\CA.cer");
var certChain = new X509Certificate[] { caCert };
var result = SignWithSystem(
Guid.NewGuid().ToByteArray(), // Any old data for sake of example
key,
signerCert,
certChain);
File.WriteAllBytes(#"C:\Temp\Signature.data", result);
}
catch (Exception ex)
{
Console.WriteLine("Failed : " + ex.ToString());
Console.ReadKey();
}
}
public static X509Certificate ReadCertFromFile(string strCertificatePath)
{
// Create file stream object to read certificate
using (var keyStream = new FileStream(strCertificatePath, FileMode.Open, FileAccess.Read))
{
var parser = new X509CertificateParser();
return parser.ReadCertificate(keyStream);
}
}
// This reads a certificate from a file.
// Thanks to: http://blog.softwarecodehelp.com/2009/06/23/CodeForRetrievePublicKeyFromCertificateAndEncryptUsingCertificatePublicKeyForBothJavaC.aspx
public static X509Certificate ReadCertFromFile(string strCertificatePath, string strCertificatePassword, out AsymmetricKeyParameter key)
{
key = null;
// Create file stream object to read certificate
using (var keyStream = new FileStream(strCertificatePath, FileMode.Open, FileAccess.Read))
{
// Read certificate using BouncyCastle component
var inputKeyStore = new Pkcs12Store();
inputKeyStore.Load(keyStream, strCertificatePassword.ToCharArray());
var keyAlias = inputKeyStore.Aliases.Cast<string>().FirstOrDefault(n => inputKeyStore.IsKeyEntry(n));
// Read Key from Aliases
if (keyAlias == null)
throw new NotImplementedException("Alias");
key = inputKeyStore.GetKey(keyAlias).Key;
//Read certificate into 509 format
return (X509Certificate)inputKeyStore.GetCertificate(keyAlias).Certificate;
}
}
}
.NET CMS (Quick-fix with rest of chain omitted from signature)
I can reproduce your problem with a certificate whose root is not in the trusted certificate store, and confirm that adding the certificate chain to the cmsSigner/signedCms Certificates collection does not avoid the A certificate chain could not be built to a trusted root authority error.
You can sign successfully by setting cmsSigner.IncludeOption = X509IncludeOption.EndCertOnly;
However, if you do this, you will not get the rest of the chain in the signature. This probably isn't what you want.
As an aside, in your example you are using X509Certificate for the array of certificates in the chain, but passing them to an X509Certificate2Collection (note the "2" in there). X509Certificate2 derives from X509Certificate, but if its not actually an X509Certificate2 that you put in one of those collections, you'll get a cast error if something iterates over the collection (you don't get an error when adding a certificate of the wrong type unfortunately, because X509Certificate2Collection also derives from X509CertificateCollection and inherits its add methods).
Adding sample code that creates detached PKCS7 signature using BouncyCastle (thanks to softwariness) without Certificate store.
It uses .net X509Certificate2 instances as input parameter. First certificate in collection have to be linked with private key to sign data.
Also I'd like to note that it is not possible to read private key associated with certificate from remote Windows cert store using .net X509Certificate2.PrivateKey property. By default private key is not loaded with certificate using X509Store(#"\\remotemachine\MY", StoreLocation.LocalMachine) and when X509Certificate2.PrivateKey property is accessed on local machine it fails with error "Keyset does not exist".
public void SignWithBouncyCastle(Collection<X509Certificate2> netCertificates)
{
// first cert have to be linked with private key
var signCert = netCertificates[0];
var Cert = Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(signCert);
var data = Encoding.ASCII.GetBytes(Cert.SubjectDN.ToString());
var bcCertificates = netCertificates.Select(_ => Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(_)).ToList();
var x509Certs = X509StoreFactory.Create("Certificate/Collection", new X509CollectionStoreParameters(bcCertificates));
var msg = new CmsProcessableByteArray(data);
var gen = new CmsSignedDataGenerator();
var privateKey = Org.BouncyCastle.Security.DotNetUtilities.GetKeyPair(signCert.PrivateKey).Private;
gen.AddSigner(privateKey, Cert, CmsSignedDataGenerator.DigestSha256);
gen.AddCertificates(x509Certs);
var signature = gen.Generate(msg, false).GetEncoded();
Trace.TraceInformation("signed");
CheckSignature(data, signature);
Trace.TraceInformation("checked");
try
{
CheckSignature(new byte[100], signature);
}
catch (CryptographicException cex)
{
Trace.TraceInformation("signature was checked for modified data '{0}'", cex.Message);
}
}
void CheckSignature(byte[] data, byte[] signature)
{
var ci = new ContentInfo(data);
SignedCms signedCms = new SignedCms(ci, true);
signedCms.Decode(signature);
foreach (X509Certificate cert in signedCms.Certificates)
Trace.TraceInformation("certificate found {0}", cert.Subject);
signedCms.CheckSignature(true);
}
To be clear, I am no security or cryptography expert.. but per my knowledge, for receiver to be able to validate the signature, the root certificate in the certificate chain you used for signing, must already be a trusted root for the receiver.
If the receiver does not have the root certificate already in their store, and marked as a trusted root... then doesn't matter how you sign the data.. it will fail validation on receiver end. And this is by design.
See more at Chain of trust
Hence the only real solution to your problem I see is to ensure that the root certificate is provisioned as trusted root on both ends... Typically done by a Certificate Authority.
Enterprise application scenario - Typically in an enterprise some group in IT department (who have access to all machines in the domain - like domain admins) would enable this scenario by ensuring that every computer in the domain has root certificate owned by this group, present on every machine as trusted root, and an application developer in the enterprise typically requests a new certificate for use with their application, which has the chain of trust going back to the root certificate already distributed to all machines in the domain.
Found out contact person for this group in your company, and have them issue a certificate you can use for signature.
Internet application scenario - There are established Certificate Authorities, who own their root certificates, and work with OS vendors to ensure that their root certificates are in trusted store, as the OS vendor ships the OS to it's customers. (One reason why using pirated OS can be harmful. It's not just about viruses / malware..). And that is why when you use a certificate issued by VeriSign to sign the data, the signature can be validated by most other machines in the world.

Categories

Resources