se.tillvaxtverket.ttsigvalws.ttwssigvalidation.pdf.PdfSignatureVerifier.java Source code

Java tutorial

Introduction

Here is the source code for se.tillvaxtverket.ttsigvalws.ttwssigvalidation.pdf.PdfSignatureVerifier.java

Source

/*
 * Copyright 2017 Swedish E-identification Board (E-legitimationsnmnden)
 *         
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package se.tillvaxtverket.ttsigvalws.ttwssigvalidation.pdf;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.asn1.ASN1BitString;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.ASN1UTCTime;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedDataParser;
import org.bouncycastle.cms.CMSTypedStream;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcDigestCalculatorProvider;
import org.bouncycastle.util.CollectionStore;
import se.tillvaxtverket.tsltrust.common.utils.core.Base64Coder;
import se.tillvaxtverket.ttsigvalws.ttwssigvalidation.document.SigDocument;

/**
 * Verifies signatures on PDF documents
 *
 * @author stefan
 */
public class PdfSignatureVerifier {

    private static final Logger LOG = Logger.getLogger(PdfSignatureVerifier.class.getName());

    /**
     * Verifies the signature on a PDF document
     *
     * @param pdfDoc The bytes of a PDF document
     * @param verifyPades If this is set to false, signature validation will
     * ignore any errors in any present PAdES Signed signature certificate data.
     * This should normally always be true. If this is set to true, signature
     * validation will still succeed if PAdES data is absent.
     * @return Signature verification result data.
     * @throws IOException
     */
    public static PdfSigVerifyResult verifyPdfSignatures(SigDocument pdfDoc, boolean verifyPades)
            throws IOException {
        PDDocument doc = PDDocument.load(pdfDoc.getDocInputStream());
        PdfSigVerifyResult result = new PdfSigVerifyResult();
        List<PDSignature> signatureDicts = doc.getSignatureDictionaries();
        for (PDSignature sig : signatureDicts) {
            byte[] signedContent = sig.getSignedContent(pdfDoc.getDocInputStream());
            byte[] sigBytes = sig.getContents(pdfDoc.getDocInputStream());

            CMSSigVerifyResult sigResult = result.addNewIndividualSignatureResult();
            try {
                verifySign(sigBytes, signedContent, sigResult, verifyPades);
            } catch (Exception ex) {
                sigResult.setStatus("Failed with exception: " + ex.getMessage());
            }
        }

        result.consolidateResults();
        return result;
    }

    /**
     * Verifies the signature on a PDF document
     *
     * @param pdfFile The PDF file to verify
     * @param verifyPades If this is set to false, signature validation will
     * ignore any errors in any present PAdES Signed signature certificate data.
     * This should normally always be true. If this is set to true, signature
     * validation will still succeed if PAdES data is absent.
     * @return Signature verification result data.
     * @throws IOException
     */
    public static PdfSigVerifyResult verifyPdfSignatures(File pdfFile, boolean verifyPades) throws IOException {
        PDDocument doc = PDDocument.load(pdfFile);
        PdfSigVerifyResult result = new PdfSigVerifyResult();
        List<PDSignature> signatureDicts = doc.getSignatureDictionaries();
        for (PDSignature sig : signatureDicts) {
            byte[] signedContent = sig.getSignedContent(new FileInputStream(pdfFile));
            byte[] sigBytes = sig.getContents(new FileInputStream(pdfFile));

            CMSSigVerifyResult sigResult = result.addNewIndividualSignatureResult();
            try {
                verifySign(sigBytes, signedContent, sigResult, verifyPades);
            } catch (Exception ex) {
                sigResult.setStatus("Failed with exception: " + ex.getMessage());
            }
        }

        result.consolidateResults();
        return result;
    }

    /**
     * Verifies one individual signature element of a signed PDF document
     *
     * @param signedData The SignedData of this signature
     * @param signedContentBytes The data being signed by this signature
     * @param sigResult The signature verification result object used to express
     * signature result data.
     * @param verifyPades The value true causes verification to check for the
     * signed signature certificate signed attributes. If present, this
     * attribute is validated against the provided signature certificate in
     * signed data.
     * @throws Exception
     */
    public static void verifySign(byte[] signedData, byte[] signedContentBytes, CMSSigVerifyResult sigResult,
            boolean verifyPades) throws Exception {
        InputStream is = new ByteArrayInputStream(signedContentBytes);
        CMSSignedDataParser sp = new CMSSignedDataParser(new BcDigestCalculatorProvider(), new CMSTypedStream(is),
                signedData);
        CMSTypedStream signedContent = sp.getSignedContent();
        signedContent.drain();
        sigResult.setSignedData(signedData);

        verifyCMSSignature(sp, sigResult);
        checkTimestamps(sp, sigResult);
    }

    private static void verifyCMSSignature(CMSSignedDataParser sp, CMSSigVerifyResult sigResult)
            throws CMSException, IOException, CertificateException, OperatorCreationException {
        CollectionStore certStore = (CollectionStore) sp.getCertificates();
        Iterator ci = certStore.iterator();
        List<X509Certificate> certList = new ArrayList<>();
        while (ci.hasNext()) {
            X509CertificateHolder ch = (X509CertificateHolder) ci.next();
            certList.add(getCert(ch));
        }
        sigResult.setCertList(certList);

        SignerInformationStore signers = sp.getSignerInfos();
        Collection c = signers.getSigners();
        Iterator it = c.iterator();
        while (it.hasNext()) {
            SignerInformation signer = (SignerInformation) it.next();
            Date claimedSigningTime = getClaimedSigningTime(signer);
            sigResult.setClaimedSigningTime(claimedSigningTime);
            Collection certCollection = certStore.getMatches(signer.getSID());
            X509CertificateHolder certHolder = (X509CertificateHolder) certCollection.iterator().next();
            sigResult.setCert(getCert(certHolder));

            //Check signature
            sigResult.setValid(
                    signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(certHolder)));

            sigResult.setStatus(sigResult.isValid() ? "Valid" : "Signature verification failed");
            if (!sigResult.isValid()) {
                return;
            }

            // Collect sig algo data
            getPkParams(sigResult.getCert().getPublicKey(), sigResult);
            DigestAlgorithm signerInfoHashAlgo = DigestAlgorithm.getDigestAlgoFromOid(signer.getDigestAlgOID());
            sigResult.setDigestAlgo(signerInfoHashAlgo);
            String encryptionAlgOID = signer.getEncryptionAlgOID();
            SupportedSigAlgoritm sigAlgoFromSignerInfoAndCert = SupportedSigAlgoritm
                    .getAlgoFromOidAndHash(new ASN1ObjectIdentifier(encryptionAlgOID), signerInfoHashAlgo);
            sigResult.setSigAlgo(sigAlgoFromSignerInfoAndCert);
            Attribute cmsAlgoProtAttr = signer.getSignedAttributes()
                    .get(new ASN1ObjectIdentifier(PdfObjectIds.ID_AA_CMS_ALGORITHM_PROTECTION));
            getCMSAlgoritmProtectionData(cmsAlgoProtAttr, sigResult);
            if (!checkAlgoritmConsistency(sigResult)) {
                sigResult.setValid(false);
                sigResult.setStatus(
                        "Signature was verified but with inconsistent Algoritm declarations or unsupported algoritms");
            }
            if (sigResult.isValid()) {
                verifyPadesProperties(signer, sigResult);
            }
        }
    }

    private static void checkTimestamps(CMSSignedDataParser sp, CMSSigVerifyResult sigResult) throws CMSException {
        List<TimeStampResult> timeStampResultList = sigResult.getTimStampResultList();
        sigResult.setTimStampResultList(timeStampResultList);
        SignerInformationStore signers = sp.getSignerInfos();
        Collection c = signers.getSigners();
        Iterator it = c.iterator();
        if (!it.hasNext()) {
            return;
        }
        SignerInformation signer = (SignerInformation) it.next();

        //Collect and check time stamps
        AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
        if (unsignedAttributes == null) {
            return;
        }
        ASN1EncodableVector timeStamps = unsignedAttributes
                .getAll(new ASN1ObjectIdentifier("1.2.840.113549.1.9.16.2.14"));
        if (timeStamps.size() == 0) {
            return;
        }
        for (int i = 0; i < timeStamps.size(); i++) {
            try {
                Attribute timestampAttr = Attribute.getInstance(timeStamps.get(i));
                byte[] timeStampBytes = timestampAttr.getAttrValues().getObjectAt(0).toASN1Primitive().getEncoded();
                TimeStampResult tsResult = new TimeStampResult();
                tsResult.setTimestamp(timeStampBytes);
                timeStampResultList.add(tsResult);

                InputStream tsis = new ByteArrayInputStream(timeStampBytes);
                CMSSignedDataParser tsSp = new CMSSignedDataParser(new BcDigestCalculatorProvider(), tsis);

                byte[] tsInfoBytes = IOUtils.toByteArray(tsSp.getSignedContent().getContentStream());
                TimeStampData timeStampData = PdfBoxSigUtil.getTimeStampData(tsInfoBytes);
                tsResult.setTsData(timeStampData);

                //Compare TimeStamp data hash with signature hash
                byte[] sigHash = getDigest(timeStampData.getImprintHashAlgo(), signer.getSignature());
                tsResult.setTimestampMatch(Arrays.equals(sigHash, timeStampData.getImprintDigest()));

                CMSSigVerifyResult tsSigResult = new CMSSigVerifyResult();
                tsSigResult.setSignedData(timeStampBytes);
                tsResult.setSignatureVerification(tsSigResult);

                verifyCMSSignature(tsSp, tsSigResult);
            } catch (Exception e) {
            }

        }
        sigResult.setTimStampResultList(timeStampResultList);
    }

    /**
     * converts an X509CertificateHolder object to an X509Certificate object.
     *
     * @param certHolder the cert holder object
     * @return X509Certificate object
     * @throws IOException
     * @throws CertificateException
     */
    public static X509Certificate getCert(X509CertificateHolder certHolder)
            throws IOException, CertificateException {
        X509Certificate cert = null;
        ByteArrayInputStream certIs = new ByteArrayInputStream(certHolder.getEncoded());

        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            cert = (X509Certificate) cf.generateCertificate(certIs);

        } finally {
            certIs.close();
        }

        return cert;
    }

    private static Date getClaimedSigningTime(SignerInformation signer) {
        try {
            AttributeTable signedAttributes = signer.getSignedAttributes();
            Attribute sigTimeAttr = signedAttributes.get(new ASN1ObjectIdentifier("1.2.840.113549.1.9.5"));
            ASN1Encodable[] attributeValues = sigTimeAttr.getAttributeValues();
            ASN1UTCTime utcTime = (ASN1UTCTime) attributeValues[0];
            return utcTime.getDate();
        } catch (Exception e) {
            return null;
        }
    }

    private static void verifyPadesProperties(SignerInformation signer, CMSSigVerifyResult sigResult) {
        try {
            AttributeTable signedAttributes = signer.getSignedAttributes();
            Attribute essSigningCertV2Attr = signedAttributes
                    .get(new ASN1ObjectIdentifier(PdfObjectIds.ID_AA_SIGNING_CERTIFICATE_V2));
            Attribute signingCertAttr = signedAttributes
                    .get(new ASN1ObjectIdentifier(PdfObjectIds.ID_AA_SIGNING_CERTIFICATE_V1));

            if (essSigningCertV2Attr == null && signingCertAttr == null) {
                sigResult.setPades(false);
                sigResult.setPadesVerified(false);
                return;
            }

            //Start assuming that PAdES validation is non-successful
            sigResult.setPades(true);
            sigResult.setPadesVerified(false);
            sigResult.setValid(false);

            DEROctetString certHashOctStr = null;
            DigestAlgorithm hashAlgo = null;

            if (essSigningCertV2Attr != null) {
                ASN1Encodable[] attributeValues = essSigningCertV2Attr.getAttributeValues();
                ASN1Sequence signingCertificateV2Seq = (ASN1Sequence) attributeValues[0]; //Holds sequence of certs and policy
                ASN1Sequence essCertV2Seq = (ASN1Sequence) signingCertificateV2Seq.getObjectAt(0); // holds sequence of cert
                ASN1Sequence certSeq = (ASN1Sequence) essCertV2Seq.getObjectAt(0); //Holds seq of algoId, cert hash and sigId
                ASN1Sequence algoSeq = (ASN1Sequence) certSeq.getObjectAt(0); //Holds sequence of OID and algo params
                ASN1ObjectIdentifier algoOid = (ASN1ObjectIdentifier) algoSeq.getObjectAt(0);
                hashAlgo = getDigestAlgo(algoOid);
                certHashOctStr = (DEROctetString) certSeq.getObjectAt(1);
            } else {
                if (signingCertAttr != null) {
                    ASN1Encodable[] attributeValues = signingCertAttr.getAttributeValues();
                    ASN1Sequence signingCertificateV2Seq = (ASN1Sequence) attributeValues[0]; //Holds sequence of certs and policy
                    ASN1Sequence essCertV2Seq = (ASN1Sequence) signingCertificateV2Seq.getObjectAt(0); // holds sequence of cert
                    ASN1Sequence certSeq = (ASN1Sequence) essCertV2Seq.getObjectAt(0); //holds sequence of cert hash and sigID                
                    certHashOctStr = (DEROctetString) certSeq.getObjectAt(0);
                    hashAlgo = DigestAlgorithm.SHA1;
                }
            }

            if (hashAlgo == null || certHashOctStr == null) {
                sigResult.setStatus("Unsupported hash algo for ESS-SigningCertAttributeV2");
                return;
            }

            MessageDigest md = MessageDigest.getInstance(hashAlgo.getName());
            md.update(sigResult.getCert().getEncoded());
            byte[] certHash = md.digest();

            //            //Debug
            //            String certHashStr = String.valueOf(Base64Coder.encode(certHash));
            //            String expectedCertHashStr = String.valueOf(Base64Coder.encode(certHashOctStr.getOctets()));
            if (!Arrays.equals(certHash, certHashOctStr.getOctets())) {
                sigResult.setStatus("Cert Hash mismatch");
                return;
            }

            //PadES validation was successful
            sigResult.setPadesVerified(true);
            sigResult.setValid(true);

        } catch (Exception e) {
            sigResult.setStatus("Exception while examining Pades signed cert attr: " + e.getMessage());
        }
    }

    private static DigestAlgorithm getDigestAlgo(ASN1ObjectIdentifier algId) {
        String oid = algId.getId();
        for (DigestAlgorithm digestAlgo : DigestAlgorithm.values()) {
            if (oid.equalsIgnoreCase(digestAlgo.getOid())) {
                return digestAlgo;
            }
        }
        return null;
    }

    /**
     * Retrieves Public key parameters from a public key
     *
     * @param pubKey The public key
     * @param sigResult The data object where result data are stored
     * @throws IOException
     */
    public static void getPkParams(PublicKey pubKey, CMSSigVerifyResult sigResult) throws IOException {

        try {
            String pkStr = String.valueOf(Base64Coder.encode(pubKey.getEncoded()));

            ASN1InputStream din = new ASN1InputStream(new ByteArrayInputStream(pubKey.getEncoded()));
            //ASN1Primitive pkObject = din.readObject();
            ASN1Sequence pkSeq = ASN1Sequence.getInstance(din.readObject());
            ASN1BitString keyBits = (ASN1BitString) pkSeq.getObjectAt(1);

            AlgorithmIdentifier algoId = AlgorithmIdentifier.getInstance(pkSeq.getObjectAt(0));
            PublicKeyType pkType = PublicKeyType.getTypeFromOid(algoId.getAlgorithm().getId());
            sigResult.setPkType(pkType);
            sigResult.setPkType(pkType);
            if (pkType.equals(PublicKeyType.EC)) {
                ASN1ObjectIdentifier curveOid = ASN1ObjectIdentifier.getInstance(algoId.getParameters());
                EcCurve curve = EcCurve.getEcCurveFromOid(curveOid.getId());
                sigResult.setEcCurve(curve);
                int totalKeyBits = getEcKeyLength(keyBits);
                sigResult.setKeyLength(totalKeyBits);
                return;
            }

            if (pkType.equals(PublicKeyType.RSA)) {
                ASN1InputStream keyIs = new ASN1InputStream(keyBits.getOctets());
                ASN1Sequence keyParamsSeq = ASN1Sequence.getInstance(keyIs.readObject());
                ASN1Integer modInt = ASN1Integer.getInstance(keyParamsSeq.getObjectAt(0));
                int modLen = getAsn1IntegerBitLength(modInt);
                sigResult.setKeyLength(modLen);
                return;
            }

        } catch (Exception e) {
            int asdf = 0;
        }

    }

    private static int getAsn1IntegerBitLength(ASN1Integer modInt) throws IOException {
        byte[] encoded = modInt.getEncoded();
        int lenOctets = 0;
        int intValOffset;

        int lenType = encoded[1] & 0x80;
        int lenInLenType = encoded[1] & 0x7f; //The lengthinformation in the first lenghth byte
        if (lenType == 0) {
            //Short length encoding (Bits 1-7)
            lenOctets = lenInLenType;
            intValOffset = 2;
        } else {
            //Long length encoding
            if (lenInLenType > 2 || lenInLenType + 2 > encoded.length) {
                //Checks that there are enough data to provide length bytes.
                //If more than 2 bytes is used to specify the byte length of an integer. Something is clearly wrong. Abort.
                return 0;
            }
            int multiplicator = 1;
            for (int i = 0; i < lenInLenType; i++) {
                lenOctets += (encoded[2 + i] & 0x0000ff) * multiplicator;
                multiplicator *= 256;
            }
            intValOffset = lenInLenType + 2;
        }
        //remove padding byte from bit length
        if (encoded[intValOffset] == 0) {
            lenOctets--;
        }
        //Return number of bits
        return lenOctets * 8;
    }

    private static int getEcKeyLength(ASN1BitString bitString) throws IOException {
        byte[] encoded = bitString.getEncoded();
        int lenOctets = 0;
        int valOffset;

        int lenType = encoded[1] & 0x80;
        int lenInLenType = encoded[1] & 0x7f; //The lengthinformation in the first lenghth byte
        if (lenType == 0) {
            //Short length encoding (Bits 1-7)
            lenOctets = lenInLenType;
            valOffset = 2;
        } else {
            //Long length encoding
            if (lenInLenType > 2 || lenInLenType + 2 > encoded.length) {
                //Checks that there are enough data to provide length bytes.
                //If more than 2 bytes is used to specify the byte length of an integer. Something is clearly wrong. Abort.
                return 0;
            }
            int multiplicator = 1;
            for (int i = 0; i < lenInLenType; i++) {
                lenOctets += (encoded[2 + i] & 0x0000ff) * multiplicator;
                multiplicator *= 256;
            }
            valOffset = lenInLenType + 2;
        }

        byte[] keyBytes = Arrays.copyOfRange(encoded, valOffset, encoded.length);
        if (keyBytes.length % 2 != 0) {
            //ERROR. Key bytes should be dividable by 2. We return a best estimate
            return lenOctets * 4;
        }

        int partLen = keyBytes.length / 2;
        //check for padding
        if (keyBytes[0] == 0 || keyBytes[partLen] == 0) {
            lenOctets -= 2;
        }

        //Return number of bits
        return lenOctets * 4;
    }

    private static void getCMSAlgoritmProtectionData(Attribute cmsAlgoProtAttr, CMSSigVerifyResult sigResult) {
        if (cmsAlgoProtAttr == null) {
            sigResult.setCmsAlgoProtection(false);
            return;
        }
        sigResult.setCmsAlgoProtection(true);

        try {
            ASN1Sequence cmsapSeq = ASN1Sequence.getInstance(cmsAlgoProtAttr.getAttrValues().getObjectAt(0));

            //Get Hash algo
            AlgorithmIdentifier hashAlgoId = AlgorithmIdentifier.getInstance(cmsapSeq.getObjectAt(0));
            DigestAlgorithm digestAlgo = DigestAlgorithm.getDigestAlgoFromOid(hashAlgoId.getAlgorithm().getId());
            sigResult.setCmsapDigestAlgo(digestAlgo);

            //GetSigAlgo
            for (int objIdx = 1; objIdx < cmsapSeq.size(); objIdx++) {
                ASN1Encodable asn1Encodable = cmsapSeq.getObjectAt(objIdx);
                if (asn1Encodable instanceof ASN1TaggedObject) {
                    ASN1TaggedObject taggedObj = ASN1TaggedObject.getInstance(asn1Encodable);
                    if (taggedObj.getTagNo() == 1) {
                        AlgorithmIdentifier algoId = AlgorithmIdentifier.getInstance(taggedObj, false);
                        SupportedSigAlgoritm sigAlgo = SupportedSigAlgoritm
                                .getAlgoFromOidAndHash(algoId.getAlgorithm(), digestAlgo);
                        sigResult.setCmsapSigAlgo(sigAlgo);
                    }
                }
            }
        } catch (Exception e) {
            LOG.warning("Failed to parse CMSAlgoritmProtection algoritms");
        }

    }

    private static boolean checkAlgoritmConsistency(CMSSigVerifyResult sigResult) {
        if (sigResult.getSigAlgo() == null) {
            return false;
        }
        if (sigResult.getDigestAlgo() == null) {
            return false;
        }
        if (!sigResult.getSigAlgo().getDigestAlgo().equals(sigResult.getDigestAlgo())) {
            return false;
        }

        //Ceheck if CML Algoprotection is present.
        if (!sigResult.isCmsAlgoProtection()) {
            return true;
        }
        if (!sigResult.getSigAlgo().equals(sigResult.getCmsapSigAlgo())) {
            return false;
        }
        if (!sigResult.getDigestAlgo().equals(sigResult.getCmsapDigestAlgo())) {
            return false;
        }

        return true;
    }

    public static byte[] getDigest(DigestAlgorithm digestAlgo, byte[] signature) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance(digestAlgo.getName());
        md.update(signature);
        byte[] hashVal = md.digest();
        return hashVal;
    }

}