git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1898287 13f79535-47bb-0310-9956-ffa450edef68tags/REL_5_2_1
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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)); |
@@ -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( |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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) { } | |||
} | |||
} |
@@ -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])); | |||
} | |||
} | |||
} |
@@ -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. |