xormar
xormar

Reputation: 103

iText7: How to verify signature in detached PKCS7?

I am writing a service that receives PKCS7 data (extracted from signed PDF documents) and needs to verify it.

I am using iText7 PdfPKCS7 for that, but the signature verification always fails. I can read all other information from the PKCS7 (certificates, timestamps etc., I have verified that also with OpenSSL). Only the signature appears as invalid.

Here's the test case:

public static void main(String[] args) throws IOException, GeneralSecurityException,
    NoSuchFieldException, IllegalArgumentException, IllegalAccessException {

    Logger logger = Logger.getLogger(PKCS7Test.class.getName());

    BouncyCastleProvider provider = new BouncyCastleProvider();
    Security.addProvider(provider);

    String path ="/tmp/signed.pdf";

    PdfDocument pdf = new PdfDocument(new PdfReader(path));
    SignatureUtil signatureUtil = new SignatureUtil(pdf);
    List<String> names = signatureUtil.getSignatureNames();
    String outerRevisionName = names.get(names.size()-1);

    PdfPKCS7 pkcs7In = signatureUtil.verifySignature(outerRevisionName);

    boolean isValidSignature = pkcs7In.verify();
    logger.log(Level.INFO, "pkcs7In signature is " + ((isValidSignature)?"":"not ") + "valid");

    // get hash of original document       
    Field digestAttrField = PdfPKCS7.class.getDeclaredField("digestAttr");
    digestAttrField.setAccessible(true);
    byte[] originalDigest = (byte[]) digestAttrField.get(pkcs7In);

    // get pkcs7 structure of original signature                
    PdfDictionary dict = signatureUtil.getSignatureDictionary(outerRevisionName);        
    PdfString contents = dict.getAsString(PdfName.Contents);
    byte [] originalBytes = contents.getValueBytes();
    String originalPkcs7 = Base64.getEncoder().encodeToString(originalBytes);

    // now reverse process and import PKCS7 data back into object
    byte[] pkcs7Bytes = Base64.getDecoder().decode(originalPkcs7);
    PdfPKCS7 pkcs7Out = new PdfPKCS7(pkcs7Bytes, PdfName.Adbe_pkcs7_detached, provider.getName());

    isValidSignature = pkcs7Out.verify();
    logger.log(Level.INFO, "pkcs7Out signature is " + ((isValidSignature)?"":"not ") + "valid");

    // get hash of original document from imported signature     
    digestAttrField = PdfPKCS7.class.getDeclaredField("digestAttr");
    digestAttrField.setAccessible(true);
    byte [] importedDigest = (byte[]) digestAttrField.get(pkcs7Out);

    logger.log(Level.INFO, "Hash values are " + ((Arrays.areEqual(originalDigest, importedDigest))?"":"not ") + "equal");

The output is invariably:

pkcs7In signature is valid
pkcs7Out signature is not valid
Hash values are equal

I suppose I'm doing something wrong with the import, but just can't find out what...

Btw. /tmp/signed.pdf validates OK (signature and content) in other PDF tools (Adobe DC, PdfOnline etc.)

Edit: I tried to verify the signature with BouncyCastle, which fails too ...

    CMSSignedData signature = new CMSSignedData(pkcs7Bytes);        

    Store                   certStore = signature.getCertificates();
    SignerInformationStore  signers = signature.getSignerInfos();
    Collection              c = signers.getSigners();
    Iterator                it = c.iterator();

    while (it.hasNext())
    {
        SignerInformation   signer = (SignerInformation)it.next();
        Collection          certCollection = certStore.getMatches(signer.getSID());

        Iterator              certIt = certCollection.iterator();
        X509CertificateHolder cert = (X509CertificateHolder)certIt.next();

       try {
            signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(cert));
        }
        catch (Exception ex) {
            logger.log(Level.INFO, "Failed to verify with: " + cert.getSubject().toString() + ": " + ex.getLocalizedMessage());
            byte [] contentDigest = signer.getContentDigest();
            bi = new BigInteger(1, contentDigest);
            String hash = String.format("%0" + (contentDigest.length << 1) + "x", bi);
            logger.log(Level.INFO, "Bouncycastle Hash from pkcs7Out is {0}", hash);
        }
    }

The result is Failed to verify with: ... message-digest attribute value does not match calculated value, and the hash value obviously differs from the real one...

Edit 2: Workaround by adding separate verify(byte[] msgDigestBytes) method to PdfPKCS7:

public boolean verify(final byte[] msgDigestBytes) throws GeneralSecurityException {
    if (verified)
        return verifyResult;
    if (isTsp) {
        TimeStampTokenInfo info = timeStampToken.getTimeStampInfo();
        MessageImprint imprint = info.toASN1Structure().getMessageImprint();
        byte[] md = msgDigestBytes; // was: messageDigest.digest();
        byte[] imphashed = imprint.getHashedMessage();
        verifyResult = Arrays.equals(md, imphashed);
    } else {
        if (sigAttr != null || sigAttrDer != null) {
            // was: final byte[] msgDigestBytes = messageDigest.digest();
            boolean verifyRSAdata = true;
            // Stefan Santesson fixed a bug, keeping the code backward compatible
            boolean encContDigestCompare = false;
            if (rsaData != null) {
                verifyRSAdata = Arrays.equals(msgDigestBytes, rsaData);
                encContDigest.update(rsaData);
                encContDigestCompare = Arrays.equals(encContDigest.digest(), digestAttr);
            }
            boolean absentEncContDigestCompare = Arrays.equals(msgDigestBytes, digestAttr);
            boolean concludingDigestCompare = absentEncContDigestCompare || encContDigestCompare;
            boolean sigVerify = verifySigAttributes(sigAttr) || verifySigAttributes(sigAttrDer);
            verifyResult = concludingDigestCompare && sigVerify && verifyRSAdata;
        } else {
            if (rsaData != null)
                sig.update(msgDigestBytes); // was: sig.update(messageDigest.digest());
            verifyResult = sig.verify(digest);
        }
    }
    verified = true;
    return verifyResult;
}

For this to work, I need the originally signed hash of the document from a trusted source, which in my case I have. Does this still verify that the signature made over the hash is correct?

Upvotes: 0

Views: 2970

Answers (1)

mkl
mkl

Reputation: 96064

From

PdfPKCS7 pkcs7Out = new PdfPKCS7(pkcs7Bytes, PdfName.Adbe_pkcs7_detached, provider.getName());

isValidSignature = pkcs7Out.verify();

you cannot expect a proper validation result of the signature: This PdfPKCS7 only knows the CMS signature container, the signature SubFilter, and the security provider to provide algorithm implementations. Thus, it has no information on the very PDF the signature actually is meant to sign. So that piece of code has no means to validate the signature in question, in particular not whether it properly signs its alleged signed data!

If you want to validate the signature using that PdfPKCS7 object, you have to finish initializing it so it does have the required information from the PDF.

To see what is required have a look at the SignatureUtil method verifySignature:

PdfPKCS7 pk = null;
if (sub.equals(PdfName.Adbe_x509_rsa_sha1)) {
    PdfString cert = signature.getPdfObject().getAsString(PdfName.Cert);
    if (cert == null)
        cert = signature.getPdfObject().getAsArray(PdfName.Cert).getAsString(0);
    pk = new PdfPKCS7(PdfEncodings.convertToBytes(contents.getValue(), null), cert.getValueBytes(), provider);
}
else
    pk = new PdfPKCS7(PdfEncodings.convertToBytes(contents.getValue(), null), sub, provider);
updateByteRange(pk, signature);
PdfString date = signature.getDate();
if (date != null)
    pk.setSignDate(PdfDate.decode(date.toString()));
String signName = signature.getName();
pk.setSignName(signName);
String reason = signature.getReason();
if (reason != null)
    pk.setReason(reason);
String location = signature.getLocation();
if (location != null)
    pk.setLocation(location);

Thus, you have to

  • update the digest of the signed data like SignatureUtil.updateByteRange does; this is the step that informs the PdfPKCS7 object about the actually signed data to allow actual validation; and
  • copy several pieces of information from the signature dictionary to the PdfPKCS7 object; for validation purposes in particular the signing time may be of interest.

Upvotes: 2

Related Questions