git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1833477 13f79535-47bb-0310-9956-ffa450edef68tags/REL_4_0_0_FINAL
@@ -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); | |||
final String execTimestr; | |||
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); | |||
} | |||
initKeyPair("Test", "CN=Test"); | |||
final X509CRL crl = PkiTestUtils.generateCrl(x509, keyPair.getPrivate()); | |||
// setup | |||
SignatureConfig signatureConfig = new SignatureConfig(); | |||
signatureConfig.setOpcPackage(pkg); | |||
signatureConfig.setKey(keyPair.getPrivate()); | |||
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); | |||
} | |||
/* | |||
* 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); | |||
} | |||
@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")); | |||
} | |||
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()); | |||
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); | |||
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) { | |||
// 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")); | |||
} | |||
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()); | |||
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); | |||
try (OPCPackage pkg = OPCPackage.open(new ByteArrayInputStream(bos.toByteArray()))) { | |||
SignatureConfig signatureConfig = new SignatureConfig(); | |||
signatureConfig.setOpcPackage(pkg); | |||
signatureConfig.setUpdateConfigOnValidate(true); | |||
pkg.close(); | |||
SignatureInfo si = new SignatureInfo(); | |||
si.setSignatureConfig(signatureConfig); | |||
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); | |||