Search code examples
validationitextbouncycastlepdfboxsignature

PDF Signature Validation


I am trying to validate a PDF Signature using PDFBox and BouncyCastle. My code works for most PDF:s, however there is one file, the cryptographic validation using BouncyCastle fails. I'm using pdfbox 1.8, BouncyCastle 1.52. The test input pdf file is randomly got from somewhere, it seems that it is generated using iText. Test pdf file

public void testValidateSignature() throws Exception
{
    byte[] pdfByte;
    PDDocument pdfDoc = null;
    SignerInformationVerifier verifier = null;
    try
    {
        pdfByte = IOUtils.toByteArray( this.getClass().getResourceAsStream( "SignatureVlidationTest.pdf" ) );
        pdfDoc = PDDocument.load( new ByteArrayInputStream( pdfByte ));
        PDSignature signature = pdfDoc.getSignatureDictionaries().get( 0 );

        byte[] signatureAsBytes = signature.getContents( pdfByte );
        byte[] signedContentAsBytes = signature.getSignedContent( pdfByte );
        CMSSignedData cms = new CMSSignedData( new CMSProcessableByteArray( signedContentAsBytes ), signatureAsBytes);
        SignerInformation signerInfo = (SignerInformation)cms.getSignerInfos().getSigners().iterator().next();
        X509CertificateHolder cert = (X509CertificateHolder)cms.getCertificates().getMatches( signerInfo.getSID() ).iterator().next();
        verifier = new JcaSimpleSignerInfoVerifierBuilder( ).setProvider( new BouncyCastleProvider() ).build( cert );

        // result if false
        boolean verifyRt = signerInfo.verify( verifier );

    }
    finally
    {
        if( pdfDoc != null )
        {
            pdfDoc.close();
        }
    }

}

Solution

  • Your code completely ignores the SubFilter of the signature. It is appropriate for signatures with SubFilter values adbe.pkcs7.detached and ETSI.CAdES.detached but will fail for signatures with SubFilter values adbe.pkcs7.sha1 and adbe.x509.rsa.sha1.

    The example document you provided has been signed with a signatures with SubFilter value adbe.pkcs7.sha1.

    For details on how signatures with those SubFilter values are created and, therefore, have to be validated, confer the PDF specification ISO 32000-1 section 12.8 Digital Signatures.


    This is a slightly improved validation method:

    boolean validateSignaturesImproved(byte[] pdfByte, String signatureFileName) throws IOException, CMSException, OperatorCreationException, GeneralSecurityException
    {
        boolean result = true;
        try (PDDocument pdfDoc = PDDocument.load(pdfByte))
        {
            List<PDSignature> signatures = pdfDoc.getSignatureDictionaries();
            int index = 0;
            for (PDSignature signature : signatures)
            {
                String subFilter = signature.getSubFilter();
                byte[] signatureAsBytes = signature.getContents(pdfByte);
                byte[] signedContentAsBytes = signature.getSignedContent(pdfByte);
                System.out.printf("\nSignature # %s (%s)\n", ++index, subFilter);
    
                if (signatureFileName != null)
                {
                    String fileName = String.format(signatureFileName, index);
                    Files.write(new File(RESULT_FOLDER, fileName).toPath(), signatureAsBytes);
                    System.out.printf("    Stored as '%s'.\n", fileName);
                }
    
                final CMSSignedData cms;
                if ("adbe.pkcs7.detached".equals(subFilter) || "ETSI.CAdES.detached".equals(subFilter))
                {
                    cms = new CMSSignedData(new CMSProcessableByteArray(signedContentAsBytes), signatureAsBytes);
                }
                else if ("adbe.pkcs7.sha1".equals(subFilter))
                {
                    cms = new CMSSignedData(new ByteArrayInputStream(signatureAsBytes));
                }
                else if ("adbe.x509.rsa.sha1".equals(subFilter) || "ETSI.RFC3161".equals(subFilter))
                {
                    result = false;
                    System.out.printf("!!! SubFilter %s not yet supported.\n", subFilter);
                    continue;
                }
                else if (subFilter != null)
                {
                    result = false;
                    System.out.printf("!!! Unknown SubFilter %s.\n", subFilter);
                    continue;
                }
                else
                {
                    result = false;
                    System.out.println("!!! Missing SubFilter.");
                    continue;
                }
    
                SignerInformation signerInfo = (SignerInformation) cms.getSignerInfos().getSigners().iterator().next();
                X509CertificateHolder cert = (X509CertificateHolder) cms.getCertificates().getMatches(signerInfo.getSID())
                        .iterator().next();
                SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider(new BouncyCastleProvider()).build(cert);
    
                boolean verifyResult = signerInfo.verify(verifier);
                if (verifyResult)
                    System.out.println("    Signature verification successful.");
                else
                {
                    result = false;
                    System.out.println("!!! Signature verification failed!");
    
                    if (signatureFileName != null)
                    {
                        String fileName = String.format(signatureFileName + "-sigAttr.der", index);
                        Files.write(new File(RESULT_FOLDER, fileName).toPath(), signerInfo.getEncodedSignedAttributes());
                        System.out.printf("    Encoded signed attributes stored as '%s'.\n", fileName);
                    }
    
                }
    
                if ("adbe.pkcs7.sha1".equals(subFilter))
                {
                    MessageDigest md = MessageDigest.getInstance("SHA1");
                    byte[] calculatedDigest = md.digest(signedContentAsBytes);
                    byte[] signedDigest = (byte[]) cms.getSignedContent().getContent();
                    boolean digestsMatch = Arrays.equals(calculatedDigest, signedDigest);
                    if (digestsMatch)
                        System.out.println("    Document SHA1 digest matches.");
                    else
                    {
                        result = false;
                        System.out.println("!!! Document SHA1 digest does not match!");
                    }
                }
            }
        }
        return result;
    }
    

    (Excerpt from ValidateSignature.java)

    This method considers the SubFilter value and properly handels signatures with SubFilter value adbe.pkcs7.sha1. It does not yet support adbe.x509.rsa.sha1 or ETSI.RFC3161 signatures / time stamps yet but at least gives appropriate output.