| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- /*
- * Copyright 2017 The Apache Software Foundation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.izouma.awesomeAdmin.web.signature;
- import com.izouma.awesomeAdmin.web.signature.cert.CertificateVerificationException;
- import com.izouma.awesomeAdmin.web.signature.cert.CertificateVerifier;
- import org.apache.commons.logging.Log;
- import org.apache.commons.logging.LogFactory;
- import org.apache.pdfbox.cos.COSArray;
- import org.apache.pdfbox.cos.COSBase;
- import org.apache.pdfbox.cos.COSDictionary;
- import org.apache.pdfbox.cos.COSName;
- import org.apache.pdfbox.pdmodel.PDDocument;
- import org.apache.pdfbox.pdmodel.encryption.SecurityProvider;
- import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
- import org.bouncycastle.asn1.ASN1Object;
- import org.bouncycastle.asn1.cms.Attribute;
- import org.bouncycastle.asn1.cms.AttributeTable;
- import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
- import org.bouncycastle.asn1.x509.KeyPurposeId;
- import org.bouncycastle.cert.X509CertificateHolder;
- import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
- import org.bouncycastle.cms.CMSException;
- import org.bouncycastle.cms.CMSSignedData;
- import org.bouncycastle.cms.SignerInformation;
- import org.bouncycastle.cms.SignerInformationVerifier;
- import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
- import org.bouncycastle.operator.OperatorCreationException;
- import org.bouncycastle.tsp.TSPException;
- import org.bouncycastle.tsp.TimeStampToken;
- import org.bouncycastle.util.Selector;
- import org.bouncycastle.util.Store;
- import java.io.ByteArrayInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.net.URL;
- import java.security.GeneralSecurityException;
- import java.security.MessageDigest;
- import java.security.cert.CertificateException;
- import java.security.cert.CertificateParsingException;
- import java.security.cert.X509Certificate;
- import java.util.*;
- /**
- * Utility class for the signature / timestamp examples.
- *
- * @author Tilman Hausherr
- */
- public class SigUtils
- {
- private static final Log LOG = LogFactory.getLog(SigUtils.class);
- private SigUtils()
- {
- }
- /**
- * Get the access permissions granted for this document in the DocMDP transform parameters
- * dictionary. Details are described in the table "Entries in the DocMDP transform parameters
- * dictionary" in the PDF specification.
- *
- * @param doc document.
- * @return the permission value. 0 means no DocMDP transform parameters dictionary exists. Other
- * return values are 1, 2 or 3. 2 is also returned if the DocMDP transform parameters dictionary
- * is found but did not contain a /P entry, or if the value is outside the valid range.
- */
- public static int getMDPPermission(PDDocument doc)
- {
- COSDictionary permsDict = doc.getDocumentCatalog().getCOSObject()
- .getCOSDictionary(COSName.PERMS);
- if (permsDict != null)
- {
- COSDictionary signatureDict = permsDict.getCOSDictionary(COSName.DOCMDP);
- if (signatureDict != null)
- {
- COSArray refArray = signatureDict.getCOSArray(COSName.REFERENCE);
- if (refArray instanceof COSArray)
- {
- for (int i = 0; i < refArray.size(); ++i)
- {
- COSBase base = refArray.getObject(i);
- if (base instanceof COSDictionary)
- {
- COSDictionary sigRefDict = (COSDictionary) base;
- if (COSName.DOCMDP.equals(sigRefDict.getDictionaryObject(COSName.TRANSFORM_METHOD)))
- {
- base = sigRefDict.getDictionaryObject(COSName.TRANSFORM_PARAMS);
- if (base instanceof COSDictionary)
- {
- COSDictionary transformDict = (COSDictionary) base;
- int accessPermissions = transformDict.getInt(COSName.P, 2);
- if (accessPermissions < 1 || accessPermissions > 3)
- {
- accessPermissions = 2;
- }
- return accessPermissions;
- }
- }
- }
- }
- }
- }
- }
- return 0;
- }
- /**
- * Set the "modification detection and prevention" permissions granted for this document in the
- * DocMDP transform parameters dictionary. Details are described in the table "Entries in the
- * DocMDP transform parameters dictionary" in the PDF specification.
- *
- * @param doc The document.
- * @param signature The signature object.
- * @param accessPermissions The permission value (1, 2 or 3).
- *
- * @throws IOException if a signature exists.
- */
- public static void setMDPPermission(PDDocument doc, PDSignature signature, int accessPermissions)
- throws IOException
- {
- for (PDSignature sig : doc.getSignatureDictionaries())
- {
- // "Approval signatures shall follow the certification signature if one is present"
- // thus we don't care about timestamp signatures
- if (COSName.DOC_TIME_STAMP.equals(sig.getCOSObject().getItem(COSName.TYPE)))
- {
- continue;
- }
- if (sig.getCOSObject().containsKey(COSName.CONTENTS))
- {
- throw new IOException("DocMDP transform method not allowed if an approval signature exists");
- }
- }
- COSDictionary sigDict = signature.getCOSObject();
- // DocMDP specific stuff
- COSDictionary transformParameters = new COSDictionary();
- transformParameters.setItem(COSName.TYPE, COSName.TRANSFORM_PARAMS);
- transformParameters.setInt(COSName.P, accessPermissions);
- transformParameters.setName(COSName.V, "1.2");
- transformParameters.setNeedToBeUpdated(true);
- COSDictionary referenceDict = new COSDictionary();
- referenceDict.setItem(COSName.TYPE, COSName.SIG_REF);
- referenceDict.setItem(COSName.TRANSFORM_METHOD, COSName.DOCMDP);
- referenceDict.setItem(COSName.DIGEST_METHOD, COSName.getPDFName("SHA1"));
- referenceDict.setItem(COSName.TRANSFORM_PARAMS, transformParameters);
- referenceDict.setNeedToBeUpdated(true);
- COSArray referenceArray = new COSArray();
- referenceArray.add(referenceDict);
- sigDict.setItem(COSName.REFERENCE, referenceArray);
- referenceArray.setNeedToBeUpdated(true);
- // Catalog
- COSDictionary catalogDict = doc.getDocumentCatalog().getCOSObject();
- COSDictionary permsDict = new COSDictionary();
- catalogDict.setItem(COSName.PERMS, permsDict);
- permsDict.setItem(COSName.DOCMDP, signature);
- catalogDict.setNeedToBeUpdated(true);
- permsDict.setNeedToBeUpdated(true);
- }
- /**
- * Log if the certificate is not valid for signature usage. Doing this
- * anyway results in Adobe Reader failing to validate the PDF.
- *
- * @param x509Certificate
- * @throws CertificateParsingException
- */
- public static void checkCertificateUsage(X509Certificate x509Certificate)
- throws CertificateParsingException
- {
- // Check whether signer certificate is "valid for usage"
- // https://stackoverflow.com/a/52765021/535646
- // https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html#id1
- boolean[] keyUsage = x509Certificate.getKeyUsage();
- if (keyUsage != null && !keyUsage[0] && !keyUsage[1])
- {
- // (unclear what "signTransaction" is)
- // https://tools.ietf.org/html/rfc5280#section-4.2.1.3
- LOG.error("Certificate key usage does not include " +
- "digitalSignature nor nonRepudiation");
- }
- List<String> extendedKeyUsage = x509Certificate.getExtendedKeyUsage();
- if (extendedKeyUsage != null &&
- !extendedKeyUsage.contains(KeyPurposeId.id_kp_emailProtection.toString()) &&
- !extendedKeyUsage.contains(KeyPurposeId.id_kp_codeSigning.toString()) &&
- !extendedKeyUsage.contains(KeyPurposeId.anyExtendedKeyUsage.toString()) &&
- !extendedKeyUsage.contains("1.2.840.113583.1.1.5") &&
- // not mentioned in Adobe document, but tolerated in practice
- !extendedKeyUsage.contains("1.3.6.1.4.1.311.10.3.12"))
- {
- LOG.error("Certificate extended key usage does not include " +
- "emailProtection, nor codeSigning, nor anyExtendedKeyUsage, " +
- "nor 'Adobe Authentic Documents Trust'");
- }
- }
- /**
- * Log if the certificate is not valid for timestamping.
- *
- * @param x509Certificate
- * @throws CertificateParsingException
- */
- public static void checkTimeStampCertificateUsage(X509Certificate x509Certificate)
- throws CertificateParsingException
- {
- List<String> extendedKeyUsage = x509Certificate.getExtendedKeyUsage();
- // https://tools.ietf.org/html/rfc5280#section-4.2.1.12
- if (extendedKeyUsage != null &&
- !extendedKeyUsage.contains(KeyPurposeId.id_kp_timeStamping.toString()))
- {
- LOG.error("Certificate extended key usage does not include timeStamping");
- }
- }
- /**
- * Log if the certificate is not valid for responding.
- *
- * @param x509Certificate
- * @throws CertificateParsingException
- */
- public static void checkResponderCertificateUsage(X509Certificate x509Certificate)
- throws CertificateParsingException
- {
- List<String> extendedKeyUsage = x509Certificate.getExtendedKeyUsage();
- // https://tools.ietf.org/html/rfc5280#section-4.2.1.12
- if (extendedKeyUsage != null &&
- !extendedKeyUsage.contains(KeyPurposeId.id_kp_OCSPSigning.toString()))
- {
- LOG.error("Certificate extended key usage does not include OCSP responding");
- }
- }
- /**
- * Gets the last relevant signature in the document, i.e. the one with the highest offset.
- *
- * @param document to get its last signature
- * @return last signature or null when none found
- */
- public static PDSignature getLastRelevantSignature(PDDocument document)
- {
- Comparator<PDSignature> comparatorByOffset =
- Comparator.comparing(sig -> sig.getByteRange()[1]);
- // we can't use getLastSignatureDictionary() because this will fail (see PDFBOX-3978)
- // if a signature is assigned to a pre-defined empty signature field that isn't the last.
- // we get the last in time by looking at the offset in the PDF file.
- Optional<PDSignature> optLastSignature =
- document.getSignatureDictionaries().stream().
- sorted(comparatorByOffset.reversed()).
- findFirst();
- if (optLastSignature.isPresent())
- {
- PDSignature lastSignature = optLastSignature.get();
- COSBase type = lastSignature.getCOSObject().getItem(COSName.TYPE);
- if (type == null || COSName.SIG.equals(type) || COSName.DOC_TIME_STAMP.equals(type))
- {
- return lastSignature;
- }
- }
- return null;
- }
- public static TimeStampToken extractTimeStampTokenFromSignerInformation(SignerInformation signerInformation)
- throws CMSException, IOException, TSPException
- {
- if (signerInformation.getUnsignedAttributes() == null)
- {
- return null;
- }
- AttributeTable unsignedAttributes = signerInformation.getUnsignedAttributes();
- // https://stackoverflow.com/questions/1647759/how-to-validate-if-a-signed-jar-contains-a-timestamp
- Attribute attribute = unsignedAttributes.get(
- PKCSObjectIdentifiers.id_aa_signatureTimeStampToken);
- if (attribute == null)
- {
- return null;
- }
- ASN1Object obj = (ASN1Object) attribute.getAttrValues().getObjectAt(0);
- CMSSignedData signedTSTData = new CMSSignedData(obj.getEncoded());
- return new TimeStampToken(signedTSTData);
- }
- public static void validateTimestampToken(TimeStampToken timeStampToken)
- throws TSPException, CertificateException, OperatorCreationException, IOException
- {
- // https://stackoverflow.com/questions/42114742/
- @SuppressWarnings("unchecked") // TimeStampToken.getSID() is untyped
- Collection<X509CertificateHolder> tstMatches =
- timeStampToken.getCertificates().getMatches((Selector<X509CertificateHolder>) timeStampToken.getSID());
- X509CertificateHolder certificateHolder = tstMatches.iterator().next();
- SignerInformationVerifier siv =
- new JcaSimpleSignerInfoVerifierBuilder().setProvider(SecurityProvider.getProvider()).build(certificateHolder);
- timeStampToken.validate(siv);
- }
- /**
- * Verify the certificate chain up to the root, including OCSP or CRL. However this does not
- * test whether the root certificate is in a trusted list.<br><br>
- * Please post bad PDF files that succeed and good PDF files that fail in
- * <a href="https://issues.apache.org/jira/browse/PDFBOX-3017">PDFBOX-3017</a>.
- *
- * @param certificatesStore
- * @param certFromSignedData
- * @param signDate
- * @throws CertificateVerificationException
- * @throws CertificateException
- */
- public static void verifyCertificateChain(Store<X509CertificateHolder> certificatesStore,
- X509Certificate certFromSignedData, Date signDate)
- throws CertificateVerificationException, CertificateException
- {
- Collection<X509CertificateHolder> certificateHolders = certificatesStore.getMatches(null);
- Set<X509Certificate> additionalCerts = new HashSet<>();
- JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
- for (X509CertificateHolder certHolder : certificateHolders)
- {
- X509Certificate certificate = certificateConverter.getCertificate(certHolder);
- if (!certificate.equals(certFromSignedData))
- {
- additionalCerts.add(certificate);
- }
- }
- CertificateVerifier.verifyCertificate(certFromSignedData, additionalCerts, true, signDate);
- //TODO check whether the root certificate is in our trusted list.
- // For the EU, get a list here:
- // https://ec.europa.eu/digital-single-market/en/eu-trusted-lists-trust-service-providers
- // ( getRootCertificates() is not helpful because these are SSL certificates)
- }
- /**
- * Get certificate of a TSA.
- *
- * @param tsaUrl URL
- * @return the X.509 certificate.
- *
- * @throws GeneralSecurityException
- * @throws IOException
- */
- public static X509Certificate getTsaCertificate(String tsaUrl)
- throws GeneralSecurityException, IOException
- {
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- TSAClient tsaClient = new TSAClient(new URL(tsaUrl), null, null, digest);
- InputStream emptyStream = new ByteArrayInputStream(new byte[0]);
- TimeStampToken timeStampToken = tsaClient.getTimeStampToken(emptyStream);
- return getCertificateFromTimeStampToken(timeStampToken);
- }
- /**
- * Extract X.509 certificate from a timestamp
- * @param timeStampToken
- * @return the X.509 certificate.
- * @throws CertificateException
- */
- public static X509Certificate getCertificateFromTimeStampToken(TimeStampToken timeStampToken)
- throws CertificateException
- {
- @SuppressWarnings("unchecked") // TimeStampToken.getSID() is untyped
- Collection<X509CertificateHolder> tstMatches =
- timeStampToken.getCertificates().getMatches(timeStampToken.getSID());
- X509CertificateHolder tstCertHolder = tstMatches.iterator().next();
- return new JcaX509CertificateConverter().getCertificate(tstCertHolder);
- }
- }
|