/*
* eID Applet Project.
* Copyright (C) 2009 FedICT.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version
* 3.0 as published by the Free Software Foundation.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, see
* http://www.gnu.org/licenses/.
*/
/*
* Copyright (C) 2008-2009 FedICT.
* This file is part of the eID Applet Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package be.fedict.eid.applet.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.security.cert.X509Certificate;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.crypto.XMLStructure;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Manifest;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureProperties;
import javax.xml.crypto.dsig.SignatureProperty;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xml.security.utils.Constants;
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.xml.sax.InputSource;
import org.xml.sax.SAXException;
import be.fedict.eid.applet.service.signer.DigestAlgo;
import be.fedict.eid.applet.service.signer.SignatureFacet;
import be.fedict.eid.applet.service.signer.jaxb.opc.contenttypes.CTDefault;
import be.fedict.eid.applet.service.signer.jaxb.opc.contenttypes.CTOverride;
import be.fedict.eid.applet.service.signer.jaxb.opc.contenttypes.CTTypes;
import be.fedict.eid.applet.service.signer.jaxb.opc.relationships.CTRelationship;
import be.fedict.eid.applet.service.signer.jaxb.opc.relationships.CTRelationships;
import be.fedict.eid.applet.service.signer.jaxb.opc.relationships.STTargetMode;
import be.fedict.eid.applet.service.signer.time.Clock;
import be.fedict.eid.applet.service.signer.time.LocalClock;
/**
* Office OpenXML Signature Facet implementation.
*
* @author fcorneli
* @see http://msdn.microsoft.com/en-us/library/cc313071.aspx
*/
public class OOXMLSignatureFacet implements SignatureFacet {
private static final Log LOG = LogFactory.getLog(OOXMLSignatureFacet.class);
public static final String OOXML_DIGSIG_NS = "http://schemas.openxmlformats.org/package/2006/digital-signature";
public static final String OFFICE_DIGSIG_NS = "http://schemas.microsoft.com/office/2006/digsig";
private final AbstractOOXMLSignatureService signatureService;
private final Clock clock;
private final DigestAlgo digestAlgo;
/**
* Main constructor.
*/
public OOXMLSignatureFacet(AbstractOOXMLSignatureService signatureService) {
this(signatureService, new LocalClock(), DigestAlgo.SHA1);
}
/**
* Main constructor.
*/
public OOXMLSignatureFacet(AbstractOOXMLSignatureService signatureService, Clock clock, DigestAlgo digestAlgo) {
this.signatureService = signatureService;
this.clock = clock;
this.digestAlgo = digestAlgo;
}
public void preSign(XMLSignatureFactory signatureFactory, Document document, String signatureId,
List<X509Certificate> signingCertificateChain, List<Reference> references, List<XMLObject> 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<Reference> references, List<XMLObject> objects)
throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
Manifest manifest = constructManifest(signatureFactory, document);
String objectId = "idPackageObject"; // really has to be this value.
List<XMLStructure> objectContent = new LinkedList<XMLStructure>();
objectContent.add(manifest);
addSignatureTime(signatureFactory, document, signatureId, objectContent);
objects.add(signatureFactory.newXMLObject(objectContent, objectId, null, null));
DigestMethod digestMethod = signatureFactory.newDigestMethod(this.digestAlgo.getXmlAlgoId(), 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<Reference> manifestReferences = new LinkedList<Reference>();
try {
addManifestReferences(signatureFactory, document, manifestReferences);
} catch (Exception e) {
throw new RuntimeException("error: " + e.getMessage(), e);
}
return signatureFactory.newManifest(manifestReferences);
}
private void addManifestReferences(XMLSignatureFactory signatureFactory, Document document,
List<Reference> manifestReferences)
throws IOException, JAXBException, NoSuchAlgorithmException, InvalidAlgorithmParameterException {
CTTypes contentTypes = getContentTypes();
List<String> relsEntryNames = getRelsEntryNames();
DigestMethod digestMethod = signatureFactory.newDigestMethod(this.digestAlgo.getXmlAlgoId(), null);
Set<String> digestedPartNames = new HashSet<String>();
for (String relsEntryName : relsEntryNames) {
CTRelationships relationships = getRelationships(relsEntryName);
List<CTRelationship> relationshipList = relationships.getRelationship();
RelationshipTransformParameterSpec parameterSpec = new RelationshipTransformParameterSpec();
for (CTRelationship relationship : relationshipList) {
String relationshipType = relationship.getType();
STTargetMode targetMode = relationship.getTargetMode();
if (null != targetMode) {
LOG.debug("TargetMode: " + targetMode.name());
if (targetMode == STTargetMode.EXTERNAL) {
/*
* ECMA-376 Part 2 - 3rd edition
*
* 13.2.4.16 Manifest Element
*
* "The producer shall not create a Manifest element that references any data outside of the package."
*/
continue;
}
}
if (false == OOXMLSignatureFacet.isSignedRelationship(relationshipType)) {
continue;
}
String baseUri = "/" + relsEntryName.substring(0, relsEntryName.indexOf("_rels/"));
String relationshipTarget = relationship.getTarget();
String partName = FilenameUtils.separatorsToUnix(FilenameUtils.normalize(baseUri + relationshipTarget));
LOG.debug("part name: " + partName);
String relationshipId = relationship.getId();
parameterSpec.addRelationshipReference(relationshipId);
String contentType = getContentType(contentTypes, partName);
if (relationshipType.endsWith("customXml")) {
if (false == contentType.equals("inkml+xml") && false == contentType.equals("text/xml")) {
LOG.debug("skipping customXml with content type: " + contentType);
continue;
}
}
if (false == digestedPartNames.contains(partName)) {
/*
* We only digest a part once.
*/
Reference reference = signatureFactory.newReference(partName + "?ContentType=" + contentType,
digestMethod);
manifestReferences.add(reference);
digestedPartNames.add(partName);
}
}
if (false == parameterSpec.getSourceIds().isEmpty()) {
List<Transform> transforms = new LinkedList<Transform>();
transforms
.add(signatureFactory.newTransform(RelationshipTransformService.TRANSFORM_URI, parameterSpec));
transforms.add(
signatureFactory.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null));
Reference reference = signatureFactory.newReference(
"/" + relsEntryName + "?ContentType=application/vnd.openxmlformats-package.relationships+xml",
digestMethod, transforms, null, null);
manifestReferences.add(reference);
}
}
}
/**
* According to ECMA-376, Part 2. 10.1.2 Mapping Content Types.
*
* @param contentTypes
* @param partName
* @return
*/
private String getContentType(CTTypes contentTypes, String partName) {
List<Object> defaultOrOverrideList = contentTypes.getDefaultOrOverride();
for (Object defaultOrOverride : defaultOrOverrideList) {
if (defaultOrOverride instanceof CTOverride) {
CTOverride override = (CTOverride) defaultOrOverride;
if (partName.equals(override.getPartName())) {
return override.getContentType();
}
}
}
for (Object defaultOrOverride : defaultOrOverrideList) {
if (defaultOrOverride instanceof CTDefault) {
CTDefault ctDefault = (CTDefault) defaultOrOverride;
if (partName.endsWith(ctDefault.getExtension())) {
return ctDefault.getContentType();
}
}
}
return null;
}
private CTRelationships getRelationships(String relsEntryName) throws IOException, JAXBException {
URL ooxmlUrl = this.signatureService.getOfficeOpenXMLDocumentURL();
ZipInputStream zipInputStream = new ZipInputStream(ooxmlUrl.openStream());
ZipEntry zipEntry;
InputStream relationshipsInputStream = null;
while (null != (zipEntry = zipInputStream.getNextEntry())) {
if (false == relsEntryName.equals(zipEntry.getName())) {
continue;
}
relationshipsInputStream = zipInputStream;
break;
}
if (null == relationshipsInputStream) {
return null;
}
JAXBContext jaxbContext = JAXBContext
.newInstance(be.fedict.eid.applet.service.signer.jaxb.opc.relationships.ObjectFactory.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
JAXBElement<CTRelationships> relationshipsElement = (JAXBElement<CTRelationships>) unmarshaller
.unmarshal(relationshipsInputStream);
return relationshipsElement.getValue();
}
private CTTypes getContentTypes() throws IOException, JAXBException {
URL ooxmlUrl = this.signatureService.getOfficeOpenXMLDocumentURL();
ZipInputStream zipInputStream = new ZipInputStream(ooxmlUrl.openStream());
ZipEntry zipEntry;
InputStream contentTypesInputStream = null;
while (null != (zipEntry = zipInputStream.getNextEntry())) {
if (!"[Content_Types].xml".equals(zipEntry.getName())) {
continue;
}
contentTypesInputStream = zipInputStream;
break;
}
if (null == contentTypesInputStream) {
return null;
}
JAXBContext jaxbContext = JAXBContext
.newInstance(be.fedict.eid.applet.service.signer.jaxb.opc.contenttypes.ObjectFactory.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
JAXBElement<CTTypes> contentTypesElement = (JAXBElement<CTTypes>) unmarshaller
.unmarshal(contentTypesInputStream);
return contentTypesElement.getValue();
}
private List<String> getRelsEntryNames() throws IOException {
List<String> relsEntryNames = new LinkedList<String>();
URL ooxmlUrl = this.signatureService.getOfficeOpenXMLDocumentURL();
InputStream inputStream = ooxmlUrl.openStream();
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while (null != (zipEntry = zipInputStream.getNextEntry())) {
String zipEntryName = zipEntry.getName();
if (false == zipEntryName.endsWith(".rels")) {
continue;
}
relsEntryNames.add(zipEntryName);
}
return relsEntryNames;
}
private void addSignatureTime(XMLSignatureFactory signatureFactory, Document document, String signatureId,
List<XMLStructure> objectContent) {
/*
* SignatureTime
*/
Element signatureTimeElement = document.createElementNS(OOXML_DIGSIG_NS, "mdssi:SignatureTime");
signatureTimeElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:mdssi", OOXML_DIGSIG_NS);
Element formatElement = document.createElementNS(OOXML_DIGSIG_NS, "mdssi:Format");
formatElement.setTextContent("YYYY-MM-DDThh:mm:ssTZD");
signatureTimeElement.appendChild(formatElement);
Element valueElement = document.createElementNS(OOXML_DIGSIG_NS, "mdssi:Value");
Date now = this.clock.getTime();
DateTime dateTime = new DateTime(now.getTime(), DateTimeZone.UTC);
DateTimeFormatter fmt = ISODateTimeFormat.dateTimeNoMillis();
String nowStr = fmt.print(dateTime);
LOG.debug("now: " + nowStr);
valueElement.setTextContent(nowStr);
signatureTimeElement.appendChild(valueElement);
List<XMLStructure> signatureTimeContent = new LinkedList<XMLStructure>();
signatureTimeContent.add(new DOMStructure(signatureTimeElement));
SignatureProperty signatureTimeSignatureProperty = signatureFactory.newSignatureProperty(signatureTimeContent,
"#" + signatureId, "idSignatureTime");
List<SignatureProperty> signaturePropertyContent = new LinkedList<SignatureProperty>();
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<Reference> references, List<XMLObject> objects)
throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
List<XMLStructure> objectContent = new LinkedList<XMLStructure>();
Element signatureInfoElement = document.createElementNS(OFFICE_DIGSIG_NS, "SignatureInfoV1");
signatureInfoElement.setAttributeNS(Constants.NamespaceSpecNS, "xmlns", OFFICE_DIGSIG_NS);
Element manifestHashAlgorithmElement = document.createElementNS(OFFICE_DIGSIG_NS, "ManifestHashAlgorithm");
manifestHashAlgorithmElement.setTextContent("http://www.w3.org/2000/09/xmldsig#sha1");
signatureInfoElement.appendChild(manifestHashAlgorithmElement);
List<XMLStructure> signatureInfoContent = new LinkedList<XMLStructure>();
signatureInfoContent.add(new DOMStructure(signatureInfoElement));
SignatureProperty signatureInfoSignatureProperty = signatureFactory.newSignatureProperty(signatureInfoContent,
"#" + signatureId, "idOfficeV1Details");
List<SignatureProperty> signaturePropertyContent = new LinkedList<SignatureProperty>();
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(this.digestAlgo.getXmlAlgoId(), null);
Reference reference = signatureFactory.newReference("#" + objectId, digestMethod, null,
"http://www.w3.org/2000/09/xmldsig#Object", null);
references.add(reference);
}
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;
}
public 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();
return documentBuilder.parse(inputSource);
}
public void postSign(Element signatureElement, List<X509Certificate> signingCertificateChain) {
// empty
}
public static String getRelationshipReferenceURI(String zipEntryName) {
return "/" + zipEntryName + "?ContentType=application/vnd.openxmlformats-package.relationships+xml";
}
public static String getResourceReferenceURI(String resourceName, String contentType) {
return "/" + resourceName + "?ContentType=" + contentType;
}
public static String[] contentTypes = {
/*
* Word
*/
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml",
"application/vnd.openxmlformats-officedocument.theme+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml",
"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml",
/*
* Word 2010
*/
"application/vnd.ms-word.stylesWithEffects+xml",
/*
* Excel
*/
"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
/*
* Powerpoint
*/
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml",
"application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml",
"application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml",
"application/vnd.openxmlformats-officedocument.presentationml.slide+xml",
"application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml",
/*
* Powerpoint 2010
*/
"application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml",
"application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" };
public static boolean isSignedRelationship(String relationshipType) {
LOG.debug("relationship type: " + relationshipType);
for (String signedTypeExtension : signed) {
if (relationshipType.endsWith(signedTypeExtension)) {
return true;
}
}
if (relationshipType.endsWith("customXml")) {
LOG.debug("customXml relationship type");
return true;
}
return false;
}
/**
* Office 2010 list of signed types (extensions).
*/
public static String[] signed = { "powerPivotData", //
"activeXControlBinary", //
"attachedToolbars", //
"connectorXml", //
"downRev", //
"functionPrototypes", //
"graphicFrameDoc", //
"groupShapeXml", //
"ink", //
"keyMapCustomizations", //
"legacyDiagramText", //
"legacyDocTextInfo", //
"officeDocument", //
"pictureXml", //
"shapeXml", //
"smartTags", //
"ui/altText", //
"ui/buttonSize", //
"ui/controlID", //
"ui/description", //
"ui/enabled", //
"ui/extensibility", //
"ui/helperText", //
"ui/imageID", //
"ui/imageMso", //
"ui/keyTip", //
"ui/label", //
"ui/lcid", //
"ui/loud", //
"ui/pressed", //
"ui/progID", //
"ui/ribbonID", //
"ui/showImage", //
"ui/showLabel", //
"ui/supertip", //
"ui/target", //
"ui/text", //
"ui/title", //
"ui/tooltip", //
"ui/userCustomization", //
"ui/visible", //
"userXmlData", //
"vbaProject", //
"wordVbaData", //
"wsSortMap", //
"xlBinaryIndex", //
"xlExternalLinkPath/xlAlternateStartup", //
"xlExternalLinkPath/xlLibrary", //
"xlExternalLinkPath/xlPathMissing", //
"xlExternalLinkPath/xlStartup", //
"xlIntlMacrosheet", //
"xlMacrosheet", //
"customData", //
"diagramDrawing", //
"hdphoto", //
"inkXml", //
"media", //
"slicer", //
"slicerCache", //
"stylesWithEffects", //
"ui/extensibility", //
"chartColorStyle", //
"chartLayout", //
"chartStyle", //
"dictionary", //
"timeline", //
"timelineCache", //
"aFChunk", //
"attachedTemplate", //
"audio", //
"calcChain", //
"chart", //
"chartsheet", //
"chartUserShapes", //
"commentAuthors", //
"comments", //
"connections", //
"control", //
"customProperty", //
"customXml", //
"diagramColors", //
"diagramData", //
"diagramLayout", //
"diagramQuickStyle", //
"dialogsheet", //
"drawing", //
"endnotes", //
"externalLink", //
"externalLinkPath", //
"font", //
"fontTable", //
"footer", //
"footnotes", //
"glossaryDocument", //
"handoutMaster", //
"header", //
"hyperlink", //
"image", //
"mailMergeHeaderSource", //
"mailMergeRecipientData", //
"mailMergeSource", //
"notesMaster", //
"notesSlide", //
"numbering", //
"officeDocument", //
"oleObject", //
"package", //
"pivotCacheDefinition", //
"pivotCacheRecords", //
"pivotTable", //
"presProps", //
"printerSettings", //
"queryTable", //
"recipientData", //
"settings", //
"sharedStrings", //
"sheetMetadata", //
"slide", //
"slideLayout", //
"slideMaster", //
"slideUpdateInfo", //
"slideUpdateUrl", //
"styles", //
"table", //
"tableSingleCells", //
"tableStyles", //
"tags", //
"theme", //
"themeOverride", //
"transform", //
"video", //
"viewProps", //
"volatileDependencies", //
"webSettings", //
"worksheet", //
"xmlMaps", //
"ctrlProp", //
"customData", //
"diagram", //
"diagramColorsHeader", //
"diagramLayoutHeader", //
"diagramQuickStyleHeader", //
"documentParts", //
"slicer", //
"slicerCache", //
"vmlDrawing" //
};
}