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 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
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
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With