Sign a Pdf Document using EvoPdf - c#

I‘m trying to sign a existing PDF with a certificate that’s located in my local PCs store. Based on this article I identified he correct certificate of type X509Certificate2.
For signing I want to use EvoPdf (unfortnately I can't use iTextSharp here; this would make things easier) and the demo for signing uses a DigitalCertificate which doesn’t provide the information (especially the subject property which is almost totally different in the two classes) I need to identify the correct certificate (I could manually identify the correct certificate from DigitalCertificatesStore.GetCertificates(CertSystemStore.PersonalCertificates) but that doesn’t help me much since the certificate might change and I need a robust and lasting solution.
What I did right now is 1st: identify the correct certificate from X509Store(StoreName.My) and keep the SerialNumber (of type String) and compare it to the SerialNumber (of type Byte[]) of the DigitalCertificate which is the best change to get a match:
foreach (DigitalCertificate cert in DigitalCertificatesStore.GetCertificates(CertSystemStore.PersonalCertificates))
{
var serialNumber = String.Empty;
foreach (var b in cert.SerialNumber)
{
serialNumber = $"{b:X2}{serialNumber}";
}
if (serialNumber != x509Cert.SerialNumber)
{
continue;
}
this.pdfCertificate = cert;
break;
}
Anyone got a better solution for this? (How) can I use the x509 cert with EvoPdf directly?

In .NET 5/6 I'm using code like this:
// Get Personal Certificates with EvoPDF API
var evoCerts = DigitalCertificatesStore.GetCertificates();
byte[] evoSerNum = Convert.FromHexString(x509Cert.SerialNumber).Reverse().ToArray();
// Match x509Cert and evoCert by SerialNumber
this.pdfCertificate = evoCerts.GetCertBySerialNumber(evoSerNum);

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).

Mimekit installing certificates upon verify

I am using MimeKit to verify that an SMIME multipart signed message is signed by a specific entity. I have the public certificate for this entity. When calling Verify(context) on the MultipartSigned object, the certificate is being imported into the servers certificate list. What I want is that this errors if the certificate is not found. otherwise if a malicious user signs a file it would pass through, and what's worse I would end up with his certificate installed.
Is my reasoning flawed here?
This is the code I'm using.
var signed =(MultipartSigned)MimeEntity.Load(ParserOptions.Default,#"C:\mysignedfile.txt");
using (var context = new WindowsSecureMimeContext(StoreLocation.LocalMachine))
{
foreach (var signature in signed.Verify(context))// This install the certificate!
{
try
{
bool valid = signature.Verify();
}
catch (DigitalSignatureVerifyException)
{
throw;
// There was an error verifying the signature.
}
}
}
Having the certificate in your certificate store does not mean that it is trusted, it just means that it is known.
That's all.
This allows you to later mark it as trusted and also allows your system to check for revocations.

What do the different status error codes for the CheckValidationResult certificateProblem parameter stand for?

In the MSDN Example of the ICertificatePolicy interface, there is an enum which lists all the possible status error codes that can be returned via the ValidationCallback:
public enum CertificateProblem : long
{
CertEXPIRED = 0x800B0101,
CertVALIDITYPERIODNESTING = 0x800B0102,
CertROLE = 0x800B0103,
CertPATHLENCONST = 0x800B0104,
CertCRITICAL = 0x800B0105,
CertPURPOSE = 0x800B0106,
CertISSUERCHAINING = 0x800B0107,
CertMALFORMED = 0x800B0108,
CertUNTRUSTEDROOT = 0x800B0109,
CertCHAINING = 0x800B010A,
CertREVOKED = 0x800B010C,
CertUNTRUSTEDTESTROOT = 0x800B010D,
CertREVOCATION_FAILURE = 0x800B010E,
CertCN_NO_MATCH = 0x800B010F,
CertWRONG_USAGE = 0x800B0110,
CertUNTRUSTEDCA = 0x800B0112
}
What do these different status codes stand for? They are not in the SSPI Status Code document that the links reference.
I spent a long time trying to figure this out, and was finally able to find a VMware C# file containing explanations of the different codes:
CertEXPIRED
A required certificate is not within its validity period
CertVALIDITYPERIODNESTING
The validity periods of the certification chain do not nest correctly
CertROLE
A certificate that can only be used as an end-entity is being used as a CA or visa versa
CertPATHLENCONST
A path length constraint in the certification chain has been violated
CertCRITICAL
An extension of unknown type that is labeled 'critical' is present in a certificate
CertPURPOSE
A certificate is being used for a purpose other than that for which it is permitted
CertISSUERCHAINING
A parent of a given certificate in fact did not issue that child certificate
CertMALFORMED
A certificate is missing or has an empty value for an important field, such as a subject or issuer name
CertUNTRUSTEDROOT
A certification chain processed correctly, but terminated in a root certificate which isn't trusted by the trust provider
CertCHAINING
A chain of certs didn't chain as they should in a certain application of chaining
CertREVOKED
A certificate was explicitly revoked by its issuer
CertUNTRUSTEDTESTROOT
The root certificate is a testing certificate and the policy settings disallow test certificates
CertREVOCATION_FAILURE
The revocation process could not continue - the certificate(s) could not be checked
CertCN_NO_MATCH
The certificate's CN name does not match the passed value
CertWRONG_USAGE
The certificate is not valid for the requested usage
CertUNTRUSTEDCA
Untrusted CA

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.

Generate and sign certificate in different machines C#

I need to generate certificates to be used in secure communication between agents. Each agent generates a certificate and must send it to the system CA, in another machine, to be signed (and trusted by the other agents). I am doing it using C# with the following code for the agent:
//generate certificate
ECDsa elipticCurveNistP256Key = ECDsa.Create(ECCurve.CreateFromValue("1.2.840.10045.3.1.7")); // nistP256 curve
CertificateRequest certificateRequest = new CertificateRequest("CN=" + agentId, elipticCurveNistP256Key, HashAlgorithmName.SHA256);
certificateRequest.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
certificateRequest.CertificateExtensions.Add(
new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation,
false));
// Add the SubjectAlternativeName extension
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddIpAddress(IPAddress.Parse(agentIpAddress));
certificateRequest.CertificateExtensions.Add(sanBuilder.Build());
certificateRequest.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection
{
new Oid("1.3.6.1.5.5.7.3.8")
},
true));
certificateRequest.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(certificateRequest.PublicKey, false));
And the following code for the CA system:
X509Certificate2 signedCertificate = certificateRequest.Create(
caCertificatePFX,
DateTimeOffset.UtcNow.AddDays(-1),
DateTimeOffset.UtcNow.AddDays(30),
new byte[] {1, 2, 3, 4});
Of course, I use also code for communication between the machines that I do not show here. But I have at least two problems:
I would like to have a complete separation between certificate generation and signing but even with lots of tries this was the only code that I could manage to get to work. If I am not mistaken this code has the certificate creation at the CA system which is not the ideal scenario (CA has access to agent private key) but if I didn't find a better one it's something I can accept.
The second problem is that even if I accept the first problem I still need to send the CertificateRequest object from one machine to another and CertificateRequest is not serializable. I have found the method CreateSigningRequest() that "Creates an ASN.1 DER-encoded PKCS#10 CertificationRequest value representing the state of the current object." however I have not found a way to then make it be a CertificateRequest object again so that I can run the CA system code.
Does anyone know how I can do this? Hopefully to completely separate certificate generation and certificate signing, but if that is not possible at least to create CertificateRequest object back.
I am running .Net Framework 4.7.2 that I need to maintain in order to use previously developed Windows Forms.
Thanks
As you noted, there's not a way to read back the PKCS#10 request. That's largely because too many of the things are missing to be an "OK" Certificate Authority, so having a reader would just make for a lot of "bad" Certificate Authorities. (Since your CA doesn't support revocation it's also a "bad" CA, but you're mitigating that with short lifetime certificates.)
The PKCS#10 request contains:
A data format version
A name (presumably the one that the requester wants)
A public key
Attributes
The requested extensions comes here (EKUs, Subject Alternative Name, etc)
A signature, to prove that the requester has the private key.
The data format version is irrelevant if you're not using the data format, and the signature is not really important for "closed" issuers (CAs that only issue certificates to directly-known parties). So you just need to transport the public key and whatever other data you need for the request (looking at your current code, the agent ID and IP address).
The only tricky part is sending the public key... but with .NET Core 3.0+ you can normalize all of the keys to their SubjectPublicKeyInfo format:
byte[] spki = elipticCurveNistP256Key.ExportSubjectPublicKeyInfo();
While it would have been exceedingly clever for the PublicKey type to have an ImportSubjectPublicKeyInfo method, that hasn't happened yet. For generic parsing you'd want to try all the major key types, but since you're a closed CA on the other side you can know a priori that it's ECDSA:
using (ECDsa clientPub = ECDsa.Create())
{
clientPub.ImportSubjectPublicKeyInfo(transmittedSpki, out _);
// the rest of your code goes here.
}
I would strongly suggest to use CA software to sign certificate requests. Period.
Any attempt to roll own your CA code will make the solution unreliable, fragile and error-prone in many aspects. There are several options, starting with Microsoft ADCS (Windows) and EJBCA (Windows/Linux). Any other design will be simply bad.

Categories