]> source.dussan.org Git - poi.git/commitdiff
#65908 - XAdES-XL modifications due to specification check errors
authorAndreas Beeker <kiwiwings@apache.org>
Mon, 21 Feb 2022 22:56:30 +0000 (22:56 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Mon, 21 Feb 2022 22:56:30 +0000 (22:56 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1898287 13f79535-47bb-0310-9956-ffa450edef68

poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java [new file with mode: 0644]
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java [new file with mode: 0644]
poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java
poi/src/main/java/org/apache/poi/util/IOUtils.java

index b2b0b66e944ae457e621c24d1d03dafb54410a6e..08052415288be8c85e8db8fbb6c459e1f69c2809 100644 (file)
@@ -20,22 +20,33 @@ package org.apache.poi.poifs.crypt.dsig;
 import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.OO_DIGSIG_NS;
 import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XADES_132_NS;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
 import java.security.PrivateKey;
 import java.security.Provider;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.text.DateFormat;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.UUID;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.xml.crypto.URIDereferencer;
 import javax.xml.crypto.dsig.CanonicalizationMethod;
@@ -58,8 +69,10 @@ import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet;
 import org.apache.poi.poifs.crypt.dsig.services.RevocationDataService;
 import org.apache.poi.poifs.crypt.dsig.services.SignaturePolicyService;
 import org.apache.poi.poifs.crypt.dsig.services.TSPTimeStampService;
+import org.apache.poi.poifs.crypt.dsig.services.TimeStampHttpClient;
 import org.apache.poi.poifs.crypt.dsig.services.TimeStampService;
 import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator;
+import org.apache.poi.poifs.crypt.dsig.services.TimeStampSimpleHttpClient;
 import org.apache.poi.util.Internal;
 import org.apache.poi.util.LocaleUtil;
 import org.apache.poi.util.Removal;
@@ -73,6 +86,29 @@ import org.apache.xml.security.signature.XMLSignature;
  */
 @SuppressWarnings({"unused","WeakerAccess"})
 public class SignatureConfig {
+    public static class CRLEntry {
+        private final String crlURL;
+        private final String certCN;
+        private final byte[] crlBytes;
+
+        public CRLEntry(String crlURL, String certCN, byte[] crlBytes) {
+            this.crlURL = crlURL;
+            this.certCN = certCN;
+            this.crlBytes = crlBytes;
+        }
+
+        public String getCrlURL() {
+            return crlURL;
+        }
+
+        public String getCertCN() {
+            return certCN;
+        }
+
+        public byte[] getCrlBytes() {
+            return crlBytes;
+        }
+    }
 
     public static final String SIGNATURE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
 
@@ -116,6 +152,9 @@ public class SignatureConfig {
      * the time-stamp service used for XAdES-T and XAdES-X.
      */
     private TimeStampService tspService = new TSPTimeStampService();
+    private TimeStampHttpClient tspHttpClient = new TimeStampSimpleHttpClient();
+
+
     /**
      * timestamp service provider URL
      */
@@ -137,7 +176,7 @@ public class SignatureConfig {
 
     /**
      * the optional revocation data service used for XAdES-C and XAdES-X-L.
-     * When <code>null</code> the signature will be limited to XAdES-T only.
+     * When {@code null} the signature will be limited to XAdES-T only.
      */
     private RevocationDataService revocationDataService;
     /**
@@ -156,7 +195,7 @@ public class SignatureConfig {
 
     /**
      * The signature Id attribute value used to create the XML signature. A
-     * <code>null</code> value will trigger an automatically generated signature Id.
+     * {@code null} value will trigger an automatically generated signature Id.
      */
     private String packageSignatureId = "idPackageSignature";
 
@@ -221,6 +260,23 @@ public class SignatureConfig {
 
     private String commitmentType = "Created and approved this document";
 
+    /**
+     * Swtich to enable/disable automatic CRL download - by default the download is with all https hostname
+     * and certificate verifications disabled.
+     *
+     * @since POI 5.3.0
+     */
+    private boolean allowCRLDownload = false;
+
+    /**
+     * List of cached / saved CRL entries
+     */
+    private final List<CRLEntry> crlEntries = new ArrayList<>();
+
+    /**
+     * Keystore used for cached certificates
+     */
+    private final KeyStore keyStore = emptyKeyStore();
 
     public SignatureConfig() {
         // OOo doesn't like ds namespaces so per default prefixing is off.
@@ -490,7 +546,7 @@ public class SignatureConfig {
 
     /**
      * @param packageSignatureId The signature Id attribute value used to create the XML signature.
-     * A <code>null</code> value will trigger an automatically generated signature Id.
+     * A {@code null} value will trigger an automatically generated signature Id.
      */
     public void setPackageSignatureId(String packageSignatureId) {
         this.packageSignatureId = nvl(packageSignatureId,"xmldsig-"+UUID.randomUUID());
@@ -536,7 +592,7 @@ public class SignatureConfig {
 
     /**
      * @param tspDigestAlgo the algorithm to be used for the timestamp entry.
-     * if <code>null</code>, the hash algorithm of the main entry
+     * if {@code null}, the hash algorithm of the main entry
      */
     public void setTspDigestAlgo(HashAlgorithm tspDigestAlgo) {
         this.tspDigestAlgo = tspDigestAlgo;
@@ -572,6 +628,24 @@ public class SignatureConfig {
         this.tspService = tspService;
     }
 
+    /**
+     * @return the http client used for timestamp server connections
+     *
+     * @since POI 5.3.0
+     */
+    public TimeStampHttpClient getTspHttpClient() {
+        return tspHttpClient;
+    }
+
+    /**
+     * @param tspHttpClient the http client used for timestamp server connections
+     *
+     * @since POI 5.3.0
+     */
+    public void setTspHttpClient(TimeStampHttpClient tspHttpClient) {
+        this.tspHttpClient = tspHttpClient;
+    }
+
     /**
      * @return the user id for the timestamp service - currently only basic authorization is supported
      */
@@ -616,7 +690,7 @@ public class SignatureConfig {
 
     /**
      * @return the optional revocation data service used for XAdES-C and XAdES-X-L.
-     * When <code>null</code> the signature will be limited to XAdES-T only.
+     * When {@code null} the signature will be limited to XAdES-T only.
      */
     public RevocationDataService getRevocationDataService() {
         return revocationDataService;
@@ -624,7 +698,7 @@ public class SignatureConfig {
 
     /**
      * @param revocationDataService the optional revocation data service used for XAdES-C and XAdES-X-L.
-     * When <code>null</code> the signature will be limited to XAdES-T only.
+     * When {@code null} the signature will be limited to XAdES-T only.
      */
     public void setRevocationDataService(RevocationDataService revocationDataService) {
         this.revocationDataService = revocationDataService;
@@ -639,7 +713,7 @@ public class SignatureConfig {
 
     /**
      * @param xadesDigestAlgo hash algorithm used for XAdES.
-     * When <code>null</code>, defaults to {@link #getDigestAlgo()}
+     * When {@code null}, defaults to {@link #getDigestAlgo()}
      */
     public void setXadesDigestAlgo(HashAlgorithm xadesDigestAlgo) {
         this.xadesDigestAlgo = xadesDigestAlgo;
@@ -647,7 +721,7 @@ public class SignatureConfig {
 
     /**
      * @param xadesDigestAlgo hash algorithm used for XAdES.
-     * When <code>null</code>, defaults to {@link #getDigestAlgo()}
+     * When {@code null}, defaults to {@link #getDigestAlgo()}
      *
      * @since POI 4.0.0
      */
@@ -671,7 +745,7 @@ public class SignatureConfig {
 
     /**
      * @return the asn.1 object id for the tsp request policy.
-     * Defaults to <code>1.3.6.1.4.1.13762.3</code>
+     * Defaults to {@code 1.3.6.1.4.1.13762.3}
      */
     public String getTspRequestPolicy() {
         return tspRequestPolicy;
@@ -729,15 +803,15 @@ public class SignatureConfig {
     }
 
     /**
-     * @return the xades role element. If <code>null</code> the claimed role element is omitted.
-     * Defaults to <code>null</code>
+     * @return the xades role element. If {@code null} the claimed role element is omitted.
+     * Defaults to {@code null}
      */
     public String getXadesRole() {
         return xadesRole;
     }
 
     /**
-     * @param xadesRole the xades role element. If <code>null</code> the claimed role element is omitted.
+     * @param xadesRole the xades role element. If {@code null} the claimed role element is omitted.
      */
     public void setXadesRole(String xadesRole) {
         this.xadesRole = xadesRole;
@@ -745,7 +819,7 @@ public class SignatureConfig {
 
     /**
      * @return the Id for the XAdES SignedProperties element.
-     * Defaults to <code>idSignedProperties</code>
+     * Defaults to {@code idSignedProperties}
      */
     public String getXadesSignatureId() {
         return nvl(xadesSignatureId, "idSignedProperties");
@@ -753,7 +827,7 @@ public class SignatureConfig {
 
     /**
      * @param xadesSignatureId the Id for the XAdES SignedProperties element.
-     * When <code>null</code> defaults to <code>idSignedProperties</code>
+     * When {@code null} defaults to {@code idSignedProperties}
      */
     public void setXadesSignatureId(String xadesSignatureId) {
         this.xadesSignatureId = xadesSignatureId;
@@ -761,7 +835,7 @@ public class SignatureConfig {
 
     /**
      * @return when true, include the policy-implied block.
-     * Defaults to <code>true</code>
+     * Defaults to {@code true}
      */
     public boolean isXadesSignaturePolicyImplied() {
         return xadesSignaturePolicyImplied;
@@ -1027,7 +1101,7 @@ public class SignatureConfig {
 
     /**
      * @return the cannonicalization method for XAdES-XL signing.
-     * Defaults to <code>EXCLUSIVE</code>
+     * Defaults to {@code EXCLUSIVE}
      * @see <a href="http://docs.oracle.com/javase/7/docs/api/javax/xml/crypto/dsig/CanonicalizationMethod.html">javax.xml.crypto.dsig.CanonicalizationMethod</a>
      */
     public String getXadesCanonicalizationMethod() {
@@ -1136,4 +1210,96 @@ public class SignatureConfig {
     public void setCommitmentType(String commitmentType) {
         this.commitmentType = commitmentType;
     }
+
+
+    public CRLEntry addCRL(String crlURL, String certCN, byte[] crlBytes) {
+        CRLEntry ce = new CRLEntry(crlURL, certCN, crlBytes);
+        crlEntries.add(ce);
+        return ce;
+    }
+
+    public List<CRLEntry> getCrlEntries() {
+        return crlEntries;
+    }
+
+    public boolean isAllowCRLDownload() {
+        return allowCRLDownload;
+    }
+
+    public void setAllowCRLDownload(boolean allowCRLDownload) {
+        this.allowCRLDownload = allowCRLDownload;
+    }
+
+    /**
+     * @return keystore with cached certificates
+     */
+    public KeyStore getKeyStore() {
+        return keyStore;
+    }
+
+    /**
+     * Add certificate into keystore (cache) for further certificate chain lookups
+     * @param alias the alias, or null if alias is taken from common name attribute of certificate
+     * @param x509 the x509 certificate
+     */
+    public void addCachedCertificate(String alias, X509Certificate x509) throws KeyStoreException {
+        String lAlias = alias;
+        if (lAlias == null) {
+            lAlias = x509.getSubjectX500Principal().getName();
+        }
+        if (keyStore != null) {
+            synchronized (keyStore) {
+                keyStore.setCertificateEntry(lAlias, x509);
+            }
+        }
+    }
+
+    public void addCachedCertificate(String alias, byte[] x509Bytes) throws KeyStoreException, CertificateException {
+        CertificateFactory certFact = CertificateFactory.getInstance("X.509");
+        X509Certificate x509 = (X509Certificate)certFact.generateCertificate(new ByteArrayInputStream(x509Bytes));
+        addCachedCertificate(null, x509);
+    }
+
+    public X509Certificate getCachedCertificateByPrinicipal(String principalName) {
+        if (keyStore == null) {
+            return null;
+        }
+        // TODO: add synchronized
+        try {
+            for (String a : Collections.list(keyStore.aliases())) {
+                Certificate[] chain = keyStore.getCertificateChain(a);
+                if (chain == null) {
+                    Certificate cert = keyStore.getCertificate(a);
+                    if (cert == null) {
+                        continue;
+                    }
+                    chain = new Certificate[]{cert};
+                }
+                Optional<X509Certificate> found = Stream.of(chain)
+                    .map(X509Certificate.class::cast)
+                    .filter(c -> principalName.equalsIgnoreCase(c.getSubjectX500Principal().getName()))
+                    .findFirst();
+                if (found.isPresent()) {
+                    return found.get();
+                }
+            }
+            return null;
+        } catch (KeyStoreException e) {
+            return null;
+        }
+    }
+
+
+    private static KeyStore emptyKeyStore() {
+        try {
+            KeyStore ks = KeyStore.getInstance("PKCS12");
+            ks.load(null, null);
+            return ks;
+        } catch (IOException | GeneralSecurityException e) {
+            LOG.atError().withThrowable(e).log("unable to create PKCS #12 keystore - XAdES certificate chain lookups disabled");
+        }
+        return null;
+    }
+
+
 }
\ No newline at end of file
index 2ffc2cfed325db3908e31b1708730c735dd7ccb2..d3c43fd2b2f0683725f14f3658b0149c9612b48c 100644 (file)
 
 package org.apache.poi.poifs.crypt.dsig;
 
+import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.MS_DIGSIG_NS;
+import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.OO_DIGSIG_NS;
 import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_DIGSIG_NS;
 import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_NS;
 
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 
+import javax.xml.XMLConstants;
+
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
@@ -46,6 +51,8 @@ public class SignatureMarshalDefaultListener implements SignatureMarshalListener
     private static final String OBJECT_TAG = "Object";
     private static final Set<String> IGNORE_NS = new HashSet<>(Arrays.asList(null, XML_NS, XML_DIGSIG_NS));
 
+    private static final List<String> DIRECT_NS = Arrays.asList(OO_DIGSIG_NS, MS_DIGSIG_NS);
+
     @Override
     public void handleElement(SignatureInfo signatureInfo, Document doc, EventTarget target, EventListener parentListener) {
         // see POI #63712 : because of Santuario change r1853805 in XmlSec 2.1.3,
@@ -58,7 +65,7 @@ public class SignatureMarshalDefaultListener implements SignatureMarshalListener
         forEachElement(doc.getElementsByTagName(OBJECT_TAG), (o) -> {
            forEachElement(o.getChildNodes(), (c) -> {
                getAllNamespaces(traversal, c, prefixCfg, prefixUsed);
-               prefixUsed.forEach((ns, prefix) -> c.setAttributeNS(XML_NS, "xmlns:"+prefix, ns));
+               prefixUsed.forEach((ns, prefix) -> setXmlns(c, prefix, ns));
            });
         });
     }
@@ -93,9 +100,22 @@ public class SignatureMarshalDefaultListener implements SignatureMarshalListener
     private void setPrefix(Node node, Map<String,String> prefixCfg, Map<String,String> prefixUsed) {
         String ns = node.getNamespaceURI();
         String prefix = prefixCfg.get(ns);
-        if (!IGNORE_NS.contains(prefix)) {
+        if (IGNORE_NS.contains(ns)) {
+            return;
+        }
+        if (prefix != null) {
             node.setPrefix(prefix);
+        }
+        if (DIRECT_NS.contains(ns)) {
+            setXmlns(node, prefix, ns);
+        } else {
             prefixUsed.put(ns, prefix);
         }
     }
+
+    private static void setXmlns(Node node, String prefix, String ns) {
+        if (node instanceof Element && !ns.equals(node.getParentNode().getNamespaceURI())) {
+            ((Element)node).setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE + (prefix == null ? "" : ":"+prefix), ns);
+        }
+    }
 }
index 265c620ec84e1cf3836ad3e4d41a200fc459d652..837a844cfb11f641a597373f560dd8cf637f75b0 100644 (file)
@@ -39,7 +39,6 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import javax.xml.XMLConstants;
 import javax.xml.crypto.URIReference;
 import javax.xml.crypto.XMLStructure;
 import javax.xml.crypto.dom.DOMStructure;
@@ -261,7 +260,7 @@ public class OOXMLSignatureFacet implements SignatureFacet {
 
         SignatureInfoV1Document sigV1 = createSignatureInfoV1(signatureInfo);
         Element n = (Element)document.importNode(sigV1.getSignatureInfoV1().getDomNode(), true);
-        n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS);
+        // n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS);
 
         List<XMLStructure> signatureInfoContent = new ArrayList<>();
         signatureInfoContent.add(new DOMStructure(n));
index 4130e2396c0a53d8e2e85c355148e77ab79cf071..a83d2912297a442055a976f17a633d9bc7163fd6 100644 (file)
@@ -36,13 +36,11 @@ import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509CRL;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
-import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
-import java.util.UUID;
 
 import javax.xml.crypto.MarshalException;
 
@@ -67,9 +65,11 @@ import org.bouncycastle.cert.ocsp.BasicOCSPResp;
 import org.bouncycastle.cert.ocsp.OCSPResp;
 import org.bouncycastle.cert.ocsp.RespID;
 import org.etsi.uri.x01903.v13.*;
+import org.etsi.uri.x01903.v14.TimeStampValidationDataDocument;
 import org.etsi.uri.x01903.v14.ValidationDataType;
 import org.w3.x2000.x09.xmldsig.CanonicalizationMethodType;
 import org.w3c.dom.Document;
+import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 
@@ -114,12 +114,38 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
         UnsignedSignaturePropertiesType unsignedSigProps =
             ofNullable(unsignedProps.getUnsignedSignatureProperties()).orElseGet(unsignedProps::addNewUnsignedSignatureProperties);
 
-        // create the XAdES-T time-stamp
-        NodeList nlSigVal = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue");
-        XAdESTimeStampType signatureTimeStamp = addTimestamp(nlSigVal, signatureInfo, unsignedSigProps);
+        final NodeList nlSigVal = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue");
+        if (nlSigVal.getLength() != 1) {
+            throw new IllegalArgumentException("SignatureValue is not set.");
+        }
+        final Element sigVal = (Element)nlSigVal.item(0);
+
 
         // Without revocation data service we cannot construct the XAdES-C extension.
         RevocationDataService revDataSvc = signatureConfig.getRevocationDataService();
+        if (revDataSvc != null) {
+            // XAdES-X-L
+            addCertificateValues(unsignedSigProps, signatureConfig);
+        }
+
+        LOG.atDebug().log("creating XAdES-T time-stamp");
+
+        // xadesv141::TimeStampValidationData
+        XAdESTimeStampType signatureTimeStamp;
+        try {
+            final RevocationData tsaRevocationDataXadesT = new RevocationData();
+            signatureTimeStamp = createXAdESTimeStamp(signatureInfo, tsaRevocationDataXadesT, sigVal);
+            unsignedSigProps.addNewSignatureTimeStamp().set(signatureTimeStamp);
+
+            if (tsaRevocationDataXadesT.hasRevocationDataEntries()) {
+                TimeStampValidationDataDocument validationData = createValidationData(tsaRevocationDataXadesT);
+                insertXChild(unsignedSigProps, validationData);
+            }
+        } catch (CertificateEncodingException e) {
+            throw new MarshalException("unable to create XAdES signatrue", e);
+        }
+
+
         if (revDataSvc != null) {
             // XAdES-C: complete certificate refs
             CompleteCertificateRefsType completeCertificateRefs = completeCertificateRefs(unsignedSigProps, signatureConfig);
@@ -130,19 +156,30 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
             addRevocationCRL(completeRevocationRefs, signatureConfig, revocationData);
             addRevocationOCSP(completeRevocationRefs, signatureConfig, revocationData);
 
-            // XAdES-X Type 1 timestamp
-            addTimestampX(unsignedSigProps, signatureInfo, nlSigVal, signatureTimeStamp, completeCertificateRefs, completeRevocationRefs);
-
-            // XAdES-X-L
-            addCertificateValues(unsignedSigProps, signatureConfig);
-
             RevocationValuesType revocationValues = unsignedSigProps.addNewRevocationValues();
             createRevocationValues(revocationValues, revocationData);
+
+            // XAdES-X Type 1 timestamp
+            LOG.atDebug().log("creating XAdES-X time-stamp");
+            revocationData = new RevocationData();
+            XAdESTimeStampType timeStampXadesX1 = createXAdESTimeStamp(signatureInfo, revocationData,
+                    sigVal, signatureTimeStamp.getDomNode(), completeCertificateRefs.getDomNode(), completeRevocationRefs.getDomNode());
+
+            // marshal XAdES-X
+            unsignedSigProps.addNewSigAndRefsTimeStamp().set(timeStampXadesX1);
         }
 
+
+
+
         // marshal XAdES-X-L
-        Node n = document.importNode(qualProps.getDomNode(), true);
-        qualNl.item(0).getParentNode().replaceChild(n, qualNl.item(0));
+        Element n = (Element)document.importNode(qualProps.getDomNode(), true);
+        NodeList nl = n.getElementsByTagName("TimeStampValidationData");
+        for (int i=0; i<nl.getLength(); i++) {
+            ((Element)nl.item(i)).setAttributeNS(XML_NS, "xmlns", "http://uri.etsi.org/01903/v1.4.1#");
+        }
+        Node qualNL0 = qualNl.item(0);
+        qualNL0.getParentNode().replaceChild(n, qualNL0);
     }
 
     private QualifyingPropertiesType getQualProps(NodeList qualNl) throws MarshalException {
@@ -160,28 +197,6 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
         }
     }
 
-    private XAdESTimeStampType addTimestamp(NodeList nlSigVal, SignatureInfo signatureInfo, UnsignedSignaturePropertiesType unsignedSigProps) {
-        if (nlSigVal.getLength() != 1) {
-            throw new IllegalArgumentException("SignatureValue is not set.");
-        }
-
-        RevocationData tsaRevocationDataXadesT = new RevocationData();
-        LOG.atDebug().log("creating XAdES-T time-stamp");
-        XAdESTimeStampType signatureTimeStamp = createXAdESTimeStamp
-            (signatureInfo, Collections.singletonList(nlSigVal.item(0)), tsaRevocationDataXadesT);
-
-        // marshal the XAdES-T extension
-        unsignedSigProps.addNewSignatureTimeStamp().set(signatureTimeStamp);
-
-        // xadesv141::TimeStampValidationData
-        if (tsaRevocationDataXadesT.hasRevocationDataEntries()) {
-            ValidationDataType validationData = createValidationData(tsaRevocationDataXadesT);
-            insertXChild(unsignedSigProps, validationData);
-        }
-
-        return signatureTimeStamp;
-    }
-
     private CompleteCertificateRefsType completeCertificateRefs(UnsignedSignaturePropertiesType unsignedSigProps, SignatureConfig signatureConfig) {
         CompleteCertificateRefsType completeCertificateRefs = unsignedSigProps.addNewCompleteCertificateRefs();
 
@@ -253,11 +268,11 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
                     ResponderID ocspResponderId = respId.toASN1Primitive();
                     DERTaggedObject derTaggedObject = (DERTaggedObject)ocspResponderId.toASN1Primitive();
                     if (2 == derTaggedObject.getTagNo()) {
-                        ASN1OctetString keyHashOctetString = (ASN1OctetString)derTaggedObject.getObject();
+                        ASN1OctetString keyHashOctetString = (ASN1OctetString)derTaggedObject.getBaseObject();
                         byte[] key = keyHashOctetString.getOctets();
                         responderId.setByKey(key);
                     } else {
-                        X500Name name = X500Name.getInstance(derTaggedObject.getObject());
+                        X500Name name = X500Name.getInstance(derTaggedObject.getBaseObject());
                         String nameStr = name.toString();
                         responderId.setByName(nameStr);
                     }
@@ -268,38 +283,18 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
         }
     }
 
-    private void addTimestampX(UnsignedSignaturePropertiesType unsignedSigProps, SignatureInfo signatureInfo, NodeList nlSigVal, XAdESTimeStampType signatureTimeStamp,
-        CompleteCertificateRefsType completeCertificateRefs, CompleteRevocationRefsType completeRevocationRefs) {
-
-        List<Node> timeStampNodesXadesX1 = new ArrayList<>();
-        timeStampNodesXadesX1.add(nlSigVal.item(0));
-        timeStampNodesXadesX1.add(signatureTimeStamp.getDomNode());
-        timeStampNodesXadesX1.add(completeCertificateRefs.getDomNode());
-        timeStampNodesXadesX1.add(completeRevocationRefs.getDomNode());
-
-        RevocationData tsaRevocationDataXadesX1 = new RevocationData();
-        LOG.atDebug().log("creating XAdES-X time-stamp");
-        XAdESTimeStampType timeStampXadesX1 = createXAdESTimeStamp
-            (signatureInfo, timeStampNodesXadesX1, tsaRevocationDataXadesX1);
-        if (tsaRevocationDataXadesX1.hasRevocationDataEntries()) {
-            ValidationDataType timeStampXadesX1ValidationData = createValidationData(tsaRevocationDataXadesX1);
-            insertXChild(unsignedSigProps, timeStampXadesX1ValidationData);
-        }
-
-        // marshal XAdES-X
-        unsignedSigProps.addNewSigAndRefsTimeStamp().set(timeStampXadesX1);
-
-    }
-
     private void addCertificateValues(UnsignedSignaturePropertiesType unsignedSigProps, SignatureConfig signatureConfig) {
+        List<X509Certificate> chain = signatureConfig.getSigningCertificateChain();
+        if (chain.size() < 2) {
+            return;
+        }
         CertificateValuesType certificateValues = unsignedSigProps.addNewCertificateValues();
-        for (X509Certificate certificate : signatureConfig.getSigningCertificateChain()) {
-            EncapsulatedPKIDataType encapsulatedPKIDataType = certificateValues.addNewEncapsulatedX509Certificate();
-            try {
-                encapsulatedPKIDataType.setByteArrayValue(certificate.getEncoded());
-            } catch (CertificateEncodingException e) {
-                throw new RuntimeException("certificate encoding error: " + e.getMessage(), e);
+        try {
+            for (X509Certificate certificate : chain.subList(1, chain.size())) {
+                certificateValues.addNewEncapsulatedX509Certificate().setByteArrayValue(certificate.getEncoded());
             }
+        } catch (CertificateEncodingException e) {
+            throw new RuntimeException("certificate encoding error: " + e.getMessage(), e);
         }
     }
 
@@ -341,45 +336,48 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
 
     private XAdESTimeStampType createXAdESTimeStamp(
             SignatureInfo signatureInfo,
-            List<Node> nodeList,
-            RevocationData revocationData) {
+            RevocationData revocationData,
+            Node... nodes) {
         SignatureConfig signatureConfig = signatureInfo.getSignatureConfig();
-        byte[] c14nSignatureValueElement = getC14nValue(nodeList, signatureConfig.getXadesCanonicalizationMethod());
-
-        return createXAdESTimeStamp(signatureInfo, c14nSignatureValueElement, revocationData);
-    }
+        byte[] c14nSignatureValueElement = getC14nValue(Arrays.asList(nodes), signatureConfig.getXadesCanonicalizationMethod());
 
-    private XAdESTimeStampType createXAdESTimeStamp(SignatureInfo signatureInfo, byte[] data, RevocationData revocationData) {
-        SignatureConfig signatureConfig = signatureInfo.getSignatureConfig();
         // create the time-stamp
         byte[] timeStampToken;
         try {
-            timeStampToken = signatureConfig.getTspService().timeStamp(signatureInfo, data, revocationData);
+            timeStampToken = signatureConfig.getTspService().timeStamp(signatureInfo, c14nSignatureValueElement, revocationData);
         } catch (Exception e) {
             throw new RuntimeException("error while creating a time-stamp: "
-                    + e.getMessage(), e);
+                + e.getMessage(), e);
         }
 
         // create a XAdES time-stamp container
         XAdESTimeStampType xadesTimeStamp = XAdESTimeStampType.Factory.newInstance();
-        xadesTimeStamp.setId("time-stamp-" + UUID.randomUUID());
         CanonicalizationMethodType c14nMethod = xadesTimeStamp.addNewCanonicalizationMethod();
         c14nMethod.setAlgorithm(signatureConfig.getXadesCanonicalizationMethod());
 
         // embed the time-stamp
         EncapsulatedPKIDataType encapsulatedTimeStamp = xadesTimeStamp.addNewEncapsulatedTimeStamp();
         encapsulatedTimeStamp.setByteArrayValue(timeStampToken);
-        encapsulatedTimeStamp.setId("time-stamp-token-" + UUID.randomUUID());
 
         return xadesTimeStamp;
     }
 
-    private ValidationDataType createValidationData(
-            RevocationData revocationData) {
-        ValidationDataType validationData = ValidationDataType.Factory.newInstance();
+    private TimeStampValidationDataDocument createValidationData(RevocationData revocationData)
+    throws CertificateEncodingException {
+        TimeStampValidationDataDocument doc = TimeStampValidationDataDocument.Factory.newInstance();
+        ValidationDataType validationData = doc.addNewTimeStampValidationData();
+        List<X509Certificate> tspChain = revocationData.getX509chain();
+
+        if (tspChain.size() > 1) {
+            CertificateValuesType cvals = validationData.addNewCertificateValues();
+            for (X509Certificate x509 : tspChain.subList(1, tspChain.size())) {
+                byte[] encoded = x509.getEncoded();
+                cvals.addNewEncapsulatedX509Certificate().setByteArrayValue(encoded);
+            }
+        }
         RevocationValuesType revocationValues = validationData.addNewRevocationValues();
         createRevocationValues(revocationValues, revocationData);
-        return validationData;
+        return doc;
     }
 
     private void createRevocationValues(
index 24430c3ac108d63d52eca82ceedb33a512bc1f6e..5465b1a9db3394592e1a8e61d6b0bb888101f761 100644 (file)
@@ -26,7 +26,9 @@ package org.apache.poi.poifs.crypt.dsig.services;
 
 import java.security.cert.CRLException;
 import java.security.cert.X509CRL;
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -34,37 +36,28 @@ import java.util.List;
  */
 public class RevocationData {
 
-    private final List<byte[]> crls;
-
-    private final List<byte[]> ocsps;
-
-    /**
-     * Default constructor.
-     */
-    public RevocationData() {
-        this.crls = new ArrayList<>();
-        this.ocsps = new ArrayList<>();
-    }
+    private final List<byte[]> crls = new ArrayList<>();
+    private final List<byte[]> ocsps = new ArrayList<>();
+    private final List<X509Certificate> x509chain = new ArrayList<>();
 
     /**
      * Adds a CRL to this revocation data set.
      */
     public void addCRL(byte[] encodedCrl) {
-        this.crls.add(encodedCrl);
+        if (this.crls.stream().noneMatch(by -> Arrays.equals(by, encodedCrl))) {
+            this.crls.add(encodedCrl);
+        }
     }
 
     /**
      * Adds a CRL to this revocation data set.
      */
     public void addCRL(X509CRL crl) {
-        byte[] encodedCrl;
         try {
-            encodedCrl = crl.getEncoded();
+            addCRL(crl.getEncoded());
         } catch (CRLException e) {
-            throw new IllegalArgumentException("CRL coding error: "
-                    + e.getMessage(), e);
+            throw new IllegalArgumentException("CRL coding error: " + e.getMessage(), e);
         }
-        addCRL(encodedCrl);
     }
 
     /**
@@ -74,6 +67,10 @@ public class RevocationData {
         this.ocsps.add(encodedOcsp);
     }
 
+    public void addCertificate(X509Certificate x509) {
+        x509chain.add(x509);
+    }
+
     /**
      * Gives back a list of all CRLs.
      *
@@ -120,4 +117,8 @@ public class RevocationData {
     public boolean hasRevocationDataEntries() {
         return hasOCSPs() || hasCRLs();
     }
+
+    public List<X509Certificate> getX509chain() {
+        return x509chain;
+    }
 }
index cefe1514d7d89ede2433d8b9d38e96b1bdb50396..0f5531245804bdc101078579c99cb2b1c261823b 100644 (file)
@@ -26,42 +26,49 @@ package org.apache.poi.poifs.crypt.dsig.services;
 
 import static org.apache.logging.log4j.util.Unbox.box;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.math.BigInteger;
-import java.net.HttpURLConnection;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Proxy;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
 import java.security.MessageDigest;
 import java.security.SecureRandom;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509CRL;
 import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Collection;
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.security.auth.x500.X500Principal;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.poi.poifs.crypt.CryptoFunctions;
 import org.apache.poi.poifs.crypt.HashAlgorithm;
 import org.apache.poi.poifs.crypt.dsig.SignatureConfig;
+import org.apache.poi.poifs.crypt.dsig.SignatureConfig.CRLEntry;
 import org.apache.poi.poifs.crypt.dsig.SignatureInfo;
-import org.apache.poi.util.HexDump;
-import org.apache.poi.util.IOUtils;
+import org.apache.poi.poifs.crypt.dsig.services.TimeStampHttpClient.TimeStampHttpClientResponse;
+import org.bouncycastle.asn1.ASN1IA5String;
 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.ASN1Primitive;
 import org.bouncycastle.asn1.cmp.PKIFailureInfo;
 import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
 import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.CRLDistPoint;
+import org.bouncycastle.asn1.x509.DistributionPoint;
+import org.bouncycastle.asn1.x509.DistributionPointName;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
 import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
 import org.bouncycastle.cert.X509CertificateHolder;
 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
 import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator;
 import org.bouncycastle.cms.SignerId;
 import org.bouncycastle.cms.SignerInformationVerifier;
@@ -81,25 +88,6 @@ public class TSPTimeStampService implements TimeStampService {
 
     private static final Logger LOG = LogManager.getLogger(TSPTimeStampService.class);
 
-    // how large a timestamp response is expected to be
-    // can be overwritten via IOUtils.setByteArrayMaxOverride()
-    private static final int DEFAULT_TIMESTAMP_RESPONSE_SIZE = 10_000_000;
-    private static int MAX_TIMESTAMP_RESPONSE_SIZE = DEFAULT_TIMESTAMP_RESPONSE_SIZE;
-
-    /**
-     * @param maxTimestampResponseSize the max timestamp response size allowed
-     */
-    public static void setMaxTimestampResponseSize(int maxTimestampResponseSize) {
-        MAX_TIMESTAMP_RESPONSE_SIZE = maxTimestampResponseSize;
-    }
-
-    /**
-     * @return the max timestamp response size allowed
-     */
-    public static int getMaxTimestampResponseSize() {
-        return MAX_TIMESTAMP_RESPONSE_SIZE;
-    }
-
     /**
      * Maps the digest algorithm to corresponding OID value.
      */
@@ -133,72 +121,16 @@ public class TSPTimeStampService implements TimeStampService {
         }
         ASN1ObjectIdentifier digestAlgoOid = mapDigestAlgoToOID(signatureConfig.getTspDigestAlgo());
         TimeStampRequest request = requestGenerator.generate(digestAlgoOid, digest, nonce);
-        byte[] encodedRequest = request.getEncoded();
-
-        // create the HTTP POST request
-        Proxy proxy = Proxy.NO_PROXY;
-        if (signatureConfig.getProxyUrl() != null) {
-            URL proxyUrl = new URL(signatureConfig.getProxyUrl());
-            String host = proxyUrl.getHost();
-            int port = proxyUrl.getPort();
-            proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByName(host), (port == -1 ? 80 : port)));
-        }
 
-        String contentType;
-        HttpURLConnection huc = (HttpURLConnection)new URL(signatureConfig.getTspUrl()).openConnection(proxy);
-        byte[] responseBytes;
-        try {
-            if (signatureConfig.getTspUser() != null) {
-                String userPassword = signatureConfig.getTspUser() + ":" + signatureConfig.getTspPass();
-                String encoding = Base64.getEncoder().encodeToString(userPassword.getBytes(StandardCharsets.ISO_8859_1));
-                huc.setRequestProperty("Authorization", "Basic " + encoding);
-            }
-
-            huc.setRequestMethod("POST");
-            huc.setConnectTimeout(20000);
-            huc.setReadTimeout(20000);
-            huc.setDoOutput(true); // also sets method to POST.
-            huc.setRequestProperty("User-Agent", signatureConfig.getUserAgent());
-            huc.setRequestProperty("Content-Type", signatureConfig.isTspOldProtocol()
-                    ? "application/timestamp-request"
-                    : "application/timestamp-query"); // "; charset=ISO-8859-1");
-
-            OutputStream hucOut = huc.getOutputStream();
-            hucOut.write(encodedRequest);
-
-            // invoke TSP service
-            huc.connect();
-
-            int statusCode = huc.getResponseCode();
-            if (statusCode != 200) {
-                final String message = "Error contacting TSP server " + signatureConfig.getTspUrl() +
-                        ", had status code " + statusCode + "/" + huc.getResponseMessage();
-                LOG.atError().log(message);
-                throw new IOException(message);
-            }
-
-            // HTTP input validation
-            contentType = huc.getHeaderField("Content-Type");
-            if (null == contentType) {
-                throw new RuntimeException("missing Content-Type header");
-            }
-
-            try (InputStream stream = huc.getInputStream()) {
-                responseBytes = IOUtils.toByteArrayWithMaxLength(stream, getMaxTimestampResponseSize());
-            }
-            LOG.atDebug().log(() -> new SimpleMessage("response content: " + HexDump.dump(responseBytes, 0, 0)));
-        } finally {
-            huc.disconnect();
+        TimeStampHttpClient httpClient = signatureConfig.getTspHttpClient();
+        httpClient.init(signatureConfig);
+        httpClient.setContentTypeIn(signatureConfig.isTspOldProtocol() ? "application/timestamp-request" : "application/timestamp-query");
+        TimeStampHttpClientResponse response = httpClient.post(signatureConfig.getTspUrl(), request.getEncoded());
+        if (!response.isOK()) {
+            throw new IOException("Requesting timestamp data failed");
         }
 
-        if (!contentType.startsWith(signatureConfig.isTspOldProtocol()
-            ? "application/timestamp-response"
-            : "application/timestamp-reply"
-        )) {
-            throw new RuntimeException("invalid Content-Type: " + contentType +
-                    // dump the first few bytes
-                    ": " + HexDump.dump(responseBytes, 0, 0, 200));
-        }
+        byte[] responseBytes = response.getResponseBytes();
 
         if (responseBytes.length == 0) {
             throw new RuntimeException("Content-Length is zero");
@@ -229,53 +161,138 @@ public class TSPTimeStampService implements TimeStampService {
         LOG.atDebug().log("signer cert issuer: {}", signerCertIssuer);
 
         // TSP signer certificates retrieval
-        Collection<X509CertificateHolder> certificates = timeStampToken.getCertificates().getMatches(null);
-
-        X509CertificateHolder signerCert = null;
-        Map<X500Name, X509CertificateHolder> certificateMap = new HashMap<>();
-        for (X509CertificateHolder certificate : certificates) {
-            if (signerCertIssuer.equals(certificate.getIssuer())
-                && signerCertSerialNumber.equals(certificate.getSerialNumber())) {
-                signerCert = certificate;
-            }
-            certificateMap.put(certificate.getSubject(), certificate);
-        }
+        Map<String, X509CertificateHolder> certificateMap =
+            timeStampToken.getCertificates().getMatches(null).stream()
+                .collect(Collectors.toMap(h -> h.getSubject().toString(), Function.identity()));
+
 
         // TSP signer cert path building
-        if (signerCert == null) {
-            throw new RuntimeException("TSP response token has no signer certificate");
-        }
-        List<X509Certificate> tspCertificateChain = new ArrayList<>();
+        X509CertificateHolder signerCert = certificateMap.values().stream()
+            .filter(h -> signerCertIssuer.equals(h.getIssuer())
+                && signerCertSerialNumber.equals(h.getSerialNumber()))
+            .findFirst()
+            .orElseThrow(() -> new RuntimeException("TSP response token has no signer certificate"));
+
         JcaX509CertificateConverter x509converter = new JcaX509CertificateConverter();
         x509converter.setProvider("BC");
-        X509CertificateHolder certificate = signerCert;
+
+        // complete certificate chain
+        X509Certificate child = x509converter.getCertificate(signerCert);
         do {
-            LOG.atDebug().log("adding to certificate chain: {}", certificate.getSubject());
-            tspCertificateChain.add(x509converter.getCertificate(certificate));
-            if (certificate.getSubject().equals(certificate.getIssuer())) {
+            revocationData.addCertificate(child);
+            X500Principal issuer = child.getIssuerX500Principal();
+            if (child.getSubjectX500Principal().equals(issuer)) {
                 break;
             }
-            certificate = certificateMap.get(certificate.getIssuer());
-        } while (null != certificate);
+            X509CertificateHolder parentHolder = certificateMap.get(issuer.getName());
+            child = (parentHolder != null)
+                ? x509converter.getCertificate(parentHolder)
+                : signatureConfig.getCachedCertificateByPrinicipal(issuer.getName());
+            if (child != null) {
+                retrieveCRL(signatureConfig, child).forEach(revocationData::addCRL);
+            }
+        } while (child != null);
 
         // verify TSP signer signature
-        X509CertificateHolder holder = new X509CertificateHolder(tspCertificateChain.get(0).getEncoded());
-        DefaultCMSSignatureAlgorithmNameGenerator nameGen = new DefaultCMSSignatureAlgorithmNameGenerator();
-        DefaultSignatureAlgorithmIdentifierFinder sigAlgoFinder = new DefaultSignatureAlgorithmIdentifierFinder();
-        DefaultDigestAlgorithmIdentifierFinder hashAlgoFinder = new DefaultDigestAlgorithmIdentifierFinder();
-        BcDigestCalculatorProvider calculator = new BcDigestCalculatorProvider();
-        BcRSASignerInfoVerifierBuilder verifierBuilder = new BcRSASignerInfoVerifierBuilder(nameGen, sigAlgoFinder, hashAlgoFinder, calculator);
-        SignerInformationVerifier verifier = verifierBuilder.build(holder);
+        BcRSASignerInfoVerifierBuilder verifierBuilder = new BcRSASignerInfoVerifierBuilder(
+            new DefaultCMSSignatureAlgorithmNameGenerator(),
+            new DefaultSignatureAlgorithmIdentifierFinder(),
+            new DefaultDigestAlgorithmIdentifierFinder(),
+            new BcDigestCalculatorProvider());
+        SignerInformationVerifier verifier = verifierBuilder.build(signerCert);
 
         timeStampToken.validate(verifier);
 
         // verify TSP signer certificate
         if (signatureConfig.getTspValidator() != null) {
-            signatureConfig.getTspValidator().validate(tspCertificateChain, revocationData);
+            signatureConfig.getTspValidator().validate(revocationData.getX509chain(), revocationData);
         }
 
         LOG.atDebug().log("time-stamp token time: {}", timeStampToken.getTimeStampInfo().getGenTime());
 
         return timeStampToken.getEncoded();
     }
+
+    /**
+     * Check if CRL is to be added, check cached CRLs in config and download if necessary.
+     * Can be overriden to suppress the logic
+     * @return empty list, if not found or suppressed, otherwise the list of CRLs as encoded bytes
+     */
+    protected List<byte[]> retrieveCRL(SignatureConfig signatureConfig, X509Certificate holder) throws IOException {
+        // TODO: add config, if crls should be added
+        final List<CRLEntry> crlEntries = signatureConfig.getCrlEntries();
+        byte[] crlPoints = holder.getExtensionValue(Extension.cRLDistributionPoints.getId());
+        if (crlPoints == null) {
+            return Collections.emptyList();
+        }
+
+        // TODO: check if parse is necessary, or if crlExt.getExtnValue() can be use directly
+        ASN1Primitive extVal = JcaX509ExtensionUtils.parseExtensionValue(crlPoints);
+        return Stream.of(CRLDistPoint.getInstance(extVal).getDistributionPoints())
+            .map(DistributionPoint::getDistributionPoint)
+            .filter(Objects::nonNull)
+            .filter(dpn -> dpn.getType() == DistributionPointName.FULL_NAME)
+            .flatMap(dpn -> Stream.of(GeneralNames.getInstance(dpn.getName()).getNames()))
+            .filter(genName -> genName.getTagNo() == GeneralName.uniformResourceIdentifier)
+            .map(genName -> ASN1IA5String.getInstance(genName.getName()).getString())
+            .flatMap(url -> {
+                List<CRLEntry> ul = crlEntries.stream().filter(ce -> matchCRLbyUrl(ce, holder, url)).collect(Collectors.toList());
+                Stream<CRLEntry> cl = crlEntries.stream().filter(ce -> matchCRLbyCN(ce, holder, url));
+                if (ul.isEmpty()) {
+                    CRLEntry ce = downloadCRL(signatureConfig, url);
+                    if (ce != null) {
+                        ul.add(ce);
+                    }
+                }
+                return Stream.concat(ul.stream(), cl).map(CRLEntry::getCrlBytes);
+            })
+            .filter(Objects::nonNull)
+            .collect(Collectors.toList());
+    }
+
+    protected boolean matchCRLbyUrl(CRLEntry other, X509Certificate holder, String url) {
+        return url.equals(other.getCrlURL());
+    }
+
+    protected boolean matchCRLbyCN(CRLEntry other, X509Certificate holder, String url) {
+        return holder.getSubjectX500Principal().getName().equals(other.getCertCN());
+    }
+
+    /**
+     * Convenience method to download a crl in an unsafe way, i.e. without verifying the
+     * https certificates.
+     * Please provide your own method, if you have imported the TSP server CA certificates
+     * in your local keystore
+     *
+     * @return the bytes of the CRL or null if unsuccessful / download is suppressed
+     */
+    protected CRLEntry downloadCRL(SignatureConfig signatureConfig, String url) {
+        if (!signatureConfig.isAllowCRLDownload()) {
+            return null;
+        }
+
+        TimeStampHttpClient httpClient = signatureConfig.getTspHttpClient();
+        httpClient.init(signatureConfig);
+        httpClient.setBasicAuthentication(null, null);
+        TimeStampHttpClientResponse response;
+        try {
+            response = httpClient.get(url);
+            if (!response.isOK()) {
+                return null;
+            }
+        } catch (IOException e) {
+            return null;
+        }
+
+        try {
+            CertificateFactory certFact = CertificateFactory.getInstance("X.509");
+            byte[] crlBytes = response.getResponseBytes();
+            // verify the downloaded bytes, throws Exception if invalid
+            X509CRL crl = (X509CRL)certFact.generateCRL(new ByteArrayInputStream(crlBytes));
+            return signatureConfig.addCRL(url, crl.getIssuerX500Principal().getName(), crlBytes);
+        } catch (GeneralSecurityException e) {
+            LOG.atWarn().withThrowable(e).log("CRL download failed from {}", url);
+            return null;
+        }
+    }
 }
\ No newline at end of file
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java
new file mode 100644 (file)
index 0000000..869159f
--- /dev/null
@@ -0,0 +1,83 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You 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 org.apache.poi.poifs.crypt.dsig.services;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+import org.apache.poi.poifs.crypt.dsig.SignatureConfig;
+
+/**
+ * This interface is used to decouple the timestamp service logic from
+ * the actual downloading code and to provide an interface for user code
+ * using a different http client implementation.
+ *
+ * The implementation must be stateless regarding the http connection and
+ * not expect to be called in a certain order, apart from being first initialized.
+ */
+public interface TimeStampHttpClient {
+    interface TimeStampHttpClientResponse {
+        default boolean isOK() {
+            return getResponseCode() == HttpURLConnection.HTTP_OK;
+        }
+
+        /**
+         * @return the http response code
+         */
+        int getResponseCode();
+
+        /**
+         * @return the http response bytes
+         */
+        byte[] getResponseBytes();
+    }
+
+    void init(SignatureConfig config);
+
+    /** set request content type */
+    void setContentTypeIn(String contentType);
+
+    /** set expected response content type - use {@code null} if contentType is ignored */
+    void setContentTypeOut(String contentType);
+
+    void setBasicAuthentication(String username, String password);
+
+    TimeStampHttpClientResponse post(String url, byte[] payload) throws IOException;
+
+    TimeStampHttpClientResponse get(String url) throws IOException;
+
+    /**
+     * @return if the connection is reckless ignoring all https certificate trust issues
+     */
+    boolean isIgnoreHttpsCertificates();
+
+    /**
+     * @param ignoreHttpsCertificates set if the connection is reckless ignoring all https certificate trust issues
+     */
+    void setIgnoreHttpsCertificates(boolean ignoreHttpsCertificates);
+
+    /**
+     * @return if http redirects are followed once
+     */
+    boolean isFollowRedirects();
+
+    /**
+     * @param followRedirects set if http redirects are followed once
+     */
+    void setFollowRedirects(boolean followRedirects);
+}
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java
new file mode 100644 (file)
index 0000000..6203e49
--- /dev/null
@@ -0,0 +1,285 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You 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 org.apache.poi.poifs.crypt.dsig.services;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.poi.poifs.crypt.dsig.SignatureConfig;
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.RandomSingleton;
+
+/**
+ * This default implementation is used to decouple the timestamp service logic from
+ * the actual downloading code and to provide a base for user code
+ * using a different http client implementation
+ */
+public class TimeStampSimpleHttpClient implements TimeStampHttpClient {
+    protected static final String CONTENT_TYPE = "Content-Type";
+    protected static final String USER_AGENT = "User-Agent";
+    protected static final String BASIC_AUTH = "Authorization";
+    protected static final String REDIRECT_LOCATION = "Location";
+
+    private static final Logger LOG = LogManager.getLogger(TSPTimeStampService.class);
+
+    // how large a timestamp response is expected to be
+    // can be overwritten via IOUtils.setByteArrayMaxOverride()
+    private static final int DEFAULT_TIMESTAMP_RESPONSE_SIZE = 10_000_000;
+    private static int MAX_TIMESTAMP_RESPONSE_SIZE = DEFAULT_TIMESTAMP_RESPONSE_SIZE;
+
+    /**
+     * @param maxTimestampResponseSize the max timestamp response size allowed
+     */
+    public static void setMaxTimestampResponseSize(int maxTimestampResponseSize) {
+        MAX_TIMESTAMP_RESPONSE_SIZE = maxTimestampResponseSize;
+    }
+
+    /**
+     * @return the max timestamp response size allowed
+     */
+    public static int getMaxTimestampResponseSize() {
+        return MAX_TIMESTAMP_RESPONSE_SIZE;
+    }
+
+
+    private static class TimeStampSimpleHttpClientResponse implements TimeStampHttpClientResponse {
+        private final int responseCode;
+        private final byte[] responseBytes;
+
+        public TimeStampSimpleHttpClientResponse(int responseCode, byte[] responseBytes) {
+            this.responseCode = responseCode;
+            this.responseBytes = responseBytes;
+        }
+
+        @Override
+        public int getResponseCode() {
+            return responseCode;
+        }
+
+        @Override
+        public byte[] getResponseBytes() {
+            return responseBytes;
+        }
+
+
+    }
+
+    protected SignatureConfig config;
+    protected Proxy proxy = Proxy.NO_PROXY;
+    protected final Map<String,String> header = new HashMap<>();
+    protected String contentTypeOut = null;
+    protected boolean ignoreHttpsCertificates = false;
+    protected boolean followRedirects = false;
+
+    @Override
+    public void init(SignatureConfig config) {
+        this.config = config;
+        header.clear();
+
+        header.put(USER_AGENT, config.getUserAgent());
+
+        contentTypeOut = null;
+        // don't reset followRedirects/ignoreHttpsCertificates, as they aren't contained in SignatureConfig by design
+        // followRedirects = false;
+        // ignoreHttpsCertificates = false;
+
+        setProxy(config.getProxyUrl());
+        setBasicAuthentication(config.getTspUser(), config.getTspPass());
+    }
+
+    public void setProxy(String proxyUrl) {
+        if (proxyUrl == null || proxyUrl.isEmpty()) {
+            proxy = Proxy.NO_PROXY;
+        } else {
+            try {
+                URL pUrl = new URL(proxyUrl);
+                String host = pUrl.getHost();
+                int port = pUrl.getPort();
+                proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByName(host), (port == -1 ? 80 : port)));
+            } catch (IOException ignored) {
+            }
+        }
+    }
+
+    public Proxy getProxy() {
+        return proxy;
+    }
+
+    @Override
+    public void setContentTypeIn(String contentType) {
+        header.put(CONTENT_TYPE, contentType);
+    }
+
+    @Override
+    public void setContentTypeOut(String contentType) {
+        contentTypeOut = contentType;
+    }
+
+    @Override
+    public void setBasicAuthentication(String username, String password) {
+        if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
+            header.remove(BASIC_AUTH);
+        } else {
+            String userPassword = username + ":" + password;
+            String encoding = Base64.getEncoder().encodeToString(userPassword.getBytes(StandardCharsets.ISO_8859_1));
+            header.put(BASIC_AUTH, "Basic " + encoding);
+        }
+
+    }
+
+    @Override
+    public boolean isIgnoreHttpsCertificates() {
+        return ignoreHttpsCertificates;
+    }
+
+    @Override
+    public void setIgnoreHttpsCertificates(boolean ignoreHttpsCertificates) {
+        this.ignoreHttpsCertificates = ignoreHttpsCertificates;
+    }
+
+    @Override
+    public boolean isFollowRedirects() {
+        return followRedirects;
+    }
+
+    @Override
+    public void setFollowRedirects(boolean followRedirects) {
+        this.followRedirects = followRedirects;
+    }
+
+    @Override
+    public TimeStampHttpClientResponse post(String url, byte[] payload) throws IOException {
+        MethodHandler handler = (huc) -> {
+            huc.setRequestMethod("POST");
+            huc.setDoOutput(true);
+            try (OutputStream hucOut = huc.getOutputStream()) {
+                hucOut.write(payload);
+            }
+        };
+        return handleRedirect(url, handler, isFollowRedirects());
+    }
+
+    @Override
+    public TimeStampHttpClientResponse get(String url)  throws IOException {
+        // connection is by default a GET call
+        return handleRedirect(url, (huc) -> {}, isFollowRedirects());
+    }
+
+    protected interface MethodHandler {
+        void handle(HttpURLConnection huc) throws IOException;
+    }
+
+    protected TimeStampHttpClientResponse handleRedirect(String url, MethodHandler handler, boolean followRedirect) throws IOException {
+        HttpURLConnection huc = (HttpURLConnection)new URL(url).openConnection(proxy);
+        if (ignoreHttpsCertificates) {
+            recklessConnection(huc);
+        }
+        huc.setConnectTimeout(20000);
+        huc.setReadTimeout(20000);
+
+        header.forEach(huc::setRequestProperty);
+
+        try {
+            handler.handle(huc);
+
+            huc.connect();
+
+            final int responseCode = huc.getResponseCode();
+            final byte[] responseBytes;
+
+            switch (responseCode) {
+                case HttpURLConnection.HTTP_MOVED_TEMP:
+                case HttpURLConnection.HTTP_MOVED_PERM:
+                case HttpURLConnection.HTTP_SEE_OTHER:
+                    String newUrl = huc.getHeaderField(REDIRECT_LOCATION);
+                    if (newUrl != null && followRedirect) {
+                        LOG.atWarn().log("Received redirect: {} -> {}", url, newUrl);
+                        return handleRedirect(newUrl, handler, false);
+                    }
+
+                    LOG.atWarn().log("Redirect ignored - giving up: {} -> {}", url, newUrl);
+                    responseBytes = null;
+                    break;
+                case HttpURLConnection.HTTP_OK:
+                    // HTTP input validation
+                    String contentType = huc.getHeaderField(CONTENT_TYPE);
+                    if (contentTypeOut != null && !contentTypeOut.equals(contentType)) {
+                        throw new IOException("Content-Type mismatch - expected `" + contentTypeOut + "', received '" + contentType + "'");
+                    }
+
+                    try (InputStream is = huc.getInputStream()) {
+                        responseBytes = IOUtils.toByteArray(is, Integer.MIN_VALUE, getMaxTimestampResponseSize());
+                    }
+                    break;
+                default:
+                    final String message = "Error contacting TSP server " + url +
+                        ", had status code " + responseCode + "/" + huc.getResponseMessage();
+                    LOG.atError().log(message);
+                    throw new IOException(message);
+            }
+
+            return new TimeStampSimpleHttpClientResponse(responseCode, responseBytes);
+        } finally {
+            huc.disconnect();
+        }
+    }
+
+    protected void recklessConnection(HttpURLConnection conn) throws IOException {
+        if (!(conn instanceof HttpsURLConnection)) {
+            return;
+        }
+        HttpsURLConnection conns = (HttpsURLConnection)conn;
+
+        try {
+            SSLContext sc = SSLContext.getInstance("SSL");
+            sc.init(null, new TrustManager[]{new UnsafeTrustManager()}, RandomSingleton.getInstance());
+            conns.setSSLSocketFactory(sc.getSocketFactory());
+            conns.setHostnameVerifier((hostname, session) -> true);
+        } catch (GeneralSecurityException e) {
+            throw new IOException("Unable to reckless wrap connection.", e);
+        }
+    }
+
+    private static class UnsafeTrustManager implements X509TrustManager {
+        @Override
+        public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; }
+        @Override
+        public void checkClientTrusted(X509Certificate[] certs, String authType) { }
+        @Override
+        public void checkServerTrusted(X509Certificate[] certs, String authType) { }
+    }
+}
index 3763558ae31420b1f594b22c6e92d7486ae1f43b..10eaa22f38a3b4dda92e2164bac5955bbffeb39e 100644 (file)
@@ -37,21 +37,34 @@ import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
 import java.security.PublicKey;
+import java.security.UnrecoverableKeyException;
 import java.security.cert.Certificate;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
 import java.security.cert.X509CRL;
 import java.security.cert.X509Certificate;
 import java.security.interfaces.RSAPublicKey;
 import java.security.spec.RSAKeyGenParameterSpec;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import javax.security.auth.x500.X500Principal;
+
 import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
 import org.apache.poi.poifs.crypt.CryptoFunctions;
 import org.apache.poi.poifs.storage.RawDataUtil;
@@ -206,14 +219,38 @@ public class DummyKeystore {
         return new KeyCertPair(getKey(keyAlias, keyPass), keystore.getCertificateChain(keyAlias));
     }
 
+    public KeyCertPair getKeyPair(int index, String keyPass) throws GeneralSecurityException {
+        Map.Entry<String, PrivateKey> me = getKeyByIndex(index, keyPass);
+        return me != null ?  getKeyPair(me.getKey(), keyPass) : null;
+    }
+
     public PrivateKey getKey(String keyAlias, String keyPass) throws GeneralSecurityException {
         return (PrivateKey)keystore.getKey(keyAlias, keyPass.toCharArray());
     }
 
+    public PrivateKey getKey(int index, String keyPass) throws GeneralSecurityException {
+        Map.Entry<String, PrivateKey> me = getKeyByIndex(index, keyPass);
+        return me != null ?  me.getValue() : null;
+    }
+
     public X509Certificate getFirstX509(String alias) throws KeyStoreException {
         return (X509Certificate)keystore.getCertificate(alias);
     }
 
+    private Map.Entry<String,PrivateKey> getKeyByIndex(int index, String keyPass) throws GeneralSecurityException {
+        for (String a : Collections.list(keystore.aliases())) {
+            try {
+                PrivateKey pk = (PrivateKey) keystore.getKey(a, keyPass.toCharArray());
+                if (pk != null) {
+                    return new AbstractMap.SimpleEntry<>(a, pk);
+                }
+            } catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
+                break;
+            }
+        }
+        return null;
+    }
+
     public void save(File storeFile, String storePass) throws IOException, GeneralSecurityException {
         try (FileOutputStream fos = new FileOutputStream(storeFile)) {
             keystore.store(fos, storePass.toCharArray());
@@ -350,4 +387,50 @@ public class DummyKeystore {
         return ocspRespBuilder.build(OCSPRespBuilder.SUCCESSFUL, basicOCSPResp);
     }
 
+    public void importX509(File file) throws CertificateException, KeyStoreException, IOException {
+        try (InputStream is = new FileInputStream(file)) {
+            X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
+            keystore.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert);
+        }
+    }
+
+    public void importKeystore(File file, String storePass, String keyPass, Function<String,String> otherKeyPass) throws GeneralSecurityException, IOException {
+        DummyKeystore dk = new DummyKeystore(file, storePass);
+
+        Map<String,X509Certificate> myCerts = new HashMap<>();
+        for (String a : Collections.list(keystore.aliases())) {
+            Certificate[] chain = keystore.getCertificateChain(a);
+            if (chain == null) {
+                Certificate cert = keystore.getCertificate(a);
+                if (cert == null) {
+                    continue;
+                }
+                chain = new Certificate[]{cert};
+            }
+            Arrays.stream(chain)
+                .map(X509Certificate.class::cast)
+                .filter(c -> !myCerts.containsKey(c.getSubjectX500Principal().getName()))
+                .forEach(c -> myCerts.put(c.getSubjectX500Principal().getName(), c));
+        }
+
+        for (String a : Collections.list(dk.keystore.aliases())) {
+            KeyCertPair keyPair = dk.getKeyPair(a, otherKeyPass.apply(a));
+            ArrayList<X509Certificate> chain = new ArrayList<>(keyPair.getX509Chain());
+            Set<String> names = chain.stream().map(X509Certificate::getSubjectX500Principal).map(X500Principal::getName).collect(Collectors.toSet());
+            X509Certificate last = chain.get(chain.size() - 1);
+            do {
+                String issuer = last.getIssuerX500Principal().getName();
+                X509Certificate parent = myCerts.get(issuer);
+                if (names.contains(issuer) || parent == null) {
+                    break;
+                } else {
+                    chain.add(parent);
+                    names.add(issuer);
+                }
+                last = parent;
+            } while (true);
+
+            keystore.setKeyEntry(a, keyPair.getKey(), keyPass.toCharArray(), chain.toArray(new X509Certificate[0]));
+        }
+    }
 }
index 1ab9f9cae9547cb336dc8047954ab9006622af1e..eca80ac4c113ce3af011a23cea02ce0f5239f448 100644 (file)
@@ -160,7 +160,7 @@ public final class IOUtils {
      * Reads up to {@code length} bytes from the input stream, and returns the bytes read.
      *
      * @param stream The byte stream of data to read.
-     * @param length The maximum length to read, use {@link Integer#MAX_VALUE} to read the stream
+     * @param length The maximum length to read, use {@link Integer#MIN_VALUE} to read the stream
      *               until EOF
      * @param maxLength if the input is equal to/longer than {@code maxLength} bytes,
      *                  then throw an {@link IOException} complaining about the length.