ソースを参照

#65908 - XAdES-XL modifications due to specification check errors

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1898287 13f79535-47bb-0310-9956-ffa450edef68
tags/REL_5_2_1
Andreas Beeker 2年前
コミット
a881c381db

+ 182
- 16
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java ファイルの表示

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


} }

+ 22
- 2
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalDefaultListener.java ファイルの表示



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

+ 1
- 2
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java ファイルの表示

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

+ 81
- 83
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java ファイルの表示

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(

+ 18
- 17
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java ファイルの表示



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

+ 143
- 126
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java ファイルの表示



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

+ 83
- 0
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampHttpClient.java ファイルの表示

/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */

package org.apache.poi.poifs.crypt.dsig.services;

import java.io.IOException;
import java.net.HttpURLConnection;

import org.apache.poi.poifs.crypt.dsig.SignatureConfig;

/**
* This interface is used to decouple the timestamp service logic from
* the actual downloading code and to provide an interface for user code
* using a different http client implementation.
*
* The implementation must be stateless regarding the http connection and
* not expect to be called in a certain order, apart from being first initialized.
*/
public interface TimeStampHttpClient {
interface TimeStampHttpClientResponse {
default boolean isOK() {
return getResponseCode() == HttpURLConnection.HTTP_OK;
}

/**
* @return the http response code
*/
int getResponseCode();

/**
* @return the http response bytes
*/
byte[] getResponseBytes();
}

void init(SignatureConfig config);

/** set request content type */
void setContentTypeIn(String contentType);

/** set expected response content type - use {@code null} if contentType is ignored */
void setContentTypeOut(String contentType);

void setBasicAuthentication(String username, String password);

TimeStampHttpClientResponse post(String url, byte[] payload) throws IOException;

TimeStampHttpClientResponse get(String url) throws IOException;

/**
* @return if the connection is reckless ignoring all https certificate trust issues
*/
boolean isIgnoreHttpsCertificates();

/**
* @param ignoreHttpsCertificates set if the connection is reckless ignoring all https certificate trust issues
*/
void setIgnoreHttpsCertificates(boolean ignoreHttpsCertificates);

/**
* @return if http redirects are followed once
*/
boolean isFollowRedirects();

/**
* @param followRedirects set if http redirects are followed once
*/
void setFollowRedirects(boolean followRedirects);
}

+ 285
- 0
poi-ooxml/src/main/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampSimpleHttpClient.java ファイルの表示

/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */

package org.apache.poi.poifs.crypt.dsig.services;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.poifs.crypt.dsig.SignatureConfig;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.RandomSingleton;

/**
* This default implementation is used to decouple the timestamp service logic from
* the actual downloading code and to provide a base for user code
* using a different http client implementation
*/
public class TimeStampSimpleHttpClient implements TimeStampHttpClient {
protected static final String CONTENT_TYPE = "Content-Type";
protected static final String USER_AGENT = "User-Agent";
protected static final String BASIC_AUTH = "Authorization";
protected static final String REDIRECT_LOCATION = "Location";

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

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

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

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


private static class TimeStampSimpleHttpClientResponse implements TimeStampHttpClientResponse {
private final int responseCode;
private final byte[] responseBytes;

public TimeStampSimpleHttpClientResponse(int responseCode, byte[] responseBytes) {
this.responseCode = responseCode;
this.responseBytes = responseBytes;
}

@Override
public int getResponseCode() {
return responseCode;
}

@Override
public byte[] getResponseBytes() {
return responseBytes;
}


}

protected SignatureConfig config;
protected Proxy proxy = Proxy.NO_PROXY;
protected final Map<String,String> header = new HashMap<>();
protected String contentTypeOut = null;
protected boolean ignoreHttpsCertificates = false;
protected boolean followRedirects = false;

@Override
public void init(SignatureConfig config) {
this.config = config;
header.clear();

header.put(USER_AGENT, config.getUserAgent());

contentTypeOut = null;
// don't reset followRedirects/ignoreHttpsCertificates, as they aren't contained in SignatureConfig by design
// followRedirects = false;
// ignoreHttpsCertificates = false;

setProxy(config.getProxyUrl());
setBasicAuthentication(config.getTspUser(), config.getTspPass());
}

public void setProxy(String proxyUrl) {
if (proxyUrl == null || proxyUrl.isEmpty()) {
proxy = Proxy.NO_PROXY;
} else {
try {
URL pUrl = new URL(proxyUrl);
String host = pUrl.getHost();
int port = pUrl.getPort();
proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByName(host), (port == -1 ? 80 : port)));
} catch (IOException ignored) {
}
}
}

public Proxy getProxy() {
return proxy;
}

@Override
public void setContentTypeIn(String contentType) {
header.put(CONTENT_TYPE, contentType);
}

@Override
public void setContentTypeOut(String contentType) {
contentTypeOut = contentType;
}

@Override
public void setBasicAuthentication(String username, String password) {
if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
header.remove(BASIC_AUTH);
} else {
String userPassword = username + ":" + password;
String encoding = Base64.getEncoder().encodeToString(userPassword.getBytes(StandardCharsets.ISO_8859_1));
header.put(BASIC_AUTH, "Basic " + encoding);
}

}

@Override
public boolean isIgnoreHttpsCertificates() {
return ignoreHttpsCertificates;
}

@Override
public void setIgnoreHttpsCertificates(boolean ignoreHttpsCertificates) {
this.ignoreHttpsCertificates = ignoreHttpsCertificates;
}

@Override
public boolean isFollowRedirects() {
return followRedirects;
}

@Override
public void setFollowRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
}

@Override
public TimeStampHttpClientResponse post(String url, byte[] payload) throws IOException {
MethodHandler handler = (huc) -> {
huc.setRequestMethod("POST");
huc.setDoOutput(true);
try (OutputStream hucOut = huc.getOutputStream()) {
hucOut.write(payload);
}
};
return handleRedirect(url, handler, isFollowRedirects());
}

@Override
public TimeStampHttpClientResponse get(String url) throws IOException {
// connection is by default a GET call
return handleRedirect(url, (huc) -> {}, isFollowRedirects());
}

protected interface MethodHandler {
void handle(HttpURLConnection huc) throws IOException;
}

protected TimeStampHttpClientResponse handleRedirect(String url, MethodHandler handler, boolean followRedirect) throws IOException {
HttpURLConnection huc = (HttpURLConnection)new URL(url).openConnection(proxy);
if (ignoreHttpsCertificates) {
recklessConnection(huc);
}
huc.setConnectTimeout(20000);
huc.setReadTimeout(20000);

header.forEach(huc::setRequestProperty);

try {
handler.handle(huc);

huc.connect();

final int responseCode = huc.getResponseCode();
final byte[] responseBytes;

switch (responseCode) {
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_SEE_OTHER:
String newUrl = huc.getHeaderField(REDIRECT_LOCATION);
if (newUrl != null && followRedirect) {
LOG.atWarn().log("Received redirect: {} -> {}", url, newUrl);
return handleRedirect(newUrl, handler, false);
}

LOG.atWarn().log("Redirect ignored - giving up: {} -> {}", url, newUrl);
responseBytes = null;
break;
case HttpURLConnection.HTTP_OK:
// HTTP input validation
String contentType = huc.getHeaderField(CONTENT_TYPE);
if (contentTypeOut != null && !contentTypeOut.equals(contentType)) {
throw new IOException("Content-Type mismatch - expected `" + contentTypeOut + "', received '" + contentType + "'");
}

try (InputStream is = huc.getInputStream()) {
responseBytes = IOUtils.toByteArray(is, Integer.MIN_VALUE, getMaxTimestampResponseSize());
}
break;
default:
final String message = "Error contacting TSP server " + url +
", had status code " + responseCode + "/" + huc.getResponseMessage();
LOG.atError().log(message);
throw new IOException(message);
}

return new TimeStampSimpleHttpClientResponse(responseCode, responseBytes);
} finally {
huc.disconnect();
}
}

protected void recklessConnection(HttpURLConnection conn) throws IOException {
if (!(conn instanceof HttpsURLConnection)) {
return;
}
HttpsURLConnection conns = (HttpsURLConnection)conn;

try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[]{new UnsafeTrustManager()}, RandomSingleton.getInstance());
conns.setSSLSocketFactory(sc.getSocketFactory());
conns.setHostnameVerifier((hostname, session) -> true);
} catch (GeneralSecurityException e) {
throw new IOException("Unable to reckless wrap connection.", e);
}
}

private static class UnsafeTrustManager implements X509TrustManager {
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; }
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) { }
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) { }
}
}

+ 83
- 0
poi-ooxml/src/test/java/org/apache/poi/poifs/crypt/dsig/DummyKeystore.java ファイルの表示

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

+ 1
- 1
poi/src/main/java/org/apache/poi/util/IOUtils.java ファイルの表示

* 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.

読み込み中…
キャンセル
保存