poi/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/XmlSignatureService.java

613 lines
24 KiB
Java

/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
/* ====================================================================
This product contains an ASLv2 licensed version of the OOXML signer
package from the eID Applet project
http://code.google.com/p/eid-applet/source/browse/trunk/README.txt
Copyright (C) 2008-2014 FedICT.
================================================================= */
package org.apache.poi.poifs.crypt.dsig.services;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
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.NoSuchProviderException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
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.XMLObject;
import javax.xml.crypto.dsig.XMLSignContext;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageNamespaces;
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.CryptoFunctions;
import org.apache.poi.poifs.crypt.HashAlgorithm;
import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DOMReferenceIf;
import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DOMSignedInfoIf;
import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DOMXMLSignatureIf;
import org.apache.poi.poifs.crypt.dsig.HorribleProxies.XMLSignatureIf;
import org.apache.poi.poifs.crypt.dsig.HorribleProxy;
import org.apache.poi.poifs.crypt.dsig.OOXMLURIDereferencer;
import org.apache.poi.poifs.crypt.dsig.SignatureInfo;
import org.apache.poi.poifs.crypt.dsig.facets.KeyInfoSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.facets.OOXMLSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.facets.Office2010SignatureFacet;
import org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet;
import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.spi.AddressDTO;
import org.apache.poi.poifs.crypt.dsig.spi.Constants;
import org.apache.poi.poifs.crypt.dsig.spi.DigestInfo;
import org.apache.poi.poifs.crypt.dsig.spi.IdentityDTO;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlOptions;
import org.w3.x2000.x09.xmldsig.SignatureDocument;
import org.w3.x2000.x09.xmldsig.SignatureType;
import org.w3.x2000.x09.xmldsig.SignatureValueType;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Abstract base class for an XML Signature Service implementation.
*/
public class XmlSignatureService implements SignatureService {
private static final POILogger LOG = POILogFactory.getLogger(XmlSignatureService.class);
protected final List<SignatureFacet> signatureFacets;
private String signatureNamespacePrefix;
private String signatureId;
private final HashAlgorithm hashAlgo;
private final OPCPackage opcPackage;
private SignatureDocument sigDoc;
private XAdESSignatureFacet xadesSignatureFacet;
/**
* Main constructor.
*/
public XmlSignatureService(HashAlgorithm digestAlgo, OPCPackage opcPackage) {
this.signatureFacets = new LinkedList<SignatureFacet>();
this.signatureNamespacePrefix = null;
this.signatureId = null;
this.hashAlgo = digestAlgo;
this.opcPackage = opcPackage;
this.sigDoc = null;
}
public void initFacets(Date clock) {
if (clock == null) clock = new Date();
addSignatureFacet(new OOXMLSignatureFacet(this, clock, hashAlgo));
addSignatureFacet(new KeyInfoSignatureFacet(true, false, false));
this.xadesSignatureFacet = new XAdESSignatureFacet(clock, hashAlgo, null);
this.xadesSignatureFacet.setIdSignedProperties("idSignedProperties");
this.xadesSignatureFacet.setSignaturePolicyImplied(true);
/*
* Work-around for Office 2010.
*/
this.xadesSignatureFacet.setIssuerNameNoReverseOrder(true);
setSignatureId("idPackageSignature");
addSignatureFacet(this.xadesSignatureFacet);
addSignatureFacet(new Office2010SignatureFacet());
}
/**
* Sets the signature Id attribute value used to create the XML signature. A
* <code>null</code> value will trigger an automatically generated signature
* Id.
*
* @param signatureId
*/
protected void setSignatureId(String signatureId) {
this.signatureId = signatureId;
}
/**
* Sets the XML Signature namespace prefix to be used for signature
* creation. A <code>null</code> value will omit the prefixing.
*
* @param signatureNamespacePrefix
*/
protected void setSignatureNamespacePrefix(String signatureNamespacePrefix) {
this.signatureNamespacePrefix = signatureNamespacePrefix;
}
/**
* Adds a signature facet to this XML signature service.
*
* @param signatureFacet
*/
public void addSignatureFacet(SignatureFacet... signatureFacets) {
for (SignatureFacet sf : signatureFacets) {
this.signatureFacets.add(sf);
}
}
/**
* 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 HashAlgorithm getSignatureDigestAlgorithm() {
return null != this.hashAlgo ? this.hashAlgo : HashAlgorithm.sha1;
}
/**
* Override this method to change the URI dereferener used by the signing
* engine.
*
* @return
*/
protected URIDereferencer getURIDereferencer() {
OPCPackage ooxmlDocument = getOfficeOpenXMLDocument();
return new OOXMLURIDereferencer(ooxmlDocument);
}
/**
* Gives back the human-readable description of what the citizen will be
* signing. The default value is "XML Document". Override this method to
* provide the citizen with another description.
*
* @return
*/
protected String getSignatureDescription() {
return "Office OpenXML Document";
}
/**
* Gives back the URL of the OOXML to be signed.
*
* @return
*/
public OPCPackage getOfficeOpenXMLDocument() {
return opcPackage;
}
/**
* Gives back the output stream to which to write the signed XML document.
*
* @return
*/
// protected abstract OutputStream getSignedDocumentOutputStream();
public DigestInfo preSign(List<DigestInfo> digestInfos,
List<X509Certificate> signingCertificateChain,
IdentityDTO identity, AddressDTO address, byte[] photo)
throws NoSuchAlgorithmException {
SignatureInfo.initXmlProvider();
LOG.log(POILogger.DEBUG, "preSign");
HashAlgorithm hashAlgo = getSignatureDigestAlgorithm();
byte[] digestValue;
try {
digestValue = getXmlSignatureDigestValue(hashAlgo, digestInfos, signingCertificateChain);
} catch (Exception e) {
throw new RuntimeException("XML signature error: " + e.getMessage(), e);
}
String description = getSignatureDescription();
return new DigestInfo(digestValue, hashAlgo, description);
}
public void postSign(byte[] signatureValue, List<X509Certificate> signingCertificateChain)
throws IOException {
LOG.log(POILogger.DEBUG, "postSign");
SignatureInfo.initXmlProvider();
/*
* Retrieve the intermediate XML signature document from the temporary
* data storage.
*/
SignatureType sigType = sigDoc.getSignature();
/*
* Check ds:Signature node.
*/
if (!signatureId.equals(sigType.getId())) {
throw new RuntimeException("ds:Signature not found for @Id: " + signatureId);
}
/*
* Insert signature value into the ds:SignatureValue element
*/
SignatureValueType sigVal = sigType.getSignatureValue();
sigVal.setByteArrayValue(signatureValue);
/*
* Allow signature facets to inject their own stuff.
*/
for (SignatureFacet signatureFacet : this.signatureFacets) {
signatureFacet.postSign(sigType, signingCertificateChain);
}
writeDocument();
}
@SuppressWarnings("unchecked")
private byte[] getXmlSignatureDigestValue(HashAlgorithm hashAlgo,
List<DigestInfo> digestInfos,
List<X509Certificate> signingCertificateChain)
throws ParserConfigurationException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, MarshalException,
javax.xml.crypto.dsig.XMLSignatureException,
TransformerFactoryConfigurationError, TransformerException,
IOException, SAXException, NoSuchProviderException, XmlException {
/*
* DOM Document construction.
*/
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document doc = dbf.newDocumentBuilder().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;
}
};
// As of JDK 7, can't use sigDoc here directly, because the
// setAttributeID will be called and it's not implemented in xmlbeans
XMLSignContext xmlSignContext = new DOMSignContext(key, doc);
URIDereferencer uriDereferencer = getURIDereferencer();
if (null != uriDereferencer) {
xmlSignContext.setURIDereferencer(uriDereferencer);
}
xmlSignContext.putNamespacePrefix(
"http://schemas.openxmlformats.org/package/2006/digital-signature",
"mdssi");
if (this.signatureNamespacePrefix != null) {
/*
* OOo doesn't like ds namespaces so per default prefixing is off.
*/
xmlSignContext.putNamespacePrefix(
javax.xml.crypto.dsig.XMLSignature.XMLNS,
this.signatureNamespacePrefix);
}
XMLSignatureFactory signatureFactory = XMLSignatureFactory.getInstance("DOM", "XMLDSig");
/*
* Add ds:References that come from signing client local files.
*/
List<Reference> references = new LinkedList<Reference>();
addDigestInfosAsReferences(digestInfos, signatureFactory, references);
/*
* Invoke the signature facets.
*/
String localSignatureId;
if (null == this.signatureId) {
localSignatureId = "xmldsig-" + UUID.randomUUID().toString();
} else {
localSignatureId = this.signatureId;
}
List<XMLObject> objects = new LinkedList<XMLObject>();
for (SignatureFacet signatureFacet : this.signatureFacets) {
LOG.log(POILogger.DEBUG, "invoking signature facet: "
+ signatureFacet.getClass().getSimpleName());
signatureFacet.preSign(signatureFactory, localSignatureId, signingCertificateChain, references, objects);
}
/*
* ds:SignedInfo
*/
SignatureMethod signatureMethod = signatureFactory.newSignatureMethod(
getSignatureMethod(hashAlgo), null);
CanonicalizationMethod canonicalizationMethod = signatureFactory
.newCanonicalizationMethod(getCanonicalizationMethod(),
(C14NMethodParameterSpec) null);
SignedInfo signedInfo = signatureFactory.newSignedInfo(
canonicalizationMethod, signatureMethod, references);
/*
* JSR105 ds:Signature creation
*/
String signatureValueId = localSignatureId + "-signature-value";
javax.xml.crypto.dsig.XMLSignature xmlSignature = signatureFactory
.newXMLSignature(signedInfo, null, objects, localSignatureId,
signatureValueId);
/*
* ds:Signature Marshalling.
*/
DOMXMLSignatureIf domXmlSignature;
try {
domXmlSignature = HorribleProxy.newProxy(DOMXMLSignatureIf.class, xmlSignature);
} catch (Exception e) {
throw new RuntimeException("DomXmlSignature instance error: " + e.getMessage(), e);
}
domXmlSignature.marshal(doc, this.signatureNamespacePrefix, (DOMCryptoContext) xmlSignContext);
registerIds(doc);
Element el = doc.getElementById("idPackageObject");
assert (el != null);
el.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:mdssi", PackageNamespaces.DIGITAL_SIGNATURE);
/*
* Completion of undigested ds:References in the ds:Manifests.
*/
for (XMLObject object : objects) {
LOG.log(POILogger.DEBUG, "object java type: " + object.getClass().getName());
List<XMLStructure> 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<Reference> manifestReferences = manifest.getReferences();
for (Reference manifestReference : manifestReferences) {
if (manifestReference.getDigestValue() != null) continue;
DOMReferenceIf manifestDOMReference;
try {
manifestDOMReference = HorribleProxy.newProxy(DOMReferenceIf.class, manifestReference);
} catch (Exception e) {
throw new RuntimeException("DOMReference instance error: " + e.getMessage(), e);
}
manifestDOMReference.digest(xmlSignContext);
}
}
}
/*
* Completion of undigested ds:References.
*/
List<Reference> signedInfoReferences = signedInfo.getReferences();
for (Reference signedInfoReference : signedInfoReferences) {
DOMReferenceIf domReference;
try {
domReference = HorribleProxy.newProxy(DOMReferenceIf.class, signedInfoReference);
} catch (Exception e) {
throw new RuntimeException("DOMReference instance error: " + e.getMessage(), e);
}
// ds:Reference with external digest value
if (domReference.getDigestValue() != null) continue;
domReference.digest(xmlSignContext);
}
/*
* Calculation of XML signature digest value.
*/
DOMSignedInfoIf domSignedInfo;
try {
domSignedInfo = HorribleProxy.newProxy(DOMSignedInfoIf.class, signedInfo);
} catch (Exception e) {
throw new RuntimeException("DOMSignedInfo instance error: " + e.getMessage(), e);
}
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
domSignedInfo.canonicalize(xmlSignContext, dataStream);
byte[] octets = dataStream.toByteArray();
sigDoc = SignatureDocument.Factory.parse(doc.getDocumentElement());
/*
* TODO: we could be using DigestOutputStream here to optimize memory
* usage.
*/
MessageDigest jcaMessageDigest = CryptoFunctions.getMessageDigest(hashAlgo);
byte[] digestValue = jcaMessageDigest.digest(octets);
return digestValue;
}
/**
* the resulting document needs to be tweaked before it can be digested -
* this applies to the verification and signing step
*
* @param doc
*/
public void registerIds(Document doc) {
NodeList nl = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Object");
registerIdAttribute(nl);
nl = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "SignedProperties");
registerIdAttribute(nl);
}
protected void registerIdAttribute(NodeList nl) {
for (int i=0; i<nl.getLength(); i++) {
Element el = (Element)nl.item(i);
if (el.hasAttribute("Id")) {
el.setIdAttribute("Id", true);
}
}
}
private void addDigestInfosAsReferences(List<DigestInfo> digestInfos,
XMLSignatureFactory signatureFactory, List<Reference> references)
throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException, MalformedURLException {
if (digestInfos == null) return;
for (DigestInfo digestInfo : digestInfos) {
byte[] documentDigestValue = digestInfo.digestValue;
DigestMethod digestMethod = signatureFactory.newDigestMethod(
digestInfo.hashAlgo.xmlSignUri, null);
String uri = new File(digestInfo.description).getName();
Reference reference = signatureFactory.newReference(uri,
digestMethod, null, null, null, documentDigestValue);
references.add(reference);
}
}
private String getSignatureMethod(HashAlgorithm hashAlgo) {
if (null == hashAlgo) {
throw new RuntimeException("digest algo is null");
}
XMLSignatureIf XmlSignature;
try {
XmlSignature = HorribleProxy.newProxy(XMLSignatureIf.class);
} catch (Exception e) {
throw new RuntimeException("JDK doesn't support XmlSignature", e);
}
switch (hashAlgo) {
case sha1: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA1();
case sha256: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA256();
case sha384: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA384();
case sha512: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA512();
case ripemd160: return XmlSignature.ALGO_ID_MAC_HMAC_RIPEMD160();
default: break;
}
throw new RuntimeException("unsupported sign algo: " + hashAlgo);
}
/**
* Gives back the used XAdES signature facet.
*
* @return
*/
protected XAdESSignatureFacet getXAdESSignatureFacet() {
return this.xadesSignatureFacet;
}
public String getFilesDigestAlgorithm() {
return null;
}
protected String getCanonicalizationMethod() {
return CanonicalizationMethod.INCLUSIVE;
}
protected void writeDocument() throws IOException {
XmlOptions xo = new XmlOptions();
Map<String,String> namespaceMap = new HashMap<String,String>();
for (SignatureFacet sf : this.signatureFacets) {
Map<String,String> sfm = sf.getNamespacePrefixMapping();
if (sfm != null) {
namespaceMap.putAll(sfm);
}
}
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 = this.getOfficeOpenXMLDocument();
PackagePartName sigPartName, sigsPartName;
try {
// <Override PartName="/_xmlsignatures/sig1.xml" ContentType="application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"/>
sigPartName = PackagingURIHelper.createPartName("/_xmlsignatures/sig1.xml");
// <Default Extension="sigs" ContentType="application/vnd.openxmlformats-package.digital-signature-origin"/>
sigsPartName = PackagingURIHelper.createPartName("/_xmlsignatures/origin.sigs");
} catch (InvalidFormatException e) {
throw new IOException(e);
}
String sigContentType = "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml";
PackagePart sigPart = pkg.getPart(sigPartName);
if (sigPart == null) {
sigPart = pkg.createPart(sigPartName, sigContentType);
}
OutputStream os = sigPart.getOutputStream();
sigDoc.save(os, xo);
os.close();
String sigsContentType = "application/vnd.openxmlformats-package.digital-signature-origin";
PackagePart sigsPart = pkg.getPart(sigsPartName);
if (sigsPart == null) {
// touch empty marker file
sigsPart = pkg.createPart(sigsPartName, sigsContentType);
}
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);
}
}