Browse Source

#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
tags/REL_5_2_1
Andreas Beeker 2 years ago
parent
commit
a881c381db

+ 182
- 16
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java View File

@@ -20,22 +20,33 @@ package org.apache.poi.poifs.crypt.dsig;
import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.OO_DIGSIG_NS;
import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XADES_132_NS;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.crypto.URIDereferencer;
import javax.xml.crypto.dsig.CanonicalizationMethod;
@@ -58,8 +69,10 @@ import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.services.RevocationDataService;
import org.apache.poi.poifs.crypt.dsig.services.SignaturePolicyService;
import org.apache.poi.poifs.crypt.dsig.services.TSPTimeStampService;
import org.apache.poi.poifs.crypt.dsig.services.TimeStampHttpClient;
import org.apache.poi.poifs.crypt.dsig.services.TimeStampService;
import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator;
import org.apache.poi.poifs.crypt.dsig.services.TimeStampSimpleHttpClient;
import org.apache.poi.util.Internal;
import org.apache.poi.util.LocaleUtil;
import org.apache.poi.util.Removal;
@@ -73,6 +86,29 @@ import org.apache.xml.security.signature.XMLSignature;
*/
@SuppressWarnings({"unused","WeakerAccess"})
public class SignatureConfig {
public static class CRLEntry {
private final String crlURL;
private final String certCN;
private final byte[] crlBytes;

public CRLEntry(String crlURL, String certCN, byte[] crlBytes) {
this.crlURL = crlURL;
this.certCN = certCN;
this.crlBytes = crlBytes;
}

public String getCrlURL() {
return crlURL;
}

public String getCertCN() {
return certCN;
}

public byte[] getCrlBytes() {
return crlBytes;
}
}

public static final String SIGNATURE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";

@@ -116,6 +152,9 @@ public class SignatureConfig {
* the time-stamp service used for XAdES-T and XAdES-X.
*/
private TimeStampService tspService = new TSPTimeStampService();
private TimeStampHttpClient tspHttpClient = new TimeStampSimpleHttpClient();


/**
* timestamp service provider URL
*/
@@ -137,7 +176,7 @@ public class SignatureConfig {

/**
* the optional revocation data service used for XAdES-C and XAdES-X-L.
* When <code>null</code> the signature will be limited to XAdES-T only.
* When {@code null} the signature will be limited to XAdES-T only.
*/
private RevocationDataService revocationDataService;
/**
@@ -156,7 +195,7 @@ public class SignatureConfig {

/**
* The signature Id attribute value used to create the XML signature. A
* <code>null</code> value will trigger an automatically generated signature Id.
* {@code null} value will trigger an automatically generated signature Id.
*/
private String packageSignatureId = "idPackageSignature";

@@ -221,6 +260,23 @@ public class SignatureConfig {

private String commitmentType = "Created and approved this document";

/**
* Swtich to enable/disable automatic CRL download - by default the download is with all https hostname
* and certificate verifications disabled.
*
* @since POI 5.3.0
*/
private boolean allowCRLDownload = false;

/**
* List of cached / saved CRL entries
*/
private final List<CRLEntry> crlEntries = new ArrayList<>();

/**
* Keystore used for cached certificates
*/
private final KeyStore keyStore = emptyKeyStore();

public SignatureConfig() {
// OOo doesn't like ds namespaces so per default prefixing is off.
@@ -490,7 +546,7 @@ public class SignatureConfig {

/**
* @param packageSignatureId The signature Id attribute value used to create the XML signature.
* A <code>null</code> value will trigger an automatically generated signature Id.
* A {@code null} value will trigger an automatically generated signature Id.
*/
public void setPackageSignatureId(String packageSignatureId) {
this.packageSignatureId = nvl(packageSignatureId,"xmldsig-"+UUID.randomUUID());
@@ -536,7 +592,7 @@ public class SignatureConfig {

/**
* @param tspDigestAlgo the algorithm to be used for the timestamp entry.
* if <code>null</code>, the hash algorithm of the main entry
* if {@code null}, the hash algorithm of the main entry
*/
public void setTspDigestAlgo(HashAlgorithm tspDigestAlgo) {
this.tspDigestAlgo = tspDigestAlgo;
@@ -572,6 +628,24 @@ public class SignatureConfig {
this.tspService = tspService;
}

/**
* @return the http client used for timestamp server connections
*
* @since POI 5.3.0
*/
public TimeStampHttpClient getTspHttpClient() {
return tspHttpClient;
}

/**
* @param tspHttpClient the http client used for timestamp server connections
*
* @since POI 5.3.0
*/
public void setTspHttpClient(TimeStampHttpClient tspHttpClient) {
this.tspHttpClient = tspHttpClient;
}

/**
* @return the user id for the timestamp service - currently only basic authorization is supported
*/
@@ -616,7 +690,7 @@ public class SignatureConfig {

/**
* @return the optional revocation data service used for XAdES-C and XAdES-X-L.
* When <code>null</code> the signature will be limited to XAdES-T only.
* When {@code null} the signature will be limited to XAdES-T only.
*/
public RevocationDataService getRevocationDataService() {
return revocationDataService;
@@ -624,7 +698,7 @@ public class SignatureConfig {

/**
* @param revocationDataService the optional revocation data service used for XAdES-C and XAdES-X-L.
* When <code>null</code> the signature will be limited to XAdES-T only.
* When {@code null} the signature will be limited to XAdES-T only.
*/
public void setRevocationDataService(RevocationDataService revocationDataService) {
this.revocationDataService = revocationDataService;
@@ -639,7 +713,7 @@ public class SignatureConfig {

/**
* @param xadesDigestAlgo hash algorithm used for XAdES.
* When <code>null</code>, defaults to {@link #getDigestAlgo()}
* When {@code null}, defaults to {@link #getDigestAlgo()}
*/
public void setXadesDigestAlgo(HashAlgorithm xadesDigestAlgo) {
this.xadesDigestAlgo = xadesDigestAlgo;
@@ -647,7 +721,7 @@ public class SignatureConfig {

/**
* @param xadesDigestAlgo hash algorithm used for XAdES.
* When <code>null</code>, defaults to {@link #getDigestAlgo()}
* When {@code null}, defaults to {@link #getDigestAlgo()}
*
* @since POI 4.0.0
*/
@@ -671,7 +745,7 @@ public class SignatureConfig {

/**
* @return the asn.1 object id for the tsp request policy.
* Defaults to <code>1.3.6.1.4.1.13762.3</code>
* Defaults to {@code 1.3.6.1.4.1.13762.3}
*/
public String getTspRequestPolicy() {
return tspRequestPolicy;
@@ -729,15 +803,15 @@ public class SignatureConfig {
}

/**
* @return the xades role element. If <code>null</code> the claimed role element is omitted.
* Defaults to <code>null</code>
* @return the xades role element. If {@code null} the claimed role element is omitted.
* Defaults to {@code null}
*/
public String getXadesRole() {
return xadesRole;
}

/**
* @param xadesRole the xades role element. If <code>null</code> the claimed role element is omitted.
* @param xadesRole the xades role element. If {@code null} the claimed role element is omitted.
*/
public void setXadesRole(String xadesRole) {
this.xadesRole = xadesRole;
@@ -745,7 +819,7 @@ public class SignatureConfig {

/**
* @return the Id for the XAdES SignedProperties element.
* Defaults to <code>idSignedProperties</code>
* Defaults to {@code idSignedProperties}
*/
public String getXadesSignatureId() {
return nvl(xadesSignatureId, "idSignedProperties");
@@ -753,7 +827,7 @@ public class SignatureConfig {

/**
* @param xadesSignatureId the Id for the XAdES SignedProperties element.
* When <code>null</code> defaults to <code>idSignedProperties</code>
* When {@code null} defaults to {@code idSignedProperties}
*/
public void setXadesSignatureId(String xadesSignatureId) {
this.xadesSignatureId = xadesSignatureId;
@@ -761,7 +835,7 @@ public class SignatureConfig {

/**
* @return when true, include the policy-implied block.
* Defaults to <code>true</code>
* Defaults to {@code true}
*/
public boolean isXadesSignaturePolicyImplied() {
return xadesSignaturePolicyImplied;
@@ -1027,7 +1101,7 @@ public class SignatureConfig {

/**
* @return the cannonicalization method for XAdES-XL signing.
* Defaults to <code>EXCLUSIVE</code>
* Defaults to {@code EXCLUSIVE}
* @see <a href="http://docs.oracle.com/javase/7/docs/api/javax/xml/crypto/dsig/CanonicalizationMethod.html">javax.xml.crypto.dsig.CanonicalizationMethod</a>
*/
public String getXadesCanonicalizationMethod() {
@@ -1136,4 +1210,96 @@ public class SignatureConfig {
public void setCommitmentType(String commitmentType) {
this.commitmentType = commitmentType;
}


public CRLEntry addCRL(String crlURL, String certCN, byte[] crlBytes) {
CRLEntry ce = new CRLEntry(crlURL, certCN, crlBytes);
crlEntries.add(ce);
return ce;
}

public List<CRLEntry> getCrlEntries() {
return crlEntries;
}

public boolean isAllowCRLDownload() {
return allowCRLDownload;
}

public void setAllowCRLDownload(boolean allowCRLDownload) {
this.allowCRLDownload = allowCRLDownload;
}

/**
* @return keystore with cached certificates
*/
public KeyStore getKeyStore() {
return keyStore;
}

/**
* Add certificate into keystore (cache) for further certificate chain lookups
* @param alias the alias, or null if alias is taken from common name attribute of certificate
* @param x509 the x509 certificate
*/
public void addCachedCertificate(String alias, X509Certificate x509) throws KeyStoreException {
String lAlias = alias;
if (lAlias == null) {
lAlias = x509.getSubjectX500Principal().getName();
}
if (keyStore != null) {
synchronized (keyStore) {
keyStore.setCertificateEntry(lAlias, x509);
}
}
}

public void addCachedCertificate(String alias, byte[] x509Bytes) throws KeyStoreException, CertificateException {
CertificateFactory certFact = CertificateFactory.getInstance("X.509");
X509Certificate x509 = (X509Certificate)certFact.generateCertificate(new ByteArrayInputStream(x509Bytes));
addCachedCertificate(null, x509);
}

public X509Certificate getCachedCertificateByPrinicipal(String principalName) {
if (keyStore == null) {
return null;
}
// TODO: add synchronized
try {
for (String a : Collections.list(keyStore.aliases())) {
Certificate[] chain = keyStore.getCertificateChain(a);
if (chain == null) {
Certificate cert = keyStore.getCertificate(a);
if (cert == null) {
continue;
}
chain = new Certificate[]{cert};
}
Optional<X509Certificate> found = Stream.of(chain)
.map(X509Certificate.class::cast)
.filter(c -> principalName.equalsIgnoreCase(c.getSubjectX500Principal().getName()))
.findFirst();
if (found.isPresent()) {
return found.get();
}
}
return null;
} catch (KeyStoreException e) {
return null;
}
}


private static KeyStore emptyKeyStore() {
try {
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, null);
return ks;
} catch (IOException | GeneralSecurityException e) {
LOG.atError().withThrowable(e).log("unable to create PKCS #12 keystore - XAdES certificate chain lookups disabled");
}
return null;
}


}

+ 22
- 2
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java View File

@@ -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);
}
}
}

+ 1
- 2
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java View File

@@ -39,7 +39,6 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.XMLConstants;
import javax.xml.crypto.URIReference;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
@@ -261,7 +260,7 @@ public class OOXMLSignatureFacet implements SignatureFacet {

SignatureInfoV1Document sigV1 = createSignatureInfoV1(signatureInfo);
Element n = (Element)document.importNode(sigV1.getSignatureInfoV1().getDomNode(), true);
n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS);
// n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS);

List<XMLStructure> signatureInfoContent = new ArrayList<>();
signatureInfoContent.add(new DOMStructure(n));

+ 81
- 83
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java View File

@@ -36,13 +36,11 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.UUID;

import javax.xml.crypto.MarshalException;

@@ -67,9 +65,11 @@ import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.RespID;
import org.etsi.uri.x01903.v13.*;
import org.etsi.uri.x01903.v14.TimeStampValidationDataDocument;
import org.etsi.uri.x01903.v14.ValidationDataType;
import org.w3.x2000.x09.xmldsig.CanonicalizationMethodType;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

@@ -114,12 +114,38 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
UnsignedSignaturePropertiesType unsignedSigProps =
ofNullable(unsignedProps.getUnsignedSignatureProperties()).orElseGet(unsignedProps::addNewUnsignedSignatureProperties);

// create the XAdES-T time-stamp
NodeList nlSigVal = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue");
XAdESTimeStampType signatureTimeStamp = addTimestamp(nlSigVal, signatureInfo, unsignedSigProps);
final NodeList nlSigVal = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue");
if (nlSigVal.getLength() != 1) {
throw new IllegalArgumentException("SignatureValue is not set.");
}
final Element sigVal = (Element)nlSigVal.item(0);


// Without revocation data service we cannot construct the XAdES-C extension.
RevocationDataService revDataSvc = signatureConfig.getRevocationDataService();
if (revDataSvc != null) {
// XAdES-X-L
addCertificateValues(unsignedSigProps, signatureConfig);
}

LOG.atDebug().log("creating XAdES-T time-stamp");

// xadesv141::TimeStampValidationData
XAdESTimeStampType signatureTimeStamp;
try {
final RevocationData tsaRevocationDataXadesT = new RevocationData();
signatureTimeStamp = createXAdESTimeStamp(signatureInfo, tsaRevocationDataXadesT, sigVal);
unsignedSigProps.addNewSignatureTimeStamp().set(signatureTimeStamp);

if (tsaRevocationDataXadesT.hasRevocationDataEntries()) {
TimeStampValidationDataDocument validationData = createValidationData(tsaRevocationDataXadesT);
insertXChild(unsignedSigProps, validationData);
}
} catch (CertificateEncodingException e) {
throw new MarshalException("unable to create XAdES signatrue", e);
}


if (revDataSvc != null) {
// XAdES-C: complete certificate refs
CompleteCertificateRefsType completeCertificateRefs = completeCertificateRefs(unsignedSigProps, signatureConfig);
@@ -130,19 +156,30 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
addRevocationCRL(completeRevocationRefs, signatureConfig, revocationData);
addRevocationOCSP(completeRevocationRefs, signatureConfig, revocationData);

// XAdES-X Type 1 timestamp
addTimestampX(unsignedSigProps, signatureInfo, nlSigVal, signatureTimeStamp, completeCertificateRefs, completeRevocationRefs);

// XAdES-X-L
addCertificateValues(unsignedSigProps, signatureConfig);

RevocationValuesType revocationValues = unsignedSigProps.addNewRevocationValues();
createRevocationValues(revocationValues, revocationData);

// XAdES-X Type 1 timestamp
LOG.atDebug().log("creating XAdES-X time-stamp");
revocationData = new RevocationData();
XAdESTimeStampType timeStampXadesX1 = createXAdESTimeStamp(signatureInfo, revocationData,
sigVal, signatureTimeStamp.getDomNode(), completeCertificateRefs.getDomNode(), completeRevocationRefs.getDomNode());

// marshal XAdES-X
unsignedSigProps.addNewSigAndRefsTimeStamp().set(timeStampXadesX1);
}




// marshal XAdES-X-L
Node n = document.importNode(qualProps.getDomNode(), true);
qualNl.item(0).getParentNode().replaceChild(n, qualNl.item(0));
Element n = (Element)document.importNode(qualProps.getDomNode(), true);
NodeList nl = n.getElementsByTagName("TimeStampValidationData");
for (int i=0; i<nl.getLength(); i++) {
((Element)nl.item(i)).setAttributeNS(XML_NS, "xmlns", "http://uri.etsi.org/01903/v1.4.1#");
}
Node qualNL0 = qualNl.item(0);
qualNL0.getParentNode().replaceChild(n, qualNL0);
}

private QualifyingPropertiesType getQualProps(NodeList qualNl) throws MarshalException {
@@ -160,28 +197,6 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
}
}

private XAdESTimeStampType addTimestamp(NodeList nlSigVal, SignatureInfo signatureInfo, UnsignedSignaturePropertiesType unsignedSigProps) {
if (nlSigVal.getLength() != 1) {
throw new IllegalArgumentException("SignatureValue is not set.");
}

RevocationData tsaRevocationDataXadesT = new RevocationData();
LOG.atDebug().log("creating XAdES-T time-stamp");
XAdESTimeStampType signatureTimeStamp = createXAdESTimeStamp
(signatureInfo, Collections.singletonList(nlSigVal.item(0)), tsaRevocationDataXadesT);

// marshal the XAdES-T extension
unsignedSigProps.addNewSignatureTimeStamp().set(signatureTimeStamp);

// xadesv141::TimeStampValidationData
if (tsaRevocationDataXadesT.hasRevocationDataEntries()) {
ValidationDataType validationData = createValidationData(tsaRevocationDataXadesT);
insertXChild(unsignedSigProps, validationData);
}

return signatureTimeStamp;
}

private CompleteCertificateRefsType completeCertificateRefs(UnsignedSignaturePropertiesType unsignedSigProps, SignatureConfig signatureConfig) {
CompleteCertificateRefsType completeCertificateRefs = unsignedSigProps.addNewCompleteCertificateRefs();

@@ -253,11 +268,11 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
ResponderID ocspResponderId = respId.toASN1Primitive();
DERTaggedObject derTaggedObject = (DERTaggedObject)ocspResponderId.toASN1Primitive();
if (2 == derTaggedObject.getTagNo()) {
ASN1OctetString keyHashOctetString = (ASN1OctetString)derTaggedObject.getObject();
ASN1OctetString keyHashOctetString = (ASN1OctetString)derTaggedObject.getBaseObject();
byte[] key = keyHashOctetString.getOctets();
responderId.setByKey(key);
} else {
X500Name name = X500Name.getInstance(derTaggedObject.getObject());
X500Name name = X500Name.getInstance(derTaggedObject.getBaseObject());
String nameStr = name.toString();
responderId.setByName(nameStr);
}
@@ -268,38 +283,18 @@ public class XAdESXLSignatureFacet implements SignatureFacet {
}
}

private void addTimestampX(UnsignedSignaturePropertiesType unsignedSigProps, SignatureInfo signatureInfo, NodeList nlSigVal, XAdESTimeStampType signatureTimeStamp,
CompleteCertificateRefsType completeCertificateRefs, CompleteRevocationRefsType completeRevocationRefs) {

List<Node> timeStampNodesXadesX1 = new ArrayList<>();
timeStampNodesXadesX1.add(nlSigVal.item(0));
timeStampNodesXadesX1.add(signatureTimeStamp.getDomNode());
timeStampNodesXadesX1.add(completeCertificateRefs.getDomNode());
timeStampNodesXadesX1.add(completeRevocationRefs.getDomNode());

RevocationData tsaRevocationDataXadesX1 = new RevocationData();
LOG.atDebug().log("creating XAdES-X time-stamp");
XAdESTimeStampType timeStampXadesX1 = createXAdESTimeStamp
(signatureInfo, timeStampNodesXadesX1, tsaRevocationDataXadesX1);
if (tsaRevocationDataXadesX1.hasRevocationDataEntries()) {
ValidationDataType timeStampXadesX1ValidationData = createValidationData(tsaRevocationDataXadesX1);
insertXChild(unsignedSigProps, timeStampXadesX1ValidationData);
}

// marshal XAdES-X
unsignedSigProps.addNewSigAndRefsTimeStamp().set(timeStampXadesX1);

}

private void addCertificateValues(UnsignedSignaturePropertiesType unsignedSigProps, SignatureConfig signatureConfig) {
List<X509Certificate> chain = signatureConfig.getSigningCertificateChain();
if (chain.size() < 2) {
return;
}
CertificateValuesType certificateValues = unsignedSigProps.addNewCertificateValues();
for (X509Certificate certificate : signatureConfig.getSigningCertificateChain()) {
EncapsulatedPKIDataType encapsulatedPKIDataType = certificateValues.addNewEncapsulatedX509Certificate();
try {
encapsulatedPKIDataType.setByteArrayValue(certificate.getEncoded());
} catch (CertificateEncodingException e) {
throw new RuntimeException("certificate encoding error: " + e.getMessage(), e);
try {
for (X509Certificate certificate : chain.subList(1, chain.size())) {
certificateValues.addNewEncapsulatedX509Certificate().setByteArrayValue(certificate.getEncoded());
}
} catch (CertificateEncodingException e) {
throw new RuntimeException("certificate encoding error: " + e.getMessage(), e);
}
}

@@ -341,45 +336,48 @@ public class XAdESXLSignatureFacet implements SignatureFacet {

private XAdESTimeStampType createXAdESTimeStamp(
SignatureInfo signatureInfo,
List<Node> nodeList,
RevocationData revocationData) {
RevocationData revocationData,
Node... nodes) {
SignatureConfig signatureConfig = signatureInfo.getSignatureConfig();
byte[] c14nSignatureValueElement = getC14nValue(nodeList, signatureConfig.getXadesCanonicalizationMethod());

return createXAdESTimeStamp(signatureInfo, c14nSignatureValueElement, revocationData);
}
byte[] c14nSignatureValueElement = getC14nValue(Arrays.asList(nodes), signatureConfig.getXadesCanonicalizationMethod());

private XAdESTimeStampType createXAdESTimeStamp(SignatureInfo signatureInfo, byte[] data, RevocationData revocationData) {
SignatureConfig signatureConfig = signatureInfo.getSignatureConfig();
// create the time-stamp
byte[] timeStampToken;
try {
timeStampToken = signatureConfig.getTspService().timeStamp(signatureInfo, data, revocationData);
timeStampToken = signatureConfig.getTspService().timeStamp(signatureInfo, c14nSignatureValueElement, revocationData);
} catch (Exception e) {
throw new RuntimeException("error while creating a time-stamp: "
+ e.getMessage(), e);
+ e.getMessage(), e);
}

// create a XAdES time-stamp container
XAdESTimeStampType xadesTimeStamp = XAdESTimeStampType.Factory.newInstance();
xadesTimeStamp.setId("time-stamp-" + UUID.randomUUID());
CanonicalizationMethodType c14nMethod = xadesTimeStamp.addNewCanonicalizationMethod();
c14nMethod.setAlgorithm(signatureConfig.getXadesCanonicalizationMethod());

// embed the time-stamp
EncapsulatedPKIDataType encapsulatedTimeStamp = xadesTimeStamp.addNewEncapsulatedTimeStamp();
encapsulatedTimeStamp.setByteArrayValue(timeStampToken);
encapsulatedTimeStamp.setId("time-stamp-token-" + UUID.randomUUID());

return xadesTimeStamp;
}

private ValidationDataType createValidationData(
RevocationData revocationData) {
ValidationDataType validationData = ValidationDataType.Factory.newInstance();
private TimeStampValidationDataDocument createValidationData(RevocationData revocationData)
throws CertificateEncodingException {
TimeStampValidationDataDocument doc = TimeStampValidationDataDocument.Factory.newInstance();
ValidationDataType validationData = doc.addNewTimeStampValidationData();
List<X509Certificate> tspChain = revocationData.getX509chain();

if (tspChain.size() > 1) {
CertificateValuesType cvals = validationData.addNewCertificateValues();
for (X509Certificate x509 : tspChain.subList(1, tspChain.size())) {
byte[] encoded = x509.getEncoded();
cvals.addNewEncapsulatedX509Certificate().setByteArrayValue(encoded);
}
}
RevocationValuesType revocationValues = validationData.addNewRevocationValues();
createRevocationValues(revocationValues, revocationData);
return validationData;
return doc;
}

private void createRevocationValues(

+ 18
- 17
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java View File

@@ -26,7 +26,9 @@ package org.apache.poi.poifs.crypt.dsig.services;

import java.security.cert.CRLException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
@@ -34,37 +36,28 @@ import java.util.List;
*/
public class RevocationData {

private final List<byte[]> crls;

private final List<byte[]> ocsps;

/**
* Default constructor.
*/
public RevocationData() {
this.crls = new ArrayList<>();
this.ocsps = new ArrayList<>();
}
private final List<byte[]> crls = new ArrayList<>();
private final List<byte[]> ocsps = new ArrayList<>();
private final List<X509Certificate> x509chain = new ArrayList<>();

/**
* Adds a CRL to this revocation data set.
*/
public void addCRL(byte[] encodedCrl) {
this.crls.add(encodedCrl);
if (this.crls.stream().noneMatch(by -> Arrays.equals(by, encodedCrl))) {
this.crls.add(encodedCrl);
}
}

/**
* Adds a CRL to this revocation data set.
*/
public void addCRL(X509CRL crl) {
byte[] encodedCrl;
try {
encodedCrl = crl.getEncoded();
addCRL(crl.getEncoded());
} catch (CRLException e) {
throw new IllegalArgumentException("CRL coding error: "
+ e.getMessage(), e);
throw new IllegalArgumentException("CRL coding error: " + e.getMessage(), e);
}
addCRL(encodedCrl);
}

/**
@@ -74,6 +67,10 @@ public class RevocationData {
this.ocsps.add(encodedOcsp);
}

public void addCertificate(X509Certificate x509) {
x509chain.add(x509);
}

/**
* Gives back a list of all CRLs.
*
@@ -120,4 +117,8 @@ public class RevocationData {
public boolean hasRevocationDataEntries() {
return hasOCSPs() || hasCRLs();
}

public List<X509Certificate> getX509chain() {
return x509chain;
}
}

+ 143
- 126
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java View File

@@ -26,42 +26,49 @@ package org.apache.poi.poifs.crypt.dsig.services;

import static org.apache.logging.log4j.util.Unbox.box;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.security.auth.x500.X500Principal;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.crypt.dsig.SignatureConfig;
import org.apache.poi.poifs.crypt.dsig.SignatureConfig.CRLEntry;
import org.apache.poi.poifs.crypt.dsig.SignatureInfo;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.IOUtils;
import org.apache.poi.poifs.crypt.dsig.services.TimeStampHttpClient.TimeStampHttpClientResponse;
import org.bouncycastle.asn1.ASN1IA5String;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.cmp.PKIFailureInfo;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator;
import org.bouncycastle.cms.SignerId;
import org.bouncycastle.cms.SignerInformationVerifier;
@@ -81,25 +88,6 @@ public class TSPTimeStampService implements TimeStampService {

private static final Logger LOG = LogManager.getLogger(TSPTimeStampService.class);

// how large a timestamp response is expected to be
// can be overwritten via IOUtils.setByteArrayMaxOverride()
private static final int DEFAULT_TIMESTAMP_RESPONSE_SIZE = 10_000_000;
private static int MAX_TIMESTAMP_RESPONSE_SIZE = DEFAULT_TIMESTAMP_RESPONSE_SIZE;

/**
* @param maxTimestampResponseSize the max timestamp response size allowed
*/
public static void setMaxTimestampResponseSize(int maxTimestampResponseSize) {
MAX_TIMESTAMP_RESPONSE_SIZE = maxTimestampResponseSize;
}

/**
* @return the max timestamp response size allowed
*/
public static int getMaxTimestampResponseSize() {
return MAX_TIMESTAMP_RESPONSE_SIZE;
}

/**
* Maps the digest algorithm to corresponding OID value.
*/
@@ -133,72 +121,16 @@ public class TSPTimeStampService implements TimeStampService {
}
ASN1ObjectIdentifier digestAlgoOid = mapDigestAlgoToOID(signatureConfig.getTspDigestAlgo());
TimeStampRequest request = requestGenerator.generate(digestAlgoOid, digest, nonce);
byte[] encodedRequest = request.getEncoded();

// create the HTTP POST request
Proxy proxy = Proxy.NO_PROXY;
if (signatureConfig.getProxyUrl() != null) {
URL proxyUrl = new URL(signatureConfig.getProxyUrl());
String host = proxyUrl.getHost();
int port = proxyUrl.getPort();
proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByName(host), (port == -1 ? 80 : port)));
}

String contentType;
HttpURLConnection huc = (HttpURLConnection)new URL(signatureConfig.getTspUrl()).openConnection(proxy);
byte[] responseBytes;
try {
if (signatureConfig.getTspUser() != null) {
String userPassword = signatureConfig.getTspUser() + ":" + signatureConfig.getTspPass();
String encoding = Base64.getEncoder().encodeToString(userPassword.getBytes(StandardCharsets.ISO_8859_1));
huc.setRequestProperty("Authorization", "Basic " + encoding);
}

huc.setRequestMethod("POST");
huc.setConnectTimeout(20000);
huc.setReadTimeout(20000);
huc.setDoOutput(true); // also sets method to POST.
huc.setRequestProperty("User-Agent", signatureConfig.getUserAgent());
huc.setRequestProperty("Content-Type", signatureConfig.isTspOldProtocol()
? "application/timestamp-request"
: "application/timestamp-query"); // "; charset=ISO-8859-1");

OutputStream hucOut = huc.getOutputStream();
hucOut.write(encodedRequest);

// invoke TSP service
huc.connect();

int statusCode = huc.getResponseCode();
if (statusCode != 200) {
final String message = "Error contacting TSP server " + signatureConfig.getTspUrl() +
", had status code " + statusCode + "/" + huc.getResponseMessage();
LOG.atError().log(message);
throw new IOException(message);
}

// HTTP input validation
contentType = huc.getHeaderField("Content-Type");
if (null == contentType) {
throw new RuntimeException("missing Content-Type header");
}

try (InputStream stream = huc.getInputStream()) {
responseBytes = IOUtils.toByteArrayWithMaxLength(stream, getMaxTimestampResponseSize());
}
LOG.atDebug().log(() -> new SimpleMessage("response content: " + HexDump.dump(responseBytes, 0, 0)));
} finally {
huc.disconnect();
TimeStampHttpClient httpClient = signatureConfig.getTspHttpClient();
httpClient.init(signatureConfig);
httpClient.setContentTypeIn(signatureConfig.isTspOldProtocol() ? "application/timestamp-request" : "application/timestamp-query");
TimeStampHttpClientResponse response = httpClient.post(signatureConfig.getTspUrl(), request.getEncoded());
if (!response.isOK()) {
throw new IOException("Requesting timestamp data failed");
}

if (!contentType.startsWith(signatureConfig.isTspOldProtocol()
? "application/timestamp-response"
: "application/timestamp-reply"
)) {
throw new RuntimeException("invalid Content-Type: " + contentType +
// dump the first few bytes
": " + HexDump.dump(responseBytes, 0, 0, 200));
}
byte[] responseBytes = response.getResponseBytes();

if (responseBytes.length == 0) {
throw new RuntimeException("Content-Length is zero");
@@ -229,53 +161,138 @@ public class TSPTimeStampService implements TimeStampService {
LOG.atDebug().log("signer cert issuer: {}", signerCertIssuer);

// TSP signer certificates retrieval
Collection<X509CertificateHolder> certificates = timeStampToken.getCertificates().getMatches(null);

X509CertificateHolder signerCert = null;
Map<X500Name, X509CertificateHolder> certificateMap = new HashMap<>();
for (X509CertificateHolder certificate : certificates) {
if (signerCertIssuer.equals(certificate.getIssuer())
&& signerCertSerialNumber.equals(certificate.getSerialNumber())) {
signerCert = certificate;
}
certificateMap.put(certificate.getSubject(), certificate);
}
Map<String, X509CertificateHolder> certificateMap =
timeStampToken.getCertificates().getMatches(null).stream()
.collect(Collectors.toMap(h -> h.getSubject().toString(), Function.identity()));


// TSP signer cert path building
if (signerCert == null) {
throw new RuntimeException("TSP response token has no signer certificate");
}
List<X509Certificate> tspCertificateChain = new ArrayList<>();
X509CertificateHolder signerCert = certificateMap.values().stream()
.filter(h -> signerCertIssuer.equals(h.getIssuer())
&& signerCertSerialNumber.equals(h.getSerialNumber()))
.findFirst()
.orElseThrow(() -> new RuntimeException("TSP response token has no signer certificate"));

JcaX509CertificateConverter x509converter = new JcaX509CertificateConverter();
x509converter.setProvider("BC");
X509CertificateHolder certificate = signerCert;

// complete certificate chain
X509Certificate child = x509converter.getCertificate(signerCert);
do {
LOG.atDebug().log("adding to certificate chain: {}", certificate.getSubject());
tspCertificateChain.add(x509converter.getCertificate(certificate));
if (certificate.getSubject().equals(certificate.getIssuer())) {
revocationData.addCertificate(child);
X500Principal issuer = child.getIssuerX500Principal();
if (child.getSubjectX500Principal().equals(issuer)) {
break;
}
certificate = certificateMap.get(certificate.getIssuer());
} while (null != certificate);
X509CertificateHolder parentHolder = certificateMap.get(issuer.getName());
child = (parentHolder != null)
? x509converter.getCertificate(parentHolder)
: signatureConfig.getCachedCertificateByPrinicipal(issuer.getName());
if (child != null) {
retrieveCRL(signatureConfig, child).forEach(revocationData::addCRL);
}
} while (child != null);

// verify TSP signer signature
X509CertificateHolder holder = new X509CertificateHolder(tspCertificateChain.get(0).getEncoded());
DefaultCMSSignatureAlgorithmNameGenerator nameGen = new DefaultCMSSignatureAlgorithmNameGenerator();
DefaultSignatureAlgorithmIdentifierFinder sigAlgoFinder = new DefaultSignatureAlgorithmIdentifierFinder();
DefaultDigestAlgorithmIdentifierFinder hashAlgoFinder = new DefaultDigestAlgorithmIdentifierFinder();
BcDigestCalculatorProvider calculator = new BcDigestCalculatorProvider();
BcRSASignerInfoVerifierBuilder verifierBuilder = new BcRSASignerInfoVerifierBuilder(nameGen, sigAlgoFinder, hashAlgoFinder, calculator);
SignerInformationVerifier verifier = verifierBuilder.build(holder);
BcRSASignerInfoVerifierBuilder verifierBuilder = new BcRSASignerInfoVerifierBuilder(
new DefaultCMSSignatureAlgorithmNameGenerator(),
new DefaultSignatureAlgorithmIdentifierFinder(),
new DefaultDigestAlgorithmIdentifierFinder(),
new BcDigestCalculatorProvider());
SignerInformationVerifier verifier = verifierBuilder.build(signerCert);

timeStampToken.validate(verifier);

// verify TSP signer certificate
if (signatureConfig.getTspValidator() != null) {
signatureConfig.getTspValidator().validate(tspCertificateChain, revocationData);
signatureConfig.getTspValidator().validate(revocationData.getX509chain(), revocationData);
}

LOG.atDebug().log("time-stamp token time: {}", timeStampToken.getTimeStampInfo().getGenTime());

return timeStampToken.getEncoded();
}

/**
* Check if CRL is to be added, check cached CRLs in config and download if necessary.
* Can be overriden to suppress the logic
* @return empty list, if not found or suppressed, otherwise the list of CRLs as encoded bytes
*/
protected List<byte[]> retrieveCRL(SignatureConfig signatureConfig, X509Certificate holder) throws IOException {
// TODO: add config, if crls should be added
final List<CRLEntry> crlEntries = signatureConfig.getCrlEntries();
byte[] crlPoints = holder.getExtensionValue(Extension.cRLDistributionPoints.getId());
if (crlPoints == null) {
return Collections.emptyList();
}

// TODO: check if parse is necessary, or if crlExt.getExtnValue() can be use directly
ASN1Primitive extVal = JcaX509ExtensionUtils.parseExtensionValue(crlPoints);
return Stream.of(CRLDistPoint.getInstance(extVal).getDistributionPoints())
.map(DistributionPoint::getDistributionPoint)
.filter(Objects::nonNull)
.filter(dpn -> dpn.getType() == DistributionPointName.FULL_NAME)
.flatMap(dpn -> Stream.of(GeneralNames.getInstance(dpn.getName()).getNames()))
.filter(genName -> genName.getTagNo() == GeneralName.uniformResourceIdentifier)
.map(genName -> ASN1IA5String.getInstance(genName.getName()).getString())
.flatMap(url -> {
List<CRLEntry> ul = crlEntries.stream().filter(ce -> matchCRLbyUrl(ce, holder, url)).collect(Collectors.toList());
Stream<CRLEntry> cl = crlEntries.stream().filter(ce -> matchCRLbyCN(ce, holder, url));
if (ul.isEmpty()) {
CRLEntry ce = downloadCRL(signatureConfig, url);
if (ce != null) {
ul.add(ce);
}
}
return Stream.concat(ul.stream(), cl).map(CRLEntry::getCrlBytes);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}

protected boolean matchCRLbyUrl(CRLEntry other, X509Certificate holder, String url) {
return url.equals(other.getCrlURL());
}

protected boolean matchCRLbyCN(CRLEntry other, X509Certificate holder, String url) {
return holder.getSubjectX500Principal().getName().equals(other.getCertCN());
}

/**
* Convenience method to download a crl in an unsafe way, i.e. without verifying the
* https certificates.
* Please provide your own method, if you have imported the TSP server CA certificates
* in your local keystore
*
* @return the bytes of the CRL or null if unsuccessful / download is suppressed
*/
protected CRLEntry downloadCRL(SignatureConfig signatureConfig, String url) {
if (!signatureConfig.isAllowCRLDownload()) {
return null;
}

TimeStampHttpClient httpClient = signatureConfig.getTspHttpClient();
httpClient.init(signatureConfig);
httpClient.setBasicAuthentication(null, null);
TimeStampHttpClientResponse response;
try {
response = httpClient.get(url);
if (!response.isOK()) {
return null;
}
} catch (IOException e) {
return null;
}

try {
CertificateFactory certFact = CertificateFactory.getInstance("X.509");
byte[] crlBytes = response.getResponseBytes();
// verify the downloaded bytes, throws Exception if invalid
X509CRL crl = (X509CRL)certFact.generateCRL(new ByteArrayInputStream(crlBytes));
return signatureConfig.addCRL(url, crl.getIssuerX500Principal().getName(), crlBytes);
} catch (GeneralSecurityException e) {
LOG.atWarn().withThrowable(e).log("CRL download failed from {}", url);
return null;
}
}
}

+ 83
- 0
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java View File

@@ -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);
}

+ 285
- 0
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java View File

@@ -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) { }
}
}

+ 83
- 0
poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java View File

@@ -37,21 +37,34 @@ import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAKeyGenParameterSpec;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.storage.RawDataUtil;
@@ -206,14 +219,38 @@ public class DummyKeystore {
return new KeyCertPair(getKey(keyAlias, keyPass), keystore.getCertificateChain(keyAlias));
}

public KeyCertPair getKeyPair(int index, String keyPass) throws GeneralSecurityException {
Map.Entry<String, PrivateKey> me = getKeyByIndex(index, keyPass);
return me != null ? getKeyPair(me.getKey(), keyPass) : null;
}

public PrivateKey getKey(String keyAlias, String keyPass) throws GeneralSecurityException {
return (PrivateKey)keystore.getKey(keyAlias, keyPass.toCharArray());
}

public PrivateKey getKey(int index, String keyPass) throws GeneralSecurityException {
Map.Entry<String, PrivateKey> me = getKeyByIndex(index, keyPass);
return me != null ? me.getValue() : null;
}

public X509Certificate getFirstX509(String alias) throws KeyStoreException {
return (X509Certificate)keystore.getCertificate(alias);
}

private Map.Entry<String,PrivateKey> getKeyByIndex(int index, String keyPass) throws GeneralSecurityException {
for (String a : Collections.list(keystore.aliases())) {
try {
PrivateKey pk = (PrivateKey) keystore.getKey(a, keyPass.toCharArray());
if (pk != null) {
return new AbstractMap.SimpleEntry<>(a, pk);
}
} catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
break;
}
}
return null;
}

public void save(File storeFile, String storePass) throws IOException, GeneralSecurityException {
try (FileOutputStream fos = new FileOutputStream(storeFile)) {
keystore.store(fos, storePass.toCharArray());
@@ -350,4 +387,50 @@ public class DummyKeystore {
return ocspRespBuilder.build(OCSPRespBuilder.SUCCESSFUL, basicOCSPResp);
}

public void importX509(File file) throws CertificateException, KeyStoreException, IOException {
try (InputStream is = new FileInputStream(file)) {
X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
keystore.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert);
}
}

public void importKeystore(File file, String storePass, String keyPass, Function<String,String> otherKeyPass) throws GeneralSecurityException, IOException {
DummyKeystore dk = new DummyKeystore(file, storePass);

Map<String,X509Certificate> myCerts = new HashMap<>();
for (String a : Collections.list(keystore.aliases())) {
Certificate[] chain = keystore.getCertificateChain(a);
if (chain == null) {
Certificate cert = keystore.getCertificate(a);
if (cert == null) {
continue;
}
chain = new Certificate[]{cert};
}
Arrays.stream(chain)
.map(X509Certificate.class::cast)
.filter(c -> !myCerts.containsKey(c.getSubjectX500Principal().getName()))
.forEach(c -> myCerts.put(c.getSubjectX500Principal().getName(), c));
}

for (String a : Collections.list(dk.keystore.aliases())) {
KeyCertPair keyPair = dk.getKeyPair(a, otherKeyPass.apply(a));
ArrayList<X509Certificate> chain = new ArrayList<>(keyPair.getX509Chain());
Set<String> names = chain.stream().map(X509Certificate::getSubjectX500Principal).map(X500Principal::getName).collect(Collectors.toSet());
X509Certificate last = chain.get(chain.size() - 1);
do {
String issuer = last.getIssuerX500Principal().getName();
X509Certificate parent = myCerts.get(issuer);
if (names.contains(issuer) || parent == null) {
break;
} else {
chain.add(parent);
names.add(issuer);
}
last = parent;
} while (true);

keystore.setKeyEntry(a, keyPair.getKey(), keyPass.toCharArray(), chain.toArray(new X509Certificate[0]));
}
}
}

+ 1
- 1
poi/src/main/java/org/apache/poi/util/IOUtils.java View File

@@ -160,7 +160,7 @@ public final class IOUtils {
* Reads up to {@code length} bytes from the input stream, and returns the bytes read.
*
* @param stream The byte stream of data to read.
* @param length The maximum length to read, use {@link Integer#MAX_VALUE} to read the stream
* @param length The maximum length to read, use {@link Integer#MIN_VALUE} to read the stream
* until EOF
* @param maxLength if the input is equal to/longer than {@code maxLength} bytes,
* then throw an {@link IOException} complaining about the length.

Loading…
Cancel
Save