iTextSharp read timestamp certificate - c#

would sameone mind helping me with code using iTextSharp?
I have signed pdf file and I need to retrieve information about signature and timestamp.
I have no problem with getting signing certificate's information. But I cannot obtain information from TSA certificate. I only get information about issuer and I need to get information about date not before and after too and other.
Here is my code:
PdfReader reader = new PdfReader(file);
AcroFields af = reader.AcroFields;
List<string> names = af.GetSignatureNames();
for (int i = 0; i < names.Count; ++i)
{
// it is working fine
string name = (string)names[i];
iTextSharp.text.pdf.security.PdfPKCS7 pk = af.VerifySignature(name);
Console.WriteLine();
Console.WriteLine(String.Format("Podepsal: {0}", pk.SignName));
Console.WriteLine(String.Format("Datum: {0}", pk.SignDate));
Console.WriteLine(String.Format("Platnost od: {0}", pk.SigningCertificate.NotBefore));
Console.WriteLine(String.Format("Platnost do: {0}", pk.SigningCertificate.NotAfter));
// here I need to help
Org.BouncyCastle.Tsp.TimeStampToken tts = pk.TimeStampToken;
string s = tts.TimeStampInfo.Tsa.Name.ToString();
// this line returns null
DateTime dt = tts.SignerID.Certificate.NotAfter;
}
Here is pdf sample http://www.filedropper.com/sample
Thank you!

In contrast to the basic signature which has already been verified by iText during your af.VerifySignature call, the signature time stamp has not yet been analyzed. In particular the actual TSA certificate has not yet been looked for.
Thus, you first have to identify the certificate in question. Usually it is included in the certificate collection a time stamp brings along, so in the following I assume it is there (it is in case of your sample file). We look it up by issuer and serial number, and then we verify the time stamp with it to be sure it is the right certificate, not a fake. Thereafter you can inspect the certificate as you like.
// Define a selector matching issuer and serial number
X509CertStoreSelector selector = new X509CertStoreSelector();
selector.Issuer = tts.SignerID.Issuer;
selector.SerialNumber = tts.SignerID.SerialNumber;
// Retrieve the matching certificates from the time stamp certificate collection
System.Collections.ICollection certs = tts.GetCertificates("COLLECTION").GetMatches(selector);
// Assuming at most one match, retrieve this matching certificate
IEnumerator enumCerts = certs.GetEnumerator();
if (enumCerts.MoveNext())
{
X509Certificate cert = (X509Certificate)enumCerts.Current;
// Verify that this is the correct certificate by verifying the time stamp token
tts.Validate(cert);
// Extracting information from the now verified tsa certificate
Console.WriteLine(String.Format("Not before: {0}", cert.NotBefore));
Console.WriteLine(String.Format("Not after: {0}", cert.NotAfter));
}

Related

SignedCms.CheckSignature with SAP certificate is failing

This is a follow-up of this question.
I am writing an external server which gets called by a SAP-Server. The SAP-Server signs the URL with a certificate before it is transmitted. In a previous step the SAP-Server sent the certificate it will be using to sign the URL to my server. So my server has the certificate the SAP-Server is using for signing.
From the SAP-documentation I know the following.
The unsigned URL looks like this
http://pswdf009:1080/ContentServer/ContentServer.dll?get&pVersion=0046&contRep=K1&docId=361A524A3ECB5459E0000800099245EC&accessMode=r&authId=pawdf054_BCE_26&expiration=19981104091537
The values of important QueryString-parameters are concatenated (in the same order they appear in the QueryString) to form the "message".
For the given QueryString-Parameters
ContRep = K1
DocId = 361A524A3ECB5459E0000800099245EC
AccessMode = r
AuthId = pawdf054_BCE_26
Expiration = 19981104091537
the generated "message" looks like this:
K1361A524A3ECB5459E0000800099245ECrpawdf054_BCE_2619981104091537
The "message" is used to calculate the hash from which the SecKey is calculated. SAP uses the Digital Signature Standard (DSS) to digitally sign the hash value according to PKCS#. The digital signature is appended to the querystring in a parameter with the name SecKey. The SecKey for the chosen procedure is about 500 bytes long. In the example from the SAP-documentation, the arbitary values 0x83, 0x70, 0x21, 0x42 are chosen for the secKey, for the sake of clarity.
The SecKey is base64 encoded and added to the URL.
0x83, 0x70, 0x21, 0x42 gets to "g3AhQg=="
and the transferred URL looks like this
http://pswdf009:1080/ContentServer/ContentServer.dll?get&pVersion=0046&contRep=K1&docId=361A524A3ECB5459E0000800099245EC&accessMode=r&authId=pawdf054_BCE_26&expiration=19981104091537&secKey=g3AhQg%3D%3D
When my server receives the URL I need to check the signature. I recreate the "message" by concatenating the QueryString-parameters the same way as it was described in point 2. (as it is described in the SAP-documentation)
SAP gives this Summary of Technical Information
Format of digital signature: PKCS#7 "signed data"
Public key procedure: DSS
Key length: 512 - 1024 bits
Public exponent: 2^16 + 1
Public key format: X.509 v3 certificate
MD (message digest) algorithm: MD5 or RIPEMD-160
The library for checking signatures can be obtained from SAP AG. Because the standard format PKCS#7 was used for the signature, other products can also be used for decoding.
I receive an "The hash value is not correct"-Exception on line cms.CheckSignature(certificates, true);
private void CheckSignature(string secKey, string message, X509Certificate2 cert)
{
byte[] signature = Convert.FromBase64String(secKey);
ContentInfo ci = new ContentInfo(System.Text.Encoding.ASCII.GetBytes(message));
SignedCms cms = new SignedCms(ci, true);
X509Certificate2Collection certificates = new X509Certificate2Collection(cert);
cms.Decode(signature);
try
{
cms.CheckSignature(certificates, true);
}
catch(Exception ex)
{
log.Error(ex.ToString());
}
}
Can anybody help, or knows what I am doing wrong?
Actually the above function CheckSignature works correct
BUT the second parameter 'message' has to be URL-encoded. Or to be more precise, when concatenating you must use the NOT-URL-DECODED queryString values. [with the same spelling (uppercase/lowercase) SAP uses]
ContRep = AA
DocId = 53730C7E18661EDCB1F816798DAA18B2
AccessMode = r
AuthId = CN=NPL (for concatenating 'CN%3DNPL' is used)
Expiration = 20220511173746
will become the message
AA53730C7E18661EDCB1F816798DAA18B2rCN%3DNPL20220511173746

Caching PIN in multiple CMS Signatures

Well, most of the questions/answers I've found here are regarding not caching a Smartcard PIN which is the opposite case of what I'm looking for.
We have a console application that signs multiple hashes. For this we use Pkcs.CmsSigner because we need to validate the signed hashes server-side.
Normally a Smartcard's PIN should be cached automatically in the CSP per process and it does in Windows 7, but if we run our code in W10 it does not. Also we support both CNG and non-CNG certificates.
The method we use to sign is the following:
public string SignX509(string data, bool chkSignature, string timestampServer, X509Certificate2 selectedCertificate)
{
CmsSigner oSigner = null;
SignedCms oSignedData = null;
string hashText = String.Empty;
try
{
if (chkSignature)
{
oSigner = new CmsSigner();
oSigner.Certificate = selectedCertificate;
byte[] arrDataHashed = HashSHA1(data);
// hash the text to sign
ContentInfo info = new ContentInfo(arrDataHashed);
// put the hashed data into the signedData object
oSignedData = new SignedCms(info);
if (string.IsNullOrEmpty(timestampServer)) {
oSigner.SignedAttributes.Add(new Pkcs9SigningTime(DateTime.Now));
}
else {
TimeStampToken tsToken = GetTSAToken(arrDataHashed, timestampServer);
AsnEncodedData timeData = new Pkcs9AttributeObject(Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.IdAASigningCertificate.Id, tsToken.GetEncoded());
oSigner.UnsignedAttributes.Add(timeData);
oSigner.SignedAttributes.Add(new Pkcs9SigningTime(tsToken.TimeStampInfo.GenTime.ToLocalTime()));
}
// sign the data
oSignedData.ComputeSignature(oSigner, false);
hashText = Convert.ToBase64String(oSignedData.Encode());
}
else
{
// just clean the hidden hash text
hashText = String.Empty;
}
}
catch (Exception ex)
{
Console.WriteLine("ERRNO [" + ex.Message + " ]");
return null;
}
return hashText;
}
What we've tried so far:
Using RSACryptoServiceProvider to explicitly persist the key in the CSP
RSACryptoServiceProvider key = (RSACryptoServiceProvider)cmsSigner.Certificate.PrivateKey;
key.PersistKeyInCsp = true;
This works if we use the SignHash method but as I've said before, we need to verify the signed data server-side and we do not have access to the certificate, therefore we need a PKCS envelope. If I set this bool and sign using the CMS code the behaviour is the same.
Setting the PIN programmatically
Another try was setting the PIN programmatically via CryptoContext, based on this answer:
private void SetPinForPrivateKey(X509Certificate2 certificate, string pin) {
if (certificate == null) throw new ArgumentNullException("certificate");
var key = (RSACryptoServiceProvider)certificate.PrivateKey;
var providerHandle = IntPtr.Zero;
var pinBuffer = System.Text.Encoding.ASCII.GetBytes(pin);
// provider handle is implicitly released when the certificate handle is released.
SafeNativeMethods.Execute(() => SafeNativeMethods.CryptAcquireContext(ref providerHandle,
key.CspKeyContainerInfo.KeyContainerName,
key.CspKeyContainerInfo.ProviderName,
key.CspKeyContainerInfo.ProviderType,
SafeNativeMethods.CryptContextFlags.Silent));
SafeNativeMethods.Execute(() => SafeNativeMethods.CryptSetProvParam(providerHandle,
SafeNativeMethods.CryptParameter.KeyExchangePin,
pinBuffer, 0));
SafeNativeMethods.Execute(() => SafeNativeMethods.CertSetCertificateContextProperty(
certificate.Handle,
SafeNativeMethods.CertificateProperty.CryptoProviderHandle,
0, providerHandle));
}
With this approach I am able to disable the PIN prompt by setting the PIN programmatically. The problem here is that I have to read the PIN the first time so I can set it in the subsequent signatures.
I've tried to read the PIN from the prompt using CryptoGetProvParam with the dwParam PP_ADMIN_PIN and PP_KEYEXCHANGE_PIN but without luck. My two guesses here are:
I'm not reading in the right time or way
CMS uses a different handler internally
Question 1:
Is there any way to read the PIN set in the Windows prompt?
Question 2:
If reading the PIN is not possible, is there any other way to force PIN caching?
Only now realized this question is still without an answer although we managed to bypass the whole 'read the PIN from Windows prompt' question.
This method does not answer my first question but I'll be answering the second question.
There was a bug in the smartcard CSP provider that disabled the PIN cache for all requests to SignHash even though they were made in the same process.
The smartcard provider has a SDK that exposes some smartcard operations, being one of those an operation to validate the smartcard PIN.
What we ended up doing was to create a simple WPF window that requests the user's PIN and uses the SDK to validate the PIN. If it is correct we use the method that I posted in the original question to force the PIN cache:
Another try was setting the PIN programmatically via CryptoContext, based on this answer:
private void SetPinForPrivateKey(X509Certificate2 certificate, string pin) {
if (certificate == null) throw new ArgumentNullException("certificate");
var key = (RSACryptoServiceProvider)certificate.PrivateKey;
var providerHandle = IntPtr.Zero;
var pinBuffer = System.Text.Encoding.ASCII.GetBytes(pin);
// provider handle is implicitly released when the certificate handle is released.
SafeNativeMethods.Execute(() => SafeNativeMethods.CryptAcquireContext(ref providerHandle,
key.CspKeyContainerInfo.KeyContainerName,
key.CspKeyContainerInfo.ProviderName,
key.CspKeyContainerInfo.ProviderType,
SafeNativeMethods.CryptContextFlags.Silent));
SafeNativeMethods.Execute(() => SafeNativeMethods.CryptSetProvParam(providerHandle,
SafeNativeMethods.CryptParameter.KeyExchangePin,
pinBuffer, 0));
SafeNativeMethods.Execute(() => SafeNativeMethods.CertSetCertificateContextProperty(
certificate.Handle,
SafeNativeMethods.CertificateProperty.CryptoProviderHandle,
0, providerHandle));
}
With this we are able to request the PIN only one time when signing multiple hashes until the smartcard provider fixes the bug on their side.

C# - Verify client certificate signed by proper root

I know there are a lot of questions on this argument but I'm stuck into this for several days now so here I am.
I've got a root certificate and a client certificate. I need to replicate in a C# web API project what the command openssl verify -CAfile ca.pem client.pem does.
This is what i know for now (hope it's actually true):
Verify() method actually validate that the certificate is signed by an authority. It's like a format control. It doesn't matter which autority signed the certificate.
X509 Chain is the way to go. Add your ca certificate inside an extra store because i'm not going to install the certificate into Windows. Then build passing the client certificate. Let's the magic happens! Unfortunately I've got some problems with the configuration.
Let me be more clear with an example
private bool VerifyCertificate(X509Certificate2 client)
{
X509Chain chain = new X509Chain();
var stringCert = WebConfigurationManager.AppSettings["CACertificate"];
var byteCert = Encoding.ASCII.GetBytes(stringCert);
var authority = new X509Certificate2(byteCert);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreWrongUsage;
chain.ChainPolicy.ExtraStore.Add(authority);
// Do the preliminary validation.
if (!chain.Build(client))
return false;
return true;
}
With this example the program returns false. The build is not passed. I'm sure the problem is with the ChainPolicy properties so i tried a different configuration
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
chain.ChainPolicy.VerificationTime = DateTime.Now;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
But this one is not going to verify anything, in fact using my ca cert the method returns true and using another ca certificate (which i didn't use to signed my client certificate) the method also returns true.
I Searched for an OpenSSL wrapper for C# and i found it but unfortunately is based on old libraries and the repo isn't manteined anymore. Also, i would achieve my goal using just .net framework if possible.
So guys, fast recap. I want to check that only the certificate that is firmed by my ca certificate can pass the validation, all the others must be stopped.
Thanks in advance for any help
ExtraStore isn't limiting, it provides extra certificates to help complete the chain. It provides no trust data.
In order to determine if the certificate is issued by the CA that you want you need to do something like:
private static readonly X509Certificate2 s_trustedRoot = ObtainTheRoot();
private static readonly byte[] s_normalizedRoot = s_trustedRoot.RawData;
private bool VerifyCertificate(X509Certificate2 candidate)
{
X509Chain chain = new X509Chain();
// set all the things you need to set to make it build
if (!chain.Build(candidate))
return false;
// Check that the root certificate was the expected one.
X509ChainElementCollection elements = chain.ChainElements;
return elements[elements.Count - 1].Certificate.RawData.SequenceEqual(s_normalizedRoot);
}
I promoted the cert and normalized byte form of it to statics on the assumption that they don't change once the process starts. If the cert can change dynamically then you should adjust accordingly.
Find the problem.
In my case this was the right response.
private bool VerifyCertificate(X509Certificate2 client)
{
X509Chain chain = new X509Chain();
var stringCert = WebConfigurationManager.AppSettings["CACertificate"];
var byteCert = Encoding.ASCII.GetBytes(stringCert);
var authority = new X509Certificate2(byteCert);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
chain.ChainPolicy.ExtraStore.Add(authority);
// Do the preliminary validation.
if (!chain.Build(client))
return false;
// This piece makes sure it actually matches your known root
var valid = chain.ChainElements
.Cast<X509ChainElement>()
.Any(x => x.Certificate.Thumbprint == authority.Thumbprint);
if (!valid)
return false;
return true;
}
Now, debugging the application i have some considerations:
What the .Build() method is supposed to do?
I mean, using the certificate that is signed by the ca or using the self-signed certificate the method Always returns true BUT if i use the trusted one it will add the certificate inside the chain.ChainElementsotherwise nothing is added.
I need to understand this thing but all the test I've done said the method works

Pades LTV verification in iTextSharp throws Public key presented not for certificate signature for root CA certificate

I'm getting an Org.BouncyCastle.Security.InvalidKeyException with error message Public key presented not for certificate signature when validating a pdf with LtvVerifier.
This problem has arisen after circumventing an issue with CRL LDAP URIs. The code used to perform the verification is the same as the previous post:
public static bool Validate(byte[] pdfIn, X509Certificate2 cert)
{
using (var reader = new PdfReader(pdfIn))
{
var fields = reader.AcroFields;
var signames = fields.GetSignatureNames();
if (!signames.Any(n => fields.SignatureCoversWholeDocument(n)))
throw new Exception("None signature covers all document");
var verifications = signames.Select(n => fields.VerifySignature(n));
var invalidSignature = verifications.Where(v => !v.Verify());
var invalidTimeStamp = verifications.Where(v => !v.VerifyTimestampImprint());
if (invalidSignature.Any())
throw new Exception("Invalid signature found");
}
using (var reader = new PdfReader(pdfIn))
{
var ltvVerifier = new LtvVerifier(reader)
{
OnlineCheckingAllowed = false,
CertificateOption = LtvVerification.CertificateOption.WHOLE_CHAIN,
Certificates = GetChain(cert).ToList(),
VerifyRootCertificate = false,
Verifier = new MyVerifier(null)
};
var ltvResult = new List<VerificationOK> { };
ltvVerifier.Verify(ltvResult);
if (!ltvResult.Any())
throw new Exception("Ltv verification failed");
}
return true;
}
Auxiliary function that builds a List of X509Certificates from the certificate chain:
private static X509.X509Certificate[] GetChain(X509Certificate2 myCert)
{
var x509Chain = new X509Chain();
x509Chain.Build(myCert);
var chain = new List<X509.X509Certificate>();
foreach(var cert in x509Chain.ChainElements)
{
chain.Add(
DotNetUtilities.FromX509Certificate(cert.Certificate)
);
}
return chain.ToArray();
}
A custom verifier, just copied from sample:
class MyVerifier : CertificateVerifier
{
public MyVerifier(CertificateVerifier verifier) : base(verifier) { }
override public List<VerificationOK> Verify(
X509.X509Certificate signCert, X509.X509Certificate issuerCert, DateTime signDate)
{
Console.WriteLine(signCert.SubjectDN + ": ALL VERIFICATIONS DONE");
return new List<VerificationOK>();
}
}
I have re-implemented LtvVerifier and CrlVerifier as explained in the previous question. The CRL validation is done ok.
The certificate chain includes the certificate that was used to sign the PDF and the CA root certificate. The function CrlVerifier.Verify is throwing the mentioned exception when calling the next line:
if (verifier != null)
result.AddRange(verifier.Verify(signCert, issuerCert, signDate));
// verify using the previous verifier in the chain (if any)
return result;
And this is the relevant stack trace of Org.BouncyCastle.Security.InvalidKeyException:
in Org.BouncyCastle.X509.X509Certificate.CheckSignature(AsymmetricKeyParameter publicKey, ISigner signature)
in Org.BouncyCastle.X509.X509Certificate.Verify(AsymmetricKeyParameter key)
in iTextSharp.text.pdf.security.CertificateVerifier.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate)
in iTextSharp.text.pdf.security.RootStoreVerifier.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate)
in PdfCommon.CrlVerifierSkippingLdap.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate) in c:\Projects\digit\Fuentes\PdfCommon\CrlVerifierSkippingLdap.cs:line 76
in iTextSharp.text.pdf.security.OcspVerifier.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate)
in PdfCommon.LtvVerifierSkippingLdap.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime sigDate) in c:\Projects\digit\Fuentes\PdfCommon\LtvVerifierSkippingLdap.cs:line 221
in PdfCommon.LtvVerifierSkippingLdap.VerifySignature() in c:\Projects\digit\Fuentes\PdfCommon\LtvVerifierSkippingLdap.cs:line 148
in PdfCommon.LtvVerifierSkippingLdap.Verify(List`1 result) in c:\Projects\digit\Fuentes\PdfCommon\LtvVerifierSkippingLdap.cs:line 112
And a link to the pdf that I try to validate
After some debugging it turns out that
iText(Sharp) 5.5.10 LtvVerifier fails in the observed manner when verifying certificates with certificate chains not ending in a self-signed certificate.
The cause
The reason is pretty simple: LtvVerifier establishes a sequence of Verifier instances (OcspVerifier, CrlVerifier, RootStoreVerifier, CertificateVerifier; the last one chained via base class calls). Then it requests the certificate chain of the signing certificate of the signature in question and for each certificate of the chain calls the Verifier sequence for the certificate couple consisting of this certificate and its issuer; in case of the final certificate in the chain, null is forwarded as issuer certificate.
Unfortunately the final Verifier, the CertificateVerifier, assumes in case of a null issuer certificate that the certificate to verify is self-signed:
// Check if the signature is valid
if (issuerCert != null) {
signCert.Verify(issuerCert.GetPublicKey());
}
// Also in case, the certificate is self-signed
else {
signCert.Verify(signCert.GetPublicKey());
}
(from CertificateVerifier method Verify)
If the certificate chain the LtvVerifier initially requested does not end in a self-signed certificate, that final test correctly results in the observed
Org.BouncyCastle.Security.InvalidKeyException with error message Public key presented not for certificate signature
The OP's example
In the case at hand we have a document timestamp issued by
cn=AUTORIDAD DE SELLADO DE TIEMPO FNMT-RCM, ou=CERES, o=FNMT-RCM, c=ES
issued by
cn=AC Administración Pública, serialNumber=Q2826004J, ou=CERES, o=FNMT-RCM, c=ES
issued by
ou=AC RAIZ FNMT-RCM, o=FNMT-RCM, c=ES
which is self-signed.
In this case already the intermediary certificate, AC Administración Pública, is on the European trusted list (cf. the TL manager for Spain, "Trust Service Provider", "Fábrica Nacional de Moneda y Timbre - Real Casa de la Moneda (FNMT-RCM)", "Trust Service", "Certificados reconocidos para su uso en el ámbito de... ", "Digital Identity").
Thus, one does not need more than the first two certificates to establish trust, the self signed root certificate is not needed. As a consequence only these first two certificates are embedded in the time stamp and returned as certificate chain to the LtvVerifier, not the self signed root.
The result is the observed error in the LtvVerifier.
What to do?
Well, as we already started creating our own copies of the involved classes in the previous question, changing them a bit more should be an option.
In this case one should additionally change the RootStoreVerifier. Its Verify method looks like this:
override public List<VerificationOK> Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate) {
LOGGER.Info("Root store verification: " + signCert.SubjectDN);
// verify using the CertificateVerifier if root store is missing
if (certificates == null)
return base.Verify(signCert, issuerCert, signDate);
try {
List<VerificationOK> result = new List<VerificationOK>();
// loop over the trusted anchors in the root store
foreach (X509Certificate anchor in certificates) {
try {
signCert.Verify(anchor.GetPublicKey());
LOGGER.Info("Certificate verified against root store");
result.Add(new VerificationOK(signCert, this, "Certificate verified against root store."));
result.AddRange(base.Verify(signCert, issuerCert, signDate));
return result;
} catch (GeneralSecurityException) {}
}
result.AddRange(base.Verify(signCert, issuerCert, signDate));
return result;
} catch (GeneralSecurityException) {
return base.Verify(signCert, issuerCert, signDate);
}
}
We merely have to remove the marked line
signCert.Verify(anchor.GetPublicKey());
LOGGER.Info("Certificate verified against root store");
result.Add(new VerificationOK(signCert, this, "Certificate verified against root store."));
// vvv remove
result.AddRange(base.Verify(signCert, issuerCert, signDate));
// ^^^ remove
return result;
in the inner try block. As we here have just established that the certificate signCert is signed by a trust anchor, there is no need for the base.Verify anyways.

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