git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1898287 13f79535-47bb-0310-9956-ffa450edef68tags/REL_5_2_1
import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.OO_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.XADES_132_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.PrivateKey; | ||||
import java.security.Provider; | 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.security.cert.X509Certificate; | ||||
import java.text.DateFormat; | import java.text.DateFormat; | ||||
import java.text.ParseException; | import java.text.ParseException; | ||||
import java.text.SimpleDateFormat; | import java.text.SimpleDateFormat; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.Collections; | |||||
import java.util.Date; | import java.util.Date; | ||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Locale; | import java.util.Locale; | ||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Optional; | |||||
import java.util.UUID; | import java.util.UUID; | ||||
import java.util.function.Supplier; | import java.util.function.Supplier; | ||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
import java.util.stream.Stream; | |||||
import javax.xml.crypto.URIDereferencer; | import javax.xml.crypto.URIDereferencer; | ||||
import javax.xml.crypto.dsig.CanonicalizationMethod; | import javax.xml.crypto.dsig.CanonicalizationMethod; | ||||
import org.apache.poi.poifs.crypt.dsig.services.RevocationDataService; | 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.SignaturePolicyService; | ||||
import org.apache.poi.poifs.crypt.dsig.services.TSPTimeStampService; | 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.TimeStampService; | ||||
import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator; | 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.Internal; | ||||
import org.apache.poi.util.LocaleUtil; | import org.apache.poi.util.LocaleUtil; | ||||
import org.apache.poi.util.Removal; | import org.apache.poi.util.Removal; | ||||
*/ | */ | ||||
@SuppressWarnings({"unused","WeakerAccess"}) | @SuppressWarnings({"unused","WeakerAccess"}) | ||||
public class SignatureConfig { | 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'"; | public static final String SIGNATURE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; | ||||
* the time-stamp service used for XAdES-T and XAdES-X. | * the time-stamp service used for XAdES-T and XAdES-X. | ||||
*/ | */ | ||||
private TimeStampService tspService = new TSPTimeStampService(); | private TimeStampService tspService = new TSPTimeStampService(); | ||||
private TimeStampHttpClient tspHttpClient = new TimeStampSimpleHttpClient(); | |||||
/** | /** | ||||
* timestamp service provider URL | * timestamp service provider URL | ||||
*/ | */ | ||||
/** | /** | ||||
* the optional revocation data service used for XAdES-C and XAdES-X-L. | * 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; | private RevocationDataService revocationDataService; | ||||
/** | /** | ||||
/** | /** | ||||
* The signature Id attribute value used to create the XML signature. A | * 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"; | private String packageSignatureId = "idPackageSignature"; | ||||
private String commitmentType = "Created and approved this document"; | 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() { | public SignatureConfig() { | ||||
// OOo doesn't like ds namespaces so per default prefixing is off. | // OOo doesn't like ds namespaces so per default prefixing is off. | ||||
/** | /** | ||||
* @param packageSignatureId The signature Id attribute value used to create the XML signature. | * @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) { | public void setPackageSignatureId(String packageSignatureId) { | ||||
this.packageSignatureId = nvl(packageSignatureId,"xmldsig-"+UUID.randomUUID()); | this.packageSignatureId = nvl(packageSignatureId,"xmldsig-"+UUID.randomUUID()); | ||||
/** | /** | ||||
* @param tspDigestAlgo the algorithm to be used for the timestamp entry. | * @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) { | public void setTspDigestAlgo(HashAlgorithm tspDigestAlgo) { | ||||
this.tspDigestAlgo = tspDigestAlgo; | this.tspDigestAlgo = tspDigestAlgo; | ||||
this.tspService = tspService; | 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 | * @return the user id for the timestamp service - currently only basic authorization is supported | ||||
*/ | */ | ||||
/** | /** | ||||
* @return the optional revocation data service used for XAdES-C and XAdES-X-L. | * @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() { | public RevocationDataService getRevocationDataService() { | ||||
return revocationDataService; | return revocationDataService; | ||||
/** | /** | ||||
* @param revocationDataService the optional revocation data service used for XAdES-C and XAdES-X-L. | * @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) { | public void setRevocationDataService(RevocationDataService revocationDataService) { | ||||
this.revocationDataService = revocationDataService; | this.revocationDataService = revocationDataService; | ||||
/** | /** | ||||
* @param xadesDigestAlgo hash algorithm used for XAdES. | * @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) { | public void setXadesDigestAlgo(HashAlgorithm xadesDigestAlgo) { | ||||
this.xadesDigestAlgo = xadesDigestAlgo; | this.xadesDigestAlgo = xadesDigestAlgo; | ||||
/** | /** | ||||
* @param xadesDigestAlgo hash algorithm used for XAdES. | * @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 | * @since POI 4.0.0 | ||||
*/ | */ | ||||
/** | /** | ||||
* @return the asn.1 object id for the tsp request policy. | * @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() { | public String getTspRequestPolicy() { | ||||
return tspRequestPolicy; | return tspRequestPolicy; | ||||
} | } | ||||
/** | /** | ||||
* @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() { | public String getXadesRole() { | ||||
return xadesRole; | 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) { | public void setXadesRole(String xadesRole) { | ||||
this.xadesRole = xadesRole; | this.xadesRole = xadesRole; | ||||
/** | /** | ||||
* @return the Id for the XAdES SignedProperties element. | * @return the Id for the XAdES SignedProperties element. | ||||
* Defaults to <code>idSignedProperties</code> | |||||
* Defaults to {@code idSignedProperties} | |||||
*/ | */ | ||||
public String getXadesSignatureId() { | public String getXadesSignatureId() { | ||||
return nvl(xadesSignatureId, "idSignedProperties"); | return nvl(xadesSignatureId, "idSignedProperties"); | ||||
/** | /** | ||||
* @param xadesSignatureId the Id for the XAdES SignedProperties element. | * @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) { | public void setXadesSignatureId(String xadesSignatureId) { | ||||
this.xadesSignatureId = xadesSignatureId; | this.xadesSignatureId = xadesSignatureId; | ||||
/** | /** | ||||
* @return when true, include the policy-implied block. | * @return when true, include the policy-implied block. | ||||
* Defaults to <code>true</code> | |||||
* Defaults to {@code true} | |||||
*/ | */ | ||||
public boolean isXadesSignaturePolicyImplied() { | public boolean isXadesSignaturePolicyImplied() { | ||||
return xadesSignaturePolicyImplied; | return xadesSignaturePolicyImplied; | ||||
/** | /** | ||||
* @return the cannonicalization method for XAdES-XL signing. | * @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> | * @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() { | public String getXadesCanonicalizationMethod() { | ||||
public void setCommitmentType(String commitmentType) { | public void setCommitmentType(String commitmentType) { | ||||
this.commitmentType = 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; | |||||
} | |||||
} | } |
package org.apache.poi.poifs.crypt.dsig; | 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_DIGSIG_NS; | ||||
import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_NS; | import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_NS; | ||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.HashSet; | import java.util.HashSet; | ||||
import java.util.List; | |||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.function.Consumer; | import java.util.function.Consumer; | ||||
import javax.xml.XMLConstants; | |||||
import org.w3c.dom.Document; | import org.w3c.dom.Document; | ||||
import org.w3c.dom.Element; | import org.w3c.dom.Element; | ||||
import org.w3c.dom.NamedNodeMap; | import org.w3c.dom.NamedNodeMap; | ||||
private static final String OBJECT_TAG = "Object"; | 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 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 | @Override | ||||
public void handleElement(SignatureInfo signatureInfo, Document doc, EventTarget target, EventListener parentListener) { | public void handleElement(SignatureInfo signatureInfo, Document doc, EventTarget target, EventListener parentListener) { | ||||
// see POI #63712 : because of Santuario change r1853805 in XmlSec 2.1.3, | // see POI #63712 : because of Santuario change r1853805 in XmlSec 2.1.3, | ||||
forEachElement(doc.getElementsByTagName(OBJECT_TAG), (o) -> { | forEachElement(doc.getElementsByTagName(OBJECT_TAG), (o) -> { | ||||
forEachElement(o.getChildNodes(), (c) -> { | forEachElement(o.getChildNodes(), (c) -> { | ||||
getAllNamespaces(traversal, c, prefixCfg, prefixUsed); | getAllNamespaces(traversal, c, prefixCfg, prefixUsed); | ||||
prefixUsed.forEach((ns, prefix) -> c.setAttributeNS(XML_NS, "xmlns:"+prefix, ns)); | |||||
prefixUsed.forEach((ns, prefix) -> setXmlns(c, prefix, ns)); | |||||
}); | }); | ||||
}); | }); | ||||
} | } | ||||
private void setPrefix(Node node, Map<String,String> prefixCfg, Map<String,String> prefixUsed) { | private void setPrefix(Node node, Map<String,String> prefixCfg, Map<String,String> prefixUsed) { | ||||
String ns = node.getNamespaceURI(); | String ns = node.getNamespaceURI(); | ||||
String prefix = prefixCfg.get(ns); | String prefix = prefixCfg.get(ns); | ||||
if (!IGNORE_NS.contains(prefix)) { | |||||
if (IGNORE_NS.contains(ns)) { | |||||
return; | |||||
} | |||||
if (prefix != null) { | |||||
node.setPrefix(prefix); | node.setPrefix(prefix); | ||||
} | |||||
if (DIRECT_NS.contains(ns)) { | |||||
setXmlns(node, prefix, ns); | |||||
} else { | |||||
prefixUsed.put(ns, prefix); | 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); | |||||
} | |||||
} | |||||
} | } |
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
import java.util.stream.Stream; | import java.util.stream.Stream; | ||||
import javax.xml.XMLConstants; | |||||
import javax.xml.crypto.URIReference; | import javax.xml.crypto.URIReference; | ||||
import javax.xml.crypto.XMLStructure; | import javax.xml.crypto.XMLStructure; | ||||
import javax.xml.crypto.dom.DOMStructure; | import javax.xml.crypto.dom.DOMStructure; | ||||
SignatureInfoV1Document sigV1 = createSignatureInfoV1(signatureInfo); | SignatureInfoV1Document sigV1 = createSignatureInfoV1(signatureInfo); | ||||
Element n = (Element)document.importNode(sigV1.getSignatureInfoV1().getDomNode(), true); | 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<>(); | List<XMLStructure> signatureInfoContent = new ArrayList<>(); | ||||
signatureInfoContent.add(new DOMStructure(n)); | signatureInfoContent.add(new DOMStructure(n)); |
import java.security.cert.CertificateFactory; | import java.security.cert.CertificateFactory; | ||||
import java.security.cert.X509CRL; | import java.security.cert.X509CRL; | ||||
import java.security.cert.X509Certificate; | import java.security.cert.X509Certificate; | ||||
import java.util.ArrayList; | |||||
import java.util.Arrays; | |||||
import java.util.Calendar; | import java.util.Calendar; | ||||
import java.util.Collections; | |||||
import java.util.List; | import java.util.List; | ||||
import java.util.Locale; | import java.util.Locale; | ||||
import java.util.TimeZone; | import java.util.TimeZone; | ||||
import java.util.UUID; | |||||
import javax.xml.crypto.MarshalException; | import javax.xml.crypto.MarshalException; | ||||
import org.bouncycastle.cert.ocsp.OCSPResp; | import org.bouncycastle.cert.ocsp.OCSPResp; | ||||
import org.bouncycastle.cert.ocsp.RespID; | import org.bouncycastle.cert.ocsp.RespID; | ||||
import org.etsi.uri.x01903.v13.*; | import org.etsi.uri.x01903.v13.*; | ||||
import org.etsi.uri.x01903.v14.TimeStampValidationDataDocument; | |||||
import org.etsi.uri.x01903.v14.ValidationDataType; | import org.etsi.uri.x01903.v14.ValidationDataType; | ||||
import org.w3.x2000.x09.xmldsig.CanonicalizationMethodType; | import org.w3.x2000.x09.xmldsig.CanonicalizationMethodType; | ||||
import org.w3c.dom.Document; | import org.w3c.dom.Document; | ||||
import org.w3c.dom.Element; | |||||
import org.w3c.dom.Node; | import org.w3c.dom.Node; | ||||
import org.w3c.dom.NodeList; | import org.w3c.dom.NodeList; | ||||
UnsignedSignaturePropertiesType unsignedSigProps = | UnsignedSignaturePropertiesType unsignedSigProps = | ||||
ofNullable(unsignedProps.getUnsignedSignatureProperties()).orElseGet(unsignedProps::addNewUnsignedSignatureProperties); | 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. | // Without revocation data service we cannot construct the XAdES-C extension. | ||||
RevocationDataService revDataSvc = signatureConfig.getRevocationDataService(); | 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) { | if (revDataSvc != null) { | ||||
// XAdES-C: complete certificate refs | // XAdES-C: complete certificate refs | ||||
CompleteCertificateRefsType completeCertificateRefs = completeCertificateRefs(unsignedSigProps, signatureConfig); | CompleteCertificateRefsType completeCertificateRefs = completeCertificateRefs(unsignedSigProps, signatureConfig); | ||||
addRevocationCRL(completeRevocationRefs, signatureConfig, revocationData); | addRevocationCRL(completeRevocationRefs, signatureConfig, revocationData); | ||||
addRevocationOCSP(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(); | RevocationValuesType revocationValues = unsignedSigProps.addNewRevocationValues(); | ||||
createRevocationValues(revocationValues, revocationData); | 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 | // 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 { | private QualifyingPropertiesType getQualProps(NodeList qualNl) throws MarshalException { | ||||
} | } | ||||
} | } | ||||
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) { | private CompleteCertificateRefsType completeCertificateRefs(UnsignedSignaturePropertiesType unsignedSigProps, SignatureConfig signatureConfig) { | ||||
CompleteCertificateRefsType completeCertificateRefs = unsignedSigProps.addNewCompleteCertificateRefs(); | CompleteCertificateRefsType completeCertificateRefs = unsignedSigProps.addNewCompleteCertificateRefs(); | ||||
ResponderID ocspResponderId = respId.toASN1Primitive(); | ResponderID ocspResponderId = respId.toASN1Primitive(); | ||||
DERTaggedObject derTaggedObject = (DERTaggedObject)ocspResponderId.toASN1Primitive(); | DERTaggedObject derTaggedObject = (DERTaggedObject)ocspResponderId.toASN1Primitive(); | ||||
if (2 == derTaggedObject.getTagNo()) { | if (2 == derTaggedObject.getTagNo()) { | ||||
ASN1OctetString keyHashOctetString = (ASN1OctetString)derTaggedObject.getObject(); | |||||
ASN1OctetString keyHashOctetString = (ASN1OctetString)derTaggedObject.getBaseObject(); | |||||
byte[] key = keyHashOctetString.getOctets(); | byte[] key = keyHashOctetString.getOctets(); | ||||
responderId.setByKey(key); | responderId.setByKey(key); | ||||
} else { | } else { | ||||
X500Name name = X500Name.getInstance(derTaggedObject.getObject()); | |||||
X500Name name = X500Name.getInstance(derTaggedObject.getBaseObject()); | |||||
String nameStr = name.toString(); | String nameStr = name.toString(); | ||||
responderId.setByName(nameStr); | responderId.setByName(nameStr); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
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) { | private void addCertificateValues(UnsignedSignaturePropertiesType unsignedSigProps, SignatureConfig signatureConfig) { | ||||
List<X509Certificate> chain = signatureConfig.getSigningCertificateChain(); | |||||
if (chain.size() < 2) { | |||||
return; | |||||
} | |||||
CertificateValuesType certificateValues = unsignedSigProps.addNewCertificateValues(); | 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); | |||||
} | } | ||||
} | } | ||||
private XAdESTimeStampType createXAdESTimeStamp( | private XAdESTimeStampType createXAdESTimeStamp( | ||||
SignatureInfo signatureInfo, | SignatureInfo signatureInfo, | ||||
List<Node> nodeList, | |||||
RevocationData revocationData) { | |||||
RevocationData revocationData, | |||||
Node... nodes) { | |||||
SignatureConfig signatureConfig = signatureInfo.getSignatureConfig(); | 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 | // create the time-stamp | ||||
byte[] timeStampToken; | byte[] timeStampToken; | ||||
try { | try { | ||||
timeStampToken = signatureConfig.getTspService().timeStamp(signatureInfo, data, revocationData); | |||||
timeStampToken = signatureConfig.getTspService().timeStamp(signatureInfo, c14nSignatureValueElement, revocationData); | |||||
} catch (Exception e) { | } catch (Exception e) { | ||||
throw new RuntimeException("error while creating a time-stamp: " | throw new RuntimeException("error while creating a time-stamp: " | ||||
+ e.getMessage(), e); | |||||
+ e.getMessage(), e); | |||||
} | } | ||||
// create a XAdES time-stamp container | // create a XAdES time-stamp container | ||||
XAdESTimeStampType xadesTimeStamp = XAdESTimeStampType.Factory.newInstance(); | XAdESTimeStampType xadesTimeStamp = XAdESTimeStampType.Factory.newInstance(); | ||||
xadesTimeStamp.setId("time-stamp-" + UUID.randomUUID()); | |||||
CanonicalizationMethodType c14nMethod = xadesTimeStamp.addNewCanonicalizationMethod(); | CanonicalizationMethodType c14nMethod = xadesTimeStamp.addNewCanonicalizationMethod(); | ||||
c14nMethod.setAlgorithm(signatureConfig.getXadesCanonicalizationMethod()); | c14nMethod.setAlgorithm(signatureConfig.getXadesCanonicalizationMethod()); | ||||
// embed the time-stamp | // embed the time-stamp | ||||
EncapsulatedPKIDataType encapsulatedTimeStamp = xadesTimeStamp.addNewEncapsulatedTimeStamp(); | EncapsulatedPKIDataType encapsulatedTimeStamp = xadesTimeStamp.addNewEncapsulatedTimeStamp(); | ||||
encapsulatedTimeStamp.setByteArrayValue(timeStampToken); | encapsulatedTimeStamp.setByteArrayValue(timeStampToken); | ||||
encapsulatedTimeStamp.setId("time-stamp-token-" + UUID.randomUUID()); | |||||
return xadesTimeStamp; | 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(); | RevocationValuesType revocationValues = validationData.addNewRevocationValues(); | ||||
createRevocationValues(revocationValues, revocationData); | createRevocationValues(revocationValues, revocationData); | ||||
return validationData; | |||||
return doc; | |||||
} | } | ||||
private void createRevocationValues( | private void createRevocationValues( |
import java.security.cert.CRLException; | import java.security.cert.CRLException; | ||||
import java.security.cert.X509CRL; | import java.security.cert.X509CRL; | ||||
import java.security.cert.X509Certificate; | |||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Arrays; | |||||
import java.util.List; | import java.util.List; | ||||
/** | /** | ||||
*/ | */ | ||||
public class RevocationData { | 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. | * Adds a CRL to this revocation data set. | ||||
*/ | */ | ||||
public void addCRL(byte[] encodedCrl) { | 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. | * Adds a CRL to this revocation data set. | ||||
*/ | */ | ||||
public void addCRL(X509CRL crl) { | public void addCRL(X509CRL crl) { | ||||
byte[] encodedCrl; | |||||
try { | try { | ||||
encodedCrl = crl.getEncoded(); | |||||
addCRL(crl.getEncoded()); | |||||
} catch (CRLException e) { | } catch (CRLException e) { | ||||
throw new IllegalArgumentException("CRL coding error: " | |||||
+ e.getMessage(), e); | |||||
throw new IllegalArgumentException("CRL coding error: " + e.getMessage(), e); | |||||
} | } | ||||
addCRL(encodedCrl); | |||||
} | } | ||||
/** | /** | ||||
this.ocsps.add(encodedOcsp); | this.ocsps.add(encodedOcsp); | ||||
} | } | ||||
public void addCertificate(X509Certificate x509) { | |||||
x509chain.add(x509); | |||||
} | |||||
/** | /** | ||||
* Gives back a list of all CRLs. | * Gives back a list of all CRLs. | ||||
* | * | ||||
public boolean hasRevocationDataEntries() { | public boolean hasRevocationDataEntries() { | ||||
return hasOCSPs() || hasCRLs(); | return hasOCSPs() || hasCRLs(); | ||||
} | } | ||||
public List<X509Certificate> getX509chain() { | |||||
return x509chain; | |||||
} | |||||
} | } |
import static org.apache.logging.log4j.util.Unbox.box; | import static org.apache.logging.log4j.util.Unbox.box; | ||||
import java.io.ByteArrayInputStream; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.InputStream; | |||||
import java.io.OutputStream; | |||||
import java.math.BigInteger; | 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.MessageDigest; | ||||
import java.security.SecureRandom; | import java.security.SecureRandom; | ||||
import java.security.cert.CertificateFactory; | |||||
import java.security.cert.X509CRL; | |||||
import java.security.cert.X509Certificate; | 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.List; | ||||
import java.util.Map; | 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.LogManager; | ||||
import org.apache.logging.log4j.Logger; | 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.CryptoFunctions; | ||||
import org.apache.poi.poifs.crypt.HashAlgorithm; | import org.apache.poi.poifs.crypt.HashAlgorithm; | ||||
import org.apache.poi.poifs.crypt.dsig.SignatureConfig; | 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.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.ASN1ObjectIdentifier; | ||||
import org.bouncycastle.asn1.ASN1Primitive; | |||||
import org.bouncycastle.asn1.cmp.PKIFailureInfo; | import org.bouncycastle.asn1.cmp.PKIFailureInfo; | ||||
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; | import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; | ||||
import org.bouncycastle.asn1.x500.X500Name; | 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.asn1.x509.X509ObjectIdentifiers; | ||||
import org.bouncycastle.cert.X509CertificateHolder; | import org.bouncycastle.cert.X509CertificateHolder; | ||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; | ||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; | |||||
import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator; | import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator; | ||||
import org.bouncycastle.cms.SignerId; | import org.bouncycastle.cms.SignerId; | ||||
import org.bouncycastle.cms.SignerInformationVerifier; | import org.bouncycastle.cms.SignerInformationVerifier; | ||||
private static final Logger LOG = LogManager.getLogger(TSPTimeStampService.class); | 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. | * Maps the digest algorithm to corresponding OID value. | ||||
*/ | */ | ||||
} | } | ||||
ASN1ObjectIdentifier digestAlgoOid = mapDigestAlgoToOID(signatureConfig.getTspDigestAlgo()); | ASN1ObjectIdentifier digestAlgoOid = mapDigestAlgoToOID(signatureConfig.getTspDigestAlgo()); | ||||
TimeStampRequest request = requestGenerator.generate(digestAlgoOid, digest, nonce); | 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) { | if (responseBytes.length == 0) { | ||||
throw new RuntimeException("Content-Length is zero"); | throw new RuntimeException("Content-Length is zero"); | ||||
LOG.atDebug().log("signer cert issuer: {}", signerCertIssuer); | LOG.atDebug().log("signer cert issuer: {}", signerCertIssuer); | ||||
// TSP signer certificates retrieval | // 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 | // 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(); | JcaX509CertificateConverter x509converter = new JcaX509CertificateConverter(); | ||||
x509converter.setProvider("BC"); | x509converter.setProvider("BC"); | ||||
X509CertificateHolder certificate = signerCert; | |||||
// complete certificate chain | |||||
X509Certificate child = x509converter.getCertificate(signerCert); | |||||
do { | 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; | 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 | // 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); | timeStampToken.validate(verifier); | ||||
// verify TSP signer certificate | // verify TSP signer certificate | ||||
if (signatureConfig.getTspValidator() != null) { | 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()); | LOG.atDebug().log("time-stamp token time: {}", timeStampToken.getTimeStampInfo().getGenTime()); | ||||
return timeStampToken.getEncoded(); | 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; | |||||
} | |||||
} | |||||
} | } |
/* ==================================================================== | |||||
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); | |||||
} |
/* ==================================================================== | |||||
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) { } | |||||
} | |||||
} |
import java.security.KeyPairGenerator; | import java.security.KeyPairGenerator; | ||||
import java.security.KeyStore; | import java.security.KeyStore; | ||||
import java.security.KeyStoreException; | import java.security.KeyStoreException; | ||||
import java.security.NoSuchAlgorithmException; | |||||
import java.security.PrivateKey; | import java.security.PrivateKey; | ||||
import java.security.PublicKey; | import java.security.PublicKey; | ||||
import java.security.UnrecoverableKeyException; | |||||
import java.security.cert.Certificate; | import java.security.cert.Certificate; | ||||
import java.security.cert.CertificateEncodingException; | import java.security.cert.CertificateEncodingException; | ||||
import java.security.cert.CertificateException; | import java.security.cert.CertificateException; | ||||
import java.security.cert.CertificateFactory; | |||||
import java.security.cert.X509CRL; | import java.security.cert.X509CRL; | ||||
import java.security.cert.X509Certificate; | import java.security.cert.X509Certificate; | ||||
import java.security.interfaces.RSAPublicKey; | import java.security.interfaces.RSAPublicKey; | ||||
import java.security.spec.RSAKeyGenParameterSpec; | import java.security.spec.RSAKeyGenParameterSpec; | ||||
import java.util.AbstractMap; | |||||
import java.util.ArrayList; | |||||
import java.util.Arrays; | |||||
import java.util.Calendar; | import java.util.Calendar; | ||||
import java.util.Collections; | |||||
import java.util.Date; | import java.util.Date; | ||||
import java.util.HashMap; | |||||
import java.util.List; | 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.Collectors; | ||||
import java.util.stream.Stream; | import java.util.stream.Stream; | ||||
import javax.security.auth.x500.X500Principal; | |||||
import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream; | import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream; | ||||
import org.apache.poi.poifs.crypt.CryptoFunctions; | import org.apache.poi.poifs.crypt.CryptoFunctions; | ||||
import org.apache.poi.poifs.storage.RawDataUtil; | import org.apache.poi.poifs.storage.RawDataUtil; | ||||
return new KeyCertPair(getKey(keyAlias, keyPass), keystore.getCertificateChain(keyAlias)); | 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 { | public PrivateKey getKey(String keyAlias, String keyPass) throws GeneralSecurityException { | ||||
return (PrivateKey)keystore.getKey(keyAlias, keyPass.toCharArray()); | 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 { | public X509Certificate getFirstX509(String alias) throws KeyStoreException { | ||||
return (X509Certificate)keystore.getCertificate(alias); | 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 { | public void save(File storeFile, String storePass) throws IOException, GeneralSecurityException { | ||||
try (FileOutputStream fos = new FileOutputStream(storeFile)) { | try (FileOutputStream fos = new FileOutputStream(storeFile)) { | ||||
keystore.store(fos, storePass.toCharArray()); | keystore.store(fos, storePass.toCharArray()); | ||||
return ocspRespBuilder.build(OCSPRespBuilder.SUCCESSFUL, basicOCSPResp); | 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])); | |||||
} | |||||
} | |||||
} | } |
* Reads up to {@code length} bytes from the input stream, and returns the bytes read. | * 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 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 | * until EOF | ||||
* @param maxLength if the input is equal to/longer than {@code maxLength} bytes, | * @param maxLength if the input is equal to/longer than {@code maxLength} bytes, | ||||
* then throw an {@link IOException} complaining about the length. | * then throw an {@link IOException} complaining about the length. |