All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.poi.poifs.crypt.dsig.SignatureInfo Maven / Gradle / Ivy

There is a newer version: 5.3.0
Show newest version
/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
==================================================================== */

/* ====================================================================
   This product contains an ASLv2 licensed version of the OOXML signer
   package from the eID Applet project
   http://code.google.com/p/eid-applet/source/browse/trunk/README.txt  
   Copyright (C) 2008-2014 FedICT.
   ================================================================= */ 

package org.apache.poi.poifs.crypt.dsig;

import static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS;
import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_DIGSIG_NS;

import javax.crypto.Cipher;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.URIDereferencer;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.Manifest;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignContext;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.XMLValidateContext;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import org.apache.jcp.xml.dsig.internal.dom.DOMReference;
import org.apache.jcp.xml.dsig.internal.dom.DOMSignedInfo;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.ContentTypes;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.PackageRelationshipCollection;
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes;
import org.apache.poi.openxml4j.opc.PackagingURIHelper;
import org.apache.poi.openxml4j.opc.TargetMode;
import org.apache.poi.poifs.crypt.ChainingMode;
import org.apache.poi.poifs.crypt.CipherAlgorithm;
import org.apache.poi.poifs.crypt.CryptoFunctions;
import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable;
import org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet;
import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService;
import org.apache.poi.util.DocumentHelper;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
import org.apache.xml.security.Init;
import org.apache.xml.security.utils.Base64;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlOptions;
import org.w3.x2000.x09.xmldsig.SignatureDocument;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;


/**
 * 

This class is the default entry point for XML signatures and can be used for * validating an existing signed office document and signing a office document.

* *

Validating a signed office document

* *
 * OPCPackage pkg = OPCPackage.open(..., PackageAccess.READ);
 * SignatureConfig sic = new SignatureConfig();
 * sic.setOpcPackage(pkg);
 * SignatureInfo si = new SignatureInfo();
 * si.setSignatureConfig(sic);
 * boolean isValid = si.validate();
 * ...
 * 
* *

Signing an office document

* *
 * // loading the keystore - pkcs12 is used here, but of course jks & co are also valid
 * // the keystore needs to contain a private key and it's certificate having a
 * // 'digitalSignature' key usage
 * char password[] = "test".toCharArray();
 * File file = new File("test.pfx");
 * KeyStore keystore = KeyStore.getInstance("PKCS12");
 * FileInputStream fis = new FileInputStream(file);
 * keystore.load(fis, password);
 * fis.close();
 * 
 * // extracting private key and certificate
 * String alias = "xyz"; // alias of the keystore entry
 * Key key = keystore.getKey(alias, password);
 * X509Certificate x509 = (X509Certificate)keystore.getCertificate(alias);
 * 
 * // filling the SignatureConfig entries (minimum fields, more options are available ...)
 * SignatureConfig signatureConfig = new SignatureConfig();
 * signatureConfig.setKey(keyPair.getPrivate());
 * signatureConfig.setSigningCertificateChain(Collections.singletonList(x509));
 * OPCPackage pkg = OPCPackage.open(..., PackageAccess.READ_WRITE);
 * signatureConfig.setOpcPackage(pkg);
 * 
 * // adding the signature document to the package
 * SignatureInfo si = new SignatureInfo();
 * si.setSignatureConfig(signatureConfig);
 * si.confirmSignature();
 * // optionally verify the generated signature
 * boolean b = si.verifySignature();
 * assert (b);
 * // write the changes back to disc
 * pkg.close();
 * 
* *

Implementation notes:

* *

Although there's a XML signature implementation in the Oracle JDKs 6 and higher, * compatibility with IBM JDKs is also in focus (... but maybe not thoroughly tested ...). * Therefore we are using the Apache Santuario libs (xmlsec) instead of the built-in classes, * as the compatibility seems to be provided there.

* *

To use SignatureInfo and its sibling classes, you'll need to have the following libs * in the classpath:

*
    *
  • BouncyCastle bcpkix and bcprov (tested against 1.54)
  • *
  • Apache Santuario "xmlsec" (tested against 2.0.5)
  • *
  • and slf4j-api (tested against 1.7.12)
  • *
*/ public class SignatureInfo implements SignatureConfigurable { private static final POILogger LOG = POILogFactory.getLogger(SignatureInfo.class); private static boolean isInitialized = false; private SignatureConfig signatureConfig; public class SignaturePart { private final PackagePart signaturePart; private X509Certificate signer; private List certChain; private SignaturePart(PackagePart signaturePart) { this.signaturePart = signaturePart; } /** * @return the package part containing the signature */ public PackagePart getPackagePart() { return signaturePart; } /** * @return the signer certificate */ public X509Certificate getSigner() { return signer; } /** * @return the certificate chain of the signer */ public List getCertChain() { return certChain; } /** * Helper method for examining the xml signature * * @return the xml signature document * @throws IOException if the xml signature doesn't exist or can't be read * @throws XmlException if the xml signature is malformed */ public SignatureDocument getSignatureDocument() throws IOException, XmlException { // TODO: check for XXE return SignatureDocument.Factory.parse(signaturePart.getInputStream(), DEFAULT_XML_OPTIONS); } /** * @return true, when the xml signature is valid, false otherwise * * @throws EncryptedDocumentException if the signature can't be extracted or if its malformed */ @SuppressWarnings("unchecked") public boolean validate() { KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); try { Document doc = DocumentHelper.readDocument(signaturePart.getInputStream()); XPath xpath = XPathFactory.newInstance().newXPath(); NodeList nl = (NodeList)xpath.compile("//*[@Id]").evaluate(doc, XPathConstants.NODESET); for (int i=0; i)xmlSignature.getSignedInfo().getReferences()) { SignatureFacet.brokenJvmWorkaround(ref); } for (XMLObject xo : (List)xmlSignature.getObjects()) { for (XMLStructure xs : (List)xo.getContent()) { if (xs instanceof Manifest) { for (Reference ref : (List)((Manifest)xs).getReferences()) { SignatureFacet.brokenJvmWorkaround(ref); } } } } boolean valid = xmlSignature.validate(domValidateContext); if (valid) { signer = keySelector.getSigner(); certChain = keySelector.getCertChain(); } return valid; } catch (Exception e) { String s = "error in marshalling and validating the signature"; LOG.log(POILogger.ERROR, s, e); throw new EncryptedDocumentException(s, e); } } } /** * Constructor initializes xml signature environment, if it hasn't been initialized before */ public SignatureInfo() { initXmlProvider(); } /** * @return the signature config */ public SignatureConfig getSignatureConfig() { return signatureConfig; } /** * @param signatureConfig the signature config, needs to be set before a SignatureInfo object is used */ public void setSignatureConfig(SignatureConfig signatureConfig) { this.signatureConfig = signatureConfig; } /** * @return true, if first signature part is valid */ public boolean verifySignature() { // http://www.oracle.com/technetwork/articles/javase/dig-signature-api-140772.html for (SignaturePart sp : getSignatureParts()){ // only validate first part return sp.validate(); } return false; } /** * add the xml signature to the document * * @throws XMLSignatureException * @throws MarshalException */ public void confirmSignature() throws XMLSignatureException, MarshalException { Document document = DocumentHelper.createDocument(); // operate DigestInfo digestInfo = preSign(document, null); // setup: key material, signature value byte[] signatureValue = signDigest(digestInfo.digestValue); // operate: postSign postSign(document, signatureValue); } /** * Sign (encrypt) the digest with the private key. * Currently only rsa is supported. * * @param digest the hashed input * @return the encrypted hash */ public byte[] signDigest(byte digest[]) { Cipher cipher = CryptoFunctions.getCipher(signatureConfig.getKey(), CipherAlgorithm.rsa , ChainingMode.ecb, null, Cipher.ENCRYPT_MODE, "PKCS1Padding"); try { ByteArrayOutputStream digestInfoValueBuf = new ByteArrayOutputStream(); digestInfoValueBuf.write(signatureConfig.getHashMagic()); digestInfoValueBuf.write(digest); byte[] digestInfoValue = digestInfoValueBuf.toByteArray(); byte[] signatureValue = cipher.doFinal(digestInfoValue); return signatureValue; } catch (Exception e) { throw new EncryptedDocumentException(e); } } /** * @return a signature part for each signature document. * the parts can be validated independently. */ public Iterable getSignatureParts() { signatureConfig.init(true); return new Iterable() { public Iterator iterator() { return new Iterator() { OPCPackage pkg = signatureConfig.getOpcPackage(); Iterator sigOrigRels = pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN).iterator(); Iterator sigRels = null; PackagePart sigPart = null; public boolean hasNext() { while (sigRels == null || !sigRels.hasNext()) { if (!sigOrigRels.hasNext()) return false; sigPart = pkg.getPart(sigOrigRels.next()); LOG.log(POILogger.DEBUG, "Digital Signature Origin part", sigPart); try { sigRels = sigPart.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE).iterator(); } catch (InvalidFormatException e) { LOG.log(POILogger.WARN, "Reference to signature is invalid.", e); } } return true; } public SignaturePart next() { PackagePart sigRelPart = null; do { try { if (!hasNext()) throw new NoSuchElementException(); sigRelPart = sigPart.getRelatedPart(sigRels.next()); LOG.log(POILogger.DEBUG, "XML Signature part", sigRelPart); } catch (InvalidFormatException e) { LOG.log(POILogger.WARN, "Reference to signature is invalid.", e); } } while (sigPart == null); return new SignaturePart(sigRelPart); } public void remove() { throw new UnsupportedOperationException(); } }; } }; } /** * Initialize the xml signing environment and the bouncycastle provider */ protected static synchronized void initXmlProvider() { if (isInitialized) return; isInitialized = true; try { Init.init(); RelationshipTransformService.registerDsigProvider(); CryptoFunctions.registerBouncyCastle(); } catch (Exception e) { throw new RuntimeException("Xml & BouncyCastle-Provider initialization failed", e); } } /** * Helper method for adding informations before the signing. * Normally {@link #confirmSignature()} is sufficient to be used. */ @SuppressWarnings("unchecked") public DigestInfo preSign(Document document, List digestInfos) throws XMLSignatureException, MarshalException { signatureConfig.init(false); // it's necessary to explicitly set the mdssi namespace, but the sign() method has no // normal way to interfere with, so we need to add the namespace under the hand ... EventTarget target = (EventTarget)document; EventListener creationListener = signatureConfig.getSignatureMarshalListener(); if (creationListener != null) { if (creationListener instanceof SignatureMarshalListener) { ((SignatureMarshalListener)creationListener).setEventTarget(target); } SignatureMarshalListener.setListener(target, creationListener, true); } /* * Signature context construction. */ XMLSignContext xmlSignContext = new DOMSignContext(signatureConfig.getKey(), document); URIDereferencer uriDereferencer = signatureConfig.getUriDereferencer(); if (null != uriDereferencer) { xmlSignContext.setURIDereferencer(uriDereferencer); } for (Map.Entry me : signatureConfig.getNamespacePrefixes().entrySet()) { xmlSignContext.putNamespacePrefix(me.getKey(), me.getValue()); } xmlSignContext.setDefaultNamespacePrefix(""); // signatureConfig.getNamespacePrefixes().get(XML_DIGSIG_NS)); brokenJvmWorkaround(xmlSignContext); XMLSignatureFactory signatureFactory = signatureConfig.getSignatureFactory(); /* * Add ds:References that come from signing client local files. */ List references = new ArrayList(); for (DigestInfo digestInfo : safe(digestInfos)) { byte[] documentDigestValue = digestInfo.digestValue; String uri = new File(digestInfo.description).getName(); Reference reference = SignatureFacet.newReference (uri, null, null, null, documentDigestValue, signatureConfig); references.add(reference); } /* * Invoke the signature facets. */ List objects = new ArrayList(); for (SignatureFacet signatureFacet : signatureConfig.getSignatureFacets()) { LOG.log(POILogger.DEBUG, "invoking signature facet: " + signatureFacet.getClass().getSimpleName()); signatureFacet.preSign(document, references, objects); } /* * ds:SignedInfo */ SignedInfo signedInfo; try { SignatureMethod signatureMethod = signatureFactory.newSignatureMethod (signatureConfig.getSignatureMethodUri(), null); CanonicalizationMethod canonicalizationMethod = signatureFactory .newCanonicalizationMethod(signatureConfig.getCanonicalizationMethod(), (C14NMethodParameterSpec) null); signedInfo = signatureFactory.newSignedInfo( canonicalizationMethod, signatureMethod, references); } catch (GeneralSecurityException e) { throw new XMLSignatureException(e); } /* * JSR105 ds:Signature creation */ String signatureValueId = signatureConfig.getPackageSignatureId() + "-signature-value"; javax.xml.crypto.dsig.XMLSignature xmlSignature = signatureFactory .newXMLSignature(signedInfo, null, objects, signatureConfig.getPackageSignatureId(), signatureValueId); /* * ds:Signature Marshalling. */ xmlSignature.sign(xmlSignContext); /* * Completion of undigested ds:References in the ds:Manifests. */ for (XMLObject object : objects) { LOG.log(POILogger.DEBUG, "object java type: " + object.getClass().getName()); List objectContentList = object.getContent(); for (XMLStructure objectContent : objectContentList) { LOG.log(POILogger.DEBUG, "object content java type: " + objectContent.getClass().getName()); if (!(objectContent instanceof Manifest)) continue; Manifest manifest = (Manifest) objectContent; List manifestReferences = manifest.getReferences(); for (Reference manifestReference : manifestReferences) { if (manifestReference.getDigestValue() != null) continue; DOMReference manifestDOMReference = (DOMReference)manifestReference; manifestDOMReference.digest(xmlSignContext); } } } /* * Completion of undigested ds:References. */ List signedInfoReferences = signedInfo.getReferences(); for (Reference signedInfoReference : signedInfoReferences) { DOMReference domReference = (DOMReference)signedInfoReference; // ds:Reference with external digest value if (domReference.getDigestValue() != null) continue; domReference.digest(xmlSignContext); } /* * Calculation of XML signature digest value. */ DOMSignedInfo domSignedInfo = (DOMSignedInfo)signedInfo; ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); domSignedInfo.canonicalize(xmlSignContext, dataStream); byte[] octets = dataStream.toByteArray(); /* * TODO: we could be using DigestOutputStream here to optimize memory * usage. */ MessageDigest md = CryptoFunctions.getMessageDigest(signatureConfig.getDigestAlgo()); byte[] digestValue = md.digest(octets); String description = signatureConfig.getSignatureDescription(); return new DigestInfo(digestValue, signatureConfig.getDigestAlgo(), description); } /** * Helper method for adding informations after the signing. * Normally {@link #confirmSignature()} is sufficient to be used. */ public void postSign(Document document, byte[] signatureValue) throws MarshalException { LOG.log(POILogger.DEBUG, "postSign"); /* * Check ds:Signature node. */ String signatureId = signatureConfig.getPackageSignatureId(); if (!signatureId.equals(document.getDocumentElement().getAttribute("Id"))) { throw new RuntimeException("ds:Signature not found for @Id: " + signatureId); } /* * Insert signature value into the ds:SignatureValue element */ NodeList sigValNl = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue"); if (sigValNl.getLength() != 1) { throw new RuntimeException("preSign has to be called before postSign"); } sigValNl.item(0).setTextContent(Base64.encode(signatureValue)); /* * Allow signature facets to inject their own stuff. */ for (SignatureFacet signatureFacet : signatureConfig.getSignatureFacets()) { signatureFacet.postSign(document); } writeDocument(document); } /** * Write XML signature into the OPC package * * @param document the xml signature document * @throws MarshalException */ protected void writeDocument(Document document) throws MarshalException { XmlOptions xo = new XmlOptions(); Map namespaceMap = new HashMap(); for(Map.Entry entry : signatureConfig.getNamespacePrefixes().entrySet()){ namespaceMap.put(entry.getValue(), entry.getKey()); } xo.setSaveSuggestedPrefixes(namespaceMap); xo.setUseDefaultNamespace(); LOG.log(POILogger.DEBUG, "output signed Office OpenXML document"); /* * Copy the original OOXML content to the signed OOXML package. During * copying some files need to changed. */ OPCPackage pkg = signatureConfig.getOpcPackage(); PackagePartName sigPartName, sigsPartName; try { // sigPartName = PackagingURIHelper.createPartName("/_xmlsignatures/sig1.xml"); // sigsPartName = PackagingURIHelper.createPartName("/_xmlsignatures/origin.sigs"); } catch (InvalidFormatException e) { throw new MarshalException(e); } PackagePart sigPart = pkg.getPart(sigPartName); if (sigPart == null) { sigPart = pkg.createPart(sigPartName, ContentTypes.DIGITAL_SIGNATURE_XML_SIGNATURE_PART); } try { OutputStream os = sigPart.getOutputStream(); SignatureDocument sigDoc = SignatureDocument.Factory.parse(document, DEFAULT_XML_OPTIONS); sigDoc.save(os, xo); os.close(); } catch (Exception e) { throw new MarshalException("Unable to write signature document", e); } PackagePart sigsPart = pkg.getPart(sigsPartName); if (sigsPart == null) { // touch empty marker file sigsPart = pkg.createPart(sigsPartName, ContentTypes.DIGITAL_SIGNATURE_ORIGIN_PART); } PackageRelationshipCollection relCol = pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); for (PackageRelationship pr : relCol) { pkg.removeRelationship(pr.getId()); } pkg.addRelationship(sigsPartName, TargetMode.INTERNAL, PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); sigsPart.addRelationship(sigPartName, TargetMode.INTERNAL, PackageRelationshipTypes.DIGITAL_SIGNATURE); } /** * Helper method for null lists, which are converted to empty lists * * @param other the reference to wrap, if null * @return if other is null, an empty lists is returned, otherwise other is returned */ private static List safe(List other) { List emptyList = Collections.emptyList(); return other == null ? emptyList : other; } private void brokenJvmWorkaround(XMLSignContext context) { // workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1155012 Provider bcProv = Security.getProvider("BC"); if (bcProv != null) { context.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", bcProv); } } private void brokenJvmWorkaround(XMLValidateContext context) { // workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1155012 Provider bcProv = Security.getProvider("BC"); if (bcProv != null) { context.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", bcProv); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy