aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java198
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java24
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java3
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java164
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java35
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java269
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java83
-rw-r--r--poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java285
-rw-r--r--poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java83
-rw-r--r--poi/src/main/java/org/apache/poi/util/IOUtils.java2
10 files changed, 899 insertions, 247 deletions
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java
index b2b0b66e94..0805241528 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java
@@ -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;
@@ -573,6 +629,24 @@ public class SignatureConfig {
}
/**
+ * @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
*/
public String getTspUser() {
@@ -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
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java
index 2ffc2cfed3..d3c43fd2b2 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java
@@ -17,16 +17,21 @@
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);
+ }
+ }
}
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
index 265c620ec8..837a844cfb 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java
@@ -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));
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java
index 4130e2396c..a83d291229 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java
@@ -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,13 +114,39 @@ 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(
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java
index 24430c3ac1..5465b1a9db 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java
@@ -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;
+ }
}
diff --git a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java
index cefe1514d7..0f55312458 100644
--- a/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java
@@ -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
index 0000000000..869159f3e3
--- /dev/null
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java
@@ -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
index 0000000000..6203e49c27
--- /dev/null
+++ b/poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java
@@ -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) { }
+ }
+}
diff --git a/poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java b/poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java
index 3763558ae3..10eaa22f38 100644
--- a/poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java
+++ b/poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java
@@ -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]));
+ }
+ }
}
diff --git a/poi/src/main/java/org/apache/poi/util/IOUtils.java b/poi/src/main/java/org/apache/poi/util/IOUtils.java
index 1ab9f9cae9..eca80ac4c1 100644
--- a/poi/src/main/java/org/apache/poi/util/IOUtils.java
+++ b/poi/src/main/java/org/apache/poi/util/IOUtils.java
@@ -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.