From 5378df317e4b15401832bde9eddfff8352a0092e Mon Sep 17 00:00:00 2001 From: Ugo Cei Date: Tue, 13 Oct 2009 16:31:28 +0000 Subject: [PATCH] Added implementation of Digital Signature support using code initially developed for the eId Applet project and re-released under Apache License. git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@824836 13f79535-47bb-0310-9956-ffa450edef68 --- build.xml | 54 ++ legal/NOTICE | 3 + .../signer/AbstractXmlSignatureService.java | 610 ++++++++++++++++++ .../service/signer/KeyInfoKeySelector.java | 99 +++ .../service/signer/NoCloseInputStream.java | 53 ++ .../service/signer/NoCloseOutputStream.java | 54 ++ .../service/signer/SignatureAspect.java | 56 ++ .../service/signer/TemporaryDataStorage.java | 65 ++ .../ooxml/AbstractOOXMLSignatureService.java | 348 ++++++++++ .../service/signer/ooxml/OOXMLProvider.java | 54 ++ .../signer/ooxml/OOXMLSignatureAspect.java | 353 ++++++++++ .../signer/ooxml/OOXMLSignatureVerifier.java | 211 ++++++ .../signer/ooxml/OOXMLURIDereferencer.java | 111 ++++ .../signer/ooxml/RelationshipComparator.java | 41 ++ .../RelationshipTransformParameterSpec.java | 58 ++ .../ooxml/RelationshipTransformService.java | 274 ++++++++ .../service/signer/ooxml/package-info.java | 28 + .../service/signer/package-info.java | 28 + .../service/spi/AuthenticationService.java | 56 ++ .../signature/service/spi/DigestInfo.java | 54 ++ .../InsecureClientEnvironmentException.java | 64 ++ .../spi/SecureClientEnvironmentService.java | 73 +++ .../service/spi/SignatureService.java | 77 +++ .../signature/service/spi/package-info.java | 28 + ...ffice-2010-technical-preview-unsigned.docx | Bin 0 -> 14447 bytes ...o-world-office-2010-technical-preview.docx | Bin 0 -> 17044 bytes .../testcases/hello-world-signed-twice.docx | Bin 0 -> 16099 bytes src/ooxml/testcases/hello-world-signed.docx | Bin 0 -> 13244 bytes src/ooxml/testcases/hello-world-signed.pptx | Bin 0 -> 36107 bytes src/ooxml/testcases/hello-world-signed.xlsx | Bin 0 -> 13072 bytes src/ooxml/testcases/hello-world-unsigned.docx | Bin 0 -> 9898 bytes src/ooxml/testcases/hello-world-unsigned.pptx | Bin 0 -> 32220 bytes src/ooxml/testcases/hello-world-unsigned.xlsx | Bin 0 -> 9748 bytes src/ooxml/testcases/invalidsig.docx | Bin 0 -> 12228 bytes .../service/signer/PkiTestUtils.java | 199 ++++++ .../signer/TemporaryTestDataStorage.java | 66 ++ .../TestAbstractOOXMLSignatureService.java | 214 ++++++ .../TestAbstractXmlSignatureService.java | 560 ++++++++++++++++ .../signer/TestOOXMLSignatureVerifier.java | 238 +++++++ 39 files changed, 4129 insertions(+) create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/AbstractXmlSignatureService.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/KeyInfoKeySelector.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseInputStream.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseOutputStream.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/SignatureAspect.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/TemporaryDataStorage.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/AbstractOOXMLSignatureService.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLProvider.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureAspect.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureVerifier.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLURIDereferencer.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipComparator.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformParameterSpec.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformService.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/package-info.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/package-info.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/AuthenticationService.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/DigestInfo.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/InsecureClientEnvironmentException.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SecureClientEnvironmentService.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SignatureService.java create mode 100644 src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/package-info.java create mode 100644 src/ooxml/testcases/hello-world-office-2010-technical-preview-unsigned.docx create mode 100644 src/ooxml/testcases/hello-world-office-2010-technical-preview.docx create mode 100644 src/ooxml/testcases/hello-world-signed-twice.docx create mode 100644 src/ooxml/testcases/hello-world-signed.docx create mode 100644 src/ooxml/testcases/hello-world-signed.pptx create mode 100644 src/ooxml/testcases/hello-world-signed.xlsx create mode 100644 src/ooxml/testcases/hello-world-unsigned.docx create mode 100644 src/ooxml/testcases/hello-world-unsigned.pptx create mode 100644 src/ooxml/testcases/hello-world-unsigned.xlsx create mode 100644 src/ooxml/testcases/invalidsig.docx create mode 100644 src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/PkiTestUtils.java create mode 100644 src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TemporaryTestDataStorage.java create mode 100644 src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractOOXMLSignatureService.java create mode 100644 src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractXmlSignatureService.java create mode 100644 src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestOOXMLSignatureVerifier.java diff --git a/build.xml b/build.xml index 6feaf34826..8b9b3550d1 100644 --- a/build.xml +++ b/build.xml @@ -132,6 +132,21 @@ under the License. + + + + + + + + + + + + + + + @@ -183,6 +198,9 @@ under the License. + + + @@ -208,6 +226,7 @@ under the License. + @@ -351,6 +370,13 @@ under the License. + + + + + + + @@ -373,6 +399,34 @@ under the License. + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/legal/NOTICE b/legal/NOTICE index 6d9855fb87..dc948c1d7f 100644 --- a/legal/NOTICE +++ b/legal/NOTICE @@ -19,3 +19,6 @@ This product contains the Piccolo XML Parser for Java This product contains the chunks_parse_cmds.tbl file from the vsdump program. Copyright (C) 2006-2007 Valek Filippov (frob@df.ru) + +This product contains parts that were originally based on the eID Applet project +(http://code.google.com/p/eid-applet/). Copyright (C) 2008-2009 FedICT. \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/AbstractXmlSignatureService.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/AbstractXmlSignatureService.java new file mode 100644 index 0000000000..73b4980817 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/AbstractXmlSignatureService.java @@ -0,0 +1,610 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.URIDereferencer; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMCryptoContext; +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.Transform; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignContext; +import javax.xml.crypto.dsig.XMLSignatureException; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; +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.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.ooxml.signature.service.spi.DigestInfo; +import org.apache.poi.ooxml.signature.service.spi.SignatureService; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.utils.Base64; +import org.apache.xml.security.utils.Constants; +import org.apache.xpath.XPathAPI; +import org.jcp.xml.dsig.internal.dom.DOMReference; +import org.jcp.xml.dsig.internal.dom.DOMSignedInfo; +import org.jcp.xml.dsig.internal.dom.DOMXMLSignature; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + + + +/** + * Abstract base class for an XML Signature Service implementation. + */ +public abstract class AbstractXmlSignatureService implements SignatureService { + + static final Log LOG = LogFactory.getLog(AbstractXmlSignatureService.class); + + private static final String SIGNATURE_ID_ATTRIBUTE = "signature-id"; + + // TODO refactor everything using the signature aspect design pattern + private final List signatureAspects; + + /** + * Main constructor. + */ + public AbstractXmlSignatureService() { + this.signatureAspects = new LinkedList(); + } + + /** + * Adds a signature aspect to this XML signature service. + * + * @param signatureAspect + */ + protected void addSignatureAspect(SignatureAspect signatureAspect) { + this.signatureAspects.add(signatureAspect); + } + + /** + * Gives back the signature digest algorithm. Allowed values are SHA-1, + * SHA-256, SHA-384, SHA-512, RIPEND160. The default algorithm is SHA-1. + * Override this method to select another signature digest algorithm. + * + * @return + */ + protected String getSignatureDigestAlgorithm() { + return "SHA-1"; + } + + /** + * Gives back a list of service digest infos. Override this method to + * provide digest infos of files located in the service itself. + * + * @return + */ + protected List getServiceDigestInfos() { + return new LinkedList(); + } + + /** + * Gives back the enveloping document. Return null in case + * ds:Signature should be the top-level element. Implementations can + * override this method to provide a custom enveloping document. + * + * @return + * @throws SAXException + * @throws IOException + */ + protected Document getEnvelopingDocument() throws ParserConfigurationException, IOException, SAXException { + return null; + } + + /** + * Gives back a list of reference URIs that need to be signed. These URIs + * can refer to elements inside the enveloping document or to external + * resources. Override this method to feed in other ds:Reference URIs. + * + * @return + */ + protected List getReferenceUris() { + return new LinkedList(); + } + + public static class ReferenceInfo { + private final String uri; + private final String transform; + + public ReferenceInfo(String uri, String transform) { + this.uri = uri; + this.transform = transform; + } + + public ReferenceInfo(String uri) { + this(uri, null); + } + + public String getUri() { + return this.uri; + } + + public String getTransform() { + return this.transform; + } + } + + /** + * Gives back a list of references that need to be signed. Implementation + * can override this method. + * + * @return + */ + protected List getReferences() { + return new LinkedList(); + } + + /** + * Override this method to change the URI dereferener used by the signing + * engine. + * + * @return + */ + protected URIDereferencer getURIDereferencer() { + return null; + } + + /** + * Gives back the human-readable description of what the citizen will be + * signing. The default value is "XML Signature". Override this method to + * provide the citizen with another description. + * + * @return + */ + protected String getSignatureDescription() { + return "XML Signature"; + } + + /** + * Gives back a temporary data storage component. This component is used for + * temporary storage of the XML signature documents. + * + * @return + */ + protected abstract TemporaryDataStorage getTemporaryDataStorage(); + + /** + * Gives back the output stream to which to write the signed XML document. + * + * @return + */ + protected abstract OutputStream getSignedDocumentOutputStream(); + + public DigestInfo preSign(List digestInfos, List signingCertificateChain) throws NoSuchAlgorithmException { + LOG.debug("preSign"); + String digestAlgo = getSignatureDigestAlgorithm(); + + byte[] digestValue; + try { + digestValue = getXmlSignatureDigestValue(digestAlgo, digestInfos); + } catch (Exception e) { + throw new RuntimeException("XML signature error: " + e.getMessage(), e); + } + + String description = getSignatureDescription(); + return new DigestInfo(digestValue, digestAlgo, description); + } + + /** + * Can be overridden by XML signature service implementation to further + * process the signed XML document. + * + * @param sinatureElement + * @param signingCertificateChain + */ + protected void postSign(Element sinatureElement, List signingCertificateChain) { + // empty + } + + public void postSign(byte[] signatureValue, List signingCertificateChain) { + LOG.debug("postSign"); + + /* + * Retrieve the intermediate XML signature document from the temporary + * data storage. + */ + TemporaryDataStorage temporaryDataStorage = getTemporaryDataStorage(); + InputStream documentInputStream = temporaryDataStorage.getTempInputStream(); + String signatureId = (String) temporaryDataStorage.getAttribute(SIGNATURE_ID_ATTRIBUTE); + LOG.debug("signature Id: " + signatureId); + + /* + * Load the signature DOM document. + */ + Document document; + try { + document = loadDocument(documentInputStream); + } catch (Exception e) { + throw new RuntimeException("DOM error: " + e.getMessage(), e); + } + + /* + * Locate the correct ds:Signature node. + */ + Element nsElement = document.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + Element signatureElement; + try { + signatureElement = (Element) XPathAPI.selectSingleNode(document, "//ds:Signature[@Id='" + signatureId + "']", nsElement); + } catch (TransformerException e) { + throw new RuntimeException("XPATH error: " + e.getMessage(), e); + } + if (null == signatureElement) { + throw new RuntimeException("ds:Signature not found for @Id: " + signatureId); + } + + /* + * Insert signature value into the ds:SignatureValue element + */ + NodeList signatureValueNodeList = signatureElement.getElementsByTagNameNS(javax.xml.crypto.dsig.XMLSignature.XMLNS, "SignatureValue"); + Element signatureValueElement = (Element) signatureValueNodeList.item(0); + signatureValueElement.setTextContent(Base64.encode(signatureValue)); + + /* + * Allow implementation classes to inject their own stuff. + */ + postSign(signatureElement, signingCertificateChain); + + OutputStream signedDocumentOutputStream = getSignedDocumentOutputStream(); + if (null == signedDocumentOutputStream) { + throw new IllegalArgumentException("signed document output stream is null"); + } + try { + writeDocument(document, signedDocumentOutputStream); + } catch (Exception e) { + LOG.debug("error writing the signed XML document: " + e.getMessage(), e); + throw new RuntimeException("error writing the signed XML document: " + e.getMessage(), e); + } + } + + protected String getCanonicalizationMethod() { + // CanonicalizationMethod.INCLUSIVE fails for OOo + return CanonicalizationMethod.EXCLUSIVE; + } + + private byte[] getXmlSignatureDigestValue(String digestAlgo, List digestInfos) throws ParserConfigurationException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, MarshalException, javax.xml.crypto.dsig.XMLSignatureException, + TransformerFactoryConfigurationError, TransformerException, IOException, SAXException { + /* + * DOM Document construction. + */ + Document document = getEnvelopingDocument(); + if (null == document) { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + document = documentBuilder.newDocument(); + } + + /* + * Signature context construction. + */ + 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; + } + }; + XMLSignContext xmlSignContext = new DOMSignContext(key, document); + URIDereferencer uriDereferencer = getURIDereferencer(); + if (null != uriDereferencer) { + xmlSignContext.setURIDereferencer(uriDereferencer); + } + + // OOo doesn't like ds namespaces. + // xmlSignContext.putNamespacePrefix( + // javax.xml.crypto.dsig.XMLSignature.XMLNS, "ds"); + + XMLSignatureFactory signatureFactory = XMLSignatureFactory.getInstance("DOM", new org.jcp.xml.dsig.internal.dom.XMLDSigRI()); + + /* + * ds:Reference + */ + List references = new LinkedList(); + addDigestInfosAsReferences(digestInfos, signatureFactory, references); + List serviceDigestInfos = getServiceDigestInfos(); + addDigestInfosAsReferences(serviceDigestInfos, signatureFactory, references); + addReferenceIds(signatureFactory, xmlSignContext, references); + addReferences(signatureFactory, references); + + /* + * Invoke the signature aspects. + */ + String signatureId = "xmldsig-" + UUID.randomUUID().toString(); + List objects = new LinkedList(); + for (SignatureAspect signatureAspect : this.signatureAspects) { + LOG.debug("invoking signature aspect: " + signatureAspect.getClass().getSimpleName()); + signatureAspect.preSign(signatureFactory, document, signatureId, references, objects); + } + + /* + * ds:SignedInfo + */ + SignatureMethod signatureMethod = signatureFactory.newSignatureMethod(getSignatureMethod(digestAlgo), null); + CanonicalizationMethod canonicalizationMethod = signatureFactory.newCanonicalizationMethod(getCanonicalizationMethod(), (C14NMethodParameterSpec) null); + SignedInfo signedInfo = signatureFactory.newSignedInfo(canonicalizationMethod, signatureMethod, references); + + /* + * JSR105 ds:Signature creation + */ + String signatureValueId = signatureId + "-signature-value"; + javax.xml.crypto.dsig.XMLSignature xmlSignature = signatureFactory.newXMLSignature(signedInfo, null, objects, signatureId, signatureValueId); + + /* + * ds:Signature Marshalling. + */ + DOMXMLSignature domXmlSignature = (DOMXMLSignature) xmlSignature; + Node documentNode = document.getDocumentElement(); + if (null == documentNode) { + /* + * In case of an empty DOM document. + */ + documentNode = document; + } + String dsPrefix = null; + // String dsPrefix = "ds"; + domXmlSignature.marshal(documentNode, dsPrefix, (DOMCryptoContext) xmlSignContext); + + /* + * Completion of undigested ds:References in the ds:Manifests. + */ + for (XMLObject object : objects) { + LOG.debug("object java type: " + object.getClass().getName()); + List objectContentList = object.getContent(); + for (XMLStructure objectContent : objectContentList) { + LOG.debug("object content java type: " + objectContent.getClass().getName()); + if (false == objectContent instanceof Manifest) { + continue; + } + Manifest manifest = (Manifest) objectContent; + List manifestReferences = manifest.getReferences(); + for (Reference manifestReference : manifestReferences) { + if (null != manifestReference.getDigestValue()) { + 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; + if (null != domReference.getDigestValue()) { + // ds:Reference with external digest value + continue; + } + domReference.digest(xmlSignContext); + } + + /* + * Store the intermediate XML signature document. + */ + TemporaryDataStorage temporaryDataStorage = getTemporaryDataStorage(); + OutputStream tempDocumentOutputStream = temporaryDataStorage.getTempOutputStream(); + writeDocument(document, tempDocumentOutputStream); + temporaryDataStorage.setAttribute(SIGNATURE_ID_ATTRIBUTE, signatureId); + + /* + * 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 jcaMessageDigest = MessageDigest.getInstance(digestAlgo); + byte[] digestValue = jcaMessageDigest.digest(octets); + return digestValue; + } + + private void addReferenceIds(XMLSignatureFactory signatureFactory, XMLSignContext xmlSignContext, List references) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, XMLSignatureException { + List referenceUris = getReferenceUris(); + if (null == referenceUris) { + return; + } + DigestMethod digestMethod = signatureFactory.newDigestMethod(DigestMethod.SHA1, null); + for (String referenceUri : referenceUris) { + Reference reference = signatureFactory.newReference(referenceUri, digestMethod); + references.add(reference); + } + } + + private void addReferences(XMLSignatureFactory xmlSignatureFactory, List references) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + List referenceInfos = getReferences(); + if (null == referenceInfos) { + return; + } + if (referenceInfos.isEmpty()) { + return; + } + DigestMethod digestMethod = xmlSignatureFactory.newDigestMethod(DigestMethod.SHA1, null); + for (ReferenceInfo referenceInfo : referenceInfos) { + List transforms = new LinkedList(); + if (null != referenceInfo.getTransform()) { + Transform transform = xmlSignatureFactory.newTransform(referenceInfo.getTransform(), (TransformParameterSpec) null); + transforms.add(transform); + } + LOG.debug("adding ds:Reference " + referenceInfo.getUri()); + Reference reference = xmlSignatureFactory.newReference(referenceInfo.getUri(), digestMethod, transforms, null, null); + references.add(reference); + } + } + + private void addDigestInfosAsReferences(List digestInfos, XMLSignatureFactory signatureFactory, List references) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException, MalformedURLException { + if (null == digestInfos) { + return; + } + for (DigestInfo digestInfo : digestInfos) { + byte[] documentDigestValue = digestInfo.digestValue; + + DigestMethod digestMethod = signatureFactory.newDigestMethod(getXmlDigestAlgo(digestInfo.digestAlgo), null); + + String uri = FilenameUtils.getName(new File(digestInfo.description).toURI().toURL().getFile()); + + Reference reference = signatureFactory.newReference(uri, digestMethod, null, null, null, documentDigestValue); + references.add(reference); + } + } + + private String getXmlDigestAlgo(String digestAlgo) { + if ("SHA-1".equals(digestAlgo)) { + return DigestMethod.SHA1; + } + if ("SHA-256".equals(digestAlgo)) { + return DigestMethod.SHA256; + } + if ("SHA-512".equals(digestAlgo)) { + return DigestMethod.SHA512; + } + throw new RuntimeException("unsupported digest algo: " + digestAlgo); + } + + private String getSignatureMethod(String digestAlgo) { + if (null == digestAlgo) { + throw new RuntimeException("digest algo is null"); + } + if ("SHA-1".equals(digestAlgo)) { + return SignatureMethod.RSA_SHA1; + } + if ("SHA-256".equals(digestAlgo)) { + return XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256; + } + if ("SHA-512".equals(digestAlgo)) { + return XMLSignature.ALGO_ID_MAC_HMAC_SHA512; + } + if ("SHA-384".equals(digestAlgo)) { + return XMLSignature.ALGO_ID_MAC_HMAC_SHA384; + } + if ("RIPEMD160".equals(digestAlgo)) { + return XMLSignature.ALGO_ID_MAC_HMAC_RIPEMD160; + } + throw new RuntimeException("unsupported sign algo: " + digestAlgo); + } + + protected void writeDocument(Document document, OutputStream documentOutputStream) throws TransformerConfigurationException, + TransformerFactoryConfigurationError, TransformerException, IOException { + writeDocumentNoClosing(document, documentOutputStream); + documentOutputStream.close(); + } + + protected void writeDocumentNoClosing(Document document, OutputStream documentOutputStream) throws TransformerConfigurationException, + TransformerFactoryConfigurationError, TransformerException, IOException { + // we need the XML processing initial line for OOXML + writeDocumentNoClosing(document, documentOutputStream, false); + } + + protected void writeDocumentNoClosing(Document document, OutputStream documentOutputStream, boolean omitXmlDeclaration) + throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException, IOException { + NoCloseOutputStream outputStream = new NoCloseOutputStream(documentOutputStream); + Result result = new StreamResult(outputStream); + Transformer xformer = TransformerFactory.newInstance().newTransformer(); + if (omitXmlDeclaration) { + xformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + } + Source source = new DOMSource(document); + xformer.transform(source, result); + } + + protected 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; + } + + protected Document loadDocumentNoClose(InputStream documentInputStream) throws ParserConfigurationException, SAXException, IOException { + NoCloseInputStream noCloseInputStream = new NoCloseInputStream(documentInputStream); + InputSource inputSource = new InputSource(noCloseInputStream); + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.parse(inputSource); + return document; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/KeyInfoKeySelector.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/KeyInfoKeySelector.java new file mode 100644 index 0000000000..fc55015e60 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/KeyInfoKeySelector.java @@ -0,0 +1,99 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.security.Key; +import java.security.cert.X509Certificate; +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.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * JSR105 key selector implementation using the ds:KeyInfo data of the signature + * itself. + */ +public class KeyInfoKeySelector extends KeySelector implements KeySelectorResult { + + private static final Log LOG = LogFactory.getLog(KeyInfoKeySelector.class); + + private X509Certificate certificate; + + @Override + public KeySelectorResult select(KeyInfo keyInfo, Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException { + LOG.debug("select key"); + if (null == keyInfo) { + throw new KeySelectorException("no ds:KeyInfo present"); + } + List keyInfoContent = keyInfo.getContent(); + this.certificate = null; + for (XMLStructure keyInfoStructure : keyInfoContent) { + if (false == (keyInfoStructure instanceof X509Data)) { + continue; + } + X509Data x509Data = (X509Data) keyInfoStructure; + List x509DataList = x509Data.getContent(); + for (Object x509DataObject : x509DataList) { + if (false == (x509DataObject instanceof X509Certificate)) { + continue; + } + X509Certificate certificate = (X509Certificate) x509DataObject; + LOG.debug("certificate: " + certificate.getSubjectX500Principal()); + if (null == this.certificate) { + /* + * The first certificate is presumably the signer. + */ + this.certificate = certificate; + } + } + if (null != this.certificate) { + return this; + } + } + throw new KeySelectorException("No key found!"); + } + + public Key getKey() { + return this.certificate.getPublicKey(); + } + + /** + * Gives back the X509 certificate used during the last signature + * verification operation. + * + * @return + */ + public X509Certificate getCertificate() { + return this.certificate; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseInputStream.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseInputStream.java new file mode 100644 index 0000000000..abd3551ead --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseInputStream.java @@ -0,0 +1,53 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.io.input.ProxyInputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Input Stream proxy that doesn't close the underlying input stream. + */ +public class NoCloseInputStream extends ProxyInputStream { + + private static final Log LOG = LogFactory.getLog(NoCloseInputStream.class); + + /** + * Main constructor. + * + * @param proxy + */ + public NoCloseInputStream(InputStream proxy) { + super(proxy); + } + + @Override + public void close() throws IOException { + LOG.debug("close"); + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseOutputStream.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseOutputStream.java new file mode 100644 index 0000000000..85464bf2ba --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/NoCloseOutputStream.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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.output.ProxyOutputStream; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Output Stream proxy that doesn't close the underlying stream. + */ +public class NoCloseOutputStream extends ProxyOutputStream { + + private static final Log LOG = LogFactory.getLog(NoCloseOutputStream.class); + + /** + * Main constructor. + * + * @param proxy + */ + public NoCloseOutputStream(OutputStream proxy) { + super(proxy); + } + + @Override + public void close() throws IOException { + LOG.debug("close"); + // empty + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/SignatureAspect.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/SignatureAspect.java new file mode 100644 index 0000000000..9865a7703f --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/SignatureAspect.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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; + +import org.w3c.dom.Document; + +/** + * JSR105 Signature Aspect interface. + */ +public interface SignatureAspect { + + /** + * This method is being invoked by the XML signature service engine during + * pre-sign phase. Via this method a signature aspect implementation can add + * signature aspects to an XML signature. + * + * @param signatureFactory + * @param document + * @param signatureId + * @param references + * @param objects + * @throws InvalidAlgorithmParameterException + * @throws NoSuchAlgorithmException + */ + void preSign(XMLSignatureFactory signatureFactory, Document document, String signatureId, List references, List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException; +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/TemporaryDataStorage.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/TemporaryDataStorage.java new file mode 100644 index 0000000000..ec5c94073f --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/TemporaryDataStorage.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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; + +/** + * Interface for temporary data storage. + */ +public interface TemporaryDataStorage { + + /** + * Gives back the temporary output stream that can be used for data storage. + * + * @return + */ + OutputStream getTempOutputStream(); + + /** + * Gives back the temporary input stream for retrieval of the previously + * stored data. + * + * @return + */ + InputStream getTempInputStream(); + + /** + * Stores an attribute to the temporary data storage. + * + * @param attributeName + * @param attributeValue + */ + void setAttribute(String attributeName, Serializable attributeValue); + + /** + * Retrieves an attribute from the temporary data storage. + * + * @param attributeName + * @return + */ + Serializable getAttribute(String attributeName); +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/AbstractOOXMLSignatureService.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/AbstractOOXMLSignatureService.java new file mode 100644 index 0000000000..f76d69d6a8 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/AbstractOOXMLSignatureService.java @@ -0,0 +1,348 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; +import java.security.Key; +import java.security.KeyException; +import java.security.cert.X509Certificate; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.URIDereferencer; +import javax.xml.crypto.dom.DOMCryptoContext; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.XMLSignContext; +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 javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactoryConfigurationError; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.ooxml.signature.service.signer.AbstractXmlSignatureService; +import org.apache.xml.security.utils.Constants; +import org.apache.xpath.XPathAPI; +import org.jcp.xml.dsig.internal.dom.DOMKeyInfo; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + + + +/** + * Signature Service implementation for Office OpenXML document format XML + * signatures. + */ +public abstract class AbstractOOXMLSignatureService extends AbstractXmlSignatureService { + + static final Log LOG = LogFactory.getLog(AbstractOOXMLSignatureService.class); + + protected AbstractOOXMLSignatureService() { + addSignatureAspect(new OOXMLSignatureAspect(this)); + } + + @Override + protected String getSignatureDescription() { + return "Office OpenXML Document"; + } + + public String getFilesDigestAlgorithm() { + return null; + } + + @Override + protected final URIDereferencer getURIDereferencer() { + URL ooxmlUrl = getOfficeOpenXMLDocumentURL(); + return new OOXMLURIDereferencer(ooxmlUrl); + } + + @Override + protected String getCanonicalizationMethod() { + return CanonicalizationMethod.INCLUSIVE; + } + + @Override + protected void postSign(Element signatureElement, List signingCertificateChain) { + // TODO: implement as SignatureAspect + LOG.debug("postSign: adding ds:KeyInfo"); + /* + * Make sure we insert right after the ds:SignatureValue element. + */ + Node nextSibling; + NodeList objectNodeList = signatureElement.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Object"); + if (0 == objectNodeList.getLength()) { + nextSibling = null; + } else { + nextSibling = objectNodeList.item(0); + } + /* + * Add a ds:KeyInfo entry. + */ + KeyInfoFactory keyInfoFactory = KeyInfoFactory.getInstance(); + List x509DataObjects = new LinkedList(); + + X509Certificate signingCertificate = signingCertificateChain.get(0); + KeyValue keyValue; + try { + keyValue = keyInfoFactory.newKeyValue(signingCertificate.getPublicKey()); + } catch (KeyException e) { + throw new RuntimeException("key exception: " + e.getMessage(), e); + } + + for (X509Certificate certificate : signingCertificateChain) { + x509DataObjects.add(certificate); + } + X509Data x509Data = keyInfoFactory.newX509Data(x509DataObjects); + List keyInfoContent = new LinkedList(); + keyInfoContent.add(keyValue); + 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; + } + }; + XMLSignContext xmlSignContext = new DOMSignContext(key, signatureElement); + DOMCryptoContext domCryptoContext = (DOMCryptoContext) xmlSignContext; + String dsPrefix = null; + // String dsPrefix = "ds"; + try { + domKeyInfo.marshal(signatureElement, nextSibling, dsPrefix, domCryptoContext); + } catch (MarshalException e) { + throw new RuntimeException("marshall error: " + e.getMessage(), e); + } + } + + private class OOXMLSignedDocumentOutputStream extends ByteArrayOutputStream { + + @Override + public void close() throws IOException { + LOG.debug("close OOXML signed document output stream"); + super.close(); + try { + outputSignedOfficeOpenXMLDocument(this.toByteArray()); + } catch (Exception e) { + throw new IOException("generic error: " + e.getMessage(), e); + } + } + } + + /** + * The output stream to which to write the signed Office OpenXML file. + * + * @return + */ + abstract protected OutputStream getSignedOfficeOpenXMLDocumentOutputStream(); + + /** + * Gives back the URL of the OOXML to be signed. + * + * @return + */ + abstract protected URL getOfficeOpenXMLDocumentURL(); + + private void outputSignedOfficeOpenXMLDocument(byte[] signatureData) throws IOException, ParserConfigurationException, SAXException, TransformerException { + LOG.debug("output signed Office OpenXML document"); + OutputStream signedOOXMLOutputStream = getSignedOfficeOpenXMLDocumentOutputStream(); + if (null == signedOOXMLOutputStream) { + throw new NullPointerException("signedOOXMLOutputStream is null"); + } + + String signatureZipEntryName = "_xmlsignatures/sig-" + UUID.randomUUID().toString() + ".xml"; + LOG.debug("signature ZIP entry name: " + signatureZipEntryName); + /* + * Copy the original OOXML content to the signed OOXML package. During + * copying some files need to changed. + */ + ZipOutputStream zipOutputStream = copyOOXMLContent(signatureZipEntryName, signedOOXMLOutputStream); + + /* + * Add the OOXML XML signature file to the OOXML package. + */ + ZipEntry zipEntry = new ZipEntry(signatureZipEntryName); + zipOutputStream.putNextEntry(zipEntry); + IOUtils.write(signatureData, zipOutputStream); + zipOutputStream.close(); + } + + private ZipOutputStream copyOOXMLContent(String signatureZipEntryName, OutputStream signedOOXMLOutputStream) throws IOException, + ParserConfigurationException, SAXException, TransformerConfigurationException, TransformerFactoryConfigurationError, + TransformerException { + ZipOutputStream zipOutputStream = new ZipOutputStream(signedOOXMLOutputStream); + ZipInputStream zipInputStream = new ZipInputStream(this.getOfficeOpenXMLDocumentURL().openStream()); + ZipEntry zipEntry; + boolean hasOriginSigsRels = false; + while (null != (zipEntry = zipInputStream.getNextEntry())) { + LOG.debug("copy ZIP entry: " + zipEntry.getName()); + ZipEntry newZipEntry = new ZipEntry(zipEntry.getName()); + zipOutputStream.putNextEntry(newZipEntry); + if ("[Content_Types].xml".equals(zipEntry.getName())) { + Document contentTypesDocument = loadDocumentNoClose(zipInputStream); + Element typesElement = contentTypesDocument.getDocumentElement(); + + /* + * We need to add an Override element. + */ + Element overrideElement = contentTypesDocument.createElementNS("http://schemas.openxmlformats.org/package/2006/content-types", "Override"); + overrideElement.setAttribute("PartName", "/" + signatureZipEntryName); + overrideElement.setAttribute("ContentType", "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"); + typesElement.appendChild(overrideElement); + + Element nsElement = contentTypesDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:tns", "http://schemas.openxmlformats.org/package/2006/content-types"); + NodeList nodeList = XPathAPI.selectNodeList(contentTypesDocument, "/tns:Types/tns:Default[@Extension='sigs']", nsElement); + if (0 == nodeList.getLength()) { + /* + * Add Default element for 'sigs' extension. + */ + Element defaultElement = contentTypesDocument.createElementNS("http://schemas.openxmlformats.org/package/2006/content-types", "Default"); + defaultElement.setAttribute("Extension", "sigs"); + defaultElement.setAttribute("ContentType", "application/vnd.openxmlformats-package.digital-signature-origin"); + typesElement.appendChild(defaultElement); + } + + writeDocumentNoClosing(contentTypesDocument, zipOutputStream, false); + } else if ("_rels/.rels".equals(zipEntry.getName())) { + Document relsDocument = loadDocumentNoClose(zipInputStream); + + Element nsElement = relsDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:tns", "http://schemas.openxmlformats.org/package/2006/relationships"); + NodeList nodeList = XPathAPI.selectNodeList(relsDocument, "/tns:Relationships/tns:Relationship[@Target='_xmlsignatures/origin.sigs']", + nsElement); + if (0 == nodeList.getLength()) { + Element relationshipElement = relsDocument.createElementNS("http://schemas.openxmlformats.org/package/2006/relationships", "Relationship"); + relationshipElement.setAttribute("Id", "rel-id-" + UUID.randomUUID().toString()); + relationshipElement.setAttribute("Type", "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin"); + relationshipElement.setAttribute("Target", "_xmlsignatures/origin.sigs"); + + relsDocument.getDocumentElement().appendChild(relationshipElement); + } + + writeDocumentNoClosing(relsDocument, zipOutputStream, false); + } else if ("_xmlsignatures/_rels/origin.sigs.rels".equals(zipEntry.getName())) { + hasOriginSigsRels = true; + Document originSignRelsDocument = loadDocumentNoClose(zipInputStream); + + Element relationshipElement = originSignRelsDocument.createElementNS("http://schemas.openxmlformats.org/package/2006/relationships", + "Relationship"); + String relationshipId = "rel-" + UUID.randomUUID().toString(); + relationshipElement.setAttribute("Id", relationshipId); + relationshipElement.setAttribute("Type", "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/signature"); + String target = FilenameUtils.getName(signatureZipEntryName); + LOG.debug("target: " + target); + relationshipElement.setAttribute("Target", target); + originSignRelsDocument.getDocumentElement().appendChild(relationshipElement); + + writeDocumentNoClosing(originSignRelsDocument, zipOutputStream, false); + } else { + IOUtils.copy(zipInputStream, zipOutputStream); + } + } + + if (false == hasOriginSigsRels) { + /* + * Add signature relationships document. + */ + addOriginSigsRels(signatureZipEntryName, zipOutputStream); + addOriginSigs(zipOutputStream); + } + + /* + * Return. + */ + zipInputStream.close(); + return zipOutputStream; + } + + private void addOriginSigs(ZipOutputStream zipOutputStream) throws IOException { + zipOutputStream.putNextEntry(new ZipEntry("_xmlsignatures/origin.sigs")); + } + + private void addOriginSigsRels(String signatureZipEntryName, ZipOutputStream zipOutputStream) throws ParserConfigurationException, IOException, + TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document originSignRelsDocument = documentBuilder.newDocument(); + + Element relationshipsElement = originSignRelsDocument.createElementNS("http://schemas.openxmlformats.org/package/2006/relationships", "Relationships"); + relationshipsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns", "http://schemas.openxmlformats.org/package/2006/relationships"); + originSignRelsDocument.appendChild(relationshipsElement); + + Element relationshipElement = originSignRelsDocument.createElementNS("http://schemas.openxmlformats.org/package/2006/relationships", "Relationship"); + String relationshipId = "rel-" + UUID.randomUUID().toString(); + relationshipElement.setAttribute("Id", relationshipId); + relationshipElement.setAttribute("Type", "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/signature"); + String target = FilenameUtils.getName(signatureZipEntryName); + LOG.debug("target: " + target); + relationshipElement.setAttribute("Target", target); + relationshipsElement.appendChild(relationshipElement); + + zipOutputStream.putNextEntry(new ZipEntry("_xmlsignatures/_rels/origin.sigs.rels")); + writeDocumentNoClosing(originSignRelsDocument, zipOutputStream, false); + } + + @Override + protected OutputStream getSignedDocumentOutputStream() { + LOG.debug("get signed document output stream"); + /* + * Create each time a new object; we want an empty output stream to + * start with. + */ + OutputStream signedDocumentOutputStream = new OOXMLSignedDocumentOutputStream(); + return signedDocumentOutputStream; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLProvider.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLProvider.java new file mode 100644 index 0000000000..16fe40cda2 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLProvider.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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.security.Provider; +import java.security.Security; + +/** + * Security Provider for Office OpenXML. + */ +public class OOXMLProvider extends Provider { + + private static final long serialVersionUID = 1L; + + public static final String NAME = "OOXMLProvider"; + + private OOXMLProvider() { + super(NAME, 1.0, "OOXML Security Provider"); + put("TransformService." + RelationshipTransformService.TRANSFORM_URI, RelationshipTransformService.class.getName()); + put("TransformService." + RelationshipTransformService.TRANSFORM_URI + " MechanismType", "DOM"); + } + + /** + * Installs this security provider. + */ + public static void install() { + Provider provider = Security.getProvider(NAME); + if (null == provider) { + Security.addProvider(new OOXMLProvider()); + } + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureAspect.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureAspect.java new file mode 100644 index 0000000000..df6956664f --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureAspect.java @@ -0,0 +1,353 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +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 javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.ooxml.signature.service.signer.NoCloseInputStream; +import org.apache.poi.ooxml.signature.service.signer.SignatureAspect; +import org.apache.xml.security.utils.Constants; +import org.apache.xpath.XPathAPI; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + + + +/** + * Office OpenXML Signature Aspect implementation. + */ +public class OOXMLSignatureAspect implements SignatureAspect { + + private static final Log LOG = LogFactory.getLog(OOXMLSignatureAspect.class); + + private final AbstractOOXMLSignatureService signatureService; + + /** + * Main constructor. + * + * @param ooxmlUrl + */ + public OOXMLSignatureAspect(AbstractOOXMLSignatureService signatureService) { + this.signatureService = signatureService; + } + + public void preSign(XMLSignatureFactory signatureFactory, Document document, String signatureId, List references, List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + LOG.debug("pre sign"); + addManifestObject(signatureFactory, document, signatureId, references, objects); + + addSignatureInfo(signatureFactory, document, signatureId, references, objects); + } + + private void addManifestObject(XMLSignatureFactory signatureFactory, Document document, String signatureId, List references, + List objects) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + Manifest manifest = constructManifest(signatureFactory, document); + String objectId = "idPackageObject"; // really has to be this value. + List objectContent = new LinkedList(); + objectContent.add(manifest); + + addSignatureTime(signatureFactory, document, signatureId, objectContent); + + objects.add(signatureFactory.newXMLObject(objectContent, objectId, null, null)); + + DigestMethod digestMethod = signatureFactory.newDigestMethod(DigestMethod.SHA1, null); + Reference reference = signatureFactory.newReference("#" + objectId, digestMethod, null, "http://www.w3.org/2000/09/xmldsig#Object", null); + references.add(reference); + } + + private Manifest constructManifest(XMLSignatureFactory signatureFactory, Document document) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + List manifestReferences = new LinkedList(); + + try { + addRelationshipsReferences(signatureFactory, document, manifestReferences); + } catch (Exception e) { + throw new RuntimeException("error: " + e.getMessage(), e); + } + + /* + * Word + */ + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.theme+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml", manifestReferences); + + /* + * Powerpoint + */ + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.presentationml.slide+xml", manifestReferences); + addParts(signatureFactory, "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml", manifestReferences); + + Manifest manifest = signatureFactory.newManifest(manifestReferences); + return manifest; + } + + private void addSignatureTime(XMLSignatureFactory signatureFactory, Document document, String signatureId, List objectContent) { + /* + * SignatureTime + */ + Element signatureTimeElement = document.createElementNS("http://schemas.openxmlformats.org/package/2006/digital-signature", "mdssi:SignatureTime"); + signatureTimeElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:mdssi", "http://schemas.openxmlformats.org/package/2006/digital-signature"); + Element formatElement = document.createElementNS("http://schemas.openxmlformats.org/package/2006/digital-signature", "mdssi:Format"); + formatElement.setTextContent("YYYY-MM-DDThh:mm:ssTZD"); + signatureTimeElement.appendChild(formatElement); + Element valueElement = document.createElementNS("http://schemas.openxmlformats.org/package/2006/digital-signature", "mdssi:Value"); + DateTime dateTime = new DateTime(DateTimeZone.UTC); + DateTimeFormatter fmt = ISODateTimeFormat.dateTimeNoMillis(); + String now = fmt.print(dateTime); + LOG.debug("now: " + now); + valueElement.setTextContent(now); + signatureTimeElement.appendChild(valueElement); + + List signatureTimeContent = new LinkedList(); + signatureTimeContent.add(new DOMStructure(signatureTimeElement)); + SignatureProperty signatureTimeSignatureProperty = signatureFactory.newSignatureProperty(signatureTimeContent, "#" + signatureId, "idSignatureTime"); + List signaturePropertyContent = new LinkedList(); + signaturePropertyContent.add(signatureTimeSignatureProperty); + SignatureProperties signatureProperties = signatureFactory.newSignatureProperties(signaturePropertyContent, "id-signature-time-" + + UUID.randomUUID().toString()); + objectContent.add(signatureProperties); + } + + private void addSignatureInfo(XMLSignatureFactory signatureFactory, Document document, String signatureId, List references, + List objects) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + List objectContent = new LinkedList(); + + Element signatureInfoElement = document.createElementNS("http://schemas.microsoft.com/office/2006/digsig", "SignatureInfoV1"); + signatureInfoElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns", "http://schemas.microsoft.com/office/2006/digsig"); + + Element manifestHashAlgorithmElement = document.createElementNS("http://schemas.microsoft.com/office/2006/digsig", "ManifestHashAlgorithm"); + manifestHashAlgorithmElement.setTextContent("http://www.w3.org/2000/09/xmldsig#sha1"); + signatureInfoElement.appendChild(manifestHashAlgorithmElement); + + List signatureInfoContent = new LinkedList(); + signatureInfoContent.add(new DOMStructure(signatureInfoElement)); + SignatureProperty signatureInfoSignatureProperty = signatureFactory.newSignatureProperty(signatureInfoContent, "#" + signatureId, "idOfficeV1Details"); + + List signaturePropertyContent = new LinkedList(); + 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(DigestMethod.SHA1, null); + Reference reference = signatureFactory.newReference("#" + objectId, digestMethod, null, "http://www.w3.org/2000/09/xmldsig#Object", null); + references.add(reference); + } + + private void addRelationshipsReferences(XMLSignatureFactory signatureFactory, Document document, List manifestReferences) throws IOException, + ParserConfigurationException, SAXException, TransformerException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + URL ooxmlUrl = this.signatureService.getOfficeOpenXMLDocumentURL(); + InputStream inputStream = ooxmlUrl.openStream(); + ZipInputStream zipInputStream = new ZipInputStream(inputStream); + ZipEntry zipEntry; + while (null != (zipEntry = zipInputStream.getNextEntry())) { + if (false == zipEntry.getName().endsWith(".rels")) { + continue; + } + Document relsDocument = loadDocumentNoClose(zipInputStream); + addRelationshipsReference(signatureFactory, document, zipEntry.getName(), relsDocument, manifestReferences); + } + } + + private void addRelationshipsReference(XMLSignatureFactory signatureFactory, Document document, String zipEntryName, Document relsDocument, + List manifestReferences) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + LOG.debug("relationships: " + zipEntryName); + RelationshipTransformParameterSpec parameterSpec = new RelationshipTransformParameterSpec(); + NodeList nodeList = relsDocument.getDocumentElement().getChildNodes(); + for (int nodeIdx = 0; nodeIdx < nodeList.getLength(); nodeIdx++) { + Node node = nodeList.item(nodeIdx); + if (node.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + Element element = (Element) node; + String relationshipType = element.getAttribute("Type"); + /* + * We skip some relationship types. + */ + if ("http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties".equals(relationshipType)) { + continue; + } + if ("http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties".equals(relationshipType)) { + continue; + } + if ("http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin".equals(relationshipType)) { + continue; + } + if ("http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail".equals(relationshipType)) { + continue; + } + if ("http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps".equals(relationshipType)) { + continue; + } + if ("http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps".equals(relationshipType)) { + continue; + } + String relationshipId = element.getAttribute("Id"); + parameterSpec.addRelationshipReference(relationshipId); + } + + List transforms = new LinkedList(); + transforms.add(signatureFactory.newTransform(RelationshipTransformService.TRANSFORM_URI, parameterSpec)); + transforms.add(signatureFactory.newTransform("http://www.w3.org/TR/2001/REC-xml-c14n-20010315", (TransformParameterSpec) null)); + DigestMethod digestMethod = signatureFactory.newDigestMethod(DigestMethod.SHA1, null); + Reference reference = signatureFactory.newReference("/" + zipEntryName + "?ContentType=application/vnd.openxmlformats-package.relationships+xml", + digestMethod, transforms, null, null); + + manifestReferences.add(reference); + } + + private void addParts(XMLSignatureFactory signatureFactory, String contentType, List references) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + List documentResourceNames; + try { + documentResourceNames = getResourceNames(this.signatureService.getOfficeOpenXMLDocumentURL(), contentType); + } catch (Exception e) { + throw new RuntimeException(e); + } + DigestMethod digestMethod = signatureFactory.newDigestMethod(DigestMethod.SHA1, null); + for (String documentResourceName : documentResourceNames) { + LOG.debug("document resource: " + documentResourceName); + + Reference reference = signatureFactory.newReference("/" + documentResourceName + "?ContentType=" + contentType, digestMethod); + + references.add(reference); + } + } + + private List getResourceNames(URL url, String contentType) throws IOException, ParserConfigurationException, SAXException, TransformerException { + List signatureResourceNames = new LinkedList(); + if (null == url) { + throw new RuntimeException("OOXML URL is null"); + } + InputStream inputStream = url.openStream(); + ZipInputStream zipInputStream = new ZipInputStream(inputStream); + ZipEntry zipEntry; + while (null != (zipEntry = zipInputStream.getNextEntry())) { + if (false == "[Content_Types].xml".equals(zipEntry.getName())) { + continue; + } + Document contentTypesDocument = loadDocument(zipInputStream); + Element nsElement = contentTypesDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:tns", "http://schemas.openxmlformats.org/package/2006/content-types"); + NodeList nodeList = XPathAPI.selectNodeList(contentTypesDocument, "/tns:Types/tns:Override[@ContentType='" + contentType + "']/@PartName", + nsElement); + for (int nodeIdx = 0; nodeIdx < nodeList.getLength(); nodeIdx++) { + String partName = nodeList.item(nodeIdx).getTextContent(); + LOG.debug("part name: " + partName); + partName = partName.substring(1); // remove '/' + signatureResourceNames.add(partName); + } + break; + } + return signatureResourceNames; + } + + protected Document loadDocument(String zipEntryName) throws IOException, ParserConfigurationException, SAXException { + Document document = findDocument(zipEntryName); + if (null != document) { + return document; + } + throw new RuntimeException("ZIP entry not found: " + zipEntryName); + } + + protected Document findDocument(String zipEntryName) throws IOException, ParserConfigurationException, SAXException { + URL ooxmlUrl = this.signatureService.getOfficeOpenXMLDocumentURL(); + InputStream inputStream = ooxmlUrl.openStream(); + ZipInputStream zipInputStream = new ZipInputStream(inputStream); + ZipEntry zipEntry; + while (null != (zipEntry = zipInputStream.getNextEntry())) { + if (false == zipEntryName.equals(zipEntry.getName())) { + continue; + } + Document document = loadDocument(zipInputStream); + return document; + } + return null; + } + + private Document loadDocumentNoClose(InputStream documentInputStream) throws ParserConfigurationException, SAXException, IOException { + NoCloseInputStream noCloseInputStream = new NoCloseInputStream(documentInputStream); + InputSource inputSource = new InputSource(noCloseInputStream); + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.parse(inputSource); + return document; + } + + private 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; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureVerifier.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureVerifier.java new file mode 100644 index 0000000000..885b7f04fa --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLSignatureVerifier.java @@ -0,0 +1,211 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureException; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.POIXMLDocument; +import org.apache.poi.ooxml.signature.service.signer.KeyInfoKeySelector; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +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.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + + + +/** + * Signature verifier util class for Office Open XML file format. + */ +public class OOXMLSignatureVerifier { + + private static final Log LOG = LogFactory.getLog(OOXMLSignatureVerifier.class); + + private OOXMLSignatureVerifier() { + super(); + } + + /** + * Checks whether the file referred by the given URL is an OOXML document. + * + * @param url + * @return + * @throws IOException + */ + public static boolean isOOXML(URL url) throws IOException { + ZipInputStream zipInputStream = new ZipInputStream(url.openStream()); + ZipEntry zipEntry; + while (null != (zipEntry = zipInputStream.getNextEntry())) { + if (false == "[Content_Types].xml".equals(zipEntry.getName())) { + continue; + } + if (zipEntry.getSize() > 0) { + return true; + } + } + return false; + } + + public static List getSigners(URL url) throws IOException, ParserConfigurationException, SAXException, TransformerException, + MarshalException, XMLSignatureException, InvalidFormatException { + List signers = new LinkedList(); + List signatureParts = getSignatureParts(url); + if (signatureParts.isEmpty()) { + LOG.debug("no signature resources"); + } + for (PackagePart signaturePart : signatureParts) { + Document signatureDocument = loadDocument(signaturePart); + if (null == signatureDocument) { + continue; + } + + NodeList signatureNodeList = signatureDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + if (0 == signatureNodeList.getLength()) { + return null; + } + Node signatureNode = signatureNodeList.item(0); + + KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); + DOMValidateContext domValidateContext = new DOMValidateContext(keySelector, signatureNode); + domValidateContext.setProperty("org.jcp.xml.dsig.validateManifests", Boolean.TRUE); + OOXMLURIDereferencer dereferencer = new OOXMLURIDereferencer(url); + domValidateContext.setURIDereferencer(dereferencer); + + XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); + XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); + boolean validity = xmlSignature.validate(domValidateContext); + + if (false == validity) { + continue; + } + // TODO: check what has been signed. + + X509Certificate signer = keySelector.getCertificate(); + signers.add(signer); + } + return signers; + } + + public static boolean verifySignature(URL url) throws InvalidFormatException, IOException, ParserConfigurationException, SAXException, MarshalException, + XMLSignatureException { + PackagePart signaturePart = getSignaturePart(url); + if (signaturePart == null) { + LOG.info(url + " does not contain a signature"); + return false; + } + LOG.debug("signature resource name: " + signaturePart.getPartName()); + + OOXMLProvider.install(); + + Document signatureDocument = loadDocument(signaturePart); + LOG.debug("signature loaded"); + NodeList signatureNodeList = signatureDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + Node signatureNode = signatureNodeList.item(0); + KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); + DOMValidateContext domValidateContext = new DOMValidateContext(keySelector, signatureNode); + domValidateContext.setProperty("org.jcp.xml.dsig.validateManifests", Boolean.TRUE); + + OOXMLURIDereferencer dereferencer = new OOXMLURIDereferencer(url); + domValidateContext.setURIDereferencer(dereferencer); + + XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); + XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); + return xmlSignature.validate(domValidateContext); + } + + private static PackagePart getSignaturePart(URL url) throws IOException, InvalidFormatException { + List packageParts = getSignatureParts(url); + if (packageParts.isEmpty()) { + return null; + } else { + return packageParts.get(0); + } + } + + private static List getSignatureParts(URL url) throws IOException, InvalidFormatException { + List packageParts = new LinkedList(); + OPCPackage pkg = POIXMLDocument.openPackage(url.getPath()); + PackageRelationshipCollection sigOrigRels = pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); + for (PackageRelationship rel : sigOrigRels) { + PackagePartName relName = PackagingURIHelper.createPartName(rel.getTargetURI()); + PackagePart sigPart = pkg.getPart(relName); + if (LOG.isDebugEnabled()) { + LOG.debug("Digital Signature Origin part = " + sigPart); + } + + PackageRelationshipCollection sigRels = sigPart.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE); + for (PackageRelationship sigRel : sigRels) { + PackagePartName sigRelName = PackagingURIHelper.createPartName(sigRel.getTargetURI()); + PackagePart sigRelPart = pkg.getPart(sigRelName); + if (LOG.isDebugEnabled()) { + LOG.debug("XML Signature part = " + sigRelPart); + } + packageParts.add(sigRelPart); + } + } + return packageParts; + } + + private static Document loadDocument(PackagePart part) throws ParserConfigurationException, SAXException, IOException { + InputStream documentInputStream = part.getInputStream(); + return loadDocument(documentInputStream); + } + + private 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; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLURIDereferencer.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLURIDereferencer.java new file mode 100644 index 0000000000..d00f010a8f --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/OOXMLURIDereferencer.java @@ -0,0 +1,111 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.net.URLDecoder; + +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.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.POIXMLDocument; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; + +/** + * JSR105 URI dereferencer for Office Open XML documents. + */ +public class OOXMLURIDereferencer implements URIDereferencer { + + private static final Log LOG = LogFactory.getLog(OOXMLURIDereferencer.class); + + private final URL ooxmlUrl; + + private final URIDereferencer baseUriDereferencer; + + public OOXMLURIDereferencer(URL ooxmlUrl) { + if (null == ooxmlUrl) { + throw new IllegalArgumentException("ooxmlUrl is null"); + } + this.ooxmlUrl = ooxmlUrl; + XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); + this.baseUriDereferencer = xmlSignatureFactory.getURIDereferencer(); + } + + 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"); + } + + String uri = uriReference.getURI(); + try { + uri = URLDecoder.decode(uri, "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOG.warn("could not URL decode the uri: " + uri); + } + LOG.debug("dereference: " + uri); + try { + InputStream dataInputStream = findDataInputStream(uri); + if (null == dataInputStream) { + LOG.debug("cannot resolve, delegating to base DOM URI dereferencer: " + uri); + return this.baseUriDereferencer.dereference(uriReference, context); + } + return new OctetStreamData(dataInputStream, uri, null); + } catch (IOException e) { + throw new URIReferenceException("I/O error: " + e.getMessage(), e); + } catch (InvalidFormatException e) { + throw new URIReferenceException("Invalid format error: " + e.getMessage(), e); + } + } + + private InputStream findDataInputStream(String uri) throws IOException, InvalidFormatException { + if (-1 != uri.indexOf("?")) { + uri = uri.substring(0, uri.indexOf("?")); + } + OPCPackage pkg = POIXMLDocument.openPackage(this.ooxmlUrl.getPath()); + for (PackagePart part : pkg.getParts()) { + if (uri.equals(part.getPartName().getURI().toString())) { + LOG.debug("Part name: " + part.getPartName()); + return part.getInputStream(); + } + } + LOG.info("No part found for URI: " + uri); + return null; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipComparator.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipComparator.java new file mode 100644 index 0000000000..5ed64642ee --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipComparator.java @@ -0,0 +1,41 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.util.Comparator; + +import org.w3c.dom.Element; + +/** + * Comparator for Relationship DOM elements. + */ +public class RelationshipComparator implements Comparator { + + public int compare(Element element1, Element element2) { + String id1 = element1.getAttribute("Id"); + String id2 = element2.getAttribute("Id"); + return id1.compareTo(id2); + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformParameterSpec.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformParameterSpec.java new file mode 100644 index 0000000000..c0bb6480c9 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformParameterSpec.java @@ -0,0 +1,58 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.util.LinkedList; +import java.util.List; + +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +/** + * Relationship Transform parameter specification class. + */ +public class RelationshipTransformParameterSpec implements TransformParameterSpec { + + private final List sourceIds; + + /** + * Main constructor. + */ + public RelationshipTransformParameterSpec() { + this.sourceIds = new LinkedList(); + } + + /** + * Adds a relationship reference for the given source identifier. + * + * @param sourceId + */ + public void addRelationshipReference(String sourceId) { + this.sourceIds.add(sourceId); + } + + List getSourceIds() { + return this.sourceIds; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformService.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformService.java new file mode 100644 index 0000000000..7f67bbf9fa --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/RelationshipTransformService.java @@ -0,0 +1,274 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer.ooxml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringWriter; +import java.security.InvalidAlgorithmParameterException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Collections; +import java.util.LinkedList; +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 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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.xml.security.utils.Constants; +import org.apache.xpath.XPathAPI; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * 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 Log LOG = LogFactory.getLog(RelationshipTransformService.class); + + public RelationshipTransformService() { + super(); + LOG.debug("constructor"); + this.sourceIds = new LinkedList(); + } + + @Override + public void init(TransformParameterSpec params) throws InvalidAlgorithmParameterException { + LOG.debug("init(params)"); + if (false == params instanceof RelationshipTransformParameterSpec) { + throw new InvalidAlgorithmParameterException(); + } + RelationshipTransformParameterSpec relParams = (RelationshipTransformParameterSpec) params; + for (String sourceId : relParams.getSourceIds()) { + this.sourceIds.add(sourceId); + } + } + + @Override + public void init(XMLStructure parent, XMLCryptoContext context) throws InvalidAlgorithmParameterException { + LOG.debug("init(parent,context)"); + LOG.debug("parent java type: " + parent.getClass().getName()); + DOMStructure domParent = (DOMStructure) parent; + Node parentNode = domParent.getNode(); + try { + LOG.debug("parent: " + toString(parentNode)); + } catch (TransformerException e) { + throw new InvalidAlgorithmParameterException(); + } + Element nsElement = parentNode.getOwnerDocument().createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:mdssi", "http://schemas.openxmlformats.org/package/2006/digital-signature"); + NodeList nodeList; + try { + nodeList = XPathAPI.selectNodeList(parentNode, "mdssi:RelationshipReference/@SourceId", nsElement); + } catch (TransformerException e) { + LOG.error("transformer exception: " + e.getMessage(), e); + throw new InvalidAlgorithmParameterException(); + } + if (0 == nodeList.getLength()) { + LOG.warn("no RelationshipReference/@SourceId parameters present"); + } + for (int nodeIdx = 0; nodeIdx < nodeList.getLength(); nodeIdx++) { + Node node = nodeList.item(nodeIdx); + String sourceId = node.getTextContent(); + LOG.debug("sourceId: " + sourceId); + this.sourceIds.add(sourceId); + } + } + + @Override + public void marshalParams(XMLStructure parent, XMLCryptoContext context) throws MarshalException { + LOG.debug("marshallParams(parent,context)"); + DOMStructure domParent = (DOMStructure) parent; + Node parentNode = domParent.getNode(); + Element parentElement = (Element) parentNode; + parentElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:mdssi", "http://schemas.openxmlformats.org/package/2006/digital-signature"); + Document document = parentNode.getOwnerDocument(); + for (String sourceId : this.sourceIds) { + Element relationshipReferenceElement = document.createElementNS("http://schemas.openxmlformats.org/package/2006/digital-signature", + "mdssi:RelationshipReference"); + relationshipReferenceElement.setAttribute("SourceId", sourceId); + parentElement.appendChild(relationshipReferenceElement); + } + } + + public AlgorithmParameterSpec getParameterSpec() { + LOG.debug("getParameterSpec"); + return null; + } + + public Data transform(Data data, XMLCryptoContext context) throws TransformException { + LOG.debug("transform(data,context)"); + LOG.debug("data java type: " + data.getClass().getName()); + OctetStreamData octetStreamData = (OctetStreamData) data; + LOG.debug("URI: " + octetStreamData.getURI()); + InputStream octetStream = octetStreamData.getOctetStream(); + Document relationshipsDocument; + try { + relationshipsDocument = loadDocument(octetStream); + } catch (Exception e) { + throw new TransformException(e.getMessage(), e); + } + try { + LOG.debug("relationships document: " + toString(relationshipsDocument)); + } catch (TransformerException e) { + throw new TransformException(e.getMessage(), e); + } + Element nsElement = relationshipsDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:tns", "http://schemas.openxmlformats.org/package/2006/relationships"); + Element relationshipsElement = relationshipsDocument.getDocumentElement(); + NodeList childNodes = relationshipsElement.getChildNodes(); + for (int nodeIdx = 0; nodeIdx < childNodes.getLength(); nodeIdx++) { + Node childNode = childNodes.item(nodeIdx); + if (Node.ELEMENT_NODE != childNode.getNodeType()) { + LOG.debug("removing node"); + relationshipsElement.removeChild(childNode); + nodeIdx--; + continue; + } + Element childElement = (Element) childNode; + String idAttribute = childElement.getAttribute("Id"); + LOG.debug("Relationship id attribute: " + idAttribute); + if (false == this.sourceIds.contains(idAttribute)) { + LOG.debug("removing element: " + idAttribute); + relationshipsElement.removeChild(childNode); + nodeIdx--; + } + /* + * See: ISO/IEC 29500-2:2008(E) - 13.2.4.24 Relationships Transform + * Algorithm. + */ + if (null == childElement.getAttributeNode("TargetMode")) { + childElement.setAttribute("TargetMode", "Internal"); + } + } + LOG.debug("# Relationship elements: " + relationshipsElement.getElementsByTagName("*").getLength()); + sortRelationshipElements(relationshipsElement); + try { + return toOctetStreamData(relationshipsDocument); + } catch (TransformerException e) { + throw new TransformException(e.getMessage(), e); + } + } + + private void sortRelationshipElements(Element relationshipsElement) { + List relationshipElements = new LinkedList(); + NodeList relationshipNodes = relationshipsElement.getElementsByTagName("*"); + int nodeCount = relationshipNodes.getLength(); + for (int nodeIdx = 0; nodeIdx < nodeCount; nodeIdx++) { + Node relationshipNode = relationshipNodes.item(0); + Element relationshipElement = (Element) relationshipNode; + LOG.debug("unsorted Id: " + relationshipElement.getAttribute("Id")); + relationshipElements.add(relationshipElement); + relationshipsElement.removeChild(relationshipNode); + } + Collections.sort(relationshipElements, new RelationshipComparator()); + for (Element relationshipElement : relationshipElements) { + LOG.debug("sorted Id: " + relationshipElement.getAttribute("Id")); + relationshipsElement.appendChild(relationshipElement); + } + } + + private 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(); + } + + private OctetStreamData toOctetStreamData(Node node) throws TransformerException { + Source source = new DOMSource(node); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Result result = new StreamResult(outputStream); + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.transform(source, result); + LOG.debug("result: " + new String(outputStream.toByteArray())); + return new OctetStreamData(new ByteArrayInputStream(outputStream.toByteArray())); + } + + private 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; + } + + public Data transform(Data data, XMLCryptoContext context, OutputStream os) throws TransformException { + LOG.debug("transform(data,context,os)"); + return null; + } + + public boolean isFeatureSupported(String feature) { + LOG.debug("isFeatureSupported(feature)"); + return false; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/package-info.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/package-info.java new file mode 100644 index 0000000000..eb7c767b97 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/ooxml/package-info.java @@ -0,0 +1,28 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ +/** + * This package contains implementation classes for the Office Open XML Signature Service. + */ +package org.apache.poi.ooxml.signature.service.signer.ooxml; + diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/package-info.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/package-info.java new file mode 100644 index 0000000000..60ff0dad9e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/signer/package-info.java @@ -0,0 +1,28 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +/** + * This package contains implementation classes for the Signature Service SPI. + */ +package org.apache.poi.ooxml.signature.service.signer; + diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/AuthenticationService.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/AuthenticationService.java new file mode 100644 index 0000000000..4ed07ffb18 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/AuthenticationService.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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.spi; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for authentication service components. + */ +public interface AuthenticationService { + + /** + * Validates the given certificate chain. After the client has + * verified the authentication signature, it will invoke this method on your + * authentication service component. The implementation of this method + * should validate the given certificate chain. This validation could be + * based on PKI validation, or could be based on simply trusting the + * incoming public key. The actual implementation is very dependent on your + * type of application. This method should only be used for certificate + * validation. + * + *

+ * Check out jTrust for an + * implementation of a PKI validation framework. + *

+ * + * @param certificateChain + * the X509 authentication certificate chain of the citizen. + * @throws SecurityException + * in case the certificate chain is invalid/not accepted. + */ + void validateCertificateChain(List certificateChain) throws SecurityException; +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/DigestInfo.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/DigestInfo.java new file mode 100644 index 0000000000..1a2b6b7309 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/DigestInfo.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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.spi; + +import java.io.Serializable; + +/** + * Digest Information data transfer class. + */ +public class DigestInfo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Main constructor. + * + * @param digestValue + * @param digestAlgo + * @param description + */ + public DigestInfo(byte[] digestValue, String digestAlgo, String description) { + this.digestValue = digestValue; + this.digestAlgo = digestAlgo; + this.description = description; + } + + public final byte[] digestValue; + + public final String description; + + public final String digestAlgo; +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/InsecureClientEnvironmentException.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/InsecureClientEnvironmentException.java new file mode 100644 index 0000000000..495bd57e65 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/InsecureClientEnvironmentException.java @@ -0,0 +1,64 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.spi; + +/** + * Insecure Client Environment Exception. + */ +public class InsecureClientEnvironmentException extends Exception { + + private static final long serialVersionUID = 1L; + + private final boolean warnOnly; + + /** + * Default constructor. + */ + public InsecureClientEnvironmentException() { + this(false); + } + + /** + * Main constructor. + * + * @param warnOnly + * only makes that the citizen is warned about a possible + * insecure enviroment. + */ + public InsecureClientEnvironmentException(boolean warnOnly) { + this.warnOnly = warnOnly; + } + + /** + * If set the eID Applet will only give a warning on case the server-side + * marks the client environment as being insecure. Else the eID Applet will + * abort the requested eID operation. + * + * @return + */ + public boolean isWarnOnly() { + return this.warnOnly; + } +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SecureClientEnvironmentService.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SecureClientEnvironmentService.java new file mode 100644 index 0000000000..f285c23c54 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SecureClientEnvironmentService.java @@ -0,0 +1,73 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.spi; + +import java.util.List; + +/** + * Interface for security environment service components. Can be used by the eID + * Applet Service to check the client environment security requirements. + */ +public interface SecureClientEnvironmentService { + + /** + * Checks whether the client environment is secure enough for this web + * application. + * + * @param javaVersion + * the version of the Java JRE on the client machine. + * @param javaVendor + * the vendor of the Java JRE on the client machine. + * @param osName + * the name of the operating system on the client machine. + * @param osArch + * the architecture of the client machine. + * @param osVersion + * the operating system version of the client machine. + * @param userAgent + * the user agent, i.e. browser, used on the client machine. + * @param navigatorAppName + * the optional navigator application name (browser) + * @param navigatorAppVersion + * the optional navigator application version (browser version) + * @param navigatorUserAgent + * the optional optional navigator user agent name. + * @param remoteAddress + * the address of the client machine. + * @param sslKeySize + * the key size of the SSL session used between server and + * client. + * @param sslCipherSuite + * the cipher suite of the SSL session used between server and + * client. + * @param readerList + * the list of smart card readers present on the client machine. + * @throws InsecureClientEnvironmentException + * if the client env is found not to be secure enough. + */ + void checkSecureClientEnvironment(String javaVersion, String javaVendor, String osName, String osArch, String osVersion, String userAgent, + String navigatorAppName, String navigatorAppVersion, String navigatorUserAgent, String remoteAddress, int sslKeySize, + String sslCipherSuite, List readerList) throws InsecureClientEnvironmentException; +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SignatureService.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SignatureService.java new file mode 100644 index 0000000000..6b86b2fb14 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/SignatureService.java @@ -0,0 +1,77 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.spi; + +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for signature service component. + */ +public interface SignatureService { + + /** + * Gives back the digest algorithm to be used for construction of the digest + * infos of the preSign method. Return a digest algorithm here if you want + * to let the client sign some locally stored files. Return + * null if no pre-sign digest infos are required. + * + * @return + * @see #preSign(List, List) + */ + String getFilesDigestAlgorithm(); + + /** + * Pre-sign callback method. Depending on the configuration some parameters + * are passed. The returned value will be signed by the eID Applet. + * + *

+ * TODO: service must be able to throw some exception on failure. + *

+ * + * @param digestInfos + * the optional list of digest infos. + * @param signingCertificateChain + * the optional list of certificates. + * @return the digest to be signed. + * @throws NoSuchAlgorithmException + */ + DigestInfo preSign(List digestInfos, List signingCertificateChain) throws NoSuchAlgorithmException; + + /** + * Post-sign callback method. Received the signature value. Depending on the + * configuration the signing certificate chain is also obtained. + * + *

+ * TODO: service must be able to throw some exception on failure. + *

+ * + * @param signatureValue + * @param signingCertificateChain + * the optional chain of signing certificates. + */ + void postSign(byte[] signatureValue, List signingCertificateChain); +} diff --git a/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/package-info.java b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/package-info.java new file mode 100644 index 0000000000..1b4a89f70d --- /dev/null +++ b/src/ooxml/java/org/apache/poi/ooxml/signature/service/spi/package-info.java @@ -0,0 +1,28 @@ + +/* ==================================================================== + 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. +==================================================================== */ + + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ +/** + * This package contains the service provider interfaces. + */ +package org.apache.poi.ooxml.signature.service.spi; + diff --git a/src/ooxml/testcases/hello-world-office-2010-technical-preview-unsigned.docx b/src/ooxml/testcases/hello-world-office-2010-technical-preview-unsigned.docx new file mode 100644 index 0000000000000000000000000000000000000000..5162b67c60539f95ee05c71ea7db516288d8719b GIT binary patch literal 14447 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 m?-T4FAR|0~yP1EUZWW{I2pmwzG9Mv31s0 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 literal 0 HcmV?d00001 diff --git a/src/ooxml/testcases/hello-world-signed.pptx b/src/ooxml/testcases/hello-world-signed.pptx new file mode 100644 index 0000000000000000000000000000000000000000..9b37033f5486ac9dc8ad1674594c84c3f76b27d8 GIT binary patch literal 36107 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 B5MBTP literal 0 HcmV?d00001 diff --git a/src/ooxml/testcases/hello-world-signed.xlsx b/src/ooxml/testcases/hello-world-signed.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0d45c53ede3435b2062d12973436d8eef8c52e63 GIT binary patch literal 13072 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 ZmpFMT2%t9p41FO37{LL6Vfvq~{{!crDb@f0 literal 0 HcmV?d00001 diff --git a/src/ooxml/testcases/hello-world-unsigned.docx b/src/ooxml/testcases/hello-world-unsigned.docx new file mode 100644 index 0000000000000000000000000000000000000000..1790c961ceb1329fc6f39e70b605fbbbb3f8e2be GIT binary patch literal 9898 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 literal 0 HcmV?d00001 diff --git a/src/ooxml/testcases/hello-world-unsigned.pptx b/src/ooxml/testcases/hello-world-unsigned.pptx new file mode 100644 index 0000000000000000000000000000000000000000..ca42529a9a8f359bad0075b8118dee6a4ff4f0f3 GIT binary patch literal 32220 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

-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 be_cXqDC48EX8`~%^g|V$tmNf#`0jrIqt%a@ literal 0 HcmV?d00001 diff --git a/src/ooxml/testcases/hello-world-unsigned.xlsx b/src/ooxml/testcases/hello-world-unsigned.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b99143e92c780306d52eeee28a61e17ae5c8809b GIT binary patch literal 9748 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 literal 0 HcmV?d00001 diff --git a/src/ooxml/testcases/invalidsig.docx b/src/ooxml/testcases/invalidsig.docx new file mode 100644 index 0000000000000000000000000000000000000000..c448e819a0cae4851c2e1fbf087f7430a9aab9df GIT binary patch literal 12228 zcmb7K1yq&W);)B0N_Tfir-afaDc#-O-HjmKf^>IxcOytCB_PuMA8-7^y}oUfJ`q6?=MJA;OxFs=vnSh(*%$G%QUx#Tf# z>v*|HP+p5Xg9}wJj$DLz0cH0tH>gp>xoX%m`Ibb~GkBap(U3JRg}@oAjd7*n!qe0u zAuV{wBn2qhHR^)I7ZE}AavvIVzgqS7r^kaMiaJe`*Qw{rF>@DV*uG(Cxyp&N#()*G zva>NF&9L>0`9xC_n+Z|ZAZ{gH1z8m;ahioQ7B?Id!6r10D~5T;_URJ-wITc0Z}b{c zLSQ5pEw;T3Q&juo=U0O2JRJ|tlgH_P5u)jUFn9IWDet@a2Fs!U8D- z0BrO)cz%2M;{y^%qqd!)g+1eM%>7_Z{qM~E82>X2Ad9EFV1cCWfC2z;|39AL2g?7` z|04wcLVszTka;%~eBhD)9n_|KP7SVp1Vw9vkhQ+SHnBx7v!sQ7vBu5Owy+x3jxZA6 zHQ(-4{DfI6F4VCov)YkaBNZ+60CjmOYpsX1c9=LdYVI_ri+Jrv#kl=1=SF8@V0Km# ztq8BS(38v-S%WEk^4xHk-}1){%WQ9kq-9PD3Z8`q>8QO%LVhqBFp8so-jryYdz5E# z;ma_1u4#{`?2oiFA zotGAG9+BXV>D=N^CH&8HApLMBa2eT~8e8c)INBNhJ1@WH731$f|0~ThFO2hwfnh=g zq?-Ic!CBjx8k<@%0PVH^*8s>L?}Y7Qf`?pdUgJslsQDa;q-rd6?e!P10CuO|QhI5O zi}Q0bxAVjYq${F>V|LlS!B=m-;iMEn_Ig40xfZW4Wj=S8iyOpE@iRt%gTre+D{B2F z(ug;>lT(=EGRgD>4=IEtf;V1?xQa1V<0b27)cD?)-RX3jb!E=)+7NcUq#U2)Of9ly zU6X(JevV_T`QDUt%xx&T=Q24=cF=B4uIN1th0grOxc3U~PlkT%dyju`{_crKKKQ5j zf4Ycn+)9+)U;uzITmS&;@0dV`GC$c*x&OW@SJX73i_}rQG7auu6pZKYLbgm-@~OR1 zQc^1OV?;Y7Qc;868N9s^I;gX)w>P?2bf;u}!3|kt=4t+VRwZ2hbnSu#p1_VaPz$e> zUvGS8(;wqm<*w-1elA&5#&vW%5|rPzhqstV1&E9Av3@LDJ!WBA6 zE#y3^uQ?Ka9}k zjTO^EFa&u&EWdD61EH+B^XW3Ef?*q*8*a;+2nX#x2-O=>15l=IzI(%1FIcyn z8uw`;+>ro=raT;(wkc=2IqUdt3c^sEn8Y(@*{N+uGPbPve5f;vtSNoKIh2X|Y?628 z`qF5d=>Rd6&6HTdztlXyUaymNd7vTd?SveXJ<}3s@Fm!I$o4>d??f?896`%i%jbAJtCwk1 zJ@*!^1lI3nQ}n2oGQKu_?d#q?fhZ2_mq$seI>KkurkC8p#IXLJ;dhk+GHek+tC0Cs zP`SD}uY9~wdg9H5E6=tP$MKG`_@Rz`Q<}y6-E@j%O;`ZocWT1}%=*GH7dv!!Tx_~% zOZQitY~&QpXL)jdwI5fcRJ`iq=`G9{oDV3*Od47z4CUUicuPOCV>AY9lG+#ut0Gu4j`iQJ*fX|z%) zcBgFfZnI`e_a?dGjlD(%5uW$<^(r=_9H`9{w49)htDIbv#4NL6RPIg}@6M>Hj)=!t zoM5)dgf4ZiPV{Lk-}M4$`0Kra1iH^y;giY-IdMxmr9_na(pgONW%aCN; zrjlP@uoWhW;Gc*O3uvR*0PSmFO#nu*oH3?pCl#*7XD8rBXIY65sGhWseyJN+9bw0ysq)##&! zjhb`06AE*}>?Ygcjuq91pa$-i>n0H)jiGS#3#re0z3>&)@Gc?aES$c0DAX(o>YgN64ApV( zF}ZewPA+wX#fi-6F}>FapR22ncrL7l;Ts9H6sESH;%m0ziLW?PD;8rY>78Hak;|4t zUVJvh=4hFbAu>&xsX7fNS4luAUkMsLz?L4)|JHT?$`)K0mMa3CGjWdN4DrfHh}_^+ zmkC>M(j}+rWZWLd&5pUJ_N}w~vZ&%R%y{bOe9|U1^Lm6^_ipFIRc&E3;iIJ*q3&8? z1pfX|jMr{FGB4=;w<#DJBly|vHG59+7^nRxUHIVPp-M1aoo4er&DdkT^OOOx+tWu! z26%?EQQn8oYsp|_+LXirWYHtmMxJh|$83idV494+kwaN2Qc|_h8SuIX_cB=5t%%39 zTxBk^yxZSeW@-t?V2=6OOB5{>`*=BO1{`-X%lBxvaqu zoR&dIU1jxv^!*gN zPqNoJ9CR@Iqp?N@vwR*11!?Uii|ju99W2-8iS8Bmb->%aO5NAG(P?2{Riq1DCvi=^ zt#gBz%KCUz2hSlRj9~O!j=UlVbgq2LR`$v}z`~&knya`sA7GKV z!mI7Zct(4pWtFq$iywKU$^z%+tISx|Bq~s-^R`0|E`1Ma-W z45uVo%h%w!Z%Nc_`x87a4TIq%vAj?=%_rK);6c(T(za?jU!00?Q9?H_+c-kd#RxpG zT~5YmwTqQb4b>L~)~y+@ua`=<4T&<=5D<#cJC>B(b5z@8Gp%t82>A z-H6EN?6(E1!v$;v;Sh0%S-)V=Qey7RENZ8q&@08NIq|FGI66P_T205g+M2>&Ty);D zgTDOYV{;;O87|)(@s@dLh>g+bf!b>RW|~X1ukA{u%N+~LO$?`dVJst(Q-p!Hg+P#| zDb1(EB9m!maPp!#x=__nIlANXmHu9Zx{~GtMFW4lt&1Kno)JbkjhjG8qSWYZ5kX1o zKm;6>_I*CfSAKo4E!@R5dO_=G)sttXRW_u!KGEDQwavNM6}DT3wy?nqyBf>OcRC7h zmnvy2>DTD7p>Dpy`*G05G#B}LNR-%`Yl*t}e3XYD;0#e5l3;OOFhh)4+C*Tq8?Px+ zw!ehBcH&#Pa&5nNm7Kh&%mm42!(OfA3m16x$%M1#D8gl4Q?>*Rp=n;z?9wxJrvKcI zr>U>a%$$#WTUoAPIr-ZAgN49Mw>QYo>)~JXDHz~Y&cIq<&d%EA->>dJQ!1K-*mHB> zJ$nF{QXxNv(6zC7at&nr`!z6KVfaT{-2zOj??E2MI!Nh{^C&uO5%|Rd^mrp9c|}8FO4!QJuh-I!G)##S6@Hgb(wEzfvWSsBRdP z#Xg}I5b!Tp;|ZJV7vyyS0e$J(E<5aN`VEs%oJo>iapq@9dLk zK9rMsTar5ZAmUi)>!=H=on>f310rx+`xq6^0?dyh6G~Q>26bE?NiDHKmrm=!n%DxBDJ( zvwLg!wfxjYV}*5;o-AXKH{j_NJ{XOZSLl~hK(ba z<=;<6df5YMGo;-*L1~;9r)#`U;|34qw*MJWXPH`?OAvVnqcx(ndM&T`p;^;bum-hd z3sIFRiu(iuiUot;eg2?amw#lVC<(P^^Dy8pl6$FB%t<>mE(1GiR(GovM+CJv@uo9s zuzzRQm0y&t&hzD_Vhk7cP;<`6$#~gp?_IncKJ4u$&Nemzh#G5;tL6#q|GfAhfFw9u z+Zp_;mhtPH`G=PA+wlK2Q=T)ZmOlU{iOgUCAjA_8;9lTp36#iwgu}lk$fa?=tz^-Q zm$&}x?i+c$0Y%&^SrRM4mw@?X-T-}#4C^xsjrd+};lzy8J5M0>*p=nz!Zx5ubEz z$JVW6g8O*NPRC?;y>ba*Tu:Go)NKcFV{R=HGv4!D+rz<*y$zwz+cdB0`Wioo4Z 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 literal 0 HcmV?d00001 diff --git a/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/PkiTestUtils.java b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/PkiTestUtils.java new file mode 100644 index 0000000000..a307ee0906 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/PkiTestUtils.java @@ -0,0 +1,199 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.RSAKeyGenParameterSpec; + +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.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.AuthorityInformationAccess; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.DistributionPointName; +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.X509Extensions; +import org.bouncycastle.asn1.x509.X509ObjectIdentifiers; +import org.bouncycastle.jce.X509Principal; +import org.bouncycastle.x509.X509V3CertificateGenerator; +import org.joda.time.DateTime; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class PkiTestUtils { + + public static final byte[] SHA1_DIGEST_INFO_PREFIX = new byte[] { 0x30, 0x1f, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x04, 0x14 }; + + 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; + } + + private static SubjectKeyIdentifier createSubjectKeyId(PublicKey publicKey) throws IOException { + ByteArrayInputStream bais = new ByteArrayInputStream(publicKey.getEncoded()); + SubjectPublicKeyInfo info = new SubjectPublicKeyInfo((ASN1Sequence) new ASN1InputStream(bais).readObject()); + return new SubjectKeyIdentifier(info); + } + + private static AuthorityKeyIdentifier createAuthorityKeyId(PublicKey publicKey) throws IOException { + + ByteArrayInputStream bais = new ByteArrayInputStream(publicKey.getEncoded()); + SubjectPublicKeyInfo info = new SubjectPublicKeyInfo((ASN1Sequence) new ASN1InputStream(bais).readObject()); + + return new AuthorityKeyIdentifier(info); + } + + static X509Certificate generateCertificate(PublicKey subjectPublicKey, String subjectDn, DateTime notBefore, DateTime notAfter, + X509Certificate issuerCertificate, PrivateKey issuerPrivateKey, boolean caFlag, int pathLength, String crlUri, + String ocspUri, KeyUsage keyUsage) throws IOException, InvalidKeyException, IllegalStateException, + NoSuchAlgorithmException, SignatureException, CertificateException { + String signatureAlgorithm = "SHA1withRSA"; + X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator(); + certificateGenerator.reset(); + certificateGenerator.setPublicKey(subjectPublicKey); + certificateGenerator.setSignatureAlgorithm(signatureAlgorithm); + certificateGenerator.setNotBefore(notBefore.toDate()); + certificateGenerator.setNotAfter(notAfter.toDate()); + X509Principal issuerDN; + if (null != issuerCertificate) { + issuerDN = new X509Principal(issuerCertificate.getSubjectX500Principal().toString()); + } else { + issuerDN = new X509Principal(subjectDn); + } + certificateGenerator.setIssuerDN(issuerDN); + certificateGenerator.setSubjectDN(new X509Principal(subjectDn)); + certificateGenerator.setSerialNumber(new BigInteger(128, new SecureRandom())); + + certificateGenerator.addExtension(X509Extensions.SubjectKeyIdentifier, false, createSubjectKeyId(subjectPublicKey)); + PublicKey issuerPublicKey; + issuerPublicKey = subjectPublicKey; + certificateGenerator.addExtension(X509Extensions.AuthorityKeyIdentifier, false, createAuthorityKeyId(issuerPublicKey)); + + if (caFlag) { + if (-1 == pathLength) { + certificateGenerator.addExtension(X509Extensions.BasicConstraints, false, new BasicConstraints(true)); + } else { + certificateGenerator.addExtension(X509Extensions.BasicConstraints, false, new BasicConstraints(pathLength)); + } + } + + if (null != crlUri) { + GeneralName gn = new GeneralName(GeneralName.uniformResourceIdentifier, new DERIA5String(crlUri)); + GeneralNames gns = new GeneralNames(new DERSequence(gn)); + DistributionPointName dpn = new DistributionPointName(0, gns); + DistributionPoint distp = new DistributionPoint(dpn, null, null); + certificateGenerator.addExtension(X509Extensions.CRLDistributionPoints, false, new DERSequence(distp)); + } + + if (null != ocspUri) { + GeneralName ocspName = new GeneralName(GeneralName.uniformResourceIdentifier, ocspUri); + AuthorityInformationAccess authorityInformationAccess = new AuthorityInformationAccess(X509ObjectIdentifiers.ocspAccessMethod, ocspName); + certificateGenerator.addExtension(X509Extensions.AuthorityInfoAccess.getId(), false, authorityInformationAccess); + } + + if (null != keyUsage) { + certificateGenerator.addExtension(X509Extensions.KeyUsage, true, keyUsage); + } + + X509Certificate certificate; + certificate = certificateGenerator.generate(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 certificate; + } + + 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(); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TemporaryTestDataStorage.java b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TemporaryTestDataStorage.java new file mode 100644 index 0000000000..83a36cc5cf --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TemporaryTestDataStorage.java @@ -0,0 +1,66 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.apache.poi.ooxml.signature.service.signer.TemporaryDataStorage; + + + +class TemporaryTestDataStorage implements TemporaryDataStorage { + + private ByteArrayOutputStream outputStream; + + private Map attributes; + + public TemporaryTestDataStorage() { + this.outputStream = new ByteArrayOutputStream(); + this.attributes = new HashMap(); + } + + public InputStream getTempInputStream() { + byte[] data = this.outputStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + return inputStream; + } + + public OutputStream getTempOutputStream() { + return this.outputStream; + } + + public Serializable getAttribute(String attributeName) { + return this.attributes.get(attributeName); + } + + public void setAttribute(String attributeName, Serializable attributeValue) { + this.attributes.put(attributeName, attributeValue); + } +} \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractOOXMLSignatureService.java b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractOOXMLSignatureService.java new file mode 100644 index 0000000000..d6cc51c65a --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractOOXMLSignatureService.java @@ -0,0 +1,214 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.OutputStream; +import java.net.URL; +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; + +import javax.crypto.Cipher; + +import junit.framework.TestCase; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.ooxml.signature.service.signer.TemporaryDataStorage; +import org.apache.poi.ooxml.signature.service.signer.ooxml.AbstractOOXMLSignatureService; +import org.apache.poi.ooxml.signature.service.signer.ooxml.OOXMLProvider; +import org.apache.poi.ooxml.signature.service.signer.ooxml.OOXMLSignatureVerifier; +import org.apache.poi.ooxml.signature.service.spi.DigestInfo; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.joda.time.DateTime; + + + +public class TestAbstractOOXMLSignatureService extends TestCase { + + private static final Log LOG = LogFactory.getLog(TestAbstractOOXMLSignatureService.class); + + static { + OOXMLProvider.install(); + } + + private static class OOXMLTestSignatureService extends AbstractOOXMLSignatureService { + + private final URL ooxmlUrl; + + private final TemporaryTestDataStorage temporaryDataStorage; + + private final ByteArrayOutputStream signedOOXMLOutputStream; + + public OOXMLTestSignatureService(URL ooxmlUrl) { + this.temporaryDataStorage = new TemporaryTestDataStorage(); + this.signedOOXMLOutputStream = new ByteArrayOutputStream(); + this.ooxmlUrl = ooxmlUrl; + } + + @Override + protected URL getOfficeOpenXMLDocumentURL() { + return this.ooxmlUrl; + } + + @Override + protected OutputStream getSignedOfficeOpenXMLDocumentOutputStream() { + return this.signedOOXMLOutputStream; + } + + public byte[] getSignedOfficeOpenXMLDocumentData() { + return this.signedOOXMLOutputStream.toByteArray(); + } + + @Override + protected TemporaryDataStorage getTemporaryDataStorage() { + return this.temporaryDataStorage; + } + } + + public void testPreSign() throws Exception { + // setup + URL ooxmlUrl = TestAbstractOOXMLSignatureService.class.getResource("/hello-world-unsigned.docx"); + assertNotNull(ooxmlUrl); + + OOXMLTestSignatureService signatureService = new OOXMLTestSignatureService(ooxmlUrl); + + // operate + DigestInfo digestInfo = signatureService.preSign(null, null); + + // verify + assertNotNull(digestInfo); + LOG.debug("digest algo: " + digestInfo.digestAlgo); + LOG.debug("digest description: " + digestInfo.description); + assertEquals("Office OpenXML Document", digestInfo.description); + assertNotNull(digestInfo.digestAlgo); + assertNotNull(digestInfo.digestValue); + + TemporaryDataStorage temporaryDataStorage = signatureService.getTemporaryDataStorage(); + String preSignResult = IOUtils.toString(temporaryDataStorage.getTempInputStream()); + LOG.debug("pre-sign result: " + preSignResult); + File tmpFile = File.createTempFile("ooxml-pre-sign-", ".xml"); + FileUtils.writeStringToFile(tmpFile, preSignResult); + LOG.debug("tmp pre-sign file: " + tmpFile.getAbsolutePath()); + } + + public void testPostSign() throws Exception { + sign("/hello-world-unsigned.docx"); + } + + public void testSignOffice2010() throws Exception { + sign("/hello-world-office-2010-technical-preview-unsigned.docx"); + } + + public void testSignTwice() throws Exception { + sign("/hello-world-signed.docx", 2); + } + + public void testSignTwiceHere() throws Exception { + File tmpFile = sign("/hello-world-unsigned.docx", 1); + sign(tmpFile.toURI().toURL(), "CN=Test2", 2); + } + + public void testSignPowerpoint() throws Exception { + sign("/hello-world-unsigned.pptx"); + } + + public void testSignSpreadsheet() throws Exception { + sign("/hello-world-unsigned.xlsx"); + } + + private void sign(String documentResourceName) throws Exception { + sign(documentResourceName, 1); + } + + private File sign(String documentResourceName, int signerCount) throws Exception { + URL ooxmlUrl = TestAbstractOOXMLSignatureService.class.getResource(documentResourceName); + return sign(ooxmlUrl, signerCount); + } + + private File sign(URL ooxmlUrl, int signerCount) throws Exception { + return sign(ooxmlUrl, "CN=Test", signerCount); + } + + private File sign(URL ooxmlUrl, String signerDn, int signerCount) throws Exception { + // setup + assertNotNull(ooxmlUrl); + + OOXMLTestSignatureService signatureService = new OOXMLTestSignatureService(ooxmlUrl); + + // operate + DigestInfo digestInfo = signatureService.preSign(null, null); + + // verify + assertNotNull(digestInfo); + LOG.debug("digest algo: " + digestInfo.digestAlgo); + LOG.debug("digest description: " + digestInfo.description); + assertEquals("Office OpenXML Document", digestInfo.description); + assertNotNull(digestInfo.digestAlgo); + assertNotNull(digestInfo.digestValue); + + TemporaryDataStorage temporaryDataStorage = signatureService.getTemporaryDataStorage(); + String preSignResult = IOUtils.toString(temporaryDataStorage.getTempInputStream()); + LOG.debug("pre-sign result: " + preSignResult); + File tmpFile = File.createTempFile("ooxml-pre-sign-", ".xml"); + FileUtils.writeStringToFile(tmpFile, preSignResult); + LOG.debug("tmp pre-sign file: " + tmpFile.getAbsolutePath()); + + // setup: key material, signature value + KeyPair keyPair = PkiTestUtils.generateKeyPair(); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate()); + byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestInfo.digestValue); + byte[] signatureValue = cipher.doFinal(digestInfoValue); + + DateTime notBefore = new DateTime(); + DateTime notAfter = notBefore.plusYears(1); + X509Certificate certificate = PkiTestUtils.generateCertificate(keyPair.getPublic(), signerDn, notBefore, notAfter, null, keyPair.getPrivate(), true, 0, + null, null, new KeyUsage(KeyUsage.nonRepudiation)); + + // operate: postSign + signatureService.postSign(signatureValue, Collections.singletonList(certificate)); + + // verify: signature + byte[] signedOOXMLData = signatureService.getSignedOfficeOpenXMLDocumentData(); + assertNotNull(signedOOXMLData); + LOG.debug("signed OOXML size: " + signedOOXMLData.length); + String extension = FilenameUtils.getExtension(ooxmlUrl.getFile()); + tmpFile = File.createTempFile("ooxml-signed-", "." + extension); + FileUtils.writeByteArrayToFile(tmpFile, signedOOXMLData); + LOG.debug("signed OOXML file: " + tmpFile.getAbsolutePath()); + List signers = OOXMLSignatureVerifier.getSigners(tmpFile.toURI().toURL()); + assertEquals(signerCount, signers.size()); + // assertEquals(certificate, signers.get(0)); + LOG.debug("signed OOXML file: " + tmpFile.getAbsolutePath()); + return tmpFile; + } +} diff --git a/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractXmlSignatureService.java b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractXmlSignatureService.java new file mode 100644 index 0000000000..c1e474f6e2 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestAbstractXmlSignatureService.java @@ -0,0 +1,560 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyPair; +import java.security.MessageDigest; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.xml.crypto.Data; +import javax.xml.crypto.KeySelector; +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.dom.DOMCryptoContext; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.XMLSignContext; +import javax.xml.crypto.dsig.XMLSignature; +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.spec.C14NMethodParameterSpec; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import junit.framework.TestCase; + +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.ooxml.signature.service.spi.DigestInfo; +import org.apache.xml.security.utils.Constants; +import org.apache.xpath.XPathAPI; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.jcp.xml.dsig.internal.dom.DOMReference; +import org.jcp.xml.dsig.internal.dom.DOMXMLSignature; +import org.joda.time.DateTime; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + + + +public class TestAbstractXmlSignatureService extends TestCase { + + private static final Log LOG = LogFactory.getLog(TestAbstractXmlSignatureService.class); + + private static class XmlSignatureTestService extends AbstractXmlSignatureService { + + private Document envelopingDocument; + + private List referenceUris; + + private TemporaryTestDataStorage temporaryDataStorage; + + private String signatureDescription; + + private ByteArrayOutputStream signedDocumentOutputStream; + + private URIDereferencer uriDereferencer; + + public XmlSignatureTestService() { + super(); + this.referenceUris = new LinkedList(); + this.temporaryDataStorage = new TemporaryTestDataStorage(); + this.signedDocumentOutputStream = new ByteArrayOutputStream(); + } + + public byte[] getSignedDocumentData() { + return this.signedDocumentOutputStream.toByteArray(); + } + + public void setEnvelopingDocument(Document envelopingDocument) { + this.envelopingDocument = envelopingDocument; + } + + @Override + protected Document getEnvelopingDocument() { + return this.envelopingDocument; + } + + @Override + protected String getSignatureDescription() { + return this.signatureDescription; + } + + public void setSignatureDescription(String signatureDescription) { + this.signatureDescription = signatureDescription; + } + + @Override + protected List getReferenceUris() { + return this.referenceUris; + } + + public void addReferenceUri(String referenceUri) { + this.referenceUris.add(referenceUri); + } + + @Override + protected OutputStream getSignedDocumentOutputStream() { + return this.signedDocumentOutputStream; + } + + @Override + protected TemporaryDataStorage getTemporaryDataStorage() { + return this.temporaryDataStorage; + } + + public String getFilesDigestAlgorithm() { + return null; + } + + @Override + protected URIDereferencer getURIDereferencer() { + return this.uriDereferencer; + } + + public void setUriDereferencer(URIDereferencer uriDereferencer) { + this.uriDereferencer = uriDereferencer; + } + } + + public void testSignEnvelopingDocument() throws Exception { + // setup + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.newDocument(); + Element rootElement = document.createElementNS("urn:test", "tns:root"); + rootElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:tns", "urn:test"); + document.appendChild(rootElement); + Element dataElement = document.createElementNS("urn:test", "tns:data"); + dataElement.setAttributeNS(null, "Id", "id-1234"); + dataElement.setTextContent("data to be signed"); + rootElement.appendChild(dataElement); + + XmlSignatureTestService testedInstance = new XmlSignatureTestService(); + testedInstance.setEnvelopingDocument(document); + testedInstance.addReferenceUri("#id-1234"); + testedInstance.setSignatureDescription("test-signature-description"); + + // operate + DigestInfo digestInfo = testedInstance.preSign(null, null); + + // verify + assertNotNull(digestInfo); + LOG.debug("digest info description: " + digestInfo.description); + assertEquals("test-signature-description", digestInfo.description); + assertNotNull(digestInfo.digestValue); + LOG.debug("digest algo: " + digestInfo.digestAlgo); + assertEquals("SHA-1", digestInfo.digestAlgo); + + TemporaryTestDataStorage temporaryDataStorage = (TemporaryTestDataStorage) testedInstance.getTemporaryDataStorage(); + assertNotNull(temporaryDataStorage); + InputStream tempInputStream = temporaryDataStorage.getTempInputStream(); + assertNotNull(tempInputStream); + Document tmpDocument = PkiTestUtils.loadDocument(tempInputStream); + + LOG.debug("tmp document: " + PkiTestUtils.toString(tmpDocument)); + Element nsElement = tmpDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + Node digestValueNode = XPathAPI.selectSingleNode(tmpDocument, "//ds:DigestValue", nsElement); + assertNotNull(digestValueNode); + String digestValueTextContent = digestValueNode.getTextContent(); + LOG.debug("digest value text content: " + digestValueTextContent); + assertFalse(digestValueTextContent.isEmpty()); + + /* + * Sign the received XML signature digest value. + */ + KeyPair keyPair = PkiTestUtils.generateKeyPair(); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate()); + byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestInfo.digestValue); + byte[] signatureValue = cipher.doFinal(digestInfoValue); + + DateTime notBefore = new DateTime(); + DateTime notAfter = notBefore.plusYears(1); + X509Certificate certificate = PkiTestUtils.generateCertificate(keyPair.getPublic(), "CN=Test", notBefore, notAfter, null, keyPair.getPrivate(), true, + 0, null, null, new KeyUsage(KeyUsage.nonRepudiation)); + + /* + * Operate: postSign + */ + testedInstance.postSign(signatureValue, Collections.singletonList(certificate)); + + byte[] signedDocumentData = testedInstance.getSignedDocumentData(); + assertNotNull(signedDocumentData); + Document signedDocument = PkiTestUtils.loadDocument(new ByteArrayInputStream(signedDocumentData)); + LOG.debug("signed document: " + PkiTestUtils.toString(signedDocument)); + + NodeList signatureNodeList = signedDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + assertEquals(1, signatureNodeList.getLength()); + Node signatureNode = signatureNodeList.item(0); + + DOMValidateContext domValidateContext = new DOMValidateContext(KeySelector.singletonKeySelector(keyPair.getPublic()), signatureNode); + XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); + XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); + boolean validity = xmlSignature.validate(domValidateContext); + assertTrue(validity); + } + + public static class UriTestDereferencer implements URIDereferencer { + + private final Map resources; + + public UriTestDereferencer() { + this.resources = new HashMap(); + } + + public void addResource(String uri, byte[] data) { + this.resources.put(uri, data); + } + + public Data dereference(URIReference uriReference, XMLCryptoContext xmlCryptoContext) throws URIReferenceException { + String uri = uriReference.getURI(); + byte[] data = this.resources.get(uri); + if (null == data) { + return null; + } + return new OctetStreamData(new ByteArrayInputStream(data)); + } + } + + public void testSignExternalUri() throws Exception { + // setup + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.newDocument(); + + XmlSignatureTestService testedInstance = new XmlSignatureTestService(); + testedInstance.setEnvelopingDocument(document); + testedInstance.addReferenceUri("external-uri"); + testedInstance.setSignatureDescription("test-signature-description"); + UriTestDereferencer uriDereferencer = new UriTestDereferencer(); + uriDereferencer.addResource("external-uri", "hello world".getBytes()); + testedInstance.setUriDereferencer(uriDereferencer); + + // operate + DigestInfo digestInfo = testedInstance.preSign(null, null); + + // verify + assertNotNull(digestInfo); + LOG.debug("digest info description: " + digestInfo.description); + assertEquals("test-signature-description", digestInfo.description); + assertNotNull(digestInfo.digestValue); + LOG.debug("digest algo: " + digestInfo.digestAlgo); + assertEquals("SHA-1", digestInfo.digestAlgo); + + TemporaryTestDataStorage temporaryDataStorage = (TemporaryTestDataStorage) testedInstance.getTemporaryDataStorage(); + assertNotNull(temporaryDataStorage); + InputStream tempInputStream = temporaryDataStorage.getTempInputStream(); + assertNotNull(tempInputStream); + Document tmpDocument = PkiTestUtils.loadDocument(tempInputStream); + + LOG.debug("tmp document: " + PkiTestUtils.toString(tmpDocument)); + Element nsElement = tmpDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + Node digestValueNode = XPathAPI.selectSingleNode(tmpDocument, "//ds:DigestValue", nsElement); + assertNotNull(digestValueNode); + String digestValueTextContent = digestValueNode.getTextContent(); + LOG.debug("digest value text content: " + digestValueTextContent); + assertFalse(digestValueTextContent.isEmpty()); + + /* + * Sign the received XML signature digest value. + */ + KeyPair keyPair = PkiTestUtils.generateKeyPair(); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate()); + byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestInfo.digestValue); + byte[] signatureValue = cipher.doFinal(digestInfoValue); + + DateTime notBefore = new DateTime(); + DateTime notAfter = notBefore.plusYears(1); + X509Certificate certificate = PkiTestUtils.generateCertificate(keyPair.getPublic(), "CN=Test", notBefore, notAfter, null, keyPair.getPrivate(), true, + 0, null, null, new KeyUsage(KeyUsage.nonRepudiation)); + + /* + * Operate: postSign + */ + testedInstance.postSign(signatureValue, Collections.singletonList(certificate)); + + byte[] signedDocumentData = testedInstance.getSignedDocumentData(); + assertNotNull(signedDocumentData); + Document signedDocument = PkiTestUtils.loadDocument(new ByteArrayInputStream(signedDocumentData)); + LOG.debug("signed document: " + PkiTestUtils.toString(signedDocument)); + + NodeList signatureNodeList = signedDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + assertEquals(1, signatureNodeList.getLength()); + Node signatureNode = signatureNodeList.item(0); + + DOMValidateContext domValidateContext = new DOMValidateContext(KeySelector.singletonKeySelector(keyPair.getPublic()), signatureNode); + domValidateContext.setURIDereferencer(uriDereferencer); + XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); + XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); + boolean validity = xmlSignature.validate(domValidateContext); + assertTrue(validity); + } + + public void testSignEnvelopingDocumentWithExternalDigestInfo() throws Exception { + // setup + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.newDocument(); + Element rootElement = document.createElementNS("urn:test", "tns:root"); + rootElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:tns", "urn:test"); + document.appendChild(rootElement); + + XmlSignatureTestService testedInstance = new XmlSignatureTestService(); + testedInstance.setEnvelopingDocument(document); + testedInstance.setSignatureDescription("test-signature-description"); + + byte[] refData = "hello world".getBytes(); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); + messageDigest.update(refData); + byte[] digestValue = messageDigest.digest(); + DigestInfo refDigestInfo = new DigestInfo(digestValue, "SHA-1", "urn:test:ref"); + + // operate + DigestInfo digestInfo = testedInstance.preSign(Collections.singletonList(refDigestInfo), null); + + // verify + assertNotNull(digestInfo); + LOG.debug("digest info description: " + digestInfo.description); + assertEquals("test-signature-description", digestInfo.description); + assertNotNull(digestInfo.digestValue); + LOG.debug("digest algo: " + digestInfo.digestAlgo); + assertEquals("SHA-1", digestInfo.digestAlgo); + + TemporaryTestDataStorage temporaryDataStorage = (TemporaryTestDataStorage) testedInstance.getTemporaryDataStorage(); + assertNotNull(temporaryDataStorage); + InputStream tempInputStream = temporaryDataStorage.getTempInputStream(); + assertNotNull(tempInputStream); + Document tmpDocument = PkiTestUtils.loadDocument(tempInputStream); + + LOG.debug("tmp document: " + PkiTestUtils.toString(tmpDocument)); + Element nsElement = tmpDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + Node digestValueNode = XPathAPI.selectSingleNode(tmpDocument, "//ds:DigestValue", nsElement); + assertNotNull(digestValueNode); + String digestValueTextContent = digestValueNode.getTextContent(); + LOG.debug("digest value text content: " + digestValueTextContent); + assertFalse(digestValueTextContent.isEmpty()); + + /* + * Sign the received XML signature digest value. + */ + KeyPair keyPair = PkiTestUtils.generateKeyPair(); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate()); + byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestInfo.digestValue); + byte[] signatureValue = cipher.doFinal(digestInfoValue); + + DateTime notBefore = new DateTime(); + DateTime notAfter = notBefore.plusYears(1); + X509Certificate certificate = PkiTestUtils.generateCertificate(keyPair.getPublic(), "CN=Test", notBefore, notAfter, null, keyPair.getPrivate(), true, + 0, null, null, new KeyUsage(KeyUsage.nonRepudiation)); + + /* + * Operate: postSign + */ + testedInstance.postSign(signatureValue, Collections.singletonList(certificate)); + + byte[] signedDocumentData = testedInstance.getSignedDocumentData(); + assertNotNull(signedDocumentData); + Document signedDocument = PkiTestUtils.loadDocument(new ByteArrayInputStream(signedDocumentData)); + LOG.debug("signed document: " + PkiTestUtils.toString(signedDocument)); + + NodeList signatureNodeList = signedDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + assertEquals(1, signatureNodeList.getLength()); + Node signatureNode = signatureNodeList.item(0); + + DOMValidateContext domValidateContext = new DOMValidateContext(KeySelector.singletonKeySelector(keyPair.getPublic()), signatureNode); + URIDereferencer dereferencer = new URITest2Dereferencer(); + domValidateContext.setURIDereferencer(dereferencer); + XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); + XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); + boolean validity = xmlSignature.validate(domValidateContext); + assertTrue(validity); + } + + public void testSignExternalDigestInfo() throws Exception { + // setup + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.newDocument(); + + XmlSignatureTestService testedInstance = new XmlSignatureTestService(); + testedInstance.setEnvelopingDocument(document); + testedInstance.setSignatureDescription("test-signature-description"); + + byte[] refData = "hello world".getBytes(); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-1"); + messageDigest.update(refData); + byte[] digestValue = messageDigest.digest(); + DigestInfo refDigestInfo = new DigestInfo(digestValue, "SHA-1", "urn:test:ref"); + + // operate + DigestInfo digestInfo = testedInstance.preSign(Collections.singletonList(refDigestInfo), null); + + // verify + assertNotNull(digestInfo); + LOG.debug("digest info description: " + digestInfo.description); + assertEquals("test-signature-description", digestInfo.description); + assertNotNull(digestInfo.digestValue); + LOG.debug("digest algo: " + digestInfo.digestAlgo); + assertEquals("SHA-1", digestInfo.digestAlgo); + + TemporaryTestDataStorage temporaryDataStorage = (TemporaryTestDataStorage) testedInstance.getTemporaryDataStorage(); + assertNotNull(temporaryDataStorage); + InputStream tempInputStream = temporaryDataStorage.getTempInputStream(); + assertNotNull(tempInputStream); + Document tmpDocument = PkiTestUtils.loadDocument(tempInputStream); + + LOG.debug("tmp document: " + PkiTestUtils.toString(tmpDocument)); + Element nsElement = tmpDocument.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + Node digestValueNode = XPathAPI.selectSingleNode(tmpDocument, "//ds:DigestValue", nsElement); + assertNotNull(digestValueNode); + String digestValueTextContent = digestValueNode.getTextContent(); + LOG.debug("digest value text content: " + digestValueTextContent); + assertFalse(digestValueTextContent.isEmpty()); + + /* + * Sign the received XML signature digest value. + */ + KeyPair keyPair = PkiTestUtils.generateKeyPair(); + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate()); + byte[] digestInfoValue = ArrayUtils.addAll(PkiTestUtils.SHA1_DIGEST_INFO_PREFIX, digestInfo.digestValue); + byte[] signatureValue = cipher.doFinal(digestInfoValue); + + DateTime notBefore = new DateTime(); + DateTime notAfter = notBefore.plusYears(1); + X509Certificate certificate = PkiTestUtils.generateCertificate(keyPair.getPublic(), "CN=Test", notBefore, notAfter, null, keyPair.getPrivate(), true, + 0, null, null, new KeyUsage(KeyUsage.nonRepudiation)); + + /* + * Operate: postSign + */ + testedInstance.postSign(signatureValue, Collections.singletonList(certificate)); + + byte[] signedDocumentData = testedInstance.getSignedDocumentData(); + assertNotNull(signedDocumentData); + Document signedDocument = PkiTestUtils.loadDocument(new ByteArrayInputStream(signedDocumentData)); + LOG.debug("signed document: " + PkiTestUtils.toString(signedDocument)); + + NodeList signatureNodeList = signedDocument.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + assertEquals(1, signatureNodeList.getLength()); + Node signatureNode = signatureNodeList.item(0); + + DOMValidateContext domValidateContext = new DOMValidateContext(KeySelector.singletonKeySelector(keyPair.getPublic()), signatureNode); + URIDereferencer dereferencer = new URITest2Dereferencer(); + domValidateContext.setURIDereferencer(dereferencer); + XMLSignatureFactory xmlSignatureFactory = XMLSignatureFactory.getInstance(); + XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); + boolean validity = xmlSignature.validate(domValidateContext); + assertTrue(validity); + } + + private static class URITest2Dereferencer implements URIDereferencer { + + private static final Log LOG = LogFactory.getLog(URITest2Dereferencer.class); + + public Data dereference(URIReference uriReference, XMLCryptoContext context) throws URIReferenceException { + LOG.debug("dereference: " + uriReference.getURI()); + return new OctetStreamData(new ByteArrayInputStream("hello world".getBytes())); + } + } + + public void testJsr105Signature() throws Exception { + KeyPair keyPair = PkiTestUtils.generateKeyPair(); + + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.newDocument(); + Element rootElement = document.createElementNS("urn:test", "tns:root"); + rootElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:tns", "urn:test"); + document.appendChild(rootElement); + Element dataElement = document.createElementNS("urn:test", "tns:data"); + dataElement.setAttributeNS(null, "Id", "id-1234"); + dataElement.setTextContent("data to be signed"); + rootElement.appendChild(dataElement); + + XMLSignatureFactory signatureFactory = XMLSignatureFactory.getInstance("DOM", new org.jcp.xml.dsig.internal.dom.XMLDSigRI()); + + XMLSignContext signContext = new DOMSignContext(keyPair.getPrivate(), document.getDocumentElement()); + signContext.putNamespacePrefix(javax.xml.crypto.dsig.XMLSignature.XMLNS, "ds"); + + DigestMethod digestMethod = signatureFactory.newDigestMethod(DigestMethod.SHA1, null); + Reference reference = signatureFactory.newReference("#id-1234", digestMethod); + DOMReference domReference = (DOMReference) reference; + assertNull(domReference.getCalculatedDigestValue()); + assertNull(domReference.getDigestValue()); + + SignatureMethod signatureMethod = signatureFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null); + CanonicalizationMethod canonicalizationMethod = signatureFactory.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS, + (C14NMethodParameterSpec) null); + SignedInfo signedInfo = signatureFactory.newSignedInfo(canonicalizationMethod, signatureMethod, Collections.singletonList(reference)); + + javax.xml.crypto.dsig.XMLSignature xmlSignature = signatureFactory.newXMLSignature(signedInfo, null); + + DOMXMLSignature domXmlSignature = (DOMXMLSignature) xmlSignature; + domXmlSignature.marshal(document.getDocumentElement(), "ds", (DOMCryptoContext) signContext); + domReference.digest(signContext); + // xmlSignature.sign(signContext); + // LOG.debug("signed document: " + toString(document)); + + Element nsElement = document.createElement("ns"); + nsElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:ds", Constants.SignatureSpecNS); + Node digestValueNode = XPathAPI.selectSingleNode(document, "//ds:DigestValue", nsElement); + assertNotNull(digestValueNode); + String digestValueTextContent = digestValueNode.getTextContent(); + LOG.debug("digest value text content: " + digestValueTextContent); + assertFalse(digestValueTextContent.isEmpty()); + } +} diff --git a/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestOOXMLSignatureVerifier.java b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestOOXMLSignatureVerifier.java new file mode 100644 index 0000000000..9aa79f304d --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/ooxml/signature/service/signer/TestOOXMLSignatureVerifier.java @@ -0,0 +1,238 @@ + +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Based on the eID Applet Project code. + * Original Copyright (C) 2008-2009 FedICT. + */ + +package org.apache.poi.ooxml.signature.service.signer; + +import java.io.InputStream; +import java.net.URL; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; + +import junit.framework.TestCase; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.POIXMLDocument; +import org.apache.poi.ooxml.signature.service.signer.ooxml.OOXMLProvider; +import org.apache.poi.ooxml.signature.service.signer.ooxml.OOXMLSignatureVerifier; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.signature.PackageDigitalSignatureManager; + + + +public class TestOOXMLSignatureVerifier extends TestCase { + + private static final Log LOG = LogFactory.getLog(TestOOXMLSignatureVerifier.class); + + static { + OOXMLProvider.install(); + } + + public void testIsOOXMLDocument() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-unsigned.docx"); + + // operate + boolean result = OOXMLSignatureVerifier.isOOXML(url); + + // verify + assertTrue(result); + } + + public void testPOI() throws Exception { + // setup + InputStream inputStream = TestOOXMLSignatureVerifier.class.getResourceAsStream("/hello-world-unsigned.docx"); + + // operate + boolean result = POIXMLDocument.hasOOXMLHeader(inputStream); + + // verify + assertTrue(result); + } + + public void testOPC() throws Exception { + // setup + InputStream inputStream = TestOOXMLSignatureVerifier.class.getResourceAsStream("/hello-world-signed.docx"); + + // operate + OPCPackage opcPackage = OPCPackage.open(inputStream); + + ArrayList parts = opcPackage.getParts(); + for (PackagePart part : parts) { + LOG.debug("part name: " + part.getPartName().getName()); + LOG.debug("part content type: " + part.getContentType()); + } + + ArrayList signatureParts = opcPackage.getPartsByContentType("application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"); + assertFalse(signatureParts.isEmpty()); + + PackagePart signaturePart = signatureParts.get(0); + LOG.debug("signature part class type: " + signaturePart.getClass().getName()); + + PackageDigitalSignatureManager packageDigitalSignatureManager = new PackageDigitalSignatureManager(); + // yeah... POI implementation still missing + } + + public void testGetSignerUnsigned() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-unsigned.docx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + public void testGetSignerOffice2010Unsigned() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-office-2010-technical-preview-unsigned.docx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + public void testGetSignerUnsignedPowerpoint() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-unsigned.pptx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + public void testGetSignerUnsignedExcel() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-unsigned.xlsx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + public void testGetSigner() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-signed.docx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertEquals(1, result.size()); + X509Certificate signer = result.get(0); + LOG.debug("signer: " + signer.getSubjectX500Principal()); + } + + public void testOffice2010TechnicalPreview() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-office-2010-technical-preview.docx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertEquals(1, result.size()); + X509Certificate signer = result.get(0); + LOG.debug("signer: " + signer.getSubjectX500Principal()); + } + + public void testGetSignerPowerpoint() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-signed.pptx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertEquals(1, result.size()); + X509Certificate signer = result.get(0); + LOG.debug("signer: " + signer.getSubjectX500Principal()); + } + + public void testGetSignerExcel() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-signed.xlsx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertEquals(1, result.size()); + X509Certificate signer = result.get(0); + LOG.debug("signer: " + signer.getSubjectX500Principal()); + } + + public void testGetSigners() throws Exception { + // setup + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-signed-twice.docx"); + + // operate + List result = OOXMLSignatureVerifier.getSigners(url); + + // verify + assertNotNull(result); + assertEquals(2, result.size()); + X509Certificate signer1 = result.get(0); + X509Certificate signer2 = result.get(1); + LOG.debug("signer 1: " + signer1.getSubjectX500Principal()); + LOG.debug("signer 2: " + signer2.getSubjectX500Principal()); + } + + public void testVerifySignature() throws Exception { + + java.util.logging.Logger logger = java.util.logging.Logger.getLogger("org.jcp.xml.dsig.internal.dom"); + logger.log(Level.FINE, "test"); + + URL url = TestOOXMLSignatureVerifier.class.getResource("/hello-world-signed.docx"); + boolean validity = OOXMLSignatureVerifier.verifySignature(url); + assertTrue(validity); + } + + public void testTamperedFile() throws Exception { + + java.util.logging.Logger logger = java.util.logging.Logger.getLogger("org.jcp.xml.dsig.internal.dom"); + logger.log(Level.FINE, "test"); + + URL url = TestOOXMLSignatureVerifier.class.getResource("/invalidsig.docx"); + boolean validity = OOXMLSignatureVerifier.verifySignature(url); + assertFalse(validity); + } +}