Adding a certificate chain to a PKCS#7/CMS container in C# - c#

I'm getting a PKCS#7/CMS container from a service I'm using that includes a user certificate and a signature.
I am able to see the single certificate using SignedCms
var cert = GetCertFromServer();
SignedCms signedCms = new SignedCms();
signedCms.Decode(cert);
// The user certificate
X509Certificate2 userCert = signedCms.Certificates[0];
But I need to add the intermediate and root certificate to that chain. SignedCms.Certificates seems to be immutable, so I can't add the certificates directly to the collection and I am not able to replace the certificates collection the following way
// root, inter and leaf certs are X509Certificate2 objects read from files
X509Certificate2[] certArr = new[] {rootCert, interCert, leafCert, userCert};
X509Certificate2Collection chain = new X509Certificate2Collection(certArr);
signedCms.Certificates = chain; // Error: No setter
Since SignedCms.Certificates has no setter. I haven't been able to find a way to create a new container from this one. I have also not had any luck finding info on how to do this in bouncy castle or any other library.
I need to have a container containing the certificate chain and a signature and write the bytes from that container to a PDF file I'm signing. Is there a way to add certificates to the chain in the container?

I know it's been several years since this question was asked, but I recently ran into the same question.
For anyone with the same issue, the X509Certificate2Collection class has an Export method.
X509Certificate2[] certArr = certificates.ToArray();
X509Certificate2Collection chain = new X509Certificate2Collection(certArr);
byte[] result = chain.Export(X509ContentType.Pkcs7);

.NET Core 3.0 has added SignedCms.AddCertificate. You can't control the ordering (SignedCms.Encode() writes the data using the DER encoding rules, which dictates that this particular collection be sorted smallest first), but that's okay... X509Chain straightens it out.
SignedCms signedCms = new SignedCms();
signedCms.Decode(data);
signedCms.AddCertificate(leafCert);
signedCms.AddCertificate(interCert);
signedCms.AddCertificate(rootCert);
byte[] reencoded = signedCms.Encode();

Related

C# X509 certificate validation, with Online CRL check, without importing root certificate to trusted root CA certificate store

I'm trying to validate an X509 certificate chain without importing the root CA certificate into the trusted root CA certificate store (in production this code will run in an Azure Function, and you can't add certificates to the trusted root CA certificate store on Azure App Services).
We also need to perform an online CRL check on this certificate chain.
I've searched on this and I see many others are facing the same problem, but none of the suggestions seem to work. I've followed the approach outlined in this SO post, which echoes the suggestions from issue #26449 on the dotnet/runtime GitHub. Here's a small console application (targetting .NET Core 3.1) reproducing the problem:
static void Main(string[] args)
{
var rootCaCertificate = new X509Certificate2("root-ca-cert.cer");
var intermediateCaCertificate = new X509Certificate2("intermediate-ca-cert.cer");
var endUserCertificate = new X509Certificate2("end-user-cert.cer");
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(rootCaCertificate);
chain.ChainPolicy.ExtraStore.Add(intermediateCaCertificate);
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.Build(endUserCertificate);
chain.Build(new X509Certificate2(endUserCertificate));
var errors = chain.ChainStatus.ToList();
if (!errors.Any())
{
Console.WriteLine("Certificate is valid");
return;
}
foreach (var error in errors)
{
Console.WriteLine($"{error.Status.ToString()}: {error.StatusInformation}");
}
}
When ran this returns three errors:
UntrustedRoot: A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider.
RevocationStatusUnknown: The revocation function was unable to check revocation for the certificate.
OfflineRevocation: The revocation function was unable to check revocation because the revocation server was offline.
However, if I add the root CA certificate to the trusted root CA certificate store then all three errors disappear.
Questions
Is this something wrong with my implementation, or is what I'm trying to do not possible?
What are my options to try to achieve this? A bit of Googling suggests the X509ChainPolicy.CustomTrustStore offered in .NET 5 might save the day. Is Bouncy Castle another option for achieving this?
A bit of Googling suggests the X509ChainPolicy.CustomTrustStore offered in .NET 5 might save the day
Yep.
Instead of putting rootCaCertificate into ExtraStore, put it into CustomTrustStore, then set chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;. Now your provided root is the only root valid for the chain. You can also remove the AllowUnknownCertificateAuthority flag.
OfflineRevocation
This error is slightly misleading. It means "revocation was requested for the chain, but one or more revocation responses is missing". In this case it's missing because the builder didn't ask for it, because it didn't trust the root. (Once you don't trust the root you can't trust the CRLs/OCSP responses, so why ask for them at all?)
RevocationStatusUnknown
Again, the unknown is because it didn't ask for it. This code is different than OfflineRevocation because technically a valid OCSP response is (effectively) "I don't know". That'd be an online/unknown.
UntrustedRoot
Solved by the custom trust code above.
Other things of note: The correct way to determine the certificate is valid is to capture the boolean return value from chain.Build. For your current chain, if you had disabled revocation (chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck) then Build would have returned true... but the UntrustedRoot error would still be present in the ChainStatus output. The boolean return from Build is false if there are any errors that the VerificationFlags didn't say to ignore.
You also only need to call Build once :).
static void Main(string[] args)
{
var rootCaCertificate = new X509Certificate2("root-ca-cert.cer");
var intermediateCaCertificate = new X509Certificate2("intermediate-ca-cert.cer");
var endUserCertificate = new X509Certificate2("end-user-cert.cer");
var chain = new X509Chain();
chain.ChainPolicy.CustomTrustStore.Add(rootCaCertificate);
chain.ChainPolicy.ExtraStore.Add(intermediateCaCertificate);
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
bool success = chain.Build(endUserCertificate);
if (success)
{
return;
}
foreach (X509ChainStatus error in chain.ChainStatus)
{
Console.WriteLine($"{error.Status.ToString()}: {error.StatusInformation}");
}
}
Well, not a full answer, but probably it will get you going.
We've built before an Azure Web App (not a function app, but I guess wouldn't matter) which did exactly what you want. Took us a week or so. Full answer would be posting the code of the whole app, we I obviously cannot do. Some hints though.
We walked around the certificate problem by uploading certificates to the app (TLS settings) and accessing them in code through WEBSITE_LOAD_CERTIFICATES setting (you put thumbprints there, it's also mentioned in the link you posted), than you can get them in code and build your own certificate store:
var certThumbprintsString = Environment.GetEnvironmentVariable("WEBSITE_LOAD_CERTIFICATES");
var certThumbprints = certThumbprintsString.Split(",").ToList();
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
for (var i = 0; i < certThumbprints.Count; i++)
{
store.Certificates.Find(X509FindType.FindByThumbprint, certThumbprint, false);
}
Than we implemented a number of validators: for subject, time, certificate chain (you basically mark your root somehow and go from the certificate you validate up the chain in your store and see if you end up in your root) and CRL.
For CRL Bouncy Castle offers support:
// Get CRL from certificate, fetch it, cache it
var crlParser = new X509CrlParser();
var crl = crlParser.ReadCrl(data);
var isRevoked = crl.IsRevoked(cert);
Getting CRL from the certificate is tricky, but doable (I followed this for the purpose, more or less https://learn.microsoft.com/en-us/archive/blogs/joetalksmicrosoft/pki-authentication-as-a-azure-web-app).

What's the simplest way to save and load a CngKey from a PFX file, given a SecureString password?

Given a newly generated exportable public/private key pair CngKey and a SecureString, what API calls do I need to make to create a PFX file containing the public/private contents secured with the password? And given a PFX file and a SecureString password, what API calls are needed to load the contents into a CngKey?
It looks like the base class library does not provide this, but I'd prefer p/invoke over taking a dependency on a third party library. I also want to avoid storing the private key decrypted in memory even momentarily if there is a way (similar to SecureString) to keep it secure between the PFX and the CngKey.
While trying to wrap my head around these concepts, all the examples I can find involve self-signed certificates. This isn't a certificate, just a public/private blob I want to password protect. I don't want to import the blob into a store, I want to use it from a PFX file.
This is as far as I get on my own:
using (var key = CngKey.Create(CngAlgorithm.ECDsaP521, null, new CngKeyCreationParameters { ExportPolicy = CngExportPolicies.AllowExport, KeyUsage = CngKeyUsages.Signing }))
using (var algorithm = new ECDsaCng(key))
{
var container = new X509Certificate2();
container.PrivateKey = algorithm; // Exception: m_safeCertContext is an invalid handle
var pfxFileContents = container.Export(X509ContentType.Pkcs12, password);
using (var pfxFile = File.Create("text.pfx"))
pfxFile.Write(pfxFileContents, 0, pfxFileContents.Length);
}
The best solution in this case is to reference Security.Cryptography.dll. This open source library is stable and perhaps would have been merged into the .NET Framework if I understand correctly; the dev behind it is from the team.
To store a CngKey in a SecureString protected PFX stream:
// NB! X509Certificate2 does not implement IDisposable in .NET 4.5.2, but it does in 4.6.1.
using (var cert = key.CreateSelfSignedCertificate(new X509CertificateCreationParameters(new X500DistinguishedName("CN=Example Name"))
{
StartTime = DateTime.Now,
EndTime = DateTime.MaxValue,
TakeOwnershipOfKey = true,
SignatureAlgorithm = X509CertificateSignatureAlgorithm.ECDsaSha256 // Manually match your CngKey type (RSA/ECDSA)
}))
{
File.WriteAllBytes(pfxPath, cert.Export(X509ContentType.Pkcs12, pfxPassword));
}
To load a CngKey from a SecureString protected PFX stream:
// NB! X509Certificate2 does not implement IDisposable in .NET 4.5.2, but it does in 4.6.1.
using (var cert = new X509Certificate2(pfxPath, pfxPassword))
return cert.GetCngPrivateKey();

Parse PKCS #7 SSL Certificate Chain (.p7b) without private key?

I have a PKCS #7, signed, .p7b file which contains an X509 SSL certificate and the intermediate and root CA certs it was signed with. I need to use C# to parse the .p7b file, pull out the SSL certificate, and pull some values off of it (expiry date, DN, etc).
I've tried reading it as an X509 certificate like so:
//certContent is a byte array with the p7b file contents
X509Certificate2 cert = new X509Certificate2(certContent);
That works fine with a regular .cer certificate, but throws a CryptographicException when used with a .p7b certificate. This is because the .p7b contains the entire certificate chain.
I've also tried parsing it as a SignedCms object, then iterating through the certificate chain and pulling out my SSL certificate:
SignedCms certContainer = new SignedCms();
certContainer.Decode(certContent);
foreach(X509Certificate2 cert in certConatiner.Certificates)
{
...
}
However that throws an exception on Decode saying ASN1 bad tag value met. After some searching, I believe that is because I do not have the private key which was used to create the certificate and/or sign the certificate.
Does anyone know how I can parse this .p7b certificate chain using C#?
Well, I'm an idiot. I opened up the .p7b file and realized it was just base64 on the inside. I pulled out the base64, decoded that, then parsed that as a signed CMS and all is well.
String content = Encoding.UTF8.GetString(certContent);
String base64Content = content.Replace("-----BEGIN CERTIFICATE-----", "").Replace("-----END CERTIFICATE-----", "").Replace("\r", "").Replace("\n", "");
byte[] decodedContent = Convert.FromBase64String(base64Content);
SignedCms certContainer = new SignedCms();
certContainer.Decode(decodedContent);

How can I do that in bouncyCastle (get installed certificates)?

Ok, I am quite new to the crypto world of bouncyCastle, and perhaps is a mental block, I can't seem to find(/google for) the equivalent to:
X509Store store =
new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
I think it might be the easiest and dumbest thing, but how can I access the windows installed certificates, using bouncy castle?
Or if I can't, how can i convert my System.Security.Cryptography.X509Certificates.X509Certificate2
to Org.BouncyCastle.X509.X509Certificate?
Bouncycastle doesn't have access to Windows certificates store, that is the role of Microsoft's .NET classes. To convert between .NET certificates and their Bouncycastle equivalents look at the methods in the Org.BouncyCastle.Security.DotNetUtilities class, particularly the ToX509Certificate and FromX509Certificate methods.
I convert the System.Security.Cryptography.X509Certificates.X509Certificate2 to a Org.BouncyCastle.X509.X509Certificate using the following method
public static org.bouncycastle.x509.X509Certificate
convertToBCX509Certificate(X509Certificate2 cert) {
X509CertificateParser parser =
new X509CertificateParser(cert.Export(X509ContentType.Cert));
return parser.ReadCertificate();
}
Init the Certificate:
Org.BouncyCastle.X509.X509Certificate certificate = new Certificate...
Then:
System.Security.Cryptography.X509Certificates.X509Store CertificateStore = new System.Security.Cryptography.X509Certificates.X509Store(StoreName.CertificateAuthority, StoreLocation.LocalMachine);
CertificateStore.Open(OpenFlags.ReadWrite);
CertificateStore.Add(new System.Security.Cryptography.X509Certificates.X509Certificate2(certificate));
CertificateStore.Close();
Pseudo code, as an example as I am away from my working Machine atm, however, this should work nicely.

Get Apple Keychain to recognize Bouncy Castle .NET created PKCS12 (.p12) store

Our organization manages a stable of iOS applications for multiple clients, which means dealing with a lot of different developer identity certificates and push notification certificates.
I have had success with the Bouncy Castle C# Crypto API in simplifying management of the certificates and private keys for push notifications, essentially eliminating the need for the Keychain for all our push notification certificates.
I would like to extend this to the developer identity certificates. The goal would be to store all the private key and certificate information in the database for each developer identity. Then when a new developer or build machine needs to be provisioned, server side code could wrap all of the certificates and private keys into one p12 archive with one password that could be imported into the target Mac's Keychain.
Unfortunately, the Mac Keychain doesn't like the p12 files I'm generating. This is annoying since I can successfully import these files into the Windows certificate manager just fine.
The code I'm using (the important parts) looks like this:
private byte[] GetP12Bytes(List<DevIdentity> identities, string password)
{
Pkcs12Store store = new Pkcs12Store();
foreach(DevIdentity ident in identities)
{
// Easiest to create a Bouncy Castle cert by converting from .NET
var dotNetCert = new X509Certificate2(ident.CertificateBytes);
// This method (not shown) parses the CN= attribute out of the cert's distinguished name
string friendlyName = GetFriendlyName(dotNetCert.Subject);
// Now reconstitute the private key from saved value strings
BigInteger modulus = new BigInteger(ident.PrivateKey.Modulus);
BigInteger publicExponent = new BigInteger(ident.PrivateKey.PublicExponent);
BigInteger privateExponent = new BigInteger(ident.PrivateKey.Exponent);
BigInteger p = new BigInteger(ident.PrivateKey.P);
BigInteger q = new BigInteger(ident.PrivateKey.Q);
BigInteger dP = new BigInteger(ident.PrivateKey.DP);
BigInteger dQ = new BigInteger(ident.PrivateKey.DQ);
BigInteger qInv = new BigInteger(ident.PrivateKey.QInv);
RsaKeyParameters kp = new RsaPrivateCrtKeyParameters(modulus, publicExponent, privateExponent, p, q, dP, dQ, qInv);
AsymmetricKeyEntry privateKey = new AsymmetricKeyEntry(kp);
// Now let's convert to a Bouncy Castle cert and wrap it for packaging
Org.BouncyCastle.X509.X509Certificate cert = DotNetUtilities.FromX509Certificate(dotNetCert);
X509CertificateEntry certEntry = new X509CertificateEntry(cert);
// Set the private key and certificate into the store
store.SetCertificateEntry(friendlyName, certEntry);
store.SetKeyEntry(ident.PrivateKeyName, privateKey, new X509CertificateEntry[] { certEntry });
}
using (MemoryStream ms = new MemoryStream())
{
store.Save(ms, password.ToCharArray(), new SecureRandom());
ms.Flush();
byte[] p12Bytes = ms.ToArray();
return p12Bytes;
}
}
Like I said, this works great for import on Windows, but fails with a very generic error when importing into the Mac Keychain.
There is one major difference I can see when loading a Keychain-generated p12 and my own generated p12 file, but I do not know if this is the cause.
If I load the Mac Keychain generated p12 into a Bouncy Castle PKCS12Store, and then examine the keys, on the Keychain p12, both the certificate and the private key have an attribute with the key "1.2.840.113549.1.9.21" with equivalent values (a DerOctetString with value #af8a1d6891efeb32756c12b7bdd96b5ec673e11e).
If I do the same to my generated p12 file, the private key contains the "1.2.840.113549.1.9.21" attribute, but the Certificate does not.
If I Google "1.2.840.113549.1.9.21", I find out that this OID means PKCS_12_LOCAL_KEY_ID . My only theory is that the Keychain relies on this to match up the certificate and private key, and that my generated file does not have this, so it fails.
However, I've tried adding these values to a Hashtable and then using the CertificateEntry constructor that takes the attribute hashtable. If I do that, and then save the bytes, and then reload the bytes, that attribute is again missing.
So I'm flummoxed. Maybe this attribute is a glitch in the Bouncy Castle API? Maybe there's something I'm doing wrong. Maybe the Keychain has ridiculous non-standard requirements for incoming p12 files. In any case, any help that could be provided would be greatly appreciated.
BouncyCastle's Pkcs12Store takes care of setting both the Friendly Name and Local Key ID attributes for you (or at least it does so in the 1.7 release, circa April 2011). My guess is that you must have used an older version where this didn't work.
Here's how I'm saving an iPhone Developer identity to a Pkcs12Store instance (extra stuff and security omitted):
var store = new Pkcs12Store();
// pairs is IEnumerable<Tuple<X509Certificate, AsymmetricKeyParameter>>
foreach (var pair in pairs)
{
var cn = pair.Item1.SubjectDN
.GetValueList(X509Name.CN).OfType<string>().Single();
var certEntry = new X509CertificateEntry(pair.Item1);
store.SetCertificateEntry(cn, certEntry);
var keyEntry = new AsymmetricKeyEntry(pair.Item2);
store.SetKeyEntry("Developer Name", keyEntry, new[] { certEntry });
}
store.Save(stream, string.Empty.ToArray(), new SecureRandom());
Importing the store in Keychain Access.app on OS X 10.7 correctly places the certificate and private key in the keychain and places the certificate within the private key in the UI, as with a certificate and key generated by Keychain Access itself.
On a side note, it seems that Pkcs12Store uses the public key of the certificate to generate the value of the LocalKeyId attribute shared by the certificate and key entries.
You can see the relevant section of the Pkcs12Store source here.

Categories