mirror of https://github.com/apache/poi.git
#62452 - Extract configuration while verifying XML signatures
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1833477 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
cbc275d4b7
commit
4873dc71d3
|
@ -23,10 +23,14 @@ import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XADES_132_NS
|
|||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -49,6 +53,7 @@ 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.TimeStampService;
|
||||
import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator;
|
||||
import org.apache.poi.util.LocaleUtil;
|
||||
import org.apache.poi.util.POILogFactory;
|
||||
import org.apache.poi.util.POILogger;
|
||||
import org.apache.xml.security.signature.XMLSignature;
|
||||
|
@ -60,10 +65,16 @@ import org.w3c.dom.events.EventListener;
|
|||
* Apart of the thread local members (e.g. opc-package) most values will probably be constant, so
|
||||
* it might be configured centrally (e.g. by spring)
|
||||
*/
|
||||
@SuppressWarnings({"unused","WeakerAccess"})
|
||||
public class SignatureConfig {
|
||||
|
||||
public static final String SIGNATURE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
|
||||
|
||||
private static final POILogger LOG = POILogFactory.getLogger(SignatureConfig.class);
|
||||
|
||||
private static final String DigestMethod_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#sha224";
|
||||
private static final String DigestMethod_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384";
|
||||
|
||||
|
||||
public interface SignatureConfigurable {
|
||||
void setSignatureConfig(SignatureConfig signatureConfig);
|
||||
}
|
||||
|
@ -150,14 +161,20 @@ public class SignatureConfig {
|
|||
* with certain namespaces, so this EventListener is used to interfere
|
||||
* with the marshalling process.
|
||||
*/
|
||||
EventListener signatureMarshalListener;
|
||||
private EventListener signatureMarshalListener;
|
||||
|
||||
/**
|
||||
* Map of namespace uris to prefix
|
||||
* If a mapping is specified, the corresponding elements will be prefixed
|
||||
*/
|
||||
Map<String,String> namespacePrefixes = new HashMap<>();
|
||||
|
||||
private final Map<String,String> namespacePrefixes = new HashMap<>();
|
||||
|
||||
/**
|
||||
* if true, the signature config is updated based on the validated document
|
||||
*/
|
||||
private boolean updateConfigOnValidate = false;
|
||||
|
||||
|
||||
/**
|
||||
* Inits and checks the config object.
|
||||
* If not set previously, complex configuration properties also get
|
||||
|
@ -308,7 +325,36 @@ public class SignatureConfig {
|
|||
public void setExecutionTime(Date executionTime) {
|
||||
this.executionTime = executionTime;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the formatted execution time ({@link #SIGNATURE_TIME_FORMAT})
|
||||
*
|
||||
* @since POI 4.0.0
|
||||
*/
|
||||
public String formatExecutionTime() {
|
||||
final DateFormat fmt = new SimpleDateFormat(SIGNATURE_TIME_FORMAT, Locale.ROOT);
|
||||
fmt.setTimeZone(LocaleUtil.TIMEZONE_UTC);
|
||||
return fmt.format(getExecutionTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the executionTime which is in standard format ({@link #SIGNATURE_TIME_FORMAT})
|
||||
* @param executionTime the execution time
|
||||
*
|
||||
* @since POI 4.0.0
|
||||
*/
|
||||
public void setExecutionTime(String executionTime) {
|
||||
if (executionTime != null && !"".equals(executionTime)){
|
||||
final DateFormat fmt = new SimpleDateFormat(SIGNATURE_TIME_FORMAT, Locale.ROOT);
|
||||
fmt.setTimeZone(LocaleUtil.TIMEZONE_UTC);
|
||||
try {
|
||||
this.executionTime = fmt.parse(executionTime);
|
||||
} catch (ParseException e) {
|
||||
LOG.log(POILogger.WARN, "Illegal execution time: "+executionTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the service to be used for XAdES-EPES properties. There's no default implementation
|
||||
*/
|
||||
|
@ -364,7 +410,24 @@ public class SignatureConfig {
|
|||
* @param canonicalizationMethod the default canonicalization method
|
||||
*/
|
||||
public void setCanonicalizationMethod(String canonicalizationMethod) {
|
||||
this.canonicalizationMethod = canonicalizationMethod;
|
||||
this.canonicalizationMethod = verifyCanonicalizationMethod(canonicalizationMethod, CanonicalizationMethod.INCLUSIVE);
|
||||
}
|
||||
|
||||
private static String verifyCanonicalizationMethod(String canonicalizationMethod, String defaultMethod) {
|
||||
if (canonicalizationMethod == null || canonicalizationMethod.isEmpty()) {
|
||||
return defaultMethod;
|
||||
}
|
||||
|
||||
switch (canonicalizationMethod) {
|
||||
case CanonicalizationMethod.INCLUSIVE:
|
||||
case CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS:
|
||||
case CanonicalizationMethod.ENVELOPED:
|
||||
case CanonicalizationMethod.EXCLUSIVE:
|
||||
case CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS:
|
||||
return canonicalizationMethod;
|
||||
}
|
||||
|
||||
throw new EncryptedDocumentException("Unknown CanonicalizationMethod: "+canonicalizationMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -532,6 +595,16 @@ public class SignatureConfig {
|
|||
this.xadesDigestAlgo = xadesDigestAlgo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param xadesDigestAlgo hash algorithm used for XAdES.
|
||||
* When <code>null</code>, defaults to {@link #getDigestAlgo()}
|
||||
*
|
||||
* @since POI 4.0.0
|
||||
*/
|
||||
public void setXadesDigestAlgo(String xadesDigestAlgo) {
|
||||
this.xadesDigestAlgo = getDigestMethodAlgo(xadesDigestAlgo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the user agent used for http communication (e.g. to the TSP)
|
||||
*/
|
||||
|
@ -700,16 +773,17 @@ public class SignatureConfig {
|
|||
* @param namespacePrefixes the map of namespace uri (key) to prefix (value)
|
||||
*/
|
||||
public void setNamespacePrefixes(Map<String, String> namespacePrefixes) {
|
||||
this.namespacePrefixes = namespacePrefixes;
|
||||
this.namespacePrefixes.clear();
|
||||
this.namespacePrefixes.putAll(namespacePrefixes);
|
||||
}
|
||||
|
||||
/**
|
||||
* helper method for null/default value handling
|
||||
* @param value
|
||||
* @param defaultValue
|
||||
* @param value the value to be tested
|
||||
* @param defaultValue the default value
|
||||
* @return if value is not null, return value otherwise defaultValue
|
||||
*/
|
||||
protected static <T> T nvl(T value, T defaultValue) {
|
||||
private static <T> T nvl(T value, T defaultValue) {
|
||||
return value == null ? defaultValue : value;
|
||||
}
|
||||
|
||||
|
@ -729,34 +803,92 @@ public class SignatureConfig {
|
|||
+getDigestAlgo()+" not supported for signing.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return the uri for the main digest
|
||||
*/
|
||||
public String getDigestMethodUri() {
|
||||
return getDigestMethodUri(getDigestAlgo());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the digest algorithm - currently only sha* and ripemd160 is supported.
|
||||
* Converts the digest algorithm - currently only sha* and ripemd160 is supported.
|
||||
* MS Office only supports sha1, sha256, sha384, sha512.
|
||||
*
|
||||
* @param digestAlgo the digest algorithm
|
||||
* @param digestAlgo the digest algorithm
|
||||
* @return the uri for the given digest
|
||||
*/
|
||||
public static String getDigestMethodUri(HashAlgorithm digestAlgo) {
|
||||
switch (digestAlgo) {
|
||||
case sha1: return DigestMethod.SHA1;
|
||||
case sha224: return "http://www.w3.org/2001/04/xmldsig-more#sha224";
|
||||
case sha224: return DigestMethod_SHA224;
|
||||
case sha256: return DigestMethod.SHA256;
|
||||
case sha384: return "http://www.w3.org/2001/04/xmldsig-more#sha384";
|
||||
case sha384: return DigestMethod_SHA384;
|
||||
case sha512: return DigestMethod.SHA512;
|
||||
case ripemd160: return DigestMethod.RIPEMD160;
|
||||
default: throw new EncryptedDocumentException("Hash algorithm "
|
||||
+digestAlgo+" not supported for signing.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts the digest algorithm ur - currently only sha* and ripemd160 is supported.
|
||||
* MS Office only supports sha1, sha256, sha384, sha512.
|
||||
*
|
||||
* @param digestAlgo the digest algorithm uri
|
||||
* @return the hash algorithm for the given digest
|
||||
*/
|
||||
private static HashAlgorithm getDigestMethodAlgo(String digestMethodUri) {
|
||||
if (digestMethodUri == null || digestMethodUri.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
switch (digestMethodUri) {
|
||||
case DigestMethod.SHA1: return HashAlgorithm.sha1;
|
||||
case DigestMethod_SHA224: return HashAlgorithm.sha224;
|
||||
case DigestMethod.SHA256: return HashAlgorithm.sha256;
|
||||
case DigestMethod_SHA384: return HashAlgorithm.sha384;
|
||||
case DigestMethod.SHA512: return HashAlgorithm.sha512;
|
||||
case DigestMethod.RIPEMD160: return HashAlgorithm.ripemd160;
|
||||
default: throw new EncryptedDocumentException("Hash algorithm "
|
||||
+digestMethodUri+" not supported for signing.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the digest algorithm based on the method uri.
|
||||
* This is used when a signature was successful validated and the signature
|
||||
* configuration is updated
|
||||
*
|
||||
* @param signatureMethodUri the method uri
|
||||
*
|
||||
* @since POI 4.0.0
|
||||
*/
|
||||
public void setSignatureMethodFromUri(final String signatureMethodUri) {
|
||||
switch (signatureMethodUri) {
|
||||
case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1:
|
||||
setDigestAlgo(HashAlgorithm.sha1);
|
||||
break;
|
||||
case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA224:
|
||||
setDigestAlgo(HashAlgorithm.sha224);
|
||||
break;
|
||||
case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256:
|
||||
setDigestAlgo(HashAlgorithm.sha256);
|
||||
break;
|
||||
case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA384:
|
||||
setDigestAlgo(HashAlgorithm.sha384);
|
||||
break;
|
||||
case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512:
|
||||
setDigestAlgo(HashAlgorithm.sha512);
|
||||
break;
|
||||
case XMLSignature.ALGO_ID_SIGNATURE_RSA_RIPEMD160:
|
||||
setDigestAlgo(HashAlgorithm.ripemd160);
|
||||
break;
|
||||
default: throw new EncryptedDocumentException("Hash algorithm "
|
||||
+signatureMethodUri+" not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param signatureFactory the xml signature factory, saved as thread-local
|
||||
*/
|
||||
|
@ -852,6 +984,28 @@ public class SignatureConfig {
|
|||
* @see <a href="http://docs.oracle.com/javase/7/docs/api/javax/xml/crypto/dsig/CanonicalizationMethod.html">javax.xml.crypto.dsig.CanonicalizationMethod</a>
|
||||
*/
|
||||
public void setXadesCanonicalizationMethod(String xadesCanonicalizationMethod) {
|
||||
this.xadesCanonicalizationMethod = xadesCanonicalizationMethod;
|
||||
this.xadesCanonicalizationMethod = verifyCanonicalizationMethod(xadesCanonicalizationMethod, CanonicalizationMethod.EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true, if the signature config is to be updated based on the successful validated document
|
||||
*
|
||||
* @since POI 4.0.0
|
||||
*/
|
||||
public boolean isUpdateConfigOnValidate() {
|
||||
return updateConfigOnValidate;
|
||||
}
|
||||
|
||||
/**
|
||||
* The signature config can be updated if a document is succesful validated.
|
||||
* This flag is used for activating this modifications.
|
||||
* Defaults to {@code false}<p>
|
||||
*
|
||||
* @param updateConfigOnValidate if true, update config on validate
|
||||
*
|
||||
* @since POI 4.0.0
|
||||
*/
|
||||
public void setUpdateConfigOnValidate(boolean updateConfigOnValidate) {
|
||||
this.updateConfigOnValidate = updateConfigOnValidate;
|
||||
}
|
||||
}
|
|
@ -18,24 +18,31 @@
|
|||
package org.apache.poi.poifs.crypt.dsig;
|
||||
|
||||
import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS;
|
||||
import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.MS_DIGSIG_NS;
|
||||
import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_DIGSIG_NS;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.xml.crypto.MarshalException;
|
||||
import javax.xml.crypto.dsig.XMLSignature;
|
||||
import javax.xml.crypto.dsig.XMLSignatureException;
|
||||
import javax.xml.crypto.dsig.XMLSignatureFactory;
|
||||
import javax.xml.crypto.dsig.dom.DOMValidateContext;
|
||||
import javax.xml.namespace.NamespaceContext;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.apache.poi.EncryptedDocumentException;
|
||||
import org.apache.poi.openxml4j.opc.PackagePart;
|
||||
import org.apache.poi.ooxml.util.DocumentHelper;
|
||||
import org.apache.poi.openxml4j.opc.PackagePart;
|
||||
import org.apache.poi.util.POILogFactory;
|
||||
import org.apache.poi.util.POILogger;
|
||||
import org.apache.xmlbeans.XmlException;
|
||||
|
@ -92,7 +99,7 @@ public class SignaturePart {
|
|||
// TODO: check for XXE
|
||||
return SignatureDocument.Factory.parse(signaturePart.getInputStream(), DEFAULT_XML_OPTIONS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return true, when the xml signature is valid, false otherwise
|
||||
*
|
||||
|
@ -100,9 +107,11 @@ public class SignaturePart {
|
|||
*/
|
||||
public boolean validate() {
|
||||
KeyInfoKeySelector keySelector = new KeyInfoKeySelector();
|
||||
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||
xpath.setNamespaceContext(new XPathNSContext());
|
||||
|
||||
try {
|
||||
Document doc = DocumentHelper.readDocument(signaturePart.getInputStream());
|
||||
XPath xpath = XPathFactory.newInstance().newXPath();
|
||||
NodeList nl = (NodeList)xpath.compile("//*[@Id]").evaluate(doc, XPathConstants.NODESET);
|
||||
final int length = nl.getLength();
|
||||
for (int i=0; i<length; i++) {
|
||||
|
@ -121,6 +130,7 @@ public class SignaturePart {
|
|||
if (valid) {
|
||||
signer = keySelector.getSigner();
|
||||
certChain = keySelector.getCertChain();
|
||||
extractConfig(doc, xmlSignature);
|
||||
}
|
||||
|
||||
return valid;
|
||||
|
@ -146,4 +156,50 @@ public class SignaturePart {
|
|||
throw new EncryptedDocumentException(s, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractConfig(final Document doc, final XMLSignature xmlSignature) throws XPathExpressionException {
|
||||
if (!signatureConfig.isUpdateConfigOnValidate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
signatureConfig.setSigningCertificateChain(certChain);
|
||||
signatureConfig.setSignatureMethodFromUri(xmlSignature.getSignedInfo().getSignatureMethod().getAlgorithm());
|
||||
|
||||
final XPath xpath = XPathFactory.newInstance().newXPath();
|
||||
xpath.setNamespaceContext(new XPathNSContext());
|
||||
|
||||
final Map<String,Consumer<String>> m = new HashMap();
|
||||
m.put("//mdssi:SignatureTime/mdssi:Value", signatureConfig::setExecutionTime);
|
||||
m.put("//xd:ClaimedRole", signatureConfig::setXadesRole);
|
||||
m.put("//dsss:SignatureComments", signatureConfig::setSignatureDescription);
|
||||
m.put("//xd:QualifyingProperties//xd:SignedSignatureProperties//ds:DigestMethod/@Algorithm", signatureConfig::setXadesDigestAlgo);
|
||||
m.put("//ds:CanonicalizationMethod", signatureConfig::setCanonicalizationMethod);
|
||||
|
||||
for (Map.Entry<String,Consumer<String>> me : m.entrySet()) {
|
||||
String val = (String)xpath.compile(me.getKey()).evaluate(doc, XPathConstants.STRING);
|
||||
me.getValue().accept(val);
|
||||
}
|
||||
}
|
||||
|
||||
private class XPathNSContext implements NamespaceContext {
|
||||
final Map<String,String> nsMap = new HashMap<>();
|
||||
|
||||
{
|
||||
for (Map.Entry<String,String> me : signatureConfig.getNamespacePrefixes().entrySet()) {
|
||||
nsMap.put(me.getValue(), me.getKey());
|
||||
}
|
||||
nsMap.put("dsss", MS_DIGSIG_NS);
|
||||
nsMap.put("ds", XML_DIGSIG_NS);
|
||||
}
|
||||
|
||||
public String getNamespaceURI(String prefix) {
|
||||
return nsMap.get(prefix);
|
||||
}
|
||||
public Iterator getPrefixes(String val) {
|
||||
return null;
|
||||
}
|
||||
public String getPrefix(String uri) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -221,15 +221,11 @@ public class OOXMLSignatureFacet extends SignatureFacet {
|
|||
/*
|
||||
* SignatureTime
|
||||
*/
|
||||
DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
|
||||
fmt.setTimeZone(LocaleUtil.TIMEZONE_UTC);
|
||||
String nowStr = fmt.format(signatureConfig.getExecutionTime());
|
||||
LOG.log(POILogger.DEBUG, "now: " + nowStr);
|
||||
|
||||
SignatureTimeDocument sigTime = SignatureTimeDocument.Factory.newInstance();
|
||||
CTSignatureTime ctTime = sigTime.addNewSignatureTime();
|
||||
ctTime.setFormat("YYYY-MM-DDThh:mm:ssTZD");
|
||||
ctTime.setValue(nowStr);
|
||||
ctTime.setValue(signatureConfig.formatExecutionTime());
|
||||
LOG.log(POILogger.DEBUG, "execution time: " + ctTime.getValue());
|
||||
|
||||
Element n = (Element)document.importNode(ctTime.getDomNode(),true);
|
||||
List<XMLStructure> signatureTimeContent = new ArrayList<>();
|
||||
|
@ -253,6 +249,11 @@ public class OOXMLSignatureFacet extends SignatureFacet {
|
|||
SignatureInfoV1Document sigV1 = SignatureInfoV1Document.Factory.newInstance();
|
||||
CTSignatureInfoV1 ctSigV1 = sigV1.addNewSignatureInfoV1();
|
||||
ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri());
|
||||
|
||||
if (signatureConfig.getSignatureDescription() != null) {
|
||||
ctSigV1.setSignatureComments(signatureConfig.getSignatureDescription());
|
||||
}
|
||||
|
||||
Element n = (Element)document.importNode(ctSigV1.getDomNode(), true);
|
||||
n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS);
|
||||
|
||||
|
|
|
@ -56,11 +56,14 @@ import java.util.Date;
|
|||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.crypto.dsig.CanonicalizationMethod;
|
||||
import javax.xml.crypto.dsig.dom.DOMSignContext;
|
||||
|
||||
import org.apache.jcp.xml.dsig.internal.dom.DOMSignedInfo;
|
||||
import org.apache.poi.POIDataSamples;
|
||||
import org.apache.poi.POITestCase;
|
||||
import org.apache.poi.ooxml.util.DocumentHelper;
|
||||
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
|
||||
import org.apache.poi.openxml4j.opc.OPCPackage;
|
||||
import org.apache.poi.openxml4j.opc.PackageAccess;
|
||||
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes;
|
||||
|
@ -69,6 +72,7 @@ import org.apache.poi.poifs.crypt.dsig.SignatureInfo;
|
|||
import org.apache.poi.poifs.crypt.dsig.SignaturePart;
|
||||
import org.apache.poi.poifs.crypt.dsig.facets.EnvelopedSignatureFacet;
|
||||
import org.apache.poi.poifs.crypt.dsig.facets.KeyInfoSignatureFacet;
|
||||
import org.apache.poi.poifs.crypt.dsig.facets.OOXMLSignatureFacet;
|
||||
import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet;
|
||||
import org.apache.poi.poifs.crypt.dsig.facets.XAdESXLSignatureFacet;
|
||||
import org.apache.poi.poifs.crypt.dsig.services.RevocationData;
|
||||
|
@ -77,7 +81,6 @@ import org.apache.poi.poifs.crypt.dsig.services.TimeStampService;
|
|||
import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator;
|
||||
import org.apache.poi.poifs.storage.RawDataUtil;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.apache.poi.ooxml.util.DocumentHelper;
|
||||
import org.apache.poi.util.IOUtils;
|
||||
import org.apache.poi.util.LocaleUtil;
|
||||
import org.apache.poi.util.POILogFactory;
|
||||
|
@ -186,7 +189,7 @@ public class TestSignatureInfo {
|
|||
bos.reset();
|
||||
pkg1.save(bos);
|
||||
pkg1.close();
|
||||
|
||||
|
||||
XSSFWorkbook wb2 = new XSSFWorkbook(new ByteArrayInputStream(bos.toByteArray()));
|
||||
assertEquals("Test", wb2.getSheetAt(0).getRow(1).getCell(1).getStringCellValue());
|
||||
OPCPackage pkg2 = wb2.getPackage();
|
||||
|
@ -395,142 +398,173 @@ public class TestSignatureInfo {
|
|||
@Test
|
||||
public void testSignEnvelopingDocument() throws Exception {
|
||||
String testFile = "hello-world-unsigned.xlsx";
|
||||
OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE);
|
||||
File sigCopy = testdata.getFile(testFile);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(50000);
|
||||
|
||||
initKeyPair("Test", "CN=Test");
|
||||
final X509CRL crl = PkiTestUtils.generateCrl(x509, keyPair.getPrivate());
|
||||
|
||||
// setup
|
||||
SignatureConfig signatureConfig = new SignatureConfig();
|
||||
signatureConfig.setOpcPackage(pkg);
|
||||
signatureConfig.setKey(keyPair.getPrivate());
|
||||
final String execTimestr;
|
||||
|
||||
/*
|
||||
* We need at least 2 certificates for the XAdES-C complete certificate
|
||||
* refs construction.
|
||||
*/
|
||||
List<X509Certificate> certificateChain = new ArrayList<>();
|
||||
certificateChain.add(x509);
|
||||
certificateChain.add(x509);
|
||||
signatureConfig.setSigningCertificateChain(certificateChain);
|
||||
|
||||
signatureConfig.addSignatureFacet(new EnvelopedSignatureFacet());
|
||||
signatureConfig.addSignatureFacet(new KeyInfoSignatureFacet());
|
||||
signatureConfig.addSignatureFacet(new XAdESSignatureFacet());
|
||||
signatureConfig.addSignatureFacet(new XAdESXLSignatureFacet());
|
||||
|
||||
// check for internet, no error means it works
|
||||
boolean mockTsp = (getAccessError("http://timestamp.comodoca.com/rfc3161", true, 10000) != null);
|
||||
|
||||
// http://timestamping.edelweb.fr/service/tsp
|
||||
// http://tsa.belgium.be/connect
|
||||
// http://timestamp.comodoca.com/authenticode
|
||||
// http://timestamp.comodoca.com/rfc3161
|
||||
// http://services.globaltrustfinder.com/adss/tsa
|
||||
signatureConfig.setTspUrl("http://timestamp.comodoca.com/rfc3161");
|
||||
signatureConfig.setTspRequestPolicy(null); // comodoca request fails, if default policy is set ...
|
||||
signatureConfig.setTspOldProtocol(false);
|
||||
|
||||
//set proxy info if any
|
||||
String proxy = System.getProperty("http_proxy");
|
||||
if (proxy != null && proxy.trim().length() > 0) {
|
||||
signatureConfig.setProxyUrl(proxy);
|
||||
}
|
||||
|
||||
if (mockTsp) {
|
||||
TimeStampService tspService = new TimeStampService(){
|
||||
@Override
|
||||
public byte[] timeStamp(byte[] data, RevocationData revocationData) {
|
||||
revocationData.addCRL(crl);
|
||||
return "time-stamp-token".getBytes(LocaleUtil.CHARSET_1252);
|
||||
try (OPCPackage pkg = OPCPackage.open(copy(sigCopy), PackageAccess.READ_WRITE)) {
|
||||
|
||||
initKeyPair("Test", "CN=Test");
|
||||
final X509CRL crl = PkiTestUtils.generateCrl(x509, keyPair.getPrivate());
|
||||
|
||||
// setup
|
||||
SignatureConfig signatureConfig = new SignatureConfig();
|
||||
signatureConfig.setOpcPackage(pkg);
|
||||
signatureConfig.setKey(keyPair.getPrivate());
|
||||
|
||||
/*
|
||||
* We need at least 2 certificates for the XAdES-C complete certificate
|
||||
* refs construction.
|
||||
*/
|
||||
List<X509Certificate> certificateChain = new ArrayList<>();
|
||||
certificateChain.add(x509);
|
||||
certificateChain.add(x509);
|
||||
signatureConfig.setSigningCertificateChain(certificateChain);
|
||||
|
||||
signatureConfig.addSignatureFacet(new OOXMLSignatureFacet());
|
||||
signatureConfig.addSignatureFacet(new EnvelopedSignatureFacet());
|
||||
signatureConfig.addSignatureFacet(new KeyInfoSignatureFacet());
|
||||
signatureConfig.addSignatureFacet(new XAdESSignatureFacet());
|
||||
signatureConfig.addSignatureFacet(new XAdESXLSignatureFacet());
|
||||
|
||||
// check for internet, no error means it works
|
||||
boolean mockTsp = (getAccessError("http://timestamp.comodoca.com/rfc3161", true, 10000) != null);
|
||||
|
||||
// http://timestamping.edelweb.fr/service/tsp
|
||||
// http://tsa.belgium.be/connect
|
||||
// http://timestamp.comodoca.com/authenticode
|
||||
// http://timestamp.comodoca.com/rfc3161
|
||||
// http://services.globaltrustfinder.com/adss/tsa
|
||||
signatureConfig.setTspUrl("http://timestamp.comodoca.com/rfc3161");
|
||||
signatureConfig.setTspRequestPolicy(null); // comodoca request fails, if default policy is set ...
|
||||
signatureConfig.setTspOldProtocol(false);
|
||||
|
||||
signatureConfig.setXadesDigestAlgo(HashAlgorithm.sha512);
|
||||
signatureConfig.setXadesRole("Xades Reviewer");
|
||||
signatureConfig.setSignatureDescription("test xades signature");
|
||||
|
||||
execTimestr = signatureConfig.formatExecutionTime();
|
||||
|
||||
//set proxy info if any
|
||||
String proxy = System.getProperty("http_proxy");
|
||||
if (proxy != null && proxy.trim().length() > 0) {
|
||||
signatureConfig.setProxyUrl(proxy);
|
||||
}
|
||||
|
||||
if (mockTsp) {
|
||||
TimeStampService tspService = new TimeStampService() {
|
||||
@Override
|
||||
public byte[] timeStamp(byte[] data, RevocationData revocationData) {
|
||||
revocationData.addCRL(crl);
|
||||
return "time-stamp-token".getBytes(LocaleUtil.CHARSET_1252);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSignatureConfig(SignatureConfig config) {
|
||||
// empty on purpose
|
||||
}
|
||||
};
|
||||
signatureConfig.setTspService(tspService);
|
||||
} else {
|
||||
TimeStampServiceValidator tspValidator = (validateChain, revocationData) -> {
|
||||
for (X509Certificate certificate : validateChain) {
|
||||
LOG.log(POILogger.DEBUG, "certificate: " + certificate.getSubjectX500Principal());
|
||||
LOG.log(POILogger.DEBUG, "validity: " + certificate.getNotBefore() + " - " + certificate.getNotAfter());
|
||||
}
|
||||
};
|
||||
signatureConfig.setTspValidator(tspValidator);
|
||||
signatureConfig.setTspOldProtocol(signatureConfig.getTspUrl().contains("edelweb"));
|
||||
}
|
||||
|
||||
final RevocationData revocationData = new RevocationData();
|
||||
revocationData.addCRL(crl);
|
||||
OCSPResp ocspResp = PkiTestUtils.createOcspResp(x509, false,
|
||||
x509, x509, keyPair.getPrivate(), "SHA1withRSA", cal.getTimeInMillis());
|
||||
revocationData.addOCSP(ocspResp.getEncoded());
|
||||
|
||||
RevocationDataService revocationDataService = revocationChain -> revocationData;
|
||||
signatureConfig.setRevocationDataService(revocationDataService);
|
||||
|
||||
// operate
|
||||
SignatureInfo si = new SignatureInfo();
|
||||
si.setSignatureConfig(signatureConfig);
|
||||
try {
|
||||
si.confirmSignature();
|
||||
} catch (RuntimeException e) {
|
||||
pkg.close();
|
||||
// only allow a ConnectException because of timeout, we see this in Jenkins from time to time...
|
||||
if (e.getCause() == null) {
|
||||
throw e;
|
||||
}
|
||||
@Override
|
||||
public void setSignatureConfig(SignatureConfig config) {
|
||||
// empty on purpose
|
||||
if ((e.getCause() instanceof ConnectException) || (e.getCause() instanceof SocketTimeoutException)) {
|
||||
Assume.assumeFalse("Only allowing ConnectException with 'timed out' as message here, but had: " + e,
|
||||
e.getCause().getMessage().contains("timed out"));
|
||||
} else if (e.getCause() instanceof IOException) {
|
||||
Assume.assumeFalse("Only allowing IOException with 'Error contacting TSP server' as message here, but had: " + e,
|
||||
e.getCause().getMessage().contains("Error contacting TSP server"));
|
||||
} else if (e.getCause() instanceof RuntimeException) {
|
||||
Assume.assumeFalse("Only allowing RuntimeException with 'This site is cur' as message here, but had: " + e,
|
||||
e.getCause().getMessage().contains("This site is cur"));
|
||||
}
|
||||
};
|
||||
signatureConfig.setTspService(tspService);
|
||||
} else {
|
||||
TimeStampServiceValidator tspValidator = (validateChain, revocationData) -> {
|
||||
for (X509Certificate certificate : validateChain) {
|
||||
LOG.log(POILogger.DEBUG, "certificate: " + certificate.getSubjectX500Principal());
|
||||
LOG.log(POILogger.DEBUG, "validity: " + certificate.getNotBefore() + " - " + certificate.getNotAfter());
|
||||
}
|
||||
};
|
||||
signatureConfig.setTspValidator(tspValidator);
|
||||
signatureConfig.setTspOldProtocol(signatureConfig.getTspUrl().contains("edelweb"));
|
||||
}
|
||||
|
||||
final RevocationData revocationData = new RevocationData();
|
||||
revocationData.addCRL(crl);
|
||||
OCSPResp ocspResp = PkiTestUtils.createOcspResp(x509, false,
|
||||
x509, x509, keyPair.getPrivate(), "SHA1withRSA", cal.getTimeInMillis());
|
||||
revocationData.addOCSP(ocspResp.getEncoded());
|
||||
|
||||
RevocationDataService revocationDataService = revocationChain -> revocationData;
|
||||
signatureConfig.setRevocationDataService(revocationDataService);
|
||||
|
||||
// operate
|
||||
SignatureInfo si = new SignatureInfo();
|
||||
si.setSignatureConfig(signatureConfig);
|
||||
try {
|
||||
si.confirmSignature();
|
||||
} catch (RuntimeException e) {
|
||||
pkg.close();
|
||||
// only allow a ConnectException because of timeout, we see this in Jenkins from time to time...
|
||||
if(e.getCause() == null) {
|
||||
throw e;
|
||||
}
|
||||
if((e.getCause() instanceof ConnectException) || (e.getCause() instanceof SocketTimeoutException)) {
|
||||
Assume.assumeFalse("Only allowing ConnectException with 'timed out' as message here, but had: " + e,
|
||||
e.getCause().getMessage().contains("timed out"));
|
||||
} else if (e.getCause() instanceof IOException) {
|
||||
Assume.assumeFalse("Only allowing IOException with 'Error contacting TSP server' as message here, but had: " + e,
|
||||
e.getCause().getMessage().contains("Error contacting TSP server"));
|
||||
} else if (e.getCause() instanceof RuntimeException) {
|
||||
Assume.assumeFalse("Only allowing RuntimeException with 'This site is cur' as message here, but had: " + e,
|
||||
e.getCause().getMessage().contains("This site is cur"));
|
||||
|
||||
// verify
|
||||
Iterator<SignaturePart> spIter = si.getSignatureParts().iterator();
|
||||
assertTrue("Had: " + si.getSignatureConfig().getOpcPackage().
|
||||
getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN),
|
||||
spIter.hasNext());
|
||||
SignaturePart sp = spIter.next();
|
||||
boolean valid = sp.validate();
|
||||
assertTrue(valid);
|
||||
|
||||
SignatureDocument sigDoc = sp.getSignatureDocument();
|
||||
String declareNS =
|
||||
"declare namespace xades='http://uri.etsi.org/01903/v1.3.2#'; "
|
||||
+ "declare namespace ds='http://www.w3.org/2000/09/xmldsig#'; ";
|
||||
|
||||
String digestValXQuery = declareNS +
|
||||
"$this/ds:Signature/ds:SignedInfo/ds:Reference";
|
||||
for (ReferenceType rt : (ReferenceType[]) sigDoc.selectPath(digestValXQuery)) {
|
||||
assertNotNull(rt.getDigestValue());
|
||||
assertEquals(signatureConfig.getDigestMethodUri(), rt.getDigestMethod().getAlgorithm());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// verify
|
||||
Iterator<SignaturePart> spIter = si.getSignatureParts().iterator();
|
||||
assertTrue("Had: " + si.getSignatureConfig().getOpcPackage().
|
||||
getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN),
|
||||
spIter.hasNext());
|
||||
SignaturePart sp = spIter.next();
|
||||
boolean valid = sp.validate();
|
||||
assertTrue(valid);
|
||||
|
||||
SignatureDocument sigDoc = sp.getSignatureDocument();
|
||||
String declareNS =
|
||||
"declare namespace xades='http://uri.etsi.org/01903/v1.3.2#'; "
|
||||
+ "declare namespace ds='http://www.w3.org/2000/09/xmldsig#'; ";
|
||||
|
||||
String digestValXQuery = declareNS +
|
||||
"$this/ds:Signature/ds:SignedInfo/ds:Reference";
|
||||
for (ReferenceType rt : (ReferenceType[])sigDoc.selectPath(digestValXQuery)) {
|
||||
assertNotNull(rt.getDigestValue());
|
||||
assertEquals(signatureConfig.getDigestMethodUri(), rt.getDigestMethod().getAlgorithm());
|
||||
|
||||
String certDigestXQuery = declareNS +
|
||||
"$this//xades:SigningCertificate/xades:Cert/xades:CertDigest";
|
||||
XmlObject xoList[] = sigDoc.selectPath(certDigestXQuery);
|
||||
assertEquals(xoList.length, 1);
|
||||
DigestAlgAndValueType certDigest = (DigestAlgAndValueType) xoList[0];
|
||||
assertNotNull(certDigest.getDigestValue());
|
||||
|
||||
String qualPropXQuery = declareNS +
|
||||
"$this/ds:Signature/ds:Object/xades:QualifyingProperties";
|
||||
xoList = sigDoc.selectPath(qualPropXQuery);
|
||||
assertEquals(xoList.length, 1);
|
||||
QualifyingPropertiesType qualProp = (QualifyingPropertiesType) xoList[0];
|
||||
boolean qualPropXsdOk = qualProp.validate();
|
||||
assertTrue(qualPropXsdOk);
|
||||
|
||||
pkg.save(bos);
|
||||
}
|
||||
|
||||
String certDigestXQuery = declareNS +
|
||||
"$this//xades:SigningCertificate/xades:Cert/xades:CertDigest";
|
||||
XmlObject xoList[] = sigDoc.selectPath(certDigestXQuery);
|
||||
assertEquals(xoList.length, 1);
|
||||
DigestAlgAndValueType certDigest = (DigestAlgAndValueType)xoList[0];
|
||||
assertNotNull(certDigest.getDigestValue());
|
||||
try (OPCPackage pkg = OPCPackage.open(new ByteArrayInputStream(bos.toByteArray()))) {
|
||||
SignatureConfig signatureConfig = new SignatureConfig();
|
||||
signatureConfig.setOpcPackage(pkg);
|
||||
signatureConfig.setUpdateConfigOnValidate(true);
|
||||
|
||||
String qualPropXQuery = declareNS +
|
||||
"$this/ds:Signature/ds:Object/xades:QualifyingProperties";
|
||||
xoList = sigDoc.selectPath(qualPropXQuery);
|
||||
assertEquals(xoList.length, 1);
|
||||
QualifyingPropertiesType qualProp = (QualifyingPropertiesType)xoList[0];
|
||||
boolean qualPropXsdOk = qualProp.validate();
|
||||
assertTrue(qualPropXsdOk);
|
||||
SignatureInfo si = new SignatureInfo();
|
||||
si.setSignatureConfig(signatureConfig);
|
||||
|
||||
pkg.close();
|
||||
assertTrue(si.verifySignature());
|
||||
|
||||
assertEquals(HashAlgorithm.sha512, signatureConfig.getXadesDigestAlgo());
|
||||
assertEquals("Xades Reviewer", signatureConfig.getXadesRole());
|
||||
assertEquals("test xades signature", signatureConfig.getSignatureDescription());
|
||||
assertEquals(execTimestr, signatureConfig.formatExecutionTime());
|
||||
}
|
||||
}
|
||||
|
||||
public static String getAccessError(String destinationUrl, boolean fireRequest, int timeout) {
|
||||
|
@ -698,6 +732,27 @@ public class TestSignatureInfo {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetrieveCertificate() throws InvalidFormatException, IOException {
|
||||
SignatureConfig sic = new SignatureConfig();
|
||||
final File file = testdata.getFile("PPT2016withComment.pptx");
|
||||
try (final OPCPackage pkg = OPCPackage.open(file, PackageAccess.READ)) {
|
||||
sic.setOpcPackage(pkg);
|
||||
sic.setUpdateConfigOnValidate(true);
|
||||
SignatureInfo si = new SignatureInfo();
|
||||
si.setSignatureConfig(sic);
|
||||
assertTrue(si.verifySignature());
|
||||
}
|
||||
|
||||
final List<X509Certificate> certs = sic.getSigningCertificateChain();
|
||||
assertEquals(1, certs.size());
|
||||
assertEquals("CN=Test", certs.get(0).getSubjectDN().getName());
|
||||
assertEquals("SuperDuper-Reviewer", sic.getXadesRole());
|
||||
assertEquals("Purpose for signing", sic.getSignatureDescription());
|
||||
assertEquals("2018-06-10T09:00:54Z", sic.formatExecutionTime());
|
||||
assertEquals(CanonicalizationMethod.INCLUSIVE, sic.getCanonicalizationMethod());
|
||||
}
|
||||
|
||||
private SignatureConfig prepareConfig(String alias, String signerDn, String pfxInput) throws Exception {
|
||||
initKeyPair(alias, signerDn, pfxInput);
|
||||
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue