diff --git a/.classpath b/.classpath index a7dc0d8fc5..313dbdb1e8 100644 --- a/.classpath +++ b/.classpath @@ -1,29 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.xml b/build.xml index ce0b900176..7c46239ccf 100644 --- a/build.xml +++ b/build.xml @@ -61,6 +61,7 @@ under the License. + @@ -118,7 +119,6 @@ under the License. - @@ -147,7 +147,17 @@ under the License. - + + + + + + + + + + + @@ -160,7 +170,7 @@ under the License. - + @@ -171,17 +181,30 @@ under the License. - - - + + + + + + + + + + + + + - - + + + @@ -222,6 +245,13 @@ under the License. + + + + + + + @@ -229,6 +259,7 @@ under the License. + @@ -361,7 +392,7 @@ under the License. - + @@ -375,6 +406,10 @@ under the License. + + + + @@ -433,6 +468,22 @@ under the License. + + + + + + + + + + + + + + + + @@ -463,19 +514,37 @@ under the License. - + - - + + + + + + + + + + + + + + + + + + + - + + @@ -486,19 +555,10 @@ under the License. - - - - - - - - - + depends="check-jars,fetch-jars,check-compiled-ooxml-xsds" + description="Unpacks the OOXML xsd files, and compiles them into XmlBeans"> @@ -510,11 +570,9 @@ under the License. - - + + + - - + - - - - - - - - - - + + + + + + + + + + + + - - + + @@ -1354,7 +1412,7 @@ under the License. - + @@ -1450,7 +1508,7 @@ under the License. - + diff --git a/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java index be507a6660..68682f496c 100644 --- a/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java +++ b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java @@ -34,6 +34,8 @@ public enum CipherAlgorithm { // need bouncycastle provider for this one ... // see http://stackoverflow.com/questions/4436397/3des-des-encryption-using-the-jce-generating-an-acceptable-key des3_112(null, "DESede", -1, 128, new int[]{128}, 8, 32, "3DES_112", true), + // only for digital signatures + rsa(null, "RSA", -1, 1024, new int[]{1024, 2048, 3072, 4096}, -1, -1, "", false); ; public final CipherProvider provider; diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index ffb7498f3f..875ade3ab9 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -19,6 +19,7 @@ package org.apache.poi.poifs.crypt; import java.nio.charset.Charset; import java.security.DigestException; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.MessageDigest; import java.security.Provider; import java.security.Security; @@ -195,7 +196,7 @@ public class CryptoFunctions { * @throws EncryptedDocumentException if the initialization failed or if an algorithm was specified, * which depends on a missing bouncy castle provider */ - public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode, String padding) { + public static Cipher getCipher(Key key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode, String padding) { int keySizeInBytes = key.getEncoded().length; if (padding == null) padding = "NoPadding"; @@ -296,10 +297,12 @@ public class CryptoFunctions { } @SuppressWarnings("unchecked") - private static void registerBouncyCastle() { + public static void registerBouncyCastle() { if (Security.getProvider("BC") != null) return; try { - Class clazz = (Class)Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + String bcProviderName = "org.bouncycastle.jce.provider.BouncyCastleProvider"; + Class clazz = (Class)cl.loadClass(bcProviderName); Security.addProvider(clazz.newInstance()); } catch (Exception e) { throw new EncryptedDocumentException("Only the BouncyCastle provider supports your encryption settings - please add it to the classpath."); diff --git a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java index 61608822f8..6a490b0148 100644 --- a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java +++ b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java @@ -33,6 +33,8 @@ public enum HashAlgorithm { ripemd128("RipeMD128", -1, "RIPEMD-128", 16, "HMac-RipeMD128", true), ripemd160("RipeMD160", -1, "RIPEMD-160", 20, "HMac-RipeMD160", true), whirlpool("Whirlpool", -1, "WHIRLPOOL", 64, "HMac-Whirlpool", true), + // only for xml signing + sha224 ( "SHA-224", -1, "SHA224", 28, "HmacSHA224", true); ; public final String jceId; diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationship.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationship.java index 2a5ade28d6..7d4741b219 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationship.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationship.java @@ -182,8 +182,6 @@ public final class PackageRelationship { } /** - * public URI getSourceUri(){ } - * * @return the targetMode */ public TargetMode getTargetMode() { diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java index 043fd632f4..08d82fbaab 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java @@ -307,7 +307,7 @@ public final class PackageRelationshipCollection implements * @throws InvalidFormatException * Throws if the relationship part is invalid. */ - private void parseRelationshipsPart(PackagePart relPart) + public void parseRelationshipsPart(PackagePart relPart) throws InvalidFormatException { try { logger.log(POILogger.DEBUG, "Parsing relationship: " + relPart.getPartName()); diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java index 581dabe000..59801d3aee 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java @@ -145,11 +145,10 @@ public abstract class ContentTypeManager { *

*/ public void addContentType(PackagePartName partName, String contentType) { - boolean defaultCTExists = false; + boolean defaultCTExists = this.defaultContentType.containsValue(contentType); String extension = partName.getExtension().toLowerCase(); if ((extension.length() == 0) - || (this.defaultContentType.containsKey(extension) && !(defaultCTExists = this.defaultContentType - .containsValue(contentType)))) + || (this.defaultContentType.containsKey(extension) && !defaultCTExists)) this.addOverrideContentType(partName, contentType); else if (!defaultCTExists) this.addDefaultContentType(extension, contentType); @@ -452,7 +451,7 @@ public abstract class ContentTypeManager { } /** - * Use to append default types XML elements, use by the save() metid. + * Use to append default types XML elements, use by the save() method. * * @param root * XML parent element use to append this default type element. diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/CertificateSecurityException.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/CertificateSecurityException.java new file mode 100644 index 0000000000..707ad758a8 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/CertificateSecurityException.java @@ -0,0 +1,38 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig; + +/** + * Exception thrown in case there is something wrong with the incoming eID + * certificate. + * + * @author Frank Cornelis + * + */ +public class CertificateSecurityException extends SecurityException { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/ExpiredCertificateSecurityException.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/ExpiredCertificateSecurityException.java new file mode 100644 index 0000000000..adbcfdb35e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/ExpiredCertificateSecurityException.java @@ -0,0 +1,38 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig; + +/** + * Exception thrown in case the incoming eID certificate is expired. + * + * @author Frank Cornelis + * + */ +public class ExpiredCertificateSecurityException extends + CertificateSecurityException { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java new file mode 100644 index 0000000000..61fedcb9ec --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java @@ -0,0 +1,103 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig; + +import java.security.Key; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.crypto.AlgorithmMethod; +import javax.xml.crypto.KeySelector; +import javax.xml.crypto.KeySelectorException; +import javax.xml.crypto.KeySelectorResult; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.X509Data; + +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * JSR105 key selector implementation using the ds:KeyInfo data of the signature + * itself. + */ +public class KeyInfoKeySelector extends KeySelector implements KeySelectorResult { + + private static final POILogger LOG = POILogFactory.getLogger(KeyInfoKeySelector.class); + + private List certChain = new ArrayList(); + + @SuppressWarnings("unchecked") + @Override + public KeySelectorResult select(KeyInfo keyInfo, Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException { + LOG.log(POILogger.DEBUG, "select key"); + if (null == keyInfo) { + throw new KeySelectorException("no ds:KeyInfo present"); + } + List keyInfoContent = keyInfo.getContent(); + certChain.clear(); + for (XMLStructure keyInfoStructure : keyInfoContent) { + if (!(keyInfoStructure instanceof X509Data)) { + continue; + } + X509Data x509Data = (X509Data) keyInfoStructure; + List x509DataList = x509Data.getContent(); + for (Object x509DataObject : x509DataList) { + if (!(x509DataObject instanceof X509Certificate)) { + continue; + } + X509Certificate certificate = (X509Certificate) x509DataObject; + LOG.log(POILogger.DEBUG, "certificate", certificate.getSubjectX500Principal()); + certChain.add(certificate); + } + } + if (certChain.isEmpty()) { + throw new KeySelectorException("No key found!"); + } + return this; + } + + public Key getKey() { + // The first certificate is presumably the signer. + return certChain.isEmpty() ? null : certChain.get(0).getPublicKey(); + } + + /** + * Gives back the X509 certificate used during the last signature + * verification operation. + * + * @return + */ + public X509Certificate getSigner() { + // The first certificate is presumably the signer. + return certChain.isEmpty() ? null : certChain.get(0); + } + + public List getCertChain() { + return certChain; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/OOXMLURIDereferencer.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/OOXMLURIDereferencer.java new file mode 100644 index 0000000000..50dd68ddef --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/OOXMLURIDereferencer.java @@ -0,0 +1,113 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import javax.xml.crypto.Data; +import javax.xml.crypto.OctetStreamData; +import javax.xml.crypto.URIDereferencer; +import javax.xml.crypto.URIReference; +import javax.xml.crypto.URIReferenceException; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.dsig.XMLSignatureFactory; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * JSR105 URI dereferencer for Office Open XML documents. + */ +public class OOXMLURIDereferencer implements URIDereferencer, SignatureConfigurable { + + private static final POILogger LOG = POILogFactory.getLogger(OOXMLURIDereferencer.class); + + private SignatureConfig signatureConfig; + private URIDereferencer baseUriDereferencer; + + public OOXMLURIDereferencer() { + XMLSignatureFactory xmlSignatureFactory = SignatureInfo.getSignatureFactory(); + this.baseUriDereferencer = xmlSignatureFactory.getURIDereferencer(); + } + + public void setSignatureConfig(SignatureConfig signatureConfig) { + this.signatureConfig = signatureConfig; + } + + public Data dereference(URIReference uriReference, XMLCryptoContext context) throws URIReferenceException { + if (null == uriReference) { + throw new NullPointerException("URIReference cannot be null"); + } + if (null == context) { + throw new NullPointerException("XMLCrytoContext cannot be null"); + } + + URI uri; + try { + uri = new URI(uriReference.getURI()); + } catch (URISyntaxException e) { + throw new URIReferenceException("could not URL decode the uri: "+uriReference.getURI(), e); + } + + PackagePart part = findPart(uri); + if (part == null) { + LOG.log(POILogger.DEBUG, "cannot resolve, delegating to base DOM URI dereferencer", uri); + return this.baseUriDereferencer.dereference(uriReference, context); + } + + try { + return new OctetStreamData(part.getInputStream(), uri.toString(), null); + } catch (IOException e) { + throw new URIReferenceException("I/O error: " + e.getMessage(), e); + } + } + + private PackagePart findPart(URI uri) { + LOG.log(POILogger.DEBUG, "dereference", uri); + + String path = uri.getPath(); + if (path == null || "".equals(path)) { + LOG.log(POILogger.DEBUG, "illegal part name (expected)", uri); + return null; + } + + PackagePartName ppn; + try { + ppn = PackagingURIHelper.createPartName(path); + } catch (InvalidFormatException e) { + LOG.log(POILogger.WARN, "illegal part name (not expected)", uri); + return null; + } + + return signatureConfig.getOpcPackage().getPart(ppn); + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/RevokedCertificateSecurityException.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/RevokedCertificateSecurityException.java new file mode 100644 index 0000000000..fa4fee7370 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/RevokedCertificateSecurityException.java @@ -0,0 +1,38 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig; + +/** + * Exception thrown in case the incoming eID certificate has been revoked. + * + * @author Frank Cornelis + * + */ +public class RevokedCertificateSecurityException extends + CertificateSecurityException { + + private static final long serialVersionUID = 1L; + +} 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 new file mode 100644 index 0000000000..7c59fbcae0 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java @@ -0,0 +1,505 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.dsig; + +import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.OO_DIGSIG_NS; +import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XADES_132_NS; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.xml.crypto.URIDereferencer; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.poifs.crypt.HashAlgorithm; +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.Office2010SignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet; +import org.apache.poi.poifs.crypt.dsig.services.RevocationDataService; +import org.apache.poi.poifs.crypt.dsig.services.SignaturePolicyService; +import org.apache.poi.poifs.crypt.dsig.services.TSPTimeStampService; +import org.apache.poi.poifs.crypt.dsig.services.TimeStampService; +import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator; +import org.apache.poi.poifs.crypt.dsig.spi.AddressDTO; +import org.apache.poi.poifs.crypt.dsig.spi.IdentityDTO; +import org.w3c.dom.events.EventListener; + +/** + * This class bundles the configuration options used for the existing + * signature facets. + * Apart of the opc-package (thread local) most values will probably be constant, so + * it might be configured centrally (e.g. by spring) + */ +public class SignatureConfig { + + public static interface SignatureConfigurable { + void setSignatureConfig(SignatureConfig signatureConfig); + } + + private ThreadLocal opcPackage = new ThreadLocal(); + + private List signatureFacets = new ArrayList(); + private HashAlgorithm digestAlgo = HashAlgorithm.sha1; + private Date executionTime = new Date(); + private PrivateKey key; + private List signingCertificateChain; + private IdentityDTO identity; + private AddressDTO address; + private byte[] photo; + + /** + * the optional signature policy service used for XAdES-EPES. + */ + private SignaturePolicyService signaturePolicyService; + private URIDereferencer uriDereferencer = new OOXMLURIDereferencer(); + private String canonicalizationMethod = CanonicalizationMethod.INCLUSIVE; + + private boolean includeEntireCertificateChain = true; + private boolean includeIssuerSerial = false; + private boolean includeKeyValue = false; + + private TimeStampService tspService = new TSPTimeStampService(); + // timestamp service provider URL + private String tspUrl; + private boolean tspOldProtocol = false; + /** + * if not defined, it's the same as the main digest + */ + private HashAlgorithm tspDigestAlgo = null; + private String tspUser; + private String tspPass; + private TimeStampServiceValidator tspValidator; + /** + * the optional TSP request policy OID. + */ + private String tspRequestPolicy = "1.3.6.1.4.1.13762.3"; + private String userAgent = "POI XmlSign Service TSP Client"; + private String proxyUrl; + + /** + * the optional revocation data service used for XAdES-C and XAdES-X-L. + * When null the signature will be limited to XAdES-T only. + */ + private RevocationDataService revocationDataService; + /** + * if not defined, it's the same as the main digest + */ + private HashAlgorithm xadesDigestAlgo = null; + private String xadesRole = null; + private String xadesSignatureId = null; + private boolean xadesSignaturePolicyImplied = true; + + /** + * Work-around for Office 2010 IssuerName encoding. + */ + private boolean xadesIssuerNameNoReverseOrder = true; + + /** + * The signature Id attribute value used to create the XML signature. A + * null value will trigger an automatically generated signature Id. + */ + private String packageSignatureId = "idPackageSignature"; + + /** + * Gives back the human-readable description of what the citizen will be + * signing. The default value is "Office OpenXML Document". + */ + private String signatureDescription = "Office OpenXML Document"; + + /** + * The process of signing includes the marshalling of xml structures. + * This also includes the canonicalization. Currently this leads to problems + * with certain namespaces, so this EventListener is used to interfere + * with the marshalling process. + */ + EventListener signatureMarshalListener = null; + + /** + * Map of namespace uris to prefix + * If a mapping is specified, the corresponding elements will be prefixed + */ + Map namespacePrefixes = new HashMap(); + + protected void init(boolean onlyValidation) { + if (uriDereferencer == null) { + throw new EncryptedDocumentException("uriDereferencer is null"); + } + if (opcPackage == null) { + throw new EncryptedDocumentException("opcPackage is null"); + } + if (uriDereferencer instanceof SignatureConfigurable) { + ((SignatureConfigurable)uriDereferencer).setSignatureConfig(this); + } + if (namespacePrefixes.isEmpty()) { + /* + * OOo doesn't like ds namespaces so per default prefixing is off. + */ + // namespacePrefixes.put(XML_DIGSIG_NS, ""); + namespacePrefixes.put(OO_DIGSIG_NS, "mdssi"); + namespacePrefixes.put(XADES_132_NS, "xd"); + } + + if (onlyValidation) return; + + if (signatureMarshalListener == null) { + signatureMarshalListener = new SignatureMarshalListener(); + } + + if (signatureMarshalListener instanceof SignatureConfigurable) { + ((SignatureConfigurable)signatureMarshalListener).setSignatureConfig(this); + } + + if (tspService != null) { + tspService.setSignatureConfig(this); + } + + if (xadesSignatureId == null || xadesSignatureId.isEmpty()) { + xadesSignatureId = "idSignedProperties"; + } + + if (signatureFacets.isEmpty()) { + addSignatureFacet(new OOXMLSignatureFacet()); + addSignatureFacet(new KeyInfoSignatureFacet()); + addSignatureFacet(new XAdESSignatureFacet()); + addSignatureFacet(new Office2010SignatureFacet()); + } + + for (SignatureFacet sf : signatureFacets) { + sf.setSignatureConfig(this); + } + } + + public void addSignatureFacet(SignatureFacet sf) { + signatureFacets.add(sf); + } + + /** + * Gives back the used XAdES signature facet. + * + * @return + */ + public XAdESSignatureFacet getXAdESSignatureFacet() { + for (SignatureFacet sf : getSignatureFacets()) { + if (sf instanceof XAdESSignatureFacet) { + return (XAdESSignatureFacet)sf; + } + } + return null; + } + + + public List getSignatureFacets() { + return signatureFacets; + } + public void setSignatureFacets(List signatureFacets) { + this.signatureFacets = signatureFacets; + } + public HashAlgorithm getDigestAlgo() { + return digestAlgo; + } + public void setDigestAlgo(HashAlgorithm digestAlgo) { + this.digestAlgo = digestAlgo; + } + public OPCPackage getOpcPackage() { + return opcPackage.get(); + } + public void setOpcPackage(OPCPackage opcPackage) { + this.opcPackage.set(opcPackage); + } + public PrivateKey getKey() { + return key; + } + public void setKey(PrivateKey key) { + this.key = key; + } + public List getSigningCertificateChain() { + return signingCertificateChain; + } + public void setSigningCertificateChain( + List signingCertificateChain) { + this.signingCertificateChain = signingCertificateChain; + } + public IdentityDTO getIdentity() { + return identity; + } + public void setIdentity(IdentityDTO identity) { + this.identity = identity; + } + public AddressDTO getAddress() { + return address; + } + public void setAddress(AddressDTO address) { + this.address = address; + } + public byte[] getPhoto() { + return photo; + } + public void setPhoto(byte[] photo) { + this.photo = photo; + } + public Date getExecutionTime() { + return executionTime; + } + public void setExecutionTime(Date executionTime) { + this.executionTime = executionTime; + } + public SignaturePolicyService getSignaturePolicyService() { + return signaturePolicyService; + } + public void setSignaturePolicyService(SignaturePolicyService signaturePolicyService) { + this.signaturePolicyService = signaturePolicyService; + } + public URIDereferencer getUriDereferencer() { + return uriDereferencer; + } + public void setUriDereferencer(URIDereferencer uriDereferencer) { + this.uriDereferencer = uriDereferencer; + } + public String getSignatureDescription() { + return signatureDescription; + } + public void setSignatureDescription(String signatureDescription) { + this.signatureDescription = signatureDescription; + } + public String getCanonicalizationMethod() { + return canonicalizationMethod; + } + public void setCanonicalizationMethod(String canonicalizationMethod) { + this.canonicalizationMethod = canonicalizationMethod; + } + public String getPackageSignatureId() { + return packageSignatureId; + } + public void setPackageSignatureId(String packageSignatureId) { + this.packageSignatureId = nvl(packageSignatureId,"xmldsig-"+UUID.randomUUID()); + } + public String getTspUrl() { + return tspUrl; + } + public void setTspUrl(String tspUrl) { + this.tspUrl = tspUrl; + } + public boolean isTspOldProtocol() { + return tspOldProtocol; + } + public void setTspOldProtocol(boolean tspOldProtocol) { + this.tspOldProtocol = tspOldProtocol; + } + public HashAlgorithm getTspDigestAlgo() { + return nvl(tspDigestAlgo,digestAlgo); + } + public void setTspDigestAlgo(HashAlgorithm tspDigestAlgo) { + this.tspDigestAlgo = tspDigestAlgo; + } + public String getProxyUrl() { + return proxyUrl; + } + public void setProxyUrl(String proxyUrl) { + this.proxyUrl = proxyUrl; + } + public TimeStampService getTspService() { + return tspService; + } + public void setTspService(TimeStampService tspService) { + this.tspService = tspService; + } + public String getTspUser() { + return tspUser; + } + public void setTspUser(String tspUser) { + this.tspUser = tspUser; + } + public String getTspPass() { + return tspPass; + } + public void setTspPass(String tspPass) { + this.tspPass = tspPass; + } + public TimeStampServiceValidator getTspValidator() { + return tspValidator; + } + public void setTspValidator(TimeStampServiceValidator tspValidator) { + this.tspValidator = tspValidator; + } + public RevocationDataService getRevocationDataService() { + return revocationDataService; + } + public void setRevocationDataService(RevocationDataService revocationDataService) { + this.revocationDataService = revocationDataService; + } + public HashAlgorithm getXadesDigestAlgo() { + return nvl(xadesDigestAlgo,digestAlgo); + } + public void setXadesDigestAlgo(HashAlgorithm xadesDigestAlgo) { + this.xadesDigestAlgo = xadesDigestAlgo; + } + public String getUserAgent() { + return userAgent; + } + public void setUserAgent(String userAgent) { + this.userAgent = userAgent; + } + public String getTspRequestPolicy() { + return tspRequestPolicy; + } + public void setTspRequestPolicy(String tspRequestPolicy) { + this.tspRequestPolicy = tspRequestPolicy; + } + public boolean isIncludeEntireCertificateChain() { + return includeEntireCertificateChain; + } + public void setIncludeEntireCertificateChain(boolean includeEntireCertificateChain) { + this.includeEntireCertificateChain = includeEntireCertificateChain; + } + public boolean isIncludeIssuerSerial() { + return includeIssuerSerial; + } + public void setIncludeIssuerSerial(boolean includeIssuerSerial) { + this.includeIssuerSerial = includeIssuerSerial; + } + public boolean isIncludeKeyValue() { + return includeKeyValue; + } + public void setIncludeKeyValue(boolean includeKeyValue) { + this.includeKeyValue = includeKeyValue; + } + public String getXadesRole() { + return xadesRole; + } + public void setXadesRole(String xadesRole) { + this.xadesRole = xadesRole; + } + public String getXadesSignatureId() { + return xadesSignatureId; + } + public void setXadesSignatureId(String xadesSignatureId) { + this.xadesSignatureId = xadesSignatureId; + } + public boolean isXadesSignaturePolicyImplied() { + return xadesSignaturePolicyImplied; + } + public void setXadesSignaturePolicyImplied(boolean xadesSignaturePolicyImplied) { + this.xadesSignaturePolicyImplied = xadesSignaturePolicyImplied; + } + public boolean isXadesIssuerNameNoReverseOrder() { + return xadesIssuerNameNoReverseOrder; + } + public void setXadesIssuerNameNoReverseOrder(boolean xadesIssuerNameNoReverseOrder) { + this.xadesIssuerNameNoReverseOrder = xadesIssuerNameNoReverseOrder; + } + public EventListener getSignatureMarshalListener() { + return signatureMarshalListener; + } + public void setSignatureMarshalListener(EventListener signatureMarshalListener) { + this.signatureMarshalListener = signatureMarshalListener; + } + public Map getNamespacePrefixes() { + return namespacePrefixes; + } + public void setNamespacePrefixes(Map namespacePrefixes) { + this.namespacePrefixes = namespacePrefixes; + } + protected static T nvl(T value, T defaultValue) { + return value == null ? defaultValue : value; + } + public byte[] getHashMagic() { + // see https://www.ietf.org/rfc/rfc3110.txt + // RSA/SHA1 SIG Resource Records + byte result[]; + switch (getDigestAlgo()) { + case sha1: result = new byte[] + { 0x30, 0x1f, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x0e + , 0x03, 0x02, 0x1a, 0x04, 0x14 }; + break; + case sha224: result = new byte[] + { 0x30, 0x2b, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x04, 0x1c }; + break; + case sha256: result = new byte[] + { 0x30, 0x2f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x04, 0x20 }; + break; + case sha384: result = new byte[] + { 0x30, 0x3f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x04, 0x30 }; + break; + case sha512: result = new byte[] + { 0x30, 0x4f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x04, 0x40 }; + break; + case ripemd128: result = new byte[] + { 0x30, 0x1b, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24 + , 0x03, 0x02, 0x02, 0x04, 0x10 }; + break; + case ripemd160: result = new byte[] + { 0x30, 0x1f, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24 + , 0x03, 0x02, 0x01, 0x04, 0x14 }; + break; + // case ripemd256: result = new byte[] + // { 0x30, 0x2b, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24 + // , 0x03, 0x02, 0x03, 0x04, 0x20 }; + // break; + default: throw new EncryptedDocumentException("Hash algorithm " + +getDigestAlgo()+" not supported for signing."); + } + + return result; + } + + public String getSignatureMethod() { + switch (getDigestAlgo()) { + case sha1: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1; + case sha224: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA224; + case sha256: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256; + case sha384: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA384; + case sha512: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512; + case ripemd160: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_RIPEMD160; + default: throw new EncryptedDocumentException("Hash algorithm " + +getDigestAlgo()+" not supported for signing."); + } + } + + public String getDigestMethodUri() { + return getDigestMethodUri(getDigestAlgo()); + } + + 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 sha256: return DigestMethod.SHA256; + case sha384: return "http://www.w3.org/2001/04/xmldsig-more#sha384"; + case sha512: return DigestMethod.SHA512; + case ripemd160: return DigestMethod.RIPEMD160; + default: throw new EncryptedDocumentException("Hash algorithm " + +digestAlgo+" not supported for signing."); + } + } + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java new file mode 100644 index 0000000000..69a771b40f --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java @@ -0,0 +1,546 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig; + +import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_DIGSIG_NS; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.security.InvalidAlgorithmParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.crypto.Cipher; +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.URIDereferencer; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Manifest; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignContext; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureException; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import org.apache.jcp.xml.dsig.internal.dom.DOMReference; +import org.apache.jcp.xml.dsig.internal.dom.DOMSignedInfo; +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.ContentTypes; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; +import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; +import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.openxml4j.opc.TargetMode; +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable; +import org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet; +import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService; +import org.apache.poi.poifs.crypt.dsig.spi.DigestInfo; +import org.apache.poi.util.DocumentHelper; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xml.security.Init; +import org.apache.xml.security.utils.Base64; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlOptions; +import org.w3.x2000.x09.xmldsig.SignatureDocument; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; +import org.xml.sax.SAXException; + +public class SignatureInfo implements SignatureConfigurable { + + private static final POILogger LOG = POILogFactory.getLogger(SignatureInfo.class); + private static boolean isInitialized = false; + + private SignatureConfig signatureConfig; + + public class SignaturePart { + private final PackagePart signaturePart; + private X509Certificate signer; + private List certChain; + + private SignaturePart(PackagePart signaturePart) { + this.signaturePart = signaturePart; + } + + public PackagePart getPackagePart() { + return signaturePart; + } + + public X509Certificate getSigner() { + return signer; + } + + public List getCertChain() { + return certChain; + } + + public SignatureDocument getSignatureDocument() throws IOException, XmlException { + // TODO: check for XXE + return SignatureDocument.Factory.parse(signaturePart.getInputStream()); + } + + public boolean validate() { + KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); + try { + Document doc = DocumentHelper.readDocument(signaturePart.getInputStream()); + XPath xpath = XPathFactory.newInstance().newXPath(); + NodeList nl = (NodeList)xpath.compile("//*[@Id]").evaluate(doc, XPathConstants.NODESET); + for (int i=0; i getSignatureParts() { + signatureConfig.init(true); + return new Iterable() { + public Iterator iterator() { + return new Iterator() { + OPCPackage pkg = signatureConfig.getOpcPackage(); + Iterator sigOrigRels = + pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN).iterator(); + Iterator sigRels = null; + PackagePart sigPart = null; + + public boolean hasNext() { + while (sigRels == null || !sigRels.hasNext()) { + if (!sigOrigRels.hasNext()) return false; + sigPart = pkg.getPart(sigOrigRels.next()); + LOG.log(POILogger.DEBUG, "Digital Signature Origin part", sigPart); + try { + sigRels = sigPart.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE).iterator(); + } catch (InvalidFormatException e) { + LOG.log(POILogger.WARN, "Reference to signature is invalid.", e); + } + } + return true; + } + + public SignaturePart next() { + PackagePart sigRelPart = null; + do { + try { + if (!hasNext()) throw new NoSuchElementException(); + sigRelPart = sigPart.getRelatedPart(sigRels.next()); + LOG.log(POILogger.DEBUG, "XML Signature part", sigRelPart); + } catch (InvalidFormatException e) { + LOG.log(POILogger.WARN, "Reference to signature is invalid.", e); + } + } while (sigPart == null); + return new SignaturePart(sigRelPart); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + public static XMLSignatureFactory getSignatureFactory() { + return XMLSignatureFactory.getInstance("DOM", getProvider()); + } + + public static KeyInfoFactory getKeyInfoFactory() { + return KeyInfoFactory.getInstance("DOM", getProvider()); + } + + // currently classes are linked to Apache Santuario, so this might be superfluous + public static Provider getProvider() { + String dsigProviderNames[] = { + System.getProperty("jsr105Provider"), + "org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI", // Santuario xmlsec + "org.jcp.xml.dsig.internal.dom.XMLDSigRI" // JDK xmlsec + }; + for (String pn : dsigProviderNames) { + if (pn == null) continue; + try { + return (Provider)Class.forName(pn).newInstance(); + } catch (Exception e) { + LOG.log(POILogger.DEBUG, "XMLDsig-Provider '"+pn+"' can't be found - trying next."); + } + } + + throw new RuntimeException("JRE doesn't support default xml signature provider - set jsr105Provider system property!"); + } + + protected static synchronized void initXmlProvider() { + if (isInitialized) return; + isInitialized = true; + + try { + Init.init(); + RelationshipTransformService.registerDsigProvider(); + CryptoFunctions.registerBouncyCastle(); + } catch (Exception e) { + throw new RuntimeException("Xml & BouncyCastle-Provider initialization failed", e); + } + } + + /** + * Helper method for adding informations before the signing. + * Normally {@link #confirmSignature()} is sufficient to be used. + */ + @SuppressWarnings("unchecked") + public DigestInfo preSign(Document document, List digestInfos) + throws ParserConfigurationException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, MarshalException, + javax.xml.crypto.dsig.XMLSignatureException, + TransformerFactoryConfigurationError, TransformerException, + IOException, SAXException, NoSuchProviderException, XmlException, URISyntaxException { + signatureConfig.init(false); + + // it's necessary to explicitly set the mdssi namespace, but the sign() method has no + // normal way to interfere with, so we need to add the namespace under the hand ... + EventTarget target = (EventTarget)document; + EventListener creationListener = signatureConfig.getSignatureMarshalListener(); + if (creationListener != null) { + if (creationListener instanceof SignatureMarshalListener) { + ((SignatureMarshalListener)creationListener).setEventTarget(target); + } + SignatureMarshalListener.setListener(target, creationListener, true); + } + + /* + * Signature context construction. + */ + XMLSignContext xmlSignContext = new DOMSignContext(signatureConfig.getKey(), document); + URIDereferencer uriDereferencer = signatureConfig.getUriDereferencer(); + if (null != uriDereferencer) { + xmlSignContext.setURIDereferencer(uriDereferencer); + } + + for (Map.Entry me : signatureConfig.getNamespacePrefixes().entrySet()) { + xmlSignContext.putNamespacePrefix(me.getKey(), me.getValue()); + } + xmlSignContext.setDefaultNamespacePrefix(""); // signatureConfig.getNamespacePrefixes().get(XML_DIGSIG_NS)); + + XMLSignatureFactory signatureFactory = SignatureInfo.getSignatureFactory(); + + /* + * Add ds:References that come from signing client local files. + */ + List references = new ArrayList(); + for (DigestInfo digestInfo : safe(digestInfos)) { + byte[] documentDigestValue = digestInfo.digestValue; + + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); + + String uri = new File(digestInfo.description).getName(); + + Reference reference = signatureFactory.newReference + (uri, digestMethod, null, null, null, documentDigestValue); + references.add(reference); + } + + /* + * Invoke the signature facets. + */ + List objects = new ArrayList(); + for (SignatureFacet signatureFacet : signatureConfig.getSignatureFacets()) { + LOG.log(POILogger.DEBUG, "invoking signature facet: " + signatureFacet.getClass().getSimpleName()); + signatureFacet.preSign(document, signatureFactory, references, objects); + } + + /* + * ds:SignedInfo + */ + SignatureMethod signatureMethod = signatureFactory.newSignatureMethod + (signatureConfig.getSignatureMethod(), null); + CanonicalizationMethod canonicalizationMethod = signatureFactory + .newCanonicalizationMethod(signatureConfig.getCanonicalizationMethod(), + (C14NMethodParameterSpec) null); + SignedInfo signedInfo = signatureFactory.newSignedInfo( + canonicalizationMethod, signatureMethod, references); + + /* + * JSR105 ds:Signature creation + */ + String signatureValueId = signatureConfig.getPackageSignatureId() + "-signature-value"; + javax.xml.crypto.dsig.XMLSignature xmlSignature = signatureFactory + .newXMLSignature(signedInfo, null, objects, signatureConfig.getPackageSignatureId(), + signatureValueId); + + /* + * ds:Signature Marshalling. + */ + xmlSignature.sign(xmlSignContext); + + /* + * Completion of undigested ds:References in the ds:Manifests. + */ + for (XMLObject object : objects) { + LOG.log(POILogger.DEBUG, "object java type: " + object.getClass().getName()); + List objectContentList = object.getContent(); + for (XMLStructure objectContent : objectContentList) { + LOG.log(POILogger.DEBUG, "object content java type: " + objectContent.getClass().getName()); + if (!(objectContent instanceof Manifest)) continue; + Manifest manifest = (Manifest) objectContent; + List manifestReferences = manifest.getReferences(); + for (Reference manifestReference : manifestReferences) { + if (manifestReference.getDigestValue() != null) continue; + + DOMReference manifestDOMReference = (DOMReference)manifestReference; + manifestDOMReference.digest(xmlSignContext); + } + } + } + + /* + * Completion of undigested ds:References. + */ + List signedInfoReferences = signedInfo.getReferences(); + for (Reference signedInfoReference : signedInfoReferences) { + DOMReference domReference = (DOMReference)signedInfoReference; + + // ds:Reference with external digest value + if (domReference.getDigestValue() != null) continue; + + domReference.digest(xmlSignContext); + } + + /* + * Calculation of XML signature digest value. + */ + DOMSignedInfo domSignedInfo = (DOMSignedInfo)signedInfo; + ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); + domSignedInfo.canonicalize(xmlSignContext, dataStream); + byte[] octets = dataStream.toByteArray(); + + /* + * TODO: we could be using DigestOutputStream here to optimize memory + * usage. + */ + + MessageDigest md = CryptoFunctions.getMessageDigest(signatureConfig.getDigestAlgo()); + byte[] digestValue = md.digest(octets); + + + String description = signatureConfig.getSignatureDescription(); + return new DigestInfo(digestValue, signatureConfig.getDigestAlgo(), description); + } + + /** + * Helper method for adding informations after the signing. + * Normally {@link #confirmSignature()} is sufficient to be used. + */ + public void postSign(Document document, byte[] signatureValue) + throws IOException, MarshalException, ParserConfigurationException, XmlException { + LOG.log(POILogger.DEBUG, "postSign"); + + /* + * Check ds:Signature node. + */ + String signatureId = signatureConfig.getPackageSignatureId(); + if (!signatureId.equals(document.getDocumentElement().getAttribute("Id"))) { + throw new RuntimeException("ds:Signature not found for @Id: " + signatureId); + } + + /* + * Insert signature value into the ds:SignatureValue element + */ + NodeList sigValNl = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue"); + if (sigValNl.getLength() != 1) { + throw new RuntimeException("preSign has to be called before postSign"); + } + sigValNl.item(0).setTextContent(Base64.encode(signatureValue)); + + /* + * Allow signature facets to inject their own stuff. + */ + for (SignatureFacet signatureFacet : signatureConfig.getSignatureFacets()) { + signatureFacet.postSign(document, signatureConfig.getSigningCertificateChain()); + } + + writeDocument(document); + } + + protected void writeDocument(Document document) throws IOException, XmlException { + XmlOptions xo = new XmlOptions(); + Map namespaceMap = new HashMap(); + for(Map.Entry entry : signatureConfig.getNamespacePrefixes().entrySet()){ + namespaceMap.put(entry.getValue(), entry.getKey()); + } + xo.setSaveSuggestedPrefixes(namespaceMap); + xo.setUseDefaultNamespace(); + + LOG.log(POILogger.DEBUG, "output signed Office OpenXML document"); + + /* + * Copy the original OOXML content to the signed OOXML package. During + * copying some files need to changed. + */ + OPCPackage pkg = signatureConfig.getOpcPackage(); + + PackagePartName sigPartName, sigsPartName; + try { + // + sigPartName = PackagingURIHelper.createPartName("/_xmlsignatures/sig1.xml"); + // + sigsPartName = PackagingURIHelper.createPartName("/_xmlsignatures/origin.sigs"); + } catch (InvalidFormatException e) { + throw new IOException(e); + } + + PackagePart sigPart = pkg.getPart(sigPartName); + if (sigPart == null) { + sigPart = pkg.createPart(sigPartName, ContentTypes.DIGITAL_SIGNATURE_XML_SIGNATURE_PART); + } + + OutputStream os = sigPart.getOutputStream(); + SignatureDocument sigDoc = SignatureDocument.Factory.parse(document); + sigDoc.save(os, xo); + os.close(); + + PackagePart sigsPart = pkg.getPart(sigsPartName); + if (sigsPart == null) { + // touch empty marker file + sigsPart = pkg.createPart(sigsPartName, ContentTypes.DIGITAL_SIGNATURE_ORIGIN_PART); + } + + PackageRelationshipCollection relCol = pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); + for (PackageRelationship pr : relCol) { + pkg.removeRelationship(pr.getId()); + } + pkg.addRelationship(sigsPartName, TargetMode.INTERNAL, PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); + + sigsPart.addRelationship(sigPartName, TargetMode.INTERNAL, PackageRelationshipTypes.DIGITAL_SIGNATURE); + } + + @SuppressWarnings("unchecked") + private static List safe(List other) { + return other == null ? Collections.EMPTY_LIST : other; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalListener.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalListener.java new file mode 100644 index 0000000000..f0f43c674d --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureMarshalListener.java @@ -0,0 +1,92 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.poifs.crypt.dsig; + +import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.OO_DIGSIG_NS; +import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_NS; + +import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; +import org.w3c.dom.events.MutationEvent; + +/** + * This listener class is used, to modify the to be digested xml document, + * e.g. to register id attributes or set prefixes for registered namespaces + */ +public class SignatureMarshalListener implements EventListener, SignatureConfigurable { + ThreadLocal target = new ThreadLocal(); + SignatureConfig signatureConfig; + public void setEventTarget(EventTarget target) { + this.target.set(target); + } + + public void handleEvent(Event e) { + if (!(e instanceof MutationEvent)) return; + MutationEvent mutEvt = (MutationEvent)e; + EventTarget et = mutEvt.getTarget(); + if (!(et instanceof Element)) return; + handleElement((Element)et); + } + + public void handleElement(Element el) { + EventTarget target = this.target.get(); + String packageId = signatureConfig.getPackageSignatureId(); + if (el.hasAttribute("Id")) { + el.setIdAttribute("Id", true); + } + + setListener(target, this, false); + if (packageId.equals(el.getAttribute("Id"))) { + el.setAttributeNS(XML_NS, "xmlns:mdssi", OO_DIGSIG_NS); + } + setPrefix(el); + setListener(target, this, true); + } + + // helper method to keep it in one place + public static void setListener(EventTarget target, EventListener listener, boolean enabled) { + String type = "DOMSubtreeModified"; + boolean useCapture = false; + if (enabled) { + target.addEventListener(type, listener, useCapture); + } else { + target.removeEventListener(type, listener, useCapture); + } + } + + protected void setPrefix(Node el) { + String prefix = signatureConfig.getNamespacePrefixes().get(el.getNamespaceURI()); + if (prefix != null && el.getPrefix() == null) { + el.setPrefix(prefix); + } + + NodeList nl = el.getChildNodes(); + for (int i=0; i signingCertificateChain) { + // empty + } + + @Override + public void preSign(Document document + , XMLSignatureFactory signatureFactory + , List references + , List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); + + List transforms = new ArrayList(); + Transform envelopedTransform = signatureFactory.newTransform + (CanonicalizationMethod.ENVELOPED, (TransformParameterSpec) null); + transforms.add(envelopedTransform); + Transform exclusiveTransform = signatureFactory.newTransform + (CanonicalizationMethod.EXCLUSIVE, (TransformParameterSpec) null); + transforms.add(exclusiveTransform); + + Reference reference = signatureFactory.newReference("", digestMethod, + transforms, null, null); + + references.add(reference); + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/KeyInfoSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/KeyInfoSignatureFacet.java new file mode 100644 index 0000000000..2bdcc4022a --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/KeyInfoSignatureFacet.java @@ -0,0 +1,168 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.facets; + +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.keyinfo.KeyValue; +import javax.xml.crypto.dsig.keyinfo.X509Data; + +import org.apache.jcp.xml.dsig.internal.dom.DOMKeyInfo; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig; +import org.apache.poi.poifs.crypt.dsig.SignatureInfo; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Signature Facet implementation that adds ds:KeyInfo to the XML signature. + * + * @author Frank Cornelis + * + */ +public class KeyInfoSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(KeyInfoSignatureFacet.class); + + SignatureConfig signatureConfig; + + public void setSignatureConfig(SignatureConfig signatureConfig) { + this.signatureConfig = signatureConfig; + } + + @Override + public void postSign(Document document, List signingCertificateChain) + throws MarshalException { + LOG.log(POILogger.DEBUG, "postSign"); + + NodeList nl = document.getElementsByTagNameNS(XML_DIGSIG_NS, "Object"); + + /* + * Make sure we insert right after the ds:SignatureValue element, just + * before the first ds:Object element. + */ + Node nextSibling = (nl.getLength() == 0) ? null : nl.item(0); + + /* + * Construct the ds:KeyInfo element using JSR 105. + */ + KeyInfoFactory keyInfoFactory = SignatureInfo.getKeyInfoFactory(); + List x509DataObjects = new ArrayList(); + X509Certificate signingCertificate = signingCertificateChain.get(0); + + List keyInfoContent = new ArrayList(); + + if (signatureConfig.isIncludeKeyValue()) { + KeyValue keyValue; + try { + keyValue = keyInfoFactory.newKeyValue(signingCertificate.getPublicKey()); + } catch (KeyException e) { + throw new RuntimeException("key exception: " + e.getMessage(), e); + } + keyInfoContent.add(keyValue); + } + + if (signatureConfig.isIncludeIssuerSerial()) { + x509DataObjects.add(keyInfoFactory.newX509IssuerSerial( + signingCertificate.getIssuerX500Principal().toString(), + signingCertificate.getSerialNumber())); + } + + if (signatureConfig.isIncludeEntireCertificateChain()) { + x509DataObjects.addAll(signingCertificateChain); + } else { + x509DataObjects.add(signingCertificate); + } + + if (!x509DataObjects.isEmpty()) { + X509Data x509Data = keyInfoFactory.newX509Data(x509DataObjects); + keyInfoContent.add(x509Data); + } + KeyInfo keyInfo = keyInfoFactory.newKeyInfo(keyInfoContent); + DOMKeyInfo domKeyInfo = (DOMKeyInfo)keyInfo; + + Key key = new Key() { + private static final long serialVersionUID = 1L; + + public String getAlgorithm() { + return null; + } + + public byte[] getEncoded() { + return null; + } + + public String getFormat() { + return null; + } + }; + + Element n = document.getDocumentElement(); + DOMSignContext domSignContext = new DOMSignContext(key, n, nextSibling); + for (Map.Entry me : signatureConfig.getNamespacePrefixes().entrySet()) { + domSignContext.putNamespacePrefix(me.getKey(), me.getValue()); + } + + DOMStructure domStructure = new DOMStructure(n); + domKeyInfo.marshal(domStructure, domSignContext); + + // move keyinfo into the right place + if (nextSibling != null) { + NodeList kiNl = document.getElementsByTagNameNS(XML_DIGSIG_NS, "KeyInfo"); + if (kiNl.getLength() != 1) { + throw new RuntimeException("KeyInfo wasn't set"); + } + nextSibling.getParentNode().insertBefore(kiNl.item(0), nextSibling); + } + } + + @Override + public void preSign( + Document document + , XMLSignatureFactory signatureFactory + , List references + , List objects + ) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + // empty + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..28626e8270 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java @@ -0,0 +1,504 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.facets; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TimeZone; + +import javax.xml.XMLConstants; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Manifest; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureProperties; +import javax.xml.crypto.dsig.SignatureProperty; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.ContentTypes; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; +import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.openxml4j.opc.TargetMode; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig; +import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService; +import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService.RelationshipTransformParameterSpec; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xmlbeans.XmlException; +import org.openxmlformats.schemas.xpackage.x2006.digitalSignature.CTSignatureTime; +import org.openxmlformats.schemas.xpackage.x2006.digitalSignature.SignatureTimeDocument; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.microsoft.schemas.office.x2006.digsig.CTSignatureInfoV1; +import com.microsoft.schemas.office.x2006.digsig.SignatureInfoV1Document; + +/** + * Office OpenXML Signature Facet implementation. + * + * @author fcorneli + * @see http://msdn.microsoft.com/en-us/library/cc313071.aspx + */ +public class OOXMLSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(OOXMLSignatureFacet.class); + + private SignatureConfig signatureConfig; + + public void setSignatureConfig(SignatureConfig signatureConfig) { + this.signatureConfig = signatureConfig; + } + + @Override + public void preSign( + Document document + , XMLSignatureFactory signatureFactory + , List references + , List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException, URISyntaxException, XmlException { + LOG.log(POILogger.DEBUG, "pre sign"); + addManifestObject(document, signatureFactory, references, objects); + addSignatureInfo(document, signatureFactory, references, objects); + } + + protected void addManifestObject( + Document document + , XMLSignatureFactory signatureFactory + , List references + , List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException, URISyntaxException, XmlException { + + List manifestReferences = new ArrayList(); + addManifestReferences(signatureFactory, manifestReferences); + Manifest manifest = signatureFactory.newManifest(manifestReferences); + + String objectId = "idPackageObject"; // really has to be this value. + List objectContent = new ArrayList(); + objectContent.add(manifest); + + addSignatureTime(document, signatureFactory, objectContent); + + XMLObject xo = signatureFactory.newXMLObject(objectContent, objectId, null, null); + objects.add(xo); + + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); + Reference reference = signatureFactory.newReference + ("#" + objectId, digestMethod, null, XML_DIGSIG_NS+"Object", null); + references.add(reference); + } + + protected void addManifestReferences + (XMLSignatureFactory signatureFactory, List manifestReferences) + throws IOException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, URISyntaxException, XmlException { + + OPCPackage ooxml = signatureConfig.getOpcPackage(); + List relsEntryNames = ooxml.getPartsByContentType(ContentTypes.RELATIONSHIPS_PART); + + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); + Set digestedPartNames = new HashSet(); + for (PackagePart pp : relsEntryNames) { + String baseUri = pp.getPartName().getName().replaceFirst("(.*)/_rels/.*", "$1"); + + PackageRelationshipCollection prc; + try { + prc = new PackageRelationshipCollection(ooxml); + prc.parseRelationshipsPart(pp); + } catch (InvalidFormatException e) { + throw new IOException("Invalid relationship descriptor: "+pp.getPartName().getName(), e); + } + + RelationshipTransformParameterSpec parameterSpec = new RelationshipTransformParameterSpec(); + for (PackageRelationship relationship : prc) { + String relationshipType = relationship.getRelationshipType(); + + /* + * ECMA-376 Part 2 - 3rd edition + * 13.2.4.16 Manifest Element + * "The producer shall not create a Manifest element that references any data outside of the package." + */ + if (TargetMode.EXTERNAL == relationship.getTargetMode()) { + continue; + } + + if (!isSignedRelationship(relationshipType)) continue; + + parameterSpec.addRelationshipReference(relationship.getId()); + + // TODO: find a better way ... + String partName = baseUri + relationship.getTargetURI().toString(); + partName = new URI(partName).normalize().getPath().replace('\\', '/'); + LOG.log(POILogger.DEBUG, "part name: " + partName); + + String contentType; + try { + PackagePartName relName = PackagingURIHelper.createPartName(partName); + PackagePart pp2 = ooxml.getPart(relName); + contentType = pp2.getContentType(); + } catch (InvalidFormatException e) { + throw new IOException(e); + } + + if (relationshipType.endsWith("customXml") + && !(contentType.equals("inkml+xml") || contentType.equals("text/xml"))) { + LOG.log(POILogger.DEBUG, "skipping customXml with content type: " + contentType); + continue; + } + + if (!digestedPartNames.contains(partName)) { + // We only digest a part once. + String uri = partName + "?ContentType=" + contentType; + Reference reference = signatureFactory.newReference(uri, digestMethod); + manifestReferences.add(reference); + digestedPartNames.add(partName); + } + } + + if (parameterSpec.hasSourceIds()) { + List transforms = new ArrayList(); + transforms.add(signatureFactory.newTransform( + RelationshipTransformService.TRANSFORM_URI, + parameterSpec)); + transforms.add(signatureFactory.newTransform( + CanonicalizationMethod.INCLUSIVE, + (TransformParameterSpec) null)); + String uri = pp.getPartName().getName() + + "?ContentType=application/vnd.openxmlformats-package.relationships+xml"; + Reference reference = signatureFactory.newReference(uri, digestMethod, transforms, null, null); + manifestReferences.add(reference); + } + } + } + + + protected void addSignatureTime( + Document document + , XMLSignatureFactory signatureFactory + , List objectContent) { + /* + * SignatureTime + */ + DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + fmt.setTimeZone(TimeZone.getTimeZone("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); + + Element n = (Element)document.importNode(ctTime.getDomNode(),true); + List signatureTimeContent = new ArrayList(); + signatureTimeContent.add(new DOMStructure(n)); + SignatureProperty signatureTimeSignatureProperty = signatureFactory + .newSignatureProperty(signatureTimeContent, "#" + signatureConfig.getPackageSignatureId(), + "idSignatureTime"); + List signaturePropertyContent = new ArrayList(); + signaturePropertyContent.add(signatureTimeSignatureProperty); + SignatureProperties signatureProperties = signatureFactory + .newSignatureProperties(signaturePropertyContent, + "id-signature-time-" + signatureConfig.getExecutionTime()); + objectContent.add(signatureProperties); + } + + protected void addSignatureInfo(Document document, + XMLSignatureFactory signatureFactory, + List references, + List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + List objectContent = new ArrayList(); + + SignatureInfoV1Document sigV1 = SignatureInfoV1Document.Factory.newInstance(); + CTSignatureInfoV1 ctSigV1 = sigV1.addNewSignatureInfoV1(); + ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri()); + Element n = (Element)document.importNode(ctSigV1.getDomNode(), true); + n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS); + + List signatureInfoContent = new ArrayList(); + signatureInfoContent.add(new DOMStructure(n)); + SignatureProperty signatureInfoSignatureProperty = signatureFactory + .newSignatureProperty(signatureInfoContent, "#" + signatureConfig.getPackageSignatureId(), + "idOfficeV1Details"); + + List signaturePropertyContent = new ArrayList(); + signaturePropertyContent.add(signatureInfoSignatureProperty); + SignatureProperties signatureProperties = signatureFactory + .newSignatureProperties(signaturePropertyContent, null); + objectContent.add(signatureProperties); + + String objectId = "idOfficeObject"; + objects.add(signatureFactory.newXMLObject(objectContent, objectId, null, null)); + + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); + Reference reference = signatureFactory.newReference + ("#" + objectId, digestMethod, null, XML_DIGSIG_NS+"Object", null); + references.add(reference); + } + + @Override + public void postSign(Document document, List signingCertificateChain) { + // empty + } + + protected static String getRelationshipReferenceURI(String zipEntryName) { + return "/" + + zipEntryName + + "?ContentType=application/vnd.openxmlformats-package.relationships+xml"; + } + + protected static String getResourceReferenceURI(String resourceName, String contentType) { + return "/" + resourceName + "?ContentType=" + contentType; + } + + protected static boolean isSignedRelationship(String relationshipType) { + LOG.log(POILogger.DEBUG, "relationship type: " + relationshipType); + for (String signedTypeExtension : signed) { + if (relationshipType.endsWith(signedTypeExtension)) { + return true; + } + } + if (relationshipType.endsWith("customXml")) { + LOG.log(POILogger.DEBUG, "customXml relationship type"); + return true; + } + return false; + } + + public static final String[] contentTypes = { + /* + * Word + */ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", + "application/vnd.openxmlformats-officedocument.theme+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", + + /* + * Word 2010 + */ + "application/vnd.ms-word.stylesWithEffects+xml", + + /* + * Excel + */ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", + + /* + * Powerpoint + */ + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slide+xml", + "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml", + + /* + * Powerpoint 2010 + */ + "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml", + "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" + }; + + /** + * Office 2010 list of signed types (extensions). + */ + public static final String[] signed = { + "powerPivotData", // + "activeXControlBinary", // + "attachedToolbars", // + "connectorXml", // + "downRev", // + "functionPrototypes", // + "graphicFrameDoc", // + "groupShapeXml", // + "ink", // + "keyMapCustomizations", // + "legacyDiagramText", // + "legacyDocTextInfo", // + "officeDocument", // + "pictureXml", // + "shapeXml", // + "smartTags", // + "ui/altText", // + "ui/buttonSize", // + "ui/controlID", // + "ui/description", // + "ui/enabled", // + "ui/extensibility", // + "ui/helperText", // + "ui/imageID", // + "ui/imageMso", // + "ui/keyTip", // + "ui/label", // + "ui/lcid", // + "ui/loud", // + "ui/pressed", // + "ui/progID", // + "ui/ribbonID", // + "ui/showImage", // + "ui/showLabel", // + "ui/supertip", // + "ui/target", // + "ui/text", // + "ui/title", // + "ui/tooltip", // + "ui/userCustomization", // + "ui/visible", // + "userXmlData", // + "vbaProject", // + "wordVbaData", // + "wsSortMap", // + "xlBinaryIndex", // + "xlExternalLinkPath/xlAlternateStartup", // + "xlExternalLinkPath/xlLibrary", // + "xlExternalLinkPath/xlPathMissing", // + "xlExternalLinkPath/xlStartup", // + "xlIntlMacrosheet", // + "xlMacrosheet", // + "customData", // + "diagramDrawing", // + "hdphoto", // + "inkXml", // + "media", // + "slicer", // + "slicerCache", // + "stylesWithEffects", // + "ui/extensibility", // + "chartColorStyle", // + "chartLayout", // + "chartStyle", // + "dictionary", // + "timeline", // + "timelineCache", // + "aFChunk", // + "attachedTemplate", // + "audio", // + "calcChain", // + "chart", // + "chartsheet", // + "chartUserShapes", // + "commentAuthors", // + "comments", // + "connections", // + "control", // + "customProperty", // + "customXml", // + "diagramColors", // + "diagramData", // + "diagramLayout", // + "diagramQuickStyle", // + "dialogsheet", // + "drawing", // + "endnotes", // + "externalLink", // + "externalLinkPath", // + "font", // + "fontTable", // + "footer", // + "footnotes", // + "glossaryDocument", // + "handoutMaster", // + "header", // + "hyperlink", // + "image", // + "mailMergeHeaderSource", // + "mailMergeRecipientData", // + "mailMergeSource", // + "notesMaster", // + "notesSlide", // + "numbering", // + "officeDocument", // + "oleObject", // + "package", // + "pivotCacheDefinition", // + "pivotCacheRecords", // + "pivotTable", // + "presProps", // + "printerSettings", // + "queryTable", // + "recipientData", // + "settings", // + "sharedStrings", // + "sheetMetadata", // + "slide", // + "slideLayout", // + "slideMaster", // + "slideUpdateInfo", // + "slideUpdateUrl", // + "styles", // + "table", // + "tableSingleCells", // + "tableStyles", // + "tags", // + "theme", // + "themeOverride", // + "transform", // + "video", // + "viewProps", // + "volatileDependencies", // + "webSettings", // + "worksheet", // + "xmlMaps", // + "ctrlProp", // + "customData", // + "diagram", // + "diagramColorsHeader", // + "diagramLayoutHeader", // + "diagramQuickStyleHeader", // + "documentParts", // + "slicer", // + "slicerCache", // + "vmlDrawing" // + }; +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/Office2010SignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/Office2010SignatureFacet.java new file mode 100644 index 0000000000..d64eac3196 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/Office2010SignatureFacet.java @@ -0,0 +1,94 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.facets; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; + +import org.apache.poi.poifs.crypt.dsig.SignatureConfig; +import org.apache.xmlbeans.XmlException; +import org.etsi.uri.x01903.v13.QualifyingPropertiesType; +import org.etsi.uri.x01903.v13.UnsignedPropertiesType; +import org.etsi.uri.x01903.v13.UnsignedSignaturePropertiesType; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Work-around for Office2010 to accept the XAdES-BES/EPES signature. + * + * xades:UnsignedProperties/xades:UnsignedSignatureProperties needs to be + * present. + * + * @author Frank Cornelis + * + */ +public class Office2010SignatureFacet implements SignatureFacet { + + public void setSignatureConfig(SignatureConfig signatureConfig) { + // this.signatureConfig = signatureConfig; + } + + @Override + public void preSign( + Document document + , XMLSignatureFactory signatureFactory + , List references + , List objects + ) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + } + + @Override + public void postSign(Document document, List signingCertificateChain) + throws XmlException { + // check for XAdES-BES + NodeList nl = document.getElementsByTagNameNS(XADES_132_NS, "QualifyingProperties"); + if (nl.getLength() != 1) { + throw new IllegalArgumentException("no XAdES-BES extension present"); + } + + QualifyingPropertiesType qualProps = + QualifyingPropertiesType.Factory.parse(nl.item(0)); + + // create basic XML container structure + UnsignedPropertiesType unsignedProps = qualProps.getUnsignedProperties(); + if (unsignedProps == null) { + unsignedProps = qualProps.addNewUnsignedProperties(); + } + UnsignedSignaturePropertiesType unsignedSigProps = unsignedProps.getUnsignedSignatureProperties(); + if (unsignedSigProps == null) { + unsignedSigProps = unsignedProps.addNewUnsignedSignatureProperties(); + } + + Node n = document.importNode(qualProps.getDomNode().getFirstChild(), true); + nl.item(0).getParentNode().replaceChild(n, nl.item(0)); + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignatureFacet.java new file mode 100644 index 0000000000..188830bdf0 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignatureFacet.java @@ -0,0 +1,96 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.facets; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.xml.XMLConstants; +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; + +import org.apache.poi.openxml4j.opc.PackageNamespaces; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable; +import org.apache.xmlbeans.XmlException; +import org.w3c.dom.Document; + +/** + * JSR105 Signature Facet interface. + * + * @author Frank Cornelis + * + */ +public interface SignatureFacet extends SignatureConfigurable { + + String XML_NS = XMLConstants.XMLNS_ATTRIBUTE_NS_URI; + String XML_DIGSIG_NS = XMLSignature.XMLNS; + String OO_DIGSIG_NS = PackageNamespaces.DIGITAL_SIGNATURE; + String MS_DIGSIG_NS = "http://schemas.microsoft.com/office/2006/digsig"; + String XADES_132_NS = "http://uri.etsi.org/01903/v1.3.2#"; + String XADES_141_NS = "http://uri.etsi.org/01903/v1.4.1#"; + + + /** + * This method is being invoked by the XML signature service engine during + * pre-sign phase. Via this method a signature facet implementation can add + * signature facets to an XML signature. + * + * @param signatureFactory + * @param document + * @param signatureId + * @param signingCertificateChain + * the optional signing certificate chain + * @param references + * @param objects + * @throws InvalidAlgorithmParameterException + * @throws NoSuchAlgorithmException + */ + void preSign( + Document document + , XMLSignatureFactory signatureFactory + , List references + , List objects + ) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, IOException, URISyntaxException, XmlException; + + /** + * This method is being invoked by the XML signature service engine during + * the post-sign phase. Via this method a signature facet can extend the XML + * signatures with for example key information. + * + * @param signatureElement + * @param signingCertificateChain + */ + void postSign( + Document document + , List signingCertificateChain + ) throws MarshalException, XmlException; +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java new file mode 100644 index 0000000000..d34b367dda --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java @@ -0,0 +1,304 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.facets; + +import java.security.InvalidAlgorithmParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig; +import org.apache.poi.poifs.crypt.dsig.services.SignaturePolicyService; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlObject; +import org.apache.xmlbeans.XmlString; +import org.etsi.uri.x01903.v13.AnyType; +import org.etsi.uri.x01903.v13.CertIDListType; +import org.etsi.uri.x01903.v13.CertIDType; +import org.etsi.uri.x01903.v13.ClaimedRolesListType; +import org.etsi.uri.x01903.v13.DataObjectFormatType; +import org.etsi.uri.x01903.v13.DigestAlgAndValueType; +import org.etsi.uri.x01903.v13.IdentifierType; +import org.etsi.uri.x01903.v13.ObjectIdentifierType; +import org.etsi.uri.x01903.v13.QualifyingPropertiesDocument; +import org.etsi.uri.x01903.v13.QualifyingPropertiesType; +import org.etsi.uri.x01903.v13.SigPolicyQualifiersListType; +import org.etsi.uri.x01903.v13.SignaturePolicyIdType; +import org.etsi.uri.x01903.v13.SignaturePolicyIdentifierType; +import org.etsi.uri.x01903.v13.SignedDataObjectPropertiesType; +import org.etsi.uri.x01903.v13.SignedPropertiesType; +import org.etsi.uri.x01903.v13.SignedSignaturePropertiesType; +import org.etsi.uri.x01903.v13.SignerRoleType; +import org.w3.x2000.x09.xmldsig.DigestMethodType; +import org.w3.x2000.x09.xmldsig.X509IssuerSerialType; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * XAdES Signature Facet. Implements XAdES v1.4.1 which is compatible with XAdES + * v1.3.2. The implemented XAdES format is XAdES-BES/EPES. It's up to another + * part of the signature service to upgrade the XAdES-BES to a XAdES-X-L. + * + * This implementation has been tested against an implementation that + * participated multiple ETSI XAdES plugtests. + * + * @author Frank Cornelis + * @see http://en.wikipedia.org/wiki/XAdES + * + */ +public class XAdESSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(XAdESSignatureFacet.class); + + private static final String XADES_TYPE = "http://uri.etsi.org/01903#SignedProperties"; + + private SignatureConfig signatureConfig; + + private Map dataObjectFormatMimeTypes = new HashMap(); + + public void setSignatureConfig(SignatureConfig signatureConfig) { + this.signatureConfig = signatureConfig; + } + + @Override + public void postSign(Document document, List signingCertificateChain) { + LOG.log(POILogger.DEBUG, "postSign"); + } + + @Override + public void preSign(Document document, + XMLSignatureFactory signatureFactory, + List references, List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + LOG.log(POILogger.DEBUG, "preSign"); + + // QualifyingProperties + QualifyingPropertiesDocument qualDoc = QualifyingPropertiesDocument.Factory.newInstance(); + QualifyingPropertiesType qualifyingProperties = qualDoc.addNewQualifyingProperties(); + qualifyingProperties.setTarget("#" + signatureConfig.getPackageSignatureId()); + + // SignedProperties + SignedPropertiesType signedProperties = qualifyingProperties.addNewSignedProperties(); + signedProperties.setId(signatureConfig.getXadesSignatureId()); + + // SignedSignatureProperties + SignedSignaturePropertiesType signedSignatureProperties = signedProperties.addNewSignedSignatureProperties(); + + // SigningTime + Calendar xmlGregorianCalendar = Calendar.getInstance(); + xmlGregorianCalendar.setTimeZone(TimeZone.getTimeZone("Z")); + xmlGregorianCalendar.setTime(signatureConfig.getExecutionTime()); + xmlGregorianCalendar.clear(Calendar.MILLISECOND); + signedSignatureProperties.setSigningTime(xmlGregorianCalendar); + + // SigningCertificate + if (signatureConfig.getSigningCertificateChain() == null + || signatureConfig.getSigningCertificateChain().isEmpty()) { + throw new RuntimeException("no signing certificate chain available"); + } + CertIDListType signingCertificates = signedSignatureProperties.addNewSigningCertificate(); + CertIDType certId = signingCertificates.addNewCert(); + X509Certificate certificate = signatureConfig.getSigningCertificateChain().get(0); + setCertID(certId, signatureConfig, signatureConfig.isXadesIssuerNameNoReverseOrder(), certificate); + + // ClaimedRole + String role = signatureConfig.getXadesRole(); + if (role != null && !role.isEmpty()) { + SignerRoleType signerRole = signedSignatureProperties.addNewSignerRole(); + signedSignatureProperties.setSignerRole(signerRole); + ClaimedRolesListType claimedRolesList = signerRole.addNewClaimedRoles(); + AnyType claimedRole = claimedRolesList.addNewClaimedRole(); + XmlString roleString = XmlString.Factory.newInstance(); + roleString.setStringValue(role); + insertXChild(claimedRole, roleString); + } + + // XAdES-EPES + SignaturePolicyService policyService = signatureConfig.getSignaturePolicyService(); + if (policyService != null) { + SignaturePolicyIdentifierType signaturePolicyIdentifier = + signedSignatureProperties.addNewSignaturePolicyIdentifier(); + + SignaturePolicyIdType signaturePolicyId = signaturePolicyIdentifier.addNewSignaturePolicyId(); + + ObjectIdentifierType objectIdentifier = signaturePolicyId.addNewSigPolicyId(); + objectIdentifier.setDescription(policyService.getSignaturePolicyDescription()); + + IdentifierType identifier = objectIdentifier.addNewIdentifier(); + identifier.setStringValue(policyService.getSignaturePolicyIdentifier()); + + byte[] signaturePolicyDocumentData = policyService.getSignaturePolicyDocument(); + DigestAlgAndValueType sigPolicyHash = signaturePolicyId.addNewSigPolicyHash(); + setDigestAlgAndValue(sigPolicyHash, signaturePolicyDocumentData, signatureConfig.getDigestAlgo()); + + String signaturePolicyDownloadUrl = policyService.getSignaturePolicyDownloadUrl(); + if (null != signaturePolicyDownloadUrl) { + SigPolicyQualifiersListType sigPolicyQualifiers = signaturePolicyId.addNewSigPolicyQualifiers(); + AnyType sigPolicyQualifier = sigPolicyQualifiers.addNewSigPolicyQualifier(); + XmlString spUriElement = XmlString.Factory.newInstance(); + spUriElement.setStringValue(signaturePolicyDownloadUrl); + insertXChild(sigPolicyQualifier, spUriElement); + } + } else if (signatureConfig.isXadesSignaturePolicyImplied()) { + SignaturePolicyIdentifierType signaturePolicyIdentifier = + signedSignatureProperties.addNewSignaturePolicyIdentifier(); + signaturePolicyIdentifier.addNewSignaturePolicyImplied(); + } + + // DataObjectFormat + if (!dataObjectFormatMimeTypes.isEmpty()) { + SignedDataObjectPropertiesType signedDataObjectProperties = + signedProperties.addNewSignedDataObjectProperties(); + + List dataObjectFormats = signedDataObjectProperties + .getDataObjectFormatList(); + for (Map.Entry dataObjectFormatMimeType : this.dataObjectFormatMimeTypes + .entrySet()) { + DataObjectFormatType dataObjectFormat = DataObjectFormatType.Factory.newInstance(); + dataObjectFormat.setObjectReference("#" + dataObjectFormatMimeType.getKey()); + dataObjectFormat.setMimeType(dataObjectFormatMimeType.getValue()); + dataObjectFormats.add(dataObjectFormat); + } + } + + // add XAdES ds:Object + List xadesObjectContent = new ArrayList(); + Element qualDocElSrc = (Element)qualifyingProperties.getDomNode(); + Element qualDocEl = (Element)document.importNode(qualDocElSrc, true); + xadesObjectContent.add(new DOMStructure(qualDocEl)); + XMLObject xadesObject = signatureFactory.newXMLObject(xadesObjectContent, null, null, null); + objects.add(xadesObject); + + // add XAdES ds:Reference + DigestMethod digestMethod = signatureFactory.newDigestMethod(signatureConfig.getDigestMethodUri(), null); + List transforms = new ArrayList(); + Transform exclusiveTransform = signatureFactory + .newTransform(CanonicalizationMethod.INCLUSIVE, + (TransformParameterSpec) null); + transforms.add(exclusiveTransform); + Reference reference = signatureFactory.newReference + ("#"+signatureConfig.getXadesSignatureId(), digestMethod, transforms, XADES_TYPE, null); + references.add(reference); + } + + /** + * Gives back the JAXB DigestAlgAndValue data structure. + * + * @param data + * @param xadesObjectFactory + * @param xmldsigObjectFactory + * @param hashAlgo + * @return + */ + protected static void setDigestAlgAndValue( + DigestAlgAndValueType digestAlgAndValue, + byte[] data, + HashAlgorithm digestAlgo) { + DigestMethodType digestMethod = digestAlgAndValue.addNewDigestMethod(); + digestMethod.setAlgorithm(SignatureConfig.getDigestMethodUri(digestAlgo)); + + MessageDigest messageDigest = CryptoFunctions.getMessageDigest(digestAlgo); + byte[] digestValue = messageDigest.digest(data); + digestAlgAndValue.setDigestValue(digestValue); + } + + /** + * Gives back the JAXB CertID data structure. + */ + protected static void setCertID + (CertIDType certId, SignatureConfig signatureConfig, boolean issuerNameNoReverseOrder, X509Certificate certificate) { + X509IssuerSerialType issuerSerial = certId.addNewIssuerSerial(); + String issuerName; + if (issuerNameNoReverseOrder) { + /* + * Make sure the DN is encoded using the same order as present + * within the certificate. This is an Office2010 work-around. + * Should be reverted back. + * + * XXX: not correct according to RFC 4514. + */ + // TODO: check if issuerName is different on getTBSCertificate + // issuerName = PrincipalUtil.getIssuerX509Principal(certificate).getName().replace(",", ", "); + issuerName = certificate.getIssuerDN().getName().replace(",", ", "); + } else { + issuerName = certificate.getIssuerX500Principal().toString(); + } + issuerSerial.setX509IssuerName(issuerName); + issuerSerial.setX509SerialNumber(certificate.getSerialNumber()); + + byte[] encodedCertificate; + try { + encodedCertificate = certificate.getEncoded(); + } catch (CertificateEncodingException e) { + throw new RuntimeException("certificate encoding error: " + + e.getMessage(), e); + } + DigestAlgAndValueType certDigest = certId.addNewCertDigest(); + setDigestAlgAndValue(certDigest, encodedCertificate, signatureConfig.getXadesDigestAlgo()); + } + + /** + * Adds a mime-type for the given ds:Reference (referred via its @URI). This + * information is added via the xades:DataObjectFormat element. + * + * @param dsReferenceUri + * @param mimetype + */ + public void addMimeType(String dsReferenceUri, String mimetype) { + this.dataObjectFormatMimeTypes.put(dsReferenceUri, mimetype); + } + + protected static void insertXChild(XmlObject root, XmlObject child) { + XmlCursor rootCursor = root.newCursor(); + rootCursor.toEndToken(); + XmlCursor childCursor = child.newCursor(); + childCursor.toNextToken(); + childCursor.moveXml(rootCursor); + childCursor.dispose(); + rootCursor.dispose(); + } + +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java new file mode 100644 index 0000000000..acbb1b9fc6 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java @@ -0,0 +1,433 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.facets; + +import static org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet.insertXChild; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CRLException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; + +import org.apache.poi.poifs.crypt.dsig.SignatureConfig; +import org.apache.poi.poifs.crypt.dsig.services.RevocationData; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xml.security.c14n.Canonicalizer; +import org.apache.xmlbeans.XmlException; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.DERTaggedObject; +import org.bouncycastle.asn1.ocsp.ResponderID; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.RespID; +import org.etsi.uri.x01903.v13.CRLIdentifierType; +import org.etsi.uri.x01903.v13.CRLRefType; +import org.etsi.uri.x01903.v13.CRLRefsType; +import org.etsi.uri.x01903.v13.CRLValuesType; +import org.etsi.uri.x01903.v13.CertIDListType; +import org.etsi.uri.x01903.v13.CertIDType; +import org.etsi.uri.x01903.v13.CertificateValuesType; +import org.etsi.uri.x01903.v13.CompleteCertificateRefsType; +import org.etsi.uri.x01903.v13.CompleteRevocationRefsType; +import org.etsi.uri.x01903.v13.DigestAlgAndValueType; +import org.etsi.uri.x01903.v13.EncapsulatedPKIDataType; +import org.etsi.uri.x01903.v13.OCSPIdentifierType; +import org.etsi.uri.x01903.v13.OCSPRefType; +import org.etsi.uri.x01903.v13.OCSPRefsType; +import org.etsi.uri.x01903.v13.OCSPValuesType; +import org.etsi.uri.x01903.v13.QualifyingPropertiesDocument; +import org.etsi.uri.x01903.v13.QualifyingPropertiesType; +import org.etsi.uri.x01903.v13.ResponderIDType; +import org.etsi.uri.x01903.v13.RevocationValuesType; +import org.etsi.uri.x01903.v13.UnsignedPropertiesType; +import org.etsi.uri.x01903.v13.UnsignedSignaturePropertiesType; +import org.etsi.uri.x01903.v13.XAdESTimeStampType; +import org.etsi.uri.x01903.v14.ValidationDataType; +import org.w3.x2000.x09.xmldsig.CanonicalizationMethodType; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * XAdES-X-L v1.4.1 signature facet. This signature facet implementation will + * upgrade a given XAdES-BES/EPES signature to XAdES-X-L. + * + * We don't inherit from XAdESSignatureFacet as we also want to be able to use + * this facet out of the context of a signature creation. This signature facet + * assumes that the signature is already XAdES-BES/EPES compliant. + * + * This implementation has been tested against an implementation that + * participated multiple ETSI XAdES plugtests. + * + * @author Frank Cornelis + * @see XAdESSignatureFacet + */ +public class XAdESXLSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(XAdESXLSignatureFacet.class); + + private SignatureConfig signatureConfig; + + private String c14nAlgoId = CanonicalizationMethod.EXCLUSIVE; + + private final CertificateFactory certificateFactory; + + public void setSignatureConfig(SignatureConfig signatureConfig) { + this.signatureConfig = signatureConfig; + } + + + + /** + * Convenience constructor. + * + * @param timeStampService + * the time-stamp service used for XAdES-T and XAdES-X. + * @param revocationDataService + */ + public XAdESXLSignatureFacet() { + try { + this.certificateFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("X509 JCA error: " + e.getMessage(), e); + } + } + + public void setCanonicalizerAlgorithm(String c14nAlgoId) { + this.c14nAlgoId = c14nAlgoId; + } + + @Override + public void postSign(Document document, + List signingCertificateChain + ) throws XmlException { + LOG.log(POILogger.DEBUG, "XAdES-X-L post sign phase"); + + QualifyingPropertiesDocument qualDoc = null; + QualifyingPropertiesType qualProps = null; + + // check for XAdES-BES + NodeList qualNl = document.getElementsByTagNameNS(XADES_132_NS, "QualifyingProperties"); + if (qualNl.getLength() == 1) { + qualDoc = QualifyingPropertiesDocument.Factory.parse(qualNl.item(0)); + qualProps = qualDoc.getQualifyingProperties(); + } else { + throw new IllegalArgumentException("no XAdES-BES extension present"); + } + + // create basic XML container structure + UnsignedPropertiesType unsignedProps = qualProps.getUnsignedProperties(); + if (unsignedProps == null) { + unsignedProps = qualProps.addNewUnsignedProperties(); + } + UnsignedSignaturePropertiesType unsignedSigProps = unsignedProps.getUnsignedSignatureProperties(); + if (unsignedSigProps == null) { + unsignedSigProps = unsignedProps.addNewUnsignedSignatureProperties(); + } + + + // create the XAdES-T time-stamp + NodeList nlSigVal = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue"); + if (nlSigVal.getLength() != 1) { + throw new IllegalArgumentException("SignatureValue is not set."); + } + + RevocationData tsaRevocationDataXadesT = new RevocationData(); + LOG.log(POILogger.DEBUG, "creating XAdES-T time-stamp"); + XAdESTimeStampType signatureTimeStamp = createXAdESTimeStamp + (Collections.singletonList(nlSigVal.item(0)), tsaRevocationDataXadesT); + + // marshal the XAdES-T extension + unsignedSigProps.addNewSignatureTimeStamp().set(signatureTimeStamp); + + // xadesv141::TimeStampValidationData + if (tsaRevocationDataXadesT.hasRevocationDataEntries()) { + ValidationDataType validationData = createValidationData(tsaRevocationDataXadesT); + insertXChild(unsignedSigProps, validationData); + } + + if (signatureConfig.getRevocationDataService() == null) { + /* + * Without revocation data service we cannot construct the XAdES-C + * extension. + */ + return; + } + + // XAdES-C: complete certificate refs + CompleteCertificateRefsType completeCertificateRefs = + unsignedSigProps.addNewCompleteCertificateRefs(); + + CertIDListType certIdList = completeCertificateRefs.addNewCertRefs(); + /* + * We skip the signing certificate itself according to section + * 4.4.3.2 of the XAdES 1.4.1 specification. + */ + int chainSize = signingCertificateChain.size(); + if (chainSize > 1) { + for (X509Certificate cert : signingCertificateChain.subList(1, chainSize)) { + CertIDType certId = certIdList.addNewCert(); + XAdESSignatureFacet.setCertID(certId, signatureConfig, false, cert); + } + } + + // XAdES-C: complete revocation refs + CompleteRevocationRefsType completeRevocationRefs = + unsignedSigProps.addNewCompleteRevocationRefs(); + RevocationData revocationData = signatureConfig.getRevocationDataService() + .getRevocationData(signingCertificateChain); + if (revocationData.hasCRLs()) { + CRLRefsType crlRefs = completeRevocationRefs.addNewCRLRefs(); + completeRevocationRefs.setCRLRefs(crlRefs); + + for (byte[] encodedCrl : revocationData.getCRLs()) { + CRLRefType crlRef = crlRefs.addNewCRLRef(); + X509CRL crl; + try { + crl = (X509CRL) this.certificateFactory + .generateCRL(new ByteArrayInputStream(encodedCrl)); + } catch (CRLException e) { + throw new RuntimeException("CRL parse error: " + + e.getMessage(), e); + } + + CRLIdentifierType crlIdentifier = crlRef.addNewCRLIdentifier(); + String issuerName = crl.getIssuerDN().getName().replace(",", ", "); + crlIdentifier.setIssuer(issuerName); + Calendar cal = Calendar.getInstance(); + cal.setTime(crl.getThisUpdate()); + crlIdentifier.setIssueTime(cal); + crlIdentifier.setNumber(getCrlNumber(crl)); + + DigestAlgAndValueType digestAlgAndValue = crlRef.addNewDigestAlgAndValue(); + XAdESSignatureFacet.setDigestAlgAndValue(digestAlgAndValue, encodedCrl, signatureConfig.getDigestAlgo()); + } + } + if (revocationData.hasOCSPs()) { + OCSPRefsType ocspRefs = completeRevocationRefs.addNewOCSPRefs(); + for (byte[] ocsp : revocationData.getOCSPs()) { + try { + OCSPRefType ocspRef = ocspRefs.addNewOCSPRef(); + + DigestAlgAndValueType digestAlgAndValue = ocspRef.addNewDigestAlgAndValue(); + XAdESSignatureFacet.setDigestAlgAndValue(digestAlgAndValue, ocsp, signatureConfig.getDigestAlgo()); + + OCSPIdentifierType ocspIdentifier = ocspRef.addNewOCSPIdentifier(); + + OCSPResp ocspResp = new OCSPResp(ocsp); + + BasicOCSPResp basicOcspResp = (BasicOCSPResp)ocspResp.getResponseObject(); + + Calendar cal = Calendar.getInstance(); + cal.setTime(basicOcspResp.getProducedAt()); + ocspIdentifier.setProducedAt(cal); + + ResponderIDType responderId = ocspIdentifier.addNewResponderID(); + + RespID respId = basicOcspResp.getResponderId(); + ResponderID ocspResponderId = respId.toASN1Object(); + DERTaggedObject derTaggedObject = (DERTaggedObject)ocspResponderId.toASN1Primitive(); + if (2 == derTaggedObject.getTagNo()) { + ASN1OctetString keyHashOctetString = (ASN1OctetString)derTaggedObject.getObject(); + byte key[] = keyHashOctetString.getOctets(); + responderId.setByKey(key); + } else { + X500Name name = X500Name.getInstance(derTaggedObject.getObject()); + String nameStr = name.toString(); + responderId.setByName(nameStr); + } + } catch (Exception e) { + throw new RuntimeException("OCSP decoding error: " + e.getMessage(), e); + } + } + } + + // marshal XAdES-C + + // XAdES-X Type 1 timestamp + List timeStampNodesXadesX1 = new ArrayList(); + timeStampNodesXadesX1.add(nlSigVal.item(0)); + timeStampNodesXadesX1.add(signatureTimeStamp.getDomNode()); + timeStampNodesXadesX1.add(completeCertificateRefs.getDomNode()); + timeStampNodesXadesX1.add(completeRevocationRefs.getDomNode()); + + RevocationData tsaRevocationDataXadesX1 = new RevocationData(); + LOG.log(POILogger.DEBUG, "creating XAdES-X time-stamp"); + XAdESTimeStampType timeStampXadesX1 = createXAdESTimeStamp + (timeStampNodesXadesX1, tsaRevocationDataXadesX1); + if (tsaRevocationDataXadesX1.hasRevocationDataEntries()) { + ValidationDataType timeStampXadesX1ValidationData = createValidationData(tsaRevocationDataXadesX1); + insertXChild(unsignedSigProps, timeStampXadesX1ValidationData); + } + + // marshal XAdES-X + unsignedSigProps.addNewSigAndRefsTimeStamp().set(timeStampXadesX1); + + // XAdES-X-L + CertificateValuesType certificateValues = unsignedSigProps.addNewCertificateValues(); + for (X509Certificate certificate : signingCertificateChain) { + EncapsulatedPKIDataType encapsulatedPKIDataType = certificateValues.addNewEncapsulatedX509Certificate(); + try { + encapsulatedPKIDataType.setByteArrayValue(certificate.getEncoded()); + } catch (CertificateEncodingException e) { + throw new RuntimeException("certificate encoding error: " + e.getMessage(), e); + } + } + + RevocationValuesType revocationValues = unsignedSigProps.addNewRevocationValues(); + createRevocationValues(revocationValues, revocationData); + + // marshal XAdES-X-L + Node n = document.importNode(qualProps.getDomNode(), true); + qualNl.item(0).getParentNode().replaceChild(n, qualNl.item(0)); + } + + public static byte[] getC14nValue(List nodeList, String c14nAlgoId) { + ByteArrayOutputStream c14nValue = new ByteArrayOutputStream(); + try { + for (Node node : nodeList) { + /* + * Re-initialize the c14n else the namespaces will get cached + * and will be missing from the c14n resulting nodes. + */ + Canonicalizer c14n = Canonicalizer.getInstance(c14nAlgoId); + c14nValue.write(c14n.canonicalizeSubtree(node)); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("c14n error: " + e.getMessage(), e); + } + return c14nValue.toByteArray(); + } + + @Override + public void preSign(Document document, + XMLSignatureFactory signatureFactory, + List references, List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + // nothing to do here + } + + private BigInteger getCrlNumber(X509CRL crl) { + try { + byte[] crlNumberExtensionValue = crl.getExtensionValue(Extension.cRLNumber.getId()); + if (null == crlNumberExtensionValue) { + return null; + } + + @SuppressWarnings("resource") + ASN1InputStream asn1InputStream = new ASN1InputStream(crlNumberExtensionValue); + ASN1OctetString octetString = (ASN1OctetString)asn1InputStream.readObject(); + byte[] octets = octetString.getOctets(); + asn1InputStream = new ASN1InputStream(octets); + ASN1Integer integer = (ASN1Integer)asn1InputStream.readObject(); + BigInteger crlNumber = integer.getPositiveValue(); + return crlNumber; + } catch (Exception e) { + throw new RuntimeException("I/O error: " + e.getMessage(), e); + } + } + + private XAdESTimeStampType createXAdESTimeStamp( + List nodeList, + RevocationData revocationData) { + byte[] c14nSignatureValueElement = getC14nValue(nodeList, c14nAlgoId); + + return createXAdESTimeStamp(c14nSignatureValueElement, revocationData); + } + + private XAdESTimeStampType createXAdESTimeStamp(byte[] data, RevocationData revocationData) { + // create the time-stamp + byte[] timeStampToken; + try { + timeStampToken = signatureConfig.getTspService().timeStamp(data, revocationData); + } catch (Exception e) { + throw new RuntimeException("error while creating a time-stamp: " + + e.getMessage(), e); + } + + // create a XAdES time-stamp container + XAdESTimeStampType xadesTimeStamp = XAdESTimeStampType.Factory.newInstance(); + xadesTimeStamp.setId("time-stamp-" + UUID.randomUUID().toString()); + CanonicalizationMethodType c14nMethod = xadesTimeStamp.addNewCanonicalizationMethod(); + c14nMethod.setAlgorithm(c14nAlgoId); + + // embed the time-stamp + EncapsulatedPKIDataType encapsulatedTimeStamp = xadesTimeStamp.addNewEncapsulatedTimeStamp(); + encapsulatedTimeStamp.setByteArrayValue(timeStampToken); + encapsulatedTimeStamp.setId("time-stamp-token-" + UUID.randomUUID().toString()); + + return xadesTimeStamp; + } + + private ValidationDataType createValidationData( + RevocationData revocationData) { + ValidationDataType validationData = ValidationDataType.Factory.newInstance(); + RevocationValuesType revocationValues = validationData.addNewRevocationValues(); + createRevocationValues(revocationValues, revocationData); + return validationData; + } + + private void createRevocationValues( + RevocationValuesType revocationValues, RevocationData revocationData) { + if (revocationData.hasCRLs()) { + CRLValuesType crlValues = revocationValues.addNewCRLValues(); + for (byte[] crl : revocationData.getCRLs()) { + EncapsulatedPKIDataType encapsulatedCrlValue = crlValues.addNewEncapsulatedCRLValue(); + encapsulatedCrlValue.setByteArrayValue(crl); + } + } + if (revocationData.hasOCSPs()) { + OCSPValuesType ocspValues = revocationValues.addNewOCSPValues(); + for (byte[] ocsp : revocationData.getOCSPs()) { + EncapsulatedPKIDataType encapsulatedOcspValue = ocspValues.addNewEncapsulatedOCSPValue(); + encapsulatedOcspValue.setByteArrayValue(ocsp); + } + } + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RelationshipTransformService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RelationshipTransformService.java new file mode 100644 index 0000000000..dea586463b --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RelationshipTransformService.java @@ -0,0 +1,246 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.services; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.Provider; +import java.security.Security; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javax.xml.crypto.Data; +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.OctetStreamData; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.TransformException; +import javax.xml.crypto.dsig.TransformService; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.poi.util.XmlSort; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; +import org.apache.xmlbeans.XmlOptions; +import org.openxmlformats.schemas.xpackage.x2006.digitalSignature.CTRelationshipReference; +import org.openxmlformats.schemas.xpackage.x2006.digitalSignature.RelationshipReferenceDocument; +import org.openxmlformats.schemas.xpackage.x2006.relationships.CTRelationship; +import org.openxmlformats.schemas.xpackage.x2006.relationships.CTRelationships; +import org.openxmlformats.schemas.xpackage.x2006.relationships.RelationshipsDocument; +import org.openxmlformats.schemas.xpackage.x2006.relationships.STTargetMode; +import org.w3.x2000.x09.xmldsig.TransformDocument; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * JSR105 implementation of the RelationshipTransform transformation. + * + *

+ * Specs: http://openiso.org/Ecma/376/Part2/12.2.4#26 + *

+ */ +public class RelationshipTransformService extends TransformService { + + public static final String TRANSFORM_URI = "http://schemas.openxmlformats.org/package/2006/RelationshipTransform"; + + private final List sourceIds; + + private static final POILogger LOG = POILogFactory.getLogger(RelationshipTransformService.class); + + /** + * Relationship Transform parameter specification class. + */ + public static class RelationshipTransformParameterSpec implements TransformParameterSpec { + List sourceIds = new ArrayList(); + public void addRelationshipReference(String relationshipId) { + sourceIds.add(relationshipId); + } + public boolean hasSourceIds() { + return !sourceIds.isEmpty(); + } + } + + + public RelationshipTransformService() { + super(); + LOG.log(POILogger.DEBUG, "constructor"); + this.sourceIds = new ArrayList(); + } + + /** + * Register the provider for this TransformService + * + * @see javax.xml.crypto.dsig.TransformService + */ + public static synchronized void registerDsigProvider() { + // the xml signature classes will try to find a special TransformerService, + // which is ofcourse unknown to JCE before ... + final String dsigProvider = "POIXmlDsigProvider"; + if (Security.getProperty(dsigProvider) == null) { + Provider p = new Provider(dsigProvider, 1.0, dsigProvider){ + static final long serialVersionUID = 1L; + }; + p.put("TransformService." + TRANSFORM_URI, RelationshipTransformService.class.getName()); + p.put("TransformService." + TRANSFORM_URI + " MechanismType", "DOM"); + Security.addProvider(p); + } + } + + + @Override + public void init(TransformParameterSpec params) throws InvalidAlgorithmParameterException { + LOG.log(POILogger.DEBUG, "init(params)"); + if (!(params instanceof RelationshipTransformParameterSpec)) { + throw new InvalidAlgorithmParameterException(); + } + RelationshipTransformParameterSpec relParams = (RelationshipTransformParameterSpec) params; + for (String sourceId : relParams.sourceIds) { + this.sourceIds.add(sourceId); + } + } + + @Override + public void init(XMLStructure parent, XMLCryptoContext context) throws InvalidAlgorithmParameterException { + LOG.log(POILogger.DEBUG, "init(parent,context)"); + LOG.log(POILogger.DEBUG, "parent java type: " + parent.getClass().getName()); + DOMStructure domParent = (DOMStructure) parent; + Node parentNode = domParent.getNode(); + + try { + TransformDocument transDoc = TransformDocument.Factory.parse(parentNode); + XmlObject xoList[] = transDoc.getTransform().selectChildren(RelationshipReferenceDocument.type.getDocumentElementName()); + if (xoList.length == 0) { + LOG.log(POILogger.WARN, "no RelationshipReference/@SourceId parameters present"); + } + for (XmlObject xo : xoList) { + String sourceId = ((CTRelationshipReference)xo).getSourceId(); + LOG.log(POILogger.DEBUG, "sourceId: ", sourceId); + this.sourceIds.add(sourceId); + } + } catch (XmlException e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + @Override + public void marshalParams(XMLStructure parent, XMLCryptoContext context) throws MarshalException { + LOG.log(POILogger.DEBUG, "marshallParams(parent,context)"); + DOMStructure domParent = (DOMStructure) parent; + Element parentNode = (Element)domParent.getNode(); + // parentNode.setAttributeNS(XML_NS, "xmlns:mdssi", XML_DIGSIG_NS); + Document doc = parentNode.getOwnerDocument(); + + for (String sourceId : this.sourceIds) { + RelationshipReferenceDocument relRef = RelationshipReferenceDocument.Factory.newInstance(); + relRef.addNewRelationshipReference().setSourceId(sourceId); + Node n = relRef.getRelationshipReference().getDomNode(); + n = doc.importNode(n, true); + parentNode.appendChild(n); + } + } + + public AlgorithmParameterSpec getParameterSpec() { + LOG.log(POILogger.DEBUG, "getParameterSpec"); + return null; + } + + public Data transform(Data data, XMLCryptoContext context) throws TransformException { + LOG.log(POILogger.DEBUG, "transform(data,context)"); + LOG.log(POILogger.DEBUG, "data java type: " + data.getClass().getName()); + OctetStreamData octetStreamData = (OctetStreamData) data; + LOG.log(POILogger.DEBUG, "URI: " + octetStreamData.getURI()); + InputStream octetStream = octetStreamData.getOctetStream(); + + RelationshipsDocument relDoc; + try { + relDoc = RelationshipsDocument.Factory.parse(octetStream); + } catch (Exception e) { + throw new TransformException(e.getMessage(), e); + } + LOG.log(POILogger.DEBUG, "relationships document", relDoc); + + CTRelationships rels = relDoc.getRelationships(); + List relList = rels.getRelationshipList(); + Iterator relIter = rels.getRelationshipList().iterator(); + while (relIter.hasNext()) { + CTRelationship rel = relIter.next(); + /* + * See: ISO/IEC 29500-2:2008(E) - 13.2.4.24 Relationships Transform + * Algorithm. + */ + if (!this.sourceIds.contains(rel.getId())) { + LOG.log(POILogger.DEBUG, "removing element: " + rel.getId()); + relIter.remove(); + } else { + if (!rel.isSetTargetMode()) { + rel.setTargetMode(STTargetMode.INTERNAL); + } + } + } + + // TODO: remove non element nodes ??? + LOG.log(POILogger.DEBUG, "# Relationship elements", relList.size()); + + XmlSort.sort(rels, new Comparator(){ + public int compare(XmlCursor c1, XmlCursor c2) { + String id1 = ((CTRelationship)c1.getObject()).getId(); + String id2 = ((CTRelationship)c2.getObject()).getId(); + return id1.compareTo(id2); + } + }); + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XmlOptions xo = new XmlOptions(); + xo.setSaveNoXmlDecl(); + relDoc.save(bos, xo); + return new OctetStreamData(new ByteArrayInputStream(bos.toByteArray())); + } catch (IOException e) { + throw new TransformException(e.getMessage(), e); + } + } + + public Data transform(Data data, XMLCryptoContext context, OutputStream os) throws TransformException { + LOG.log(POILogger.DEBUG, "transform(data,context,os)"); + return null; + } + + public boolean isFeatureSupported(String feature) { + LOG.log(POILogger.DEBUG, "isFeatureSupported(feature)"); + return false; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java new file mode 100644 index 0000000000..5f0089a53c --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java @@ -0,0 +1,131 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.services; + +import java.security.cert.CRLException; +import java.security.cert.X509CRL; +import java.util.ArrayList; +import java.util.List; + +/** + * Container class for PKI revocation data. + * + * @author Frank Cornelis + * + */ +public class RevocationData { + + private final List crls; + + private final List ocsps; + + /** + * Default constructor. + */ + public RevocationData() { + this.crls = new ArrayList(); + this.ocsps = new ArrayList(); + } + + /** + * Adds a CRL to this revocation data set. + * + * @param encodedCrl + */ + public void addCRL(byte[] encodedCrl) { + this.crls.add(encodedCrl); + } + + /** + * Adds a CRL to this revocation data set. + * + * @param crl + */ + public void addCRL(X509CRL crl) { + byte[] encodedCrl; + try { + encodedCrl = crl.getEncoded(); + } catch (CRLException e) { + throw new IllegalArgumentException("CRL coding error: " + + e.getMessage(), e); + } + addCRL(encodedCrl); + } + + /** + * Adds an OCSP response to this revocation data set. + * + * @param encodedOcsp + */ + public void addOCSP(byte[] encodedOcsp) { + this.ocsps.add(encodedOcsp); + } + + /** + * Gives back a list of all CRLs. + * + * @return + */ + public List getCRLs() { + return this.crls; + } + + /** + * Gives back a list of all OCSP responses. + * + * @return + */ + public List getOCSPs() { + return this.ocsps; + } + + /** + * Returns true if this revocation data set holds OCSP + * responses. + * + * @return + */ + public boolean hasOCSPs() { + return false == this.ocsps.isEmpty(); + } + + /** + * Returns true if this revocation data set holds CRLs. + * + * @return + */ + public boolean hasCRLs() { + return false == this.crls.isEmpty(); + } + + /** + * Returns true if this revocation data is not empty. + * + * @return + */ + public boolean hasRevocationDataEntries() { + return hasOCSPs() || hasCRLs(); + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationDataService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationDataService.java new file mode 100644 index 0000000000..b519c40e3d --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationDataService.java @@ -0,0 +1,47 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.services; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for a service that retrieves revocation data about some given + * certificate chain. + * + * @author Frank Cornelis + * + */ +public interface RevocationDataService { + + /** + * Gives back the revocation data corresponding with the given certificate + * chain. + * + * @param certificateChain + * @return + */ + RevocationData getRevocationData(List certificateChain); +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/SignaturePolicyService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/SignaturePolicyService.java new file mode 100644 index 0000000000..1dbe1b1a15 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/SignaturePolicyService.java @@ -0,0 +1,65 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.services; + +/** + * Interface for the signature policy service. + * + * @author Frank Cornelis + * + */ +public interface SignaturePolicyService { + + /** + * Gives back the signature policy identifier URI. + * + * @return + */ + String getSignaturePolicyIdentifier(); + + /** + * Gives back the short description of the signature policy or + * null if a description is not available. + * + * @return the description, or null. + */ + String getSignaturePolicyDescription(); + + /** + * Gives back the download URL where the signature policy document can be + * found. Can be null in case such a download location does not + * exist. + * + * @return the download URL, or null. + */ + String getSignaturePolicyDownloadUrl(); + + /** + * Gives back the signature policy document. + * + * @return the bytes of the signature policy document. + */ + byte[] getSignaturePolicyDocument(); +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java new file mode 100644 index 0000000000..0937b0f360 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java @@ -0,0 +1,253 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.services; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.bind.DatatypeConverter; + +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.cmp.PKIFailureInfo; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cms.DefaultCMSSignatureAlgorithmNameGenerator; +import org.bouncycastle.cms.SignerId; +import org.bouncycastle.cms.SignerInformationVerifier; +import org.bouncycastle.cms.bc.BcRSASignerInfoVerifierBuilder; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.bc.BcDigestCalculatorProvider; +import org.bouncycastle.tsp.TimeStampRequest; +import org.bouncycastle.tsp.TimeStampRequestGenerator; +import org.bouncycastle.tsp.TimeStampResponse; +import org.bouncycastle.tsp.TimeStampToken; + +/** + * A TSP time-stamp service implementation. + * + * @author Frank Cornelis + * + */ +public class TSPTimeStampService implements TimeStampService { + + private static final POILogger LOG = POILogFactory.getLogger(TSPTimeStampService.class); + + private SignatureConfig signatureConfig; + + /** + * Maps the digest algorithm to corresponding OID value. + */ + public ASN1ObjectIdentifier mapDigestAlgoToOID(HashAlgorithm digestAlgo) { + switch (digestAlgo) { + case sha1: return X509ObjectIdentifiers.id_SHA1; + case sha256: return NISTObjectIdentifiers.id_sha256; + case sha384: return NISTObjectIdentifiers.id_sha384; + case sha512: return NISTObjectIdentifiers.id_sha512; + default: + throw new IllegalArgumentException("unsupported digest algo: " + digestAlgo); + } + } + + @SuppressWarnings("unchecked") + public byte[] timeStamp(byte[] data, RevocationData revocationData) + throws Exception { + // digest the message + MessageDigest messageDigest = CryptoFunctions.getMessageDigest(signatureConfig.getTspDigestAlgo()); + byte[] digest = messageDigest.digest(data); + + // generate the TSP request + BigInteger nonce = new BigInteger(128, new SecureRandom()); + TimeStampRequestGenerator requestGenerator = new TimeStampRequestGenerator(); + requestGenerator.setCertReq(true); + String requestPolicy = signatureConfig.getTspRequestPolicy(); + if (requestPolicy != null) { + requestGenerator.setReqPolicy(new ASN1ObjectIdentifier(requestPolicy)); + } + ASN1ObjectIdentifier digestAlgoOid = mapDigestAlgoToOID(signatureConfig.getTspDigestAlgo()); + TimeStampRequest request = requestGenerator.generate(digestAlgoOid, digest, nonce); + byte[] encodedRequest = request.getEncoded(); + + // create the HTTP POST request + Proxy proxy = Proxy.NO_PROXY; + if (signatureConfig.getProxyUrl() != null) { + URL proxyUrl = new URL(signatureConfig.getProxyUrl()); + String host = proxyUrl.getHost(); + int port = proxyUrl.getPort(); + proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, (port == -1 ? 80 : port))); + } + + HttpURLConnection huc = (HttpURLConnection)new URL(signatureConfig.getTspUrl()).openConnection(proxy); + + if (signatureConfig.getTspUser() != null) { + String userPassword = signatureConfig.getTspUser() + ":" + signatureConfig.getTspPass(); + String encoding = DatatypeConverter.printBase64Binary(userPassword.getBytes(Charset.forName("iso-8859-1"))); + huc.setRequestProperty("Authorization", "Basic " + encoding); + } + + huc.setDoOutput(true); // also sets method to POST. + huc.setRequestProperty("User-Agent", signatureConfig.getUserAgent()); + huc.setRequestProperty("Content-Type", signatureConfig.isTspOldProtocol() + ? "application/timestamp-request" + : "application/timestamp-query;charset=ISO-8859-1"); + + OutputStream hucOut = huc.getOutputStream(); + hucOut.write(encodedRequest); + + // invoke TSP service + huc.connect(); + + int statusCode = huc.getResponseCode(); + if (statusCode != 200) { + LOG.log(POILogger.ERROR, "Error contacting TSP server ", signatureConfig.getTspUrl()); + throw new IOException("Error contacting TSP server " + signatureConfig.getTspUrl()); + } + + // HTTP input validation + String contentType = huc.getHeaderField("Content-Type"); + if (null == contentType) { + throw new RuntimeException("missing Content-Type header"); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(huc.getInputStream(), bos); + LOG.log(POILogger.DEBUG, "response content: ", bos.toString()); + + if (!contentType.startsWith(signatureConfig.isTspOldProtocol() + ? "application/timestamp-response" + : "application/timestamp-reply" + )) { + throw new RuntimeException("invalid Content-Type: " + contentType); + } + + if (bos.size() == 0) { + throw new RuntimeException("Content-Length is zero"); + } + + // TSP response parsing and validation + TimeStampResponse timeStampResponse = new TimeStampResponse(bos.toByteArray()); + timeStampResponse.validate(request); + + if (0 != timeStampResponse.getStatus()) { + LOG.log(POILogger.DEBUG, "status: " + timeStampResponse.getStatus()); + LOG.log(POILogger.DEBUG, "status string: " + timeStampResponse.getStatusString()); + PKIFailureInfo failInfo = timeStampResponse.getFailInfo(); + if (null != failInfo) { + LOG.log(POILogger.DEBUG, "fail info int value: " + failInfo.intValue()); + if (/*PKIFailureInfo.unacceptedPolicy*/(1 << 8) == failInfo.intValue()) { + LOG.log(POILogger.DEBUG, "unaccepted policy"); + } + } + throw new RuntimeException("timestamp response status != 0: " + + timeStampResponse.getStatus()); + } + TimeStampToken timeStampToken = timeStampResponse.getTimeStampToken(); + SignerId signerId = timeStampToken.getSID(); + BigInteger signerCertSerialNumber = signerId.getSerialNumber(); + X500Name signerCertIssuer = signerId.getIssuer(); + LOG.log(POILogger.DEBUG, "signer cert serial number: " + signerCertSerialNumber); + LOG.log(POILogger.DEBUG, "signer cert issuer: " + signerCertIssuer); + + // TSP signer certificates retrieval + Collection certificates = timeStampToken.getCertificates().getMatches(null); + + X509CertificateHolder signerCert = null; + Map certificateMap = new HashMap(); + for (X509CertificateHolder certificate : certificates) { + if (signerCertIssuer.equals(certificate.getIssuer()) + && signerCertSerialNumber.equals(certificate.getSerialNumber())) { + signerCert = certificate; + } + certificateMap.put(certificate.getSubject(), certificate); + } + + // TSP signer cert path building + if (signerCert == null) { + throw new RuntimeException("TSP response token has no signer certificate"); + } + List tspCertificateChain = new ArrayList(); + JcaX509CertificateConverter x509converter = new JcaX509CertificateConverter(); + x509converter.setProvider("BC"); + X509CertificateHolder certificate = signerCert; + do { + LOG.log(POILogger.DEBUG, "adding to certificate chain: " + certificate.getSubject()); + tspCertificateChain.add(x509converter.getCertificate(certificate)); + if (certificate.getSubject().equals(certificate.getIssuer())) { + break; + } + certificate = certificateMap.get(certificate.getIssuer()); + } while (null != certificate); + + // verify TSP signer signature + X509CertificateHolder holder = new X509CertificateHolder(tspCertificateChain.get(0).getEncoded()); + DefaultCMSSignatureAlgorithmNameGenerator nameGen = new DefaultCMSSignatureAlgorithmNameGenerator(); + DefaultSignatureAlgorithmIdentifierFinder sigAlgoFinder = new DefaultSignatureAlgorithmIdentifierFinder(); + DefaultDigestAlgorithmIdentifierFinder hashAlgoFinder = new DefaultDigestAlgorithmIdentifierFinder(); + BcDigestCalculatorProvider calculator = new BcDigestCalculatorProvider(); + BcRSASignerInfoVerifierBuilder verifierBuilder = new BcRSASignerInfoVerifierBuilder(nameGen, sigAlgoFinder, hashAlgoFinder, calculator); + SignerInformationVerifier verifier = verifierBuilder.build(holder); + + timeStampToken.validate(verifier); + + // verify TSP signer certificate + if (signatureConfig.getTspValidator() != null) { + signatureConfig.getTspValidator().validate(tspCertificateChain, revocationData); + } + + LOG.log(POILogger.DEBUG, "time-stamp token time: " + + timeStampToken.getTimeStampInfo().getGenTime()); + + byte[] timestamp = timeStampToken.getEncoded(); + return timestamp; + } + + public void setSignatureConfig(SignatureConfig signatureConfig) { + this.signatureConfig = signatureConfig; + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampService.java new file mode 100644 index 0000000000..84cde92377 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampService.java @@ -0,0 +1,54 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.services; + +import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable; + + +/** + * Interface for a time-stamp service. + * + * @author Frank Cornelis + * + */ +public interface TimeStampService extends SignatureConfigurable { + + /** + * Gives back the encoded time-stamp token for the given array of data + * bytes. We assume that the time-stamp token itself contains its full + * certificate chain required for proper validation. + * + * @param data + * the data to be time-stamped. + * @param revocationData + * the optional container that needs to be filled up with the + * revocation data used to validate the TSA certificate chain. + * @return the DER encoded time-stamp token. + * @throws Exception + * in case something went wrong. + */ + byte[] timeStamp(byte[] data, RevocationData revocationData) + throws Exception; +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampServiceValidator.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampServiceValidator.java new file mode 100644 index 0000000000..4d36be9ec5 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampServiceValidator.java @@ -0,0 +1,51 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.services; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for trust validator of a TSP. + * + * @author Frank Cornelis + * + */ +public interface TimeStampServiceValidator { + + /** + * Validates the given certificate chain. + * + * @param certificateChain + * @param revocationData + * the optional data container that should be filled with + * revocation data that was used to validate the given + * certificate chain. + * @throws Exception + * in case the certificate chain is invalid. + */ + void validate(List certificateChain, + RevocationData revocationData) throws Exception; +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/AddressDTO.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/AddressDTO.java new file mode 100644 index 0000000000..a164046319 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/AddressDTO.java @@ -0,0 +1,51 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.spi; + +import java.io.Serializable; +import java.security.Identity; + +/** + * Address Data Transfer Object. + * + * @author Frank Cornelis + * @see Identity + * + */ +public class AddressDTO implements Serializable { + + /* + * We implement serializable to allow this class to be used in distributed + * containers as defined in the Servlet v2.4 specification. + */ + + private static final long serialVersionUID = 1L; + + public String streetAndNumber; + + public String zip; + + public String city; +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/DigestInfo.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/DigestInfo.java new file mode 100644 index 0000000000..2f7c58c338 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/DigestInfo.java @@ -0,0 +1,56 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.spi; + +import java.io.Serializable; + +import org.apache.poi.poifs.crypt.HashAlgorithm; + +/** + * Digest Information data transfer class. + */ +public class DigestInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Main constructor. + * + * @param digestValue + * @param hashAlgo + * @param description + */ + public DigestInfo(byte[] digestValue, HashAlgorithm hashAlgo, String description) { + this.digestValue = digestValue; + this.hashAlgo = hashAlgo; + this.description = description; + } + + public final byte[] digestValue; + + public final String description; + + public final HashAlgorithm hashAlgo; +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/IdentityDTO.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/IdentityDTO.java new file mode 100644 index 0000000000..9cfa0aae25 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/IdentityDTO.java @@ -0,0 +1,75 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ + +package org.apache.poi.poifs.crypt.dsig.spi; + +import java.io.Serializable; +import java.util.GregorianCalendar; + +/** + * Identity Data Transfer Object. + * + * @author Frank Cornelis + * + */ +public class IdentityDTO implements Serializable { + + /* + * We implement serializable to allow this class to be used in distributed + * containers as defined in the Servlet v2.4 specification. + */ + private static final long serialVersionUID = 1L; + + public String cardNumber; + + public String chipNumber; + + public GregorianCalendar cardValidityDateBegin; + + public GregorianCalendar cardValidityDateEnd; + + public String cardDeliveryMunicipality; + + public String nationalNumber; + + public String name; + + public String firstName; + + public String middleName; + + public String nationality; + + public String placeOfBirth; + + public GregorianCalendar dateOfBirth; + + public boolean male; + + public boolean female; + + public String nobleCondition; + + public String duplicate; +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/util/MethodUtils.java b/src/ooxml/java/org/apache/poi/util/MethodUtils.java new file mode 100644 index 0000000000..c006c85462 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/util/MethodUtils.java @@ -0,0 +1,1334 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.util; + + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + *

Utility reflection methods focussed on methods in general rather than properties in particular.

+ * + *

Known Limitations

+ *

Accessing Public Methods In A Default Access Superclass

+ *

There is an issue when invoking public methods contained in a default access superclass. + * Reflection locates these methods fine and correctly assigns them as public. + * However, an IllegalAccessException is thrown if the method is invoked.

+ * + *

MethodUtils contains a workaround for this situation. + * It will attempt to call setAccessible on this method. + * If this call succeeds, then the method can be invoked as normal. + * This call will only succeed when the application has sufficient security privilages. + * If this call fails then a warning will be logged and the method may fail.

+ * + * @author Craig R. McClanahan + * @author Ralph Schaer + * @author Chris Audley + * @author Rey François + * @author Gregor Raýman + * @author Jan Sorensen + * @author Robert Burrell Donkin + */ + +public class MethodUtils { + + // --------------------------------------------------------- Private Methods + + /** + * Only log warning about accessibility work around once. + *

+ * Note that this is broken when this class is deployed via a shared + * classloader in a container, as the warning message will be emitted + * only once, not once per webapp. However making the warning appear + * once per webapp means having a map keyed by context classloader + * which introduces nasty memory-leak problems. As this warning is + * really optional we can ignore this problem; only one of the webapps + * will get the warning in its logs but that should be good enough. + */ + private static boolean loggedAccessibleWarning = false; + + /** + * Indicates whether methods should be cached for improved performance. + *

+ * Note that when this class is deployed via a shared classloader in + * a container, this will affect all webapps. However making this + * configurable per webapp would mean having a map keyed by context classloader + * which may introduce memory-leak problems. + */ + private static boolean CACHE_METHODS = true; + + /** An empty class array */ + private static final Class[] EMPTY_CLASS_PARAMETERS = new Class[0]; + /** An empty object array */ + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + // --------------------------------------------------------- Public Methods + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than invokeExactMethod(). + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeMethod(Object object,String methodName,Object [] args)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeMethod(object, methodName, args); + + } + + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeMethod(object, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link + * #invokeExactMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + Method method = getMatchingAccessibleMethod( + object.getClass(), + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + object.getClass().getName()); + } + return method.invoke(object, args); + } + + + /** + *

Invoke a method whose parameter type matches exactly the object + * type.

+ * + *

This is a convenient wrapper for + * {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeExactMethod(object, methodName, args); + + } + + + /** + *

Invoke a method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactMethod(object, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + + Method method = getAccessibleMethod( + object.getClass(), + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + object.getClass().getName()); + } + return method.invoke(object, args); + + } + + /** + *

Invoke a static method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + + Method method = getAccessibleMethod( + objectClass, + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + objectClass.getName()); + } + return method.invoke(null, args); + + } + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object, String, Object[], Class[])}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeStaticMethod(Class objectClass,String methodName,Object [] args)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeStaticMethod (objectClass, methodName, args); + + } + + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeStaticMethod(Class objectClass,String methodName,Object [] args,Class[] parameterTypes)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeStaticMethod (objectClass, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link + * #invokeExactStaticMethod(Class objectClass,String methodName,Object [] args,Class[] parameterTypes)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + Method method = getMatchingAccessibleMethod( + objectClass, + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + objectClass.getName()); + } + return method.invoke(null, args); + } + + + /** + *

Invoke a static method whose parameter type matches exactly the object + * type.

+ * + *

This is a convenient wrapper for + * {@link #invokeExactStaticMethod(Class objectClass,String methodName,Object [] args)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeExactStaticMethod (objectClass, methodName, args); + + } + + + /** + *

Invoke a static method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactStaticMethod(objectClass, methodName, args, parameterTypes); + + } + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) with given name and a single parameter. If no such method + * can be found, return null. + * Basically, a convenience wrapper that constructs a Class + * array for you.

+ * + * @param clazz get method from this class + * @param methodName get method with this name + * @param parameterType taking this type of parameter + * @return The accessible method + */ + public static Method getAccessibleMethod( + Class clazz, + String methodName, + Class parameterType) { + + Class[] parameterTypes = {parameterType}; + return getAccessibleMethod(clazz, methodName, parameterTypes); + + } + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) with given name and parameters. If no such method + * can be found, return null. + * This is just a convenient wrapper for + * {@link #getAccessibleMethod(Method method)}.

+ * + * @param clazz get method from this class + * @param methodName get method with this name + * @param parameterTypes with these parameters types + * @return The accessible method + */ + public static Method getAccessibleMethod( + Class clazz, + String methodName, + Class[] parameterTypes) { + + try { + MethodDescriptor md = new MethodDescriptor(clazz, methodName, parameterTypes, true); + Method method = getAccessibleMethod + (clazz, clazz.getMethod(methodName, parameterTypes)); + return method; + } catch (NoSuchMethodException e) { + return (null); + } + + } + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified Method. If no such method + * can be found, return null.

+ * + * @param method The method that we wish to call + * @return The accessible method + */ + public static Method getAccessibleMethod(Method method) { + + // Make sure we have a method to check + if (method == null) { + return (null); + } + + return getAccessibleMethod(method.getDeclaringClass(), method); + + } + + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified Method. If no such method + * can be found, return null.

+ * + * @param clazz The class of the object + * @param method The method that we wish to call + * @return The accessible method + */ + public static Method getAccessibleMethod(Class clazz, Method method) { + + // Make sure we have a method to check + if (method == null) { + return (null); + } + + // If the requested method is not public we cannot call it + if (!Modifier.isPublic(method.getModifiers())) { + return (null); + } + + boolean sameClass = true; + if (clazz == null) { + clazz = method.getDeclaringClass(); + } else { + sameClass = clazz.equals(method.getDeclaringClass()); + if (!method.getDeclaringClass().isAssignableFrom(clazz)) { + throw new IllegalArgumentException(clazz.getName() + + " is not assignable from " + method.getDeclaringClass().getName()); + } + } + + // If the class is public, we are done + if (Modifier.isPublic(clazz.getModifiers())) { + if (!sameClass && !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + setMethodAccessible(method); // Default access superclass workaround + } + return (method); + } + + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); + + // Check the implemented interfaces and subinterfaces + method = + getAccessibleMethodFromInterfaceNest(clazz, + methodName, + parameterTypes); + + // Check the superclass chain + if (method == null) { + method = getAccessibleMethodFromSuperclass(clazz, + methodName, + parameterTypes); + } + + return (method); + + } + + + // -------------------------------------------------------- Private Methods + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) by scanning through the superclasses. If no such method + * can be found, return null.

+ * + * @param clazz Class to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + */ + private static Method getAccessibleMethodFromSuperclass + (Class clazz, String methodName, Class[] parameterTypes) { + + Class parentClazz = clazz.getSuperclass(); + while (parentClazz != null) { + if (Modifier.isPublic(parentClazz.getModifiers())) { + try { + return parentClazz.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + parentClazz = parentClazz.getSuperclass(); + } + return null; + } + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified method, by scanning through + * all implemented interfaces and subinterfaces. If no such method + * can be found, return null.

+ * + *

There isn't any good reason why this method must be private. + * It is because there doesn't seem any reason why other classes should + * call this rather than the higher level methods.

+ * + * @param clazz Parent class for the interfaces to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + */ + private static Method getAccessibleMethodFromInterfaceNest + (Class clazz, String methodName, Class[] parameterTypes) { + + Method method = null; + + // Search up the superclass chain + for (; clazz != null; clazz = clazz.getSuperclass()) { + + // Check the implemented interfaces of the parent class + Class[] interfaces = clazz.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + + // Is this interface public? + if (!Modifier.isPublic(interfaces[i].getModifiers())) { + continue; + } + + // Does the method exist on this interface? + try { + method = interfaces[i].getDeclaredMethod(methodName, + parameterTypes); + } catch (NoSuchMethodException e) { + /* Swallow, if no method is found after the loop then this + * method returns null. + */ + } + if (method != null) { + return method; + } + + // Recursively check our parent interfaces + method = + getAccessibleMethodFromInterfaceNest(interfaces[i], + methodName, + parameterTypes); + if (method != null) { + return method; + } + + } + + } + + // If we found a method return it + if (method != null) { + return (method); + } + + // We did not find anything + return (null); + + } + + /** + *

Find an accessible method that matches the given name and has compatible parameters. + * Compatible parameters mean that every method parameter is assignable from + * the given parameters. + * In other words, it finds a method with the given name + * that will take the parameters given.

+ * + *

This method is slightly undeterminstic since it loops + * through methods names and return the first matching method.

+ * + *

This method is used by + * {@link + * #invokeMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + * + *

This method can match primitive parameter by passing in wrapper classes. + * For example, a Boolean will match a primitive boolean + * parameter. + * + * @param clazz find method in this class + * @param methodName find method with this name + * @param parameterTypes find method with compatible parameters + * @return The accessible method + */ + public static Method getMatchingAccessibleMethod( + Class clazz, + String methodName, + Class[] parameterTypes) { + // trace logging + Log log = LogFactory.getLog(MethodUtils.class); + if (log.isTraceEnabled()) { + log.trace("Matching name=" + methodName + " on " + clazz); + } + MethodDescriptor md = new MethodDescriptor(clazz, methodName, parameterTypes, false); + + // see if we can find the method directly + // most of the time this works and it's much faster + try { + Method method = clazz.getMethod(methodName, parameterTypes); + if (log.isTraceEnabled()) { + log.trace("Found straight match: " + method); + log.trace("isPublic:" + Modifier.isPublic(method.getModifiers())); + } + + setMethodAccessible(method); // Default access superclass workaround + + return method; + + } catch (NoSuchMethodException e) { /* SWALLOW */ } + + // search through all methods + int paramSize = parameterTypes.length; + Method bestMatch = null; + Method[] methods = clazz.getMethods(); + float bestMatchCost = Float.MAX_VALUE; + float myCost = Float.MAX_VALUE; + for (int i = 0, size = methods.length; i < size ; i++) { + if (methods[i].getName().equals(methodName)) { + // log some trace information + if (log.isTraceEnabled()) { + log.trace("Found matching name:"); + log.trace(methods[i]); + } + + // compare parameters + Class[] methodsParams = methods[i].getParameterTypes(); + int methodParamSize = methodsParams.length; + if (methodParamSize == paramSize) { + boolean match = true; + for (int n = 0 ; n < methodParamSize; n++) { + if (log.isTraceEnabled()) { + log.trace("Param=" + parameterTypes[n].getName()); + log.trace("Method=" + methodsParams[n].getName()); + } + if (!isAssignmentCompatible(methodsParams[n], parameterTypes[n])) { + if (log.isTraceEnabled()) { + log.trace(methodsParams[n] + " is not assignable from " + + parameterTypes[n]); + } + match = false; + break; + } + } + + if (match) { + // get accessible version of method + Method method = getAccessibleMethod(clazz, methods[i]); + if (method != null) { + if (log.isTraceEnabled()) { + log.trace(method + " accessible version of " + + methods[i]); + } + setMethodAccessible(method); // Default access superclass workaround + myCost = getTotalTransformationCost(parameterTypes,method.getParameterTypes()); + if ( myCost < bestMatchCost ) { + bestMatch = method; + bestMatchCost = myCost; + } + } + + log.trace("Couldn't find accessible method."); + } + } + } + } + if ( bestMatch == null ){ + // didn't find a match + log.trace("No match found."); + } + + return bestMatch; + } + + public static Constructor getMatchingAccessibleConstructor( + Class clazz, + Class[] parameterTypes) { + // trace logging + Log log = LogFactory.getLog(MethodUtils.class); + MethodDescriptor md = new MethodDescriptor(clazz, "dummy", parameterTypes, false); + + // see if we can find the method directly + // most of the time this works and it's much faster + try { + Constructor constructor = clazz.getConstructor(parameterTypes); + if (log.isTraceEnabled()) { + log.trace("Found straight match: " + constructor); + log.trace("isPublic:" + Modifier.isPublic(constructor.getModifiers())); + } + + setMethodAccessible(constructor); // Default access superclass workaround + + return constructor; + + } catch (NoSuchMethodException e) { /* SWALLOW */ } + + // search through all methods + int paramSize = parameterTypes.length; + Constructor bestMatch = null; + Constructor[] constructors = clazz.getConstructors(); + float bestMatchCost = Float.MAX_VALUE; + float myCost = Float.MAX_VALUE; + for (int i = 0, size = constructors.length; i < size ; i++) { + // compare parameters + Class[] methodsParams = constructors[i].getParameterTypes(); + int methodParamSize = methodsParams.length; + if (methodParamSize == paramSize) { + boolean match = true; + for (int n = 0 ; n < methodParamSize; n++) { + if (log.isTraceEnabled()) { + log.trace("Param=" + parameterTypes[n].getName()); + log.trace("Method=" + methodsParams[n].getName()); + } + if (!isAssignmentCompatible(methodsParams[n], parameterTypes[n])) { + if (log.isTraceEnabled()) { + log.trace(methodsParams[n] + " is not assignable from " + + parameterTypes[n]); + } + match = false; + break; + } + } + + if (match) { + // get accessible version of method + Constructor cons = (Constructor)constructors[i]; + myCost = getTotalTransformationCost(parameterTypes,cons.getParameterTypes()); + if ( myCost < bestMatchCost ) { + bestMatch = cons; + bestMatchCost = myCost; + } + } + } + } + if ( bestMatch == null ){ + // didn't find a match + log.trace("No match found."); + } + + return bestMatch; + } + + /** + * Try to make the method accessible + * @param method The source arguments + */ + private static void setMethodAccessible(Object method) { + try { + // + // XXX Default access superclass workaround + // + // When a public class has a default access superclass + // with public methods, these methods are accessible. + // Calling them from compiled code works fine. + // + // Unfortunately, using reflection to invoke these methods + // seems to (wrongly) to prevent access even when the method + // modifer is public. + // + // The following workaround solves the problem but will only + // work from sufficiently privilages code. + // + // Better workarounds would be greatfully accepted. + // + if (method instanceof Method) { + ((Method)method).setAccessible(true); + } else if (method instanceof Constructor) { + ((Constructor)method).setAccessible(true); + } else { + throw new RuntimeException("invalid parameter"); + } + + } catch (SecurityException se) { + // log but continue just in case the method.invoke works anyway + Log log = LogFactory.getLog(MethodUtils.class); + if (!loggedAccessibleWarning) { + boolean vulnerableJVM = false; + try { + String specVersion = System.getProperty("java.specification.version"); + if (specVersion.charAt(0) == '1' && + (specVersion.charAt(2) == '0' || + specVersion.charAt(2) == '1' || + specVersion.charAt(2) == '2' || + specVersion.charAt(2) == '3')) { + + vulnerableJVM = true; + } + } catch (SecurityException e) { + // don't know - so display warning + vulnerableJVM = true; + } + if (vulnerableJVM) { + log.warn( + "Current Security Manager restricts use of workarounds for reflection bugs " + + " in pre-1.4 JVMs."); + } + loggedAccessibleWarning = true; + } + log.debug("Cannot setAccessible on method. Therefore cannot use jvm access bug workaround.", se); + } + } + + /** + * Returns the sum of the object transformation cost for each class in the source + * argument list. + * @param srcArgs The source arguments + * @param destArgs The destination arguments + * @return The total transformation cost + */ + private static float getTotalTransformationCost(Class[] srcArgs, Class[] destArgs) { + + float totalCost = 0.0f; + for (int i = 0; i < srcArgs.length; i++) { + Class srcClass, destClass; + srcClass = srcArgs[i]; + destClass = destArgs[i]; + totalCost += getObjectTransformationCost(srcClass, destClass); + } + + return totalCost; + } + + /** + * Gets the number of steps required needed to turn the source class into the + * destination class. This represents the number of steps in the object hierarchy + * graph. + * @param srcClass The source class + * @param destClass The destination class + * @return The cost of transforming an object + */ + private static float getObjectTransformationCost(Class srcClass, Class destClass) { + float cost = 0.0f; + while (destClass != null && !destClass.equals(srcClass)) { + if (destClass.isInterface() && isAssignmentCompatible(destClass,srcClass)) { + // slight penalty for interface match. + // we still want an exact match to override an interface match, but + // an interface match should override anything where we have to get a + // superclass. + cost += 0.25f; + break; + } + cost++; + destClass = destClass.getSuperclass(); + } + + /* + * If the destination class is null, we've travelled all the way up to + * an Object match. We'll penalize this by adding 1.5 to the cost. + */ + if (destClass == null) { + cost += 1.5f; + } + + return cost; + } + + + /** + *

Determine whether a type can be used as a parameter in a method invocation. + * This method handles primitive conversions correctly.

+ * + *

In order words, it will match a Boolean to a boolean, + * a Long to a long, + * a Float to a float, + * a Integer to a int, + * and a Double to a double. + * Now logic widening matches are allowed. + * For example, a Long will not match a int. + * + * @param parameterType the type of parameter accepted by the method + * @param parameterization the type of parameter being tested + * + * @return true if the assignement is compatible. + */ + public static final boolean isAssignmentCompatible(Class parameterType, Class parameterization) { + // try plain assignment + if (parameterType.isAssignableFrom(parameterization)) { + return true; + } + + if (parameterType.isPrimitive()) { + // this method does *not* do widening - you must specify exactly + // is this the right behaviour? + Class parameterWrapperClazz = getPrimitiveWrapper(parameterType); + if (parameterWrapperClazz != null) { + return parameterWrapperClazz.equals(parameterization); + } + } + + return false; + } + + /** + * Gets the wrapper object class for the given primitive type class. + * For example, passing boolean.class returns Boolean.class + * @param primitiveType the primitive type class for which a match is to be found + * @return the wrapper type associated with the given primitive + * or null if no match is found + */ + public static Class getPrimitiveWrapper(Class primitiveType) { + // does anyone know a better strategy than comparing names? + if (boolean.class.equals(primitiveType)) { + return Boolean.class; + } else if (float.class.equals(primitiveType)) { + return Float.class; + } else if (long.class.equals(primitiveType)) { + return Long.class; + } else if (int.class.equals(primitiveType)) { + return Integer.class; + } else if (short.class.equals(primitiveType)) { + return Short.class; + } else if (byte.class.equals(primitiveType)) { + return Byte.class; + } else if (double.class.equals(primitiveType)) { + return Double.class; + } else if (char.class.equals(primitiveType)) { + return Character.class; + } else { + + return null; + } + } + + /** + * Gets the class for the primitive type corresponding to the primitive wrapper class given. + * For example, an instance of Boolean.class returns a boolean.class. + * @param wrapperType the + * @return the primitive type class corresponding to the given wrapper class, + * null if no match is found + */ + public static Class getPrimitiveType(Class wrapperType) { + // does anyone know a better strategy than comparing names? + if (Boolean.class.equals(wrapperType)) { + return boolean.class; + } else if (Float.class.equals(wrapperType)) { + return float.class; + } else if (Long.class.equals(wrapperType)) { + return long.class; + } else if (Integer.class.equals(wrapperType)) { + return int.class; + } else if (Short.class.equals(wrapperType)) { + return short.class; + } else if (Byte.class.equals(wrapperType)) { + return byte.class; + } else if (Double.class.equals(wrapperType)) { + return double.class; + } else if (Character.class.equals(wrapperType)) { + return char.class; + } else { + Log log = LogFactory.getLog(MethodUtils.class); + if (log.isDebugEnabled()) { + log.debug("Not a known primitive wrapper class: " + wrapperType); + } + return null; + } + } + + /** + * Find a non primitive representation for given primitive class. + * + * @param clazz the class to find a representation for, not null + * @return the original class if it not a primitive. Otherwise the wrapper class. Not null + */ + public static Class toNonPrimitiveClass(Class clazz) { + if (clazz.isPrimitive()) { + Class primitiveClazz = MethodUtils.getPrimitiveWrapper(clazz); + // the above method returns + if (primitiveClazz != null) { + return primitiveClazz; + } else { + return clazz; + } + } else { + return clazz; + } + } + + + /** + * Represents the key to looking up a Method by reflection. + */ + private static class MethodDescriptor { + private Class cls; + private String methodName; + private Class[] paramTypes; + private boolean exact; + private int hashCode; + + /** + * The sole constructor. + * + * @param cls the class to reflect, must not be null + * @param methodName the method name to obtain + * @param paramTypes the array of classes representing the paramater types + * @param exact whether the match has to be exact. + */ + public MethodDescriptor(Class cls, String methodName, Class[] paramTypes, boolean exact) { + if (cls == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + if (methodName == null) { + throw new IllegalArgumentException("Method Name cannot be null"); + } + if (paramTypes == null) { + paramTypes = EMPTY_CLASS_PARAMETERS; + } + + this.cls = cls; + this.methodName = methodName; + this.paramTypes = paramTypes; + this.exact= exact; + + this.hashCode = methodName.length(); + } + /** + * Checks for equality. + * @param obj object to be tested for equality + * @return true, if the object describes the same Method. + */ + public boolean equals(Object obj) { + if (!(obj instanceof MethodDescriptor)) { + return false; + } + MethodDescriptor md = (MethodDescriptor)obj; + + return ( + exact == md.exact && + methodName.equals(md.methodName) && + cls.equals(md.cls) && + java.util.Arrays.equals(paramTypes, md.paramTypes) + ); + } + /** + * Returns the string length of method name. I.e. if the + * hashcodes are different, the objects are different. If the + * hashcodes are the same, need to use the equals method to + * determine equality. + * @return the string length of method name. + */ + public int hashCode() { + return hashCode; + } + } +} diff --git a/src/ooxml/java/org/apache/poi/util/XmlSort.java b/src/ooxml/java/org/apache/poi/util/XmlSort.java new file mode 100644 index 0000000000..4e1ffa54f0 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/util/XmlSort.java @@ -0,0 +1,221 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.util; + +import java.io.File; +import java.io.IOException; +import java.util.Comparator; + +import javax.xml.namespace.QName; + +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; + +/** + */ +public final class XmlSort +{ + /** + * Receives an XML element instance and sorts the children of this + * element in lexicographical (by default) order. + * + * @param args An array in which the first item is a + * path to the XML instance file and the second item (optional) is + * an XPath inside the document identifying the element to be sorted + */ + public static void main(String[] args) + { + if (args.length < 1 || args.length > 2) + { + System.out.println(" java XmlSort []"); + return; + } + File f = new File(args[0]); + try + { + XmlObject docInstance = XmlObject.Factory.parse(f); + XmlObject element = null; + if (args.length > 1) + { + String xpath = args[1]; + XmlObject[] result = docInstance.selectPath(xpath); + if (result.length == 0) + { + System.out.println("ERROR: XPath \"" + xpath + "\" did not return any results"); + } + else if (result.length > 1) + { + System.out.println("ERROR: XPath \"" + xpath + "\" returned more than one " + + "node (" + result.length + ")"); + } + else + element = result[0]; + } + else + { + // Navigate to the root element + XmlCursor c = docInstance.newCursor(); + c.toFirstChild(); + element = c.getObject(); + c.dispose(); + } + if (element != null) + sort(element, new QNameComparator(QNameComparator.ASCENDING)); + System.out.println(docInstance.xmlText()); + } + catch (IOException ioe) + { + System.out.println("ERROR: Could not open file: \"" + args[0] + "\": " + + ioe.getMessage()); + } + catch (XmlException xe) + { + System.out.println("ERROR: Could not parse file: \"" + args[0] + "\": " + + xe.getMessage()); + } + } + + /** + * Sorts the children of element according to the order indicated by the + * comparator. + * @param element the element whose content is to be sorted. Only element children are sorted, + * attributes are not touched. When elements are reordered, all the text, comments and PIs + * follow the element that they come immediately after. + * @param comp a comparator that is to be used when comparing the QNames of two + * elements. See {@link org.apache.xmlbeans.samples.cursor.XmlSort.QNameComparator} for a simple + * implementation that compares two elements based on the value of their QName, but more + * complicated implementations are possible, for instance, ones that compare two elements based + * on the value of a specifc attribute etc. + * @throws IllegalArgumentException if the input XmlObject does not represent + * an element + */ + public static void sort(XmlObject element, Comparator comp) + { + XmlCursor headCursor = element.newCursor(); + if (!headCursor.isStart()) + throw new IllegalStateException("The element parameter must point to a STARTDOC"); + // We use insertion sort to minimize the number of swaps, because each swap means + // moving a part of the document + /* headCursor points to the beginning of the list of the already sorted items and + listCursor points to the beginning of the list of unsorted items + At the beginning, headCursor points to the first element and listCursor points to the + second element. The algorithm ends when listCursor cannot be moved to the "next" + element in the unsorted list, i.e. the unsorted list becomes empty */ + boolean moved = headCursor.toFirstChild(); + if (!moved) + { + // Cursor was not moved, which means that the given element has no children and + // therefore there is nothing to sort + return; + } + XmlCursor listCursor = headCursor.newCursor(); + boolean moreElements = listCursor.toNextSibling(); + while (moreElements) + { + moved = false; + // While we can move the head of the unsorted list, it means that there are still + // items (elements) that need to be sorted + while (headCursor.comparePosition(listCursor) < 0) + { + if (comp.compare(headCursor, listCursor) > 0) + { + // We have found the position in the sorted list, insert the element and the + // text following the element in the current position + /* + * Uncomment this code to cause the text before the element to move along + * with the element, rather than the text after the element. Notice that this + * is more difficult to do, because the cursor's "type" refers to the position + * to the right of the cursor, so to get the type of the token to the left, the + * cursor needs to be first moved to the left (previous token) + * + headCursor.toPrevToken(); + while (headCursor.isComment() || headCursor.isProcinst() || headCursor.isText()) + headCursor.toPrevToken(); + headCursor.toNextToken(); + listCursor.toPrevToken(); + while (listCursor.isComment() || listCursor.isProcinst() || listCursor.isText()) + listCursor.toPrevToken(); + listCursor.toNextToken(); + while (!listCursor.isStart()) + listCursor.moveXml(headCursor); + listCursor.moveXml(headCursor); + */ + // Move the element + listCursor.moveXml(headCursor); + // Move the text following the element + while (!listCursor.isStart() && !listCursor.isEnd()) + listCursor.moveXml(headCursor); + moreElements = listCursor.isStart(); + moved = true; + break; + } + headCursor.toNextSibling(); + } + if (!moved) + { + // Because during the move of a fragment of XML, the listCursor is also moved, in + // case we didn't need to move XML (the new element to be inserted happened to + // be the last one in order), we need to move this cursor + moreElements = listCursor.toNextSibling(); + } + // Reposition the head of the sorted list + headCursor.toParent(); + headCursor.toFirstChild(); + } + } + + /** + * Implements a java.util.Comparator for comparing QNamevalues. + * The namespace URIs are compared first and if they are equal, the local parts are compared. + *

+ * The constructor accepts an argument indicating whether the comparison order is the same as + * the lexicographic order of the strings or the reverse. + */ + public static final class QNameComparator implements Comparator + { + public static final int ASCENDING = 1; + public static final int DESCENDING = 2; + + private int order; + + public QNameComparator(int order) + { + this.order = order; + if (order != ASCENDING && order != DESCENDING) + throw new IllegalArgumentException("Please specify one of ASCENDING or DESCENDING "+ + "comparison orders"); + } + + public int compare(Object o, Object o1) + { + XmlCursor cursor1 = (XmlCursor) o; + XmlCursor cursor2 = (XmlCursor) o1; + QName qname1 = cursor1.getName(); + QName qname2 = cursor2.getName(); + int qnameComparisonRes = qname1.getNamespaceURI().compareTo(qname2.getNamespaceURI()); + if (qnameComparisonRes == 0) + return order == ASCENDING ? + qname1.getLocalPart().compareTo(qname2.getLocalPart()) : + -qname1.getLocalPart().compareTo(qname2.getLocalPart()); + else + return order == ASCENDING ? qnameComparisonRes : -qnameComparisonRes; + } + } +} + \ No newline at end of file diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/signatureInfo.xsd b/src/ooxml/resources/org/apache/poi/poifs/crypt/signatureInfo.xsd new file mode 100644 index 0000000000..f7019f13f5 --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/signatureInfo.xsd @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/PkiTestUtils.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/PkiTestUtils.java new file mode 100644 index 0000000000..afd70f08e5 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/PkiTestUtils.java @@ -0,0 +1,317 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.poifs.crypt; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.CRLException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.Date; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AuthorityInformationAccess; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.CRLNumber; +import org.bouncycastle.asn1.x509.CRLReason; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.DistributionPointName; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; +import org.bouncycastle.cert.X509CRLHolder; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509ExtensionUtils; +import org.bouncycastle.cert.X509v2CRLBuilder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CRLConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.ocsp.BasicOCSPResp; +import org.bouncycastle.cert.ocsp.BasicOCSPRespBuilder; +import org.bouncycastle.cert.ocsp.CertificateID; +import org.bouncycastle.cert.ocsp.CertificateStatus; +import org.bouncycastle.cert.ocsp.OCSPReq; +import org.bouncycastle.cert.ocsp.OCSPReqBuilder; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.bouncycastle.cert.ocsp.OCSPRespBuilder; +import org.bouncycastle.cert.ocsp.Req; +import org.bouncycastle.cert.ocsp.RevokedStatus; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class PkiTestUtils { + + private PkiTestUtils() { + super(); + } + + static KeyPair generateKeyPair() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + SecureRandom random = new SecureRandom(); + keyPairGenerator.initialize(new RSAKeyGenParameterSpec(1024, + RSAKeyGenParameterSpec.F4), random); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + return keyPair; + } + + static X509Certificate generateCertificate(PublicKey subjectPublicKey, + String subjectDn, Date notBefore, Date notAfter, + X509Certificate issuerCertificate, PrivateKey issuerPrivateKey, + boolean caFlag, int pathLength, String crlUri, String ocspUri, + KeyUsage keyUsage) + throws IOException, OperatorCreationException, CertificateException + { + String signatureAlgorithm = "SHA1withRSA"; + X500Name issuerName; + if (issuerCertificate != null) { + issuerName = new X509CertificateHolder(issuerCertificate.getEncoded()).getIssuer(); + } else { + issuerName = new X500Name(subjectDn); + } + + RSAPublicKey rsaPubKey = (RSAPublicKey)subjectPublicKey; + RSAKeyParameters rsaSpec = new RSAKeyParameters(false, rsaPubKey.getModulus(), rsaPubKey.getPublicExponent()); + + SubjectPublicKeyInfo subjectPublicKeyInfo = + SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(rsaSpec); + + DigestCalculator digestCalc = new JcaDigestCalculatorProviderBuilder() + .setProvider("BC").build().get(CertificateID.HASH_SHA1); + + X509v3CertificateBuilder certificateGenerator = new X509v3CertificateBuilder( + issuerName + , new BigInteger(128, new SecureRandom()) + , notBefore + , notAfter + , new X500Name(subjectDn) + , subjectPublicKeyInfo + ); + + X509ExtensionUtils exUtils = new X509ExtensionUtils(digestCalc); + SubjectKeyIdentifier subKeyId = exUtils.createSubjectKeyIdentifier(subjectPublicKeyInfo); + AuthorityKeyIdentifier autKeyId = (issuerCertificate != null) + ? exUtils.createAuthorityKeyIdentifier(new X509CertificateHolder(issuerCertificate.getEncoded())) + : exUtils.createAuthorityKeyIdentifier(subjectPublicKeyInfo); + + certificateGenerator.addExtension(Extension.subjectKeyIdentifier, false, subKeyId); + certificateGenerator.addExtension(Extension.authorityKeyIdentifier, false, autKeyId); + + if (caFlag) { + BasicConstraints bc; + + if (-1 == pathLength) { + bc = new BasicConstraints(true); + } else { + bc = new BasicConstraints(pathLength); + } + certificateGenerator.addExtension(Extension.basicConstraints, false, bc); + } + + if (null != crlUri) { + int uri = GeneralName.uniformResourceIdentifier; + DERIA5String crlUriDer = new DERIA5String(crlUri); + GeneralName gn = new GeneralName(uri, crlUriDer); + + DERSequence gnDer = new DERSequence(gn); + GeneralNames gns = GeneralNames.getInstance(gnDer); + + DistributionPointName dpn = new DistributionPointName(0, gns); + DistributionPoint distp = new DistributionPoint(dpn, null, null); + DERSequence distpDer = new DERSequence(distp); + certificateGenerator.addExtension(Extension.cRLDistributionPoints, false, distpDer); + } + + if (null != ocspUri) { + int uri = GeneralName.uniformResourceIdentifier; + GeneralName ocspName = new GeneralName(uri, ocspUri); + + AuthorityInformationAccess authorityInformationAccess = + new AuthorityInformationAccess(X509ObjectIdentifiers.ocspAccessMethod, ocspName); + + certificateGenerator.addExtension(Extension.authorityInfoAccess, false, authorityInformationAccess); + } + + if (null != keyUsage) { + certificateGenerator.addExtension(Extension.keyUsage, true, keyUsage); + } + + JcaContentSignerBuilder signerBuilder = new JcaContentSignerBuilder(signatureAlgorithm); + signerBuilder.setProvider("BC"); + + X509CertificateHolder certHolder = + certificateGenerator.build(signerBuilder.build(issuerPrivateKey)); + + /* + * Next certificate factory trick is needed to make sure that the + * certificate delivered to the caller is provided by the default + * security provider instead of BouncyCastle. If we don't do this trick + * we might run into trouble when trying to use the CertPath validator. + */ +// CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); +// certificate = (X509Certificate) certificateFactory +// .generateCertificate(new ByteArrayInputStream(certificate +// .getEncoded())); + return new JcaX509CertificateConverter().getCertificate(certHolder); + } + + static Document loadDocument(InputStream documentInputStream) + throws ParserConfigurationException, SAXException, IOException { + InputSource inputSource = new InputSource(documentInputStream); + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory + .newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory + .newDocumentBuilder(); + Document document = documentBuilder.parse(inputSource); + return document; + } + + static String toString(Node dom) throws TransformerException { + Source source = new DOMSource(dom); + StringWriter stringWriter = new StringWriter(); + Result result = new StreamResult(stringWriter); + TransformerFactory transformerFactory = TransformerFactory + .newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + /* + * We have to omit the ?xml declaration if we want to embed the + * document. + */ + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.transform(source, result); + return stringWriter.getBuffer().toString(); + } + + public static X509CRL generateCrl(X509Certificate issuer, PrivateKey issuerPrivateKey) + throws CertificateEncodingException, IOException, CRLException, OperatorCreationException { + + X509CertificateHolder holder = new X509CertificateHolder(issuer.getEncoded()); + X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(holder.getIssuer(), new Date()); + crlBuilder.setNextUpdate(new Date(new Date().getTime() + 100000)); + JcaContentSignerBuilder contentBuilder = new JcaContentSignerBuilder("SHA1withRSA").setProvider("BC"); + + CRLNumber crlNumber = new CRLNumber(new BigInteger("1234")); + + crlBuilder.addExtension(Extension.cRLNumber, false, crlNumber); + X509CRLHolder x509Crl = crlBuilder.build(contentBuilder.build(issuerPrivateKey)); + return new JcaX509CRLConverter().setProvider("BC").getCRL(x509Crl); + } + + public static OCSPResp createOcspResp(X509Certificate certificate, + boolean revoked, X509Certificate issuerCertificate, + X509Certificate ocspResponderCertificate, + PrivateKey ocspResponderPrivateKey, String signatureAlgorithm, + long nonceTimeinMillis) + throws Exception { + DigestCalculator digestCalc = new JcaDigestCalculatorProviderBuilder() + .setProvider("BC").build().get(CertificateID.HASH_SHA1); + X509CertificateHolder issuerHolder = new X509CertificateHolder(issuerCertificate.getEncoded()); + CertificateID certId = new CertificateID(digestCalc, issuerHolder, certificate.getSerialNumber()); + + // request + //create a nonce to avoid replay attack + BigInteger nonce = BigInteger.valueOf(nonceTimeinMillis); + DEROctetString nonceDer = new DEROctetString(nonce.toByteArray()); + Extension ext = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, true, nonceDer); + Extensions exts = new Extensions(ext); + + OCSPReqBuilder ocspReqBuilder = new OCSPReqBuilder(); + ocspReqBuilder.addRequest(certId); + ocspReqBuilder.setRequestExtensions(exts); + OCSPReq ocspReq = ocspReqBuilder.build(); + + + SubjectPublicKeyInfo keyInfo = new SubjectPublicKeyInfo + (CertificateID.HASH_SHA1, ocspResponderCertificate.getPublicKey().getEncoded()); + + BasicOCSPRespBuilder basicOCSPRespBuilder = new BasicOCSPRespBuilder(keyInfo, digestCalc); + basicOCSPRespBuilder.setResponseExtensions(exts); + + // request processing + Req[] requestList = ocspReq.getRequestList(); + for (Req ocspRequest : requestList) { + CertificateID certificateID = ocspRequest.getCertID(); + CertificateStatus certificateStatus = CertificateStatus.GOOD; + if (revoked) { + certificateStatus = new RevokedStatus(new Date(), CRLReason.privilegeWithdrawn); + } + basicOCSPRespBuilder.addResponse(certificateID, certificateStatus); + } + + // basic response generation + X509CertificateHolder[] chain = null; + if (!ocspResponderCertificate.equals(issuerCertificate)) { + // TODO: HorribleProxy can't convert array input params yet + chain = new X509CertificateHolder[] { + new X509CertificateHolder(ocspResponderCertificate.getEncoded()), + issuerHolder + }; + } + + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA1withRSA") + .setProvider("BC").build(ocspResponderPrivateKey); + BasicOCSPResp basicOCSPResp = basicOCSPRespBuilder.build(contentSigner, chain, new Date(nonceTimeinMillis)); + + + OCSPRespBuilder ocspRespBuilder = new OCSPRespBuilder(); + OCSPResp ocspResp = ocspRespBuilder.build(OCSPRespBuilder.SUCCESSFUL, basicOCSPResp); + + return ocspResp; + } +} diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java new file mode 100644 index 0000000000..4444abe89d --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java @@ -0,0 +1,544 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ +package org.apache.poi.poifs.crypt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageAccess; +import org.apache.poi.poifs.crypt.dsig.SignatureConfig; +import org.apache.poi.poifs.crypt.dsig.SignatureInfo; +import org.apache.poi.poifs.crypt.dsig.SignatureInfo.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.XAdESSignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.XAdESXLSignatureFacet; +import org.apache.poi.poifs.crypt.dsig.services.RevocationData; +import org.apache.poi.poifs.crypt.dsig.services.RevocationDataService; +import org.apache.poi.poifs.crypt.dsig.services.TimeStampService; +import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator; +import org.apache.poi.poifs.crypt.dsig.spi.DigestInfo; +import org.apache.poi.util.DocumentHelper; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.xmlbeans.XmlObject; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.ocsp.OCSPResp; +import org.etsi.uri.x01903.v13.DigestAlgAndValueType; +import org.etsi.uri.x01903.v13.QualifyingPropertiesType; +import org.junit.BeforeClass; +import org.junit.Test; +import org.w3.x2000.x09.xmldsig.ReferenceType; +import org.w3.x2000.x09.xmldsig.SignatureDocument; +import org.w3c.dom.Document; + +public class TestSignatureInfo { + private static final POILogger LOG = POILogFactory.getLogger(TestSignatureInfo.class); + private static final POIDataSamples testdata = POIDataSamples.getXmlDSignInstance(); + + private static Calendar cal; + private KeyPair keyPair = null; + private X509Certificate x509 = null; + + + + @BeforeClass + public static void initBouncy() throws MalformedURLException { + File bcProvJar = new File("lib/bcprov-ext-jdk15on-1.51.jar"); + File bcPkixJar = new File("lib/bcpkix-jdk15on-151.jar"); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + URLClassLoader ucl = new URLClassLoader(new URL[]{bcProvJar.toURI().toURL(),bcPkixJar.toURI().toURL()}, cl); + Thread.currentThread().setContextClassLoader(ucl); + CryptoFunctions.registerBouncyCastle(); + + /*** TODO : set cal to now ... only set to fixed date for debugging ... */ + cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.set(2014, 7, 6, 21, 42, 12); + } + + @Test + public void getSignerUnsigned() throws Exception { + String testFiles[] = { + "hello-world-unsigned.docx", + "hello-world-unsigned.pptx", + "hello-world-unsigned.xlsx", + "hello-world-office-2010-technical-preview-unsigned.docx" + }; + + for (String testFile : testFiles) { + OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ); + SignatureConfig sic = new SignatureConfig(); + sic.setOpcPackage(pkg); + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(sic); + List result = new ArrayList(); + for (SignaturePart sp : si.getSignatureParts()) { + if (sp.validate()) { + result.add(sp.getSigner()); + } + } + pkg.revert(); + pkg.close(); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + @Test + public void getSigner() throws Exception { + String testFiles[] = { + "hyperlink-example-signed.docx", + "hello-world-signed.docx", + "hello-world-signed.pptx", + "hello-world-signed.xlsx", + "hello-world-office-2010-technical-preview.docx", + "ms-office-2010-signed.docx", + "ms-office-2010-signed.pptx", + "ms-office-2010-signed.xlsx", + "Office2010-SP1-XAdES-X-L.docx", + "signed.docx", + }; + + for (String testFile : testFiles) { + OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ); + SignatureConfig sic = new SignatureConfig(); + sic.setOpcPackage(pkg); + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(sic); + List result = new ArrayList(); + for (SignaturePart sp : si.getSignatureParts()) { + if (sp.validate()) { + result.add(sp.getSigner()); + } + } + + assertNotNull(result); + assertEquals("test-file: "+testFile, 1, result.size()); + X509Certificate signer = result.get(0); + LOG.log(POILogger.DEBUG, "signer: " + signer.getSubjectX500Principal()); + + boolean b = si.verifySignature(); + assertTrue("test-file: "+testFile, b); + pkg.revert(); + } + } + + @Test + public void getMultiSigners() throws Exception { + String testFile = "hello-world-signed-twice.docx"; + OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ); + SignatureConfig sic = new SignatureConfig(); + sic.setOpcPackage(pkg); + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(sic); + List result = new ArrayList(); + for (SignaturePart sp : si.getSignatureParts()) { + if (sp.validate()) { + result.add(sp.getSigner()); + } + } + + assertNotNull(result); + assertEquals("test-file: "+testFile, 2, result.size()); + X509Certificate signer1 = result.get(0); + X509Certificate signer2 = result.get(1); + LOG.log(POILogger.DEBUG, "signer 1: " + signer1.getSubjectX500Principal()); + LOG.log(POILogger.DEBUG, "signer 2: " + signer2.getSubjectX500Principal()); + + boolean b = si.verifySignature(); + assertTrue("test-file: "+testFile, b); + pkg.revert(); + } + + @Test + public void testSignSpreadsheet() throws Exception { + String testFile = "hello-world-unsigned.xlsx"; + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + sign(pkg, "Test", "CN=Test", 1); + pkg.close(); + } + + @Test + public void testManipulation() throws Exception { + // sign & validate + String testFile = "hello-world-unsigned.xlsx"; + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + sign(pkg, "Test", "CN=Test", 1); + + // manipulate + XSSFWorkbook wb = new XSSFWorkbook(pkg); + wb.setSheetName(0, "manipulated"); + // ... I don't know, why commit is protected ... + Method m = XSSFWorkbook.class.getDeclaredMethod("commit"); + m.setAccessible(true); + m.invoke(wb); + + // todo: test a manipulation on a package part, which is not signed + // ... maybe in combination with #56164 + + // validate + SignatureConfig sic = new SignatureConfig(); + sic.setOpcPackage(pkg); + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(sic); + boolean b = si.verifySignature(); + assertFalse("signature should be broken", b); + + wb.close(); + } + + @Test + public void testSignSpreadsheetWithSignatureInfo() throws Exception { + initKeyPair("Test", "CN=Test"); + String testFile = "hello-world-unsigned.xlsx"; + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + SignatureConfig sic = new SignatureConfig(); + sic.setOpcPackage(pkg); + sic.setKey(keyPair.getPrivate()); + sic.setSigningCertificateChain(Collections.singletonList(x509)); + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(sic); + // hash > sha1 doesn't work in excel viewer ... + si.confirmSignature(); + List result = new ArrayList(); + for (SignaturePart sp : si.getSignatureParts()) { + if (sp.validate()) { + result.add(sp.getSigner()); + } + } + assertEquals(1, result.size()); + pkg.close(); + } + + @Test + public void testSignEnvelopingDocument() throws Exception { + String testFile = "hello-world-unsigned.xlsx"; + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), 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 EnvelopedSignatureFacet()); + signatureConfig.addSignatureFacet(new KeyInfoSignatureFacet()); + signatureConfig.addSignatureFacet(new XAdESSignatureFacet()); + signatureConfig.addSignatureFacet(new XAdESXLSignatureFacet()); + + boolean mockTsp = false; + // http://timestamping.edelweb.fr/service/tsp + // http://tsa.belgium.be/connect + signatureConfig.setTspUrl("http://timestamping.edelweb.fr/service/tsp"); + signatureConfig.setTspOldProtocol(true); + + if (mockTsp) { + TimeStampService tspService = new TimeStampService(){ + public byte[] timeStamp(byte[] data, RevocationData revocationData) throws Exception { + revocationData.addCRL(crl); + return "time-stamp-token".getBytes(); + } + public void setSignatureConfig(SignatureConfig config) {} + }; + signatureConfig.setTspService(tspService); + } else { + TimeStampServiceValidator tspValidator = new TimeStampServiceValidator() { + @Override + public void validate(List certificateChain, + RevocationData revocationData) throws Exception { + for (X509Certificate certificate : certificateChain) { + 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 = new RevocationDataService(){ + public RevocationData getRevocationData(List certificateChain) { + return revocationData; + } + }; + signatureConfig.setRevocationDataService(revocationDataService); + + // operate + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(signatureConfig); + si.confirmSignature(); + + // verify + Iterator spIter = si.getSignatureParts().iterator(); + assertTrue(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.close(); + } + + @Test + public void testCertChain() throws Exception { + KeyStore keystore = KeyStore.getInstance("PKCS12"); + String password = "test"; + InputStream is = testdata.openResourceAsStream("chaintest.pfx"); + keystore.load(is, password.toCharArray()); + is.close(); + + Key key = keystore.getKey("poitest", password.toCharArray()); + Certificate chainList[] = keystore.getCertificateChain("poitest"); + List certChain = new ArrayList(); + for (Certificate c : chainList) { + certChain.add((X509Certificate)c); + } + x509 = certChain.get(0); + keyPair = new KeyPair(x509.getPublicKey(), (PrivateKey)key); + + String testFile = "hello-world-unsigned.xlsx"; + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + + SignatureConfig signatureConfig = new SignatureConfig(); + signatureConfig.setKey(keyPair.getPrivate()); + signatureConfig.setSigningCertificateChain(certChain); + Calendar cal = Calendar.getInstance(); + cal.set(2007, 7, 1); + signatureConfig.setExecutionTime(cal.getTime()); + signatureConfig.setDigestAlgo(HashAlgorithm.sha1); + signatureConfig.setOpcPackage(pkg); + + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(signatureConfig); + + si.confirmSignature(); + + for (SignaturePart sp : si.getSignatureParts()){ + boolean b = sp.validate(); + assertTrue(b); + X509Certificate signer = sp.getSigner(); + assertNotNull("signer undefined?!", signer); + List certChainRes = sp.getCertChain(); + assertEquals(3, certChainRes.size()); + } + + pkg.close(); + } + + @Test + public void testNonSha1() throws Exception { + String testFile = "hello-world-unsigned.xlsx"; + initKeyPair("Test", "CN=Test"); + + SignatureConfig signatureConfig = new SignatureConfig(); + signatureConfig.setKey(keyPair.getPrivate()); + signatureConfig.setSigningCertificateChain(Collections.singletonList(x509)); + + HashAlgorithm testAlgo[] = { HashAlgorithm.sha224, HashAlgorithm.sha256 + , HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.ripemd160 }; + + for (HashAlgorithm ha : testAlgo) { + signatureConfig.setDigestAlgo(ha); + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + signatureConfig.setOpcPackage(pkg); + + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(signatureConfig); + + si.confirmSignature(); + boolean b = si.verifySignature(); + pkg.close(); + + assertTrue(b); + } + } + + + private void sign(OPCPackage pkgCopy, String alias, String signerDn, int signerCount) throws Exception { + initKeyPair(alias, signerDn); + + SignatureConfig signatureConfig = new SignatureConfig(); + signatureConfig.setKey(keyPair.getPrivate()); + signatureConfig.setSigningCertificateChain(Collections.singletonList(x509)); + signatureConfig.setExecutionTime(cal.getTime()); + signatureConfig.setDigestAlgo(HashAlgorithm.sha1); + signatureConfig.setOpcPackage(pkgCopy); + + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(signatureConfig); + + Document document = DocumentHelper.createDocument(); + + // operate + DigestInfo digestInfo = si.preSign(document, null); + + // verify + assertNotNull(digestInfo); + LOG.log(POILogger.DEBUG, "digest algo: " + digestInfo.hashAlgo); + LOG.log(POILogger.DEBUG, "digest description: " + digestInfo.description); + assertEquals("Office OpenXML Document", digestInfo.description); + assertNotNull(digestInfo.hashAlgo); + assertNotNull(digestInfo.digestValue); + + // setup: key material, signature value + byte[] signatureValue = si.signDigest(digestInfo.digestValue); + + // operate: postSign + si.postSign(document, signatureValue); + + // verify: signature + si.getSignatureConfig().setOpcPackage(pkgCopy); + List result = new ArrayList(); + for (SignaturePart sp : si.getSignatureParts()) { + if (sp.validate()) { + result.add(sp.getSigner()); + } + } + assertEquals(signerCount, result.size()); + } + + private void initKeyPair(String alias, String subjectDN) throws Exception { + final char password[] = "test".toCharArray(); + File file = new File("build/test.pfx"); + + KeyStore keystore = KeyStore.getInstance("PKCS12"); + + if (file.exists()) { + FileInputStream fis = new FileInputStream(file); + keystore.load(fis, password); + fis.close(); + } else { + keystore.load(null, password); + } + + if (keystore.isKeyEntry(alias)) { + Key key = keystore.getKey(alias, password); + x509 = (X509Certificate)keystore.getCertificate(alias); + keyPair = new KeyPair(x509.getPublicKey(), (PrivateKey)key); + } else { + keyPair = PkiTestUtils.generateKeyPair(); + Calendar cal = Calendar.getInstance(); + Date notBefore = cal.getTime(); + cal.add(Calendar.YEAR, 1); + Date notAfter = cal.getTime(); + KeyUsage keyUsage = new KeyUsage(KeyUsage.digitalSignature); + + x509 = PkiTestUtils.generateCertificate(keyPair.getPublic(), subjectDN + , notBefore, notAfter, null, keyPair.getPrivate(), true, 0, null, null, keyUsage); + + keystore.setKeyEntry(alias, keyPair.getPrivate(), password, new Certificate[]{x509}); + FileOutputStream fos = new FileOutputStream(file); + keystore.store(fos, password); + fos.close(); + } + } + + private static File copy(File input) throws IOException { + String extension = input.getName().replaceAll(".*?(\\.[^.]+)?$", "$1"); + if (extension == null || "".equals(extension)) extension = ".zip"; + File tmpFile = new File("build", "sigtest"+extension); + FileOutputStream fos = new FileOutputStream(tmpFile); + FileInputStream fis = new FileInputStream(input); + IOUtils.copy(fis, fos); + fis.close(); + fos.close(); + return tmpFile; + } + +} diff --git a/src/testcases/org/apache/poi/POIDataSamples.java b/src/testcases/org/apache/poi/POIDataSamples.java index b335215390..a62e664e1e 100644 --- a/src/testcases/org/apache/poi/POIDataSamples.java +++ b/src/testcases/org/apache/poi/POIDataSamples.java @@ -44,6 +44,7 @@ public final class POIDataSamples { private static POIDataSamples _instHPSF; private static POIDataSamples _instHPBF; private static POIDataSamples _instHSMF; + private static POIDataSamples _instXmlDSign; private File _resolvedDataDir; /** true if standard system propery is not set, @@ -114,6 +115,12 @@ public final class POIDataSamples { if(_instHSMF == null) _instHSMF = new POIDataSamples("hsmf"); return _instHSMF; } + + public static POIDataSamples getXmlDSignInstance(){ + if(_instXmlDSign == null) _instXmlDSign = new POIDataSamples("xmldsign"); + return _instXmlDSign; + } + /** * Opens a sample file from the test data directory * diff --git a/test-data/xmldsign/Office2010-SP1-XAdES-X-L.docx b/test-data/xmldsign/Office2010-SP1-XAdES-X-L.docx new file mode 100644 index 0000000000..4aaa772a0d Binary files /dev/null and b/test-data/xmldsign/Office2010-SP1-XAdES-X-L.docx differ diff --git a/test-data/xmldsign/chaintest.pfx b/test-data/xmldsign/chaintest.pfx new file mode 100644 index 0000000000..e92106d2bb Binary files /dev/null and b/test-data/xmldsign/chaintest.pfx differ diff --git a/test-data/xmldsign/hello-world-office-2010-technical-preview-unsigned.docx b/test-data/xmldsign/hello-world-office-2010-technical-preview-unsigned.docx new file mode 100644 index 0000000000..5162b67c60 Binary files /dev/null and b/test-data/xmldsign/hello-world-office-2010-technical-preview-unsigned.docx differ diff --git a/test-data/xmldsign/hello-world-office-2010-technical-preview.docx b/test-data/xmldsign/hello-world-office-2010-technical-preview.docx new file mode 100644 index 0000000000..cbd4277564 Binary files /dev/null and b/test-data/xmldsign/hello-world-office-2010-technical-preview.docx differ diff --git a/test-data/xmldsign/hello-world-signed-twice.docx b/test-data/xmldsign/hello-world-signed-twice.docx new file mode 100644 index 0000000000..96c91e957e Binary files /dev/null and b/test-data/xmldsign/hello-world-signed-twice.docx differ diff --git a/test-data/xmldsign/hello-world-signed.docx b/test-data/xmldsign/hello-world-signed.docx new file mode 100644 index 0000000000..79a7bbb81f Binary files /dev/null and b/test-data/xmldsign/hello-world-signed.docx differ diff --git a/test-data/xmldsign/hello-world-signed.pptx b/test-data/xmldsign/hello-world-signed.pptx new file mode 100644 index 0000000000..9b37033f54 Binary files /dev/null and b/test-data/xmldsign/hello-world-signed.pptx differ diff --git a/test-data/xmldsign/hello-world-signed.xlsx b/test-data/xmldsign/hello-world-signed.xlsx new file mode 100644 index 0000000000..0d45c53ede Binary files /dev/null and b/test-data/xmldsign/hello-world-signed.xlsx differ diff --git a/test-data/xmldsign/hello-world-unsigned.docx b/test-data/xmldsign/hello-world-unsigned.docx new file mode 100644 index 0000000000..1790c961ce Binary files /dev/null and b/test-data/xmldsign/hello-world-unsigned.docx differ diff --git a/test-data/xmldsign/hello-world-unsigned.pptx b/test-data/xmldsign/hello-world-unsigned.pptx new file mode 100644 index 0000000000..ca42529a9a Binary files /dev/null and b/test-data/xmldsign/hello-world-unsigned.pptx differ diff --git a/test-data/xmldsign/hello-world-unsigned.xlsx b/test-data/xmldsign/hello-world-unsigned.xlsx new file mode 100644 index 0000000000..b99143e92c Binary files /dev/null and b/test-data/xmldsign/hello-world-unsigned.xlsx differ diff --git a/test-data/xmldsign/hyperlink-example-signed.docx b/test-data/xmldsign/hyperlink-example-signed.docx new file mode 100644 index 0000000000..f8698fe4d7 Binary files /dev/null and b/test-data/xmldsign/hyperlink-example-signed.docx differ diff --git a/test-data/xmldsign/ms-office-2010-signed.docx b/test-data/xmldsign/ms-office-2010-signed.docx new file mode 100644 index 0000000000..61e4e2a1b8 Binary files /dev/null and b/test-data/xmldsign/ms-office-2010-signed.docx differ diff --git a/test-data/xmldsign/ms-office-2010-signed.pptx b/test-data/xmldsign/ms-office-2010-signed.pptx new file mode 100644 index 0000000000..c70f153248 Binary files /dev/null and b/test-data/xmldsign/ms-office-2010-signed.pptx differ diff --git a/test-data/xmldsign/ms-office-2010-signed.xlsx b/test-data/xmldsign/ms-office-2010-signed.xlsx new file mode 100644 index 0000000000..d8ba05b1da Binary files /dev/null and b/test-data/xmldsign/ms-office-2010-signed.xlsx differ diff --git a/test-data/xmldsign/signed.docx b/test-data/xmldsign/signed.docx new file mode 100644 index 0000000000..98f3b8d841 Binary files /dev/null and b/test-data/xmldsign/signed.docx differ