diff --git a/build.xml b/build.xml index 6feaf3482..8b9b3550d 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 6d9855fb8..dc948c1d7 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 000000000..73b498081 --- /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 000000000..fc55015e6 --- /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 000000000..abd3551ea --- /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 000000000..85464bf2b --- /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 000000000..9865a7703 --- /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 000000000..ec5c94073 --- /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 000000000..f76d69d6a --- /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 000000000..16fe40cda --- /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 000000000..df6956664 --- /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 000000000..885b7f04f --- /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 000000000..d00f010a8 --- /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 000000000..5ed64642e --- /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 000000000..c0bb6480c --- /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 000000000..7f67bbf9f --- /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 000000000..eb7c767b9 --- /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 000000000..60ff0dad9 --- /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 000000000..4ed07ffb1 --- /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 000000000..1a2b6b730 --- /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 000000000..495bd57e6 --- /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 000000000..f285c23c5 --- /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 000000000..6b86b2fb1 --- /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 000000000..1b4a89f70 --- /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 000000000..5162b67c6 Binary files /dev/null and b/src/ooxml/testcases/hello-world-office-2010-technical-preview-unsigned.docx differ diff --git a/src/ooxml/testcases/hello-world-office-2010-technical-preview.docx b/src/ooxml/testcases/hello-world-office-2010-technical-preview.docx new file mode 100644 index 000000000..cbd427756 Binary files /dev/null and b/src/ooxml/testcases/hello-world-office-2010-technical-preview.docx differ diff --git a/src/ooxml/testcases/hello-world-signed-twice.docx b/src/ooxml/testcases/hello-world-signed-twice.docx new file mode 100644 index 000000000..96c91e957 Binary files /dev/null and b/src/ooxml/testcases/hello-world-signed-twice.docx differ diff --git a/src/ooxml/testcases/hello-world-signed.docx b/src/ooxml/testcases/hello-world-signed.docx new file mode 100644 index 000000000..79a7bbb81 Binary files /dev/null and b/src/ooxml/testcases/hello-world-signed.docx differ diff --git a/src/ooxml/testcases/hello-world-signed.pptx b/src/ooxml/testcases/hello-world-signed.pptx new file mode 100644 index 000000000..9b37033f5 Binary files /dev/null and b/src/ooxml/testcases/hello-world-signed.pptx differ diff --git a/src/ooxml/testcases/hello-world-signed.xlsx b/src/ooxml/testcases/hello-world-signed.xlsx new file mode 100644 index 000000000..0d45c53ed Binary files /dev/null and b/src/ooxml/testcases/hello-world-signed.xlsx differ diff --git a/src/ooxml/testcases/hello-world-unsigned.docx b/src/ooxml/testcases/hello-world-unsigned.docx new file mode 100644 index 000000000..1790c961c Binary files /dev/null and b/src/ooxml/testcases/hello-world-unsigned.docx differ diff --git a/src/ooxml/testcases/hello-world-unsigned.pptx b/src/ooxml/testcases/hello-world-unsigned.pptx new file mode 100644 index 000000000..ca42529a9 Binary files /dev/null and b/src/ooxml/testcases/hello-world-unsigned.pptx differ diff --git a/src/ooxml/testcases/hello-world-unsigned.xlsx b/src/ooxml/testcases/hello-world-unsigned.xlsx new file mode 100644 index 000000000..b99143e92 Binary files /dev/null and b/src/ooxml/testcases/hello-world-unsigned.xlsx differ diff --git a/src/ooxml/testcases/invalidsig.docx b/src/ooxml/testcases/invalidsig.docx new file mode 100644 index 000000000..c448e819a Binary files /dev/null and b/src/ooxml/testcases/invalidsig.docx differ 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 000000000..a307ee090 --- /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 000000000..83a36cc5c --- /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 000000000..d6cc51c65 --- /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 000000000..c1e474f6e --- /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 000000000..9aa79f304 --- /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); + } +}