diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java index f3184ab09..9fa93f7a0 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java @@ -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 namespacePrefixes = new HashMap<>(); - + private final Map 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 null, 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 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 nvl(T value, T defaultValue) { + private static 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 javax.xml.crypto.dsig.CanonicalizationMethod */ 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}

+ * + * @param updateConfigOnValidate if true, update config on validate + * + * @since POI 4.0.0 + */ + public void setUpdateConfigOnValidate(boolean updateConfigOnValidate) { + this.updateConfigOnValidate = updateConfigOnValidate; + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java index cff1e1d9a..81e5b2197 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java @@ -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> 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> 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 nsMap = new HashMap<>(); + + { + for (Map.Entry 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; + } + } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java index ee61027f7..5d4ccbd86 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java @@ -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 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); diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java index 7bb4f6ac8..dd2bc1705 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java @@ -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 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 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 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 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 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); diff --git a/test-data/xmldsign/PPT2016withComment.pptx b/test-data/xmldsign/PPT2016withComment.pptx new file mode 100644 index 000000000..92154f9f0 Binary files /dev/null and b/test-data/xmldsign/PPT2016withComment.pptx differ