From a881c381db69aeb4cf8622c57dfff247fc084c60 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 21 Feb 2022 22:56:30 +0000 Subject: [PATCH] #65908 - XAdES-XL modifications due to specification check errors git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1898287 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/poifs/crypt/dsig/SignatureConfig.java | 198 +++++++++++- .../dsig/SignatureMarshalDefaultListener.java | 24 +- .../dsig/facets/OOXMLSignatureFacet.java | 3 +- .../dsig/facets/XAdESXLSignatureFacet.java | 164 +++++----- .../crypt/dsig/services/RevocationData.java | 35 +-- .../dsig/services/TSPTimeStampService.java | 269 +++++++++-------- .../dsig/services/TimeStampHttpClient.java | 83 +++++ .../services/TimeStampSimpleHttpClient.java | 285 ++++++++++++++++++ .../poi/poifs/crypt/dsig/DummyKeystore.java | 83 +++++ .../java/org/apache/poi/util/IOUtils.java | 2 +- 10 files changed, 899 insertions(+), 247 deletions(-) create mode 100644 poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java create mode 100644 poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java 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 null 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 - * null 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 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 null 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 null, 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 null 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 null 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 null, 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 null, 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 1.3.6.1.4.1.13762.3 + * 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 null the claimed role element is omitted. - * Defaults to null + * @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 null 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 idSignedProperties + * 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 null defaults to idSignedProperties + * 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 true + * 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 EXCLUSIVE + * Defaults to {@code EXCLUSIVE} * @see javax.xml.crypto.dsig.CanonicalizationMethod */ 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 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 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 IGNORE_NS = new HashSet<>(Arrays.asList(null, XML_NS, XML_DIGSIG_NS)); + private static final List 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 prefixCfg, Map 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 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,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 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 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 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 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 crls; - - private final List ocsps; - - /** - * Default constructor. - */ - public RevocationData() { - this.crls = new ArrayList<>(); - this.ocsps = new ArrayList<>(); - } + private final List crls = new ArrayList<>(); + private final List ocsps = new ArrayList<>(); + private final List 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 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 certificates = timeStampToken.getCertificates().getMatches(null); - - X509CertificateHolder signerCert = null; - Map certificateMap = new HashMap<>(); - for (X509CertificateHolder certificate : certificates) { - if (signerCertIssuer.equals(certificate.getIssuer()) - && signerCertSerialNumber.equals(certificate.getSerialNumber())) { - signerCert = certificate; - } - certificateMap.put(certificate.getSubject(), certificate); - } + Map 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 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 retrieveCRL(SignatureConfig signatureConfig, X509Certificate holder) throws IOException { + // TODO: add config, if crls should be added + final List 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 ul = crlEntries.stream().filter(ce -> matchCRLbyUrl(ce, holder, url)).collect(Collectors.toList()); + Stream 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 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 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 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 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 otherKeyPass) throws GeneralSecurityException, IOException { + DummyKeystore dk = new DummyKeystore(file, storePass); + + Map 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 chain = new ArrayList<>(keyPair.getX509Chain()); + Set 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. -- 2.39.5