blob: ded14dfba9662167c25768ae437146b15b1d0458 [file] [log] [blame]
/* ====================================================================
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;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TimeZone;
import org.apache.poi.POIDataSamples;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.poifs.crypt.dsig.DigestInfo;
import org.apache.poi.poifs.crypt.dsig.SignatureConfig;
import org.apache.poi.poifs.crypt.dsig.SignatureInfo;
import org.apache.poi.poifs.crypt.dsig.SignatureInfo.SignaturePart;
import org.apache.poi.poifs.crypt.dsig.facets.EnvelopedSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.facets.KeyInfoSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.facets.XAdESXLSignatureFacet;
import org.apache.poi.poifs.crypt.dsig.services.RevocationData;
import org.apache.poi.poifs.crypt.dsig.services.RevocationDataService;
import org.apache.poi.poifs.crypt.dsig.services.TimeStampService;
import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator;
import org.apache.poi.util.DocumentHelper;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.xmlbeans.XmlObject;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.etsi.uri.x01903.v13.DigestAlgAndValueType;
import org.etsi.uri.x01903.v13.QualifyingPropertiesType;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
import org.w3.x2000.x09.xmldsig.ReferenceType;
import org.w3.x2000.x09.xmldsig.SignatureDocument;
import org.w3c.dom.Document;
public class TestSignatureInfo {
private static final POILogger LOG = POILogFactory.getLogger(TestSignatureInfo.class);
private static final POIDataSamples testdata = POIDataSamples.getXmlDSignInstance();
private static Calendar cal;
private KeyPair keyPair = null;
private X509Certificate x509 = null;
@BeforeClass
public static void initBouncy() throws IOException {
CryptoFunctions.registerBouncyCastle();
/*** TODO : set cal to now ... only set to fixed date for debugging ... */
cal = Calendar.getInstance();
cal.clear();
cal.setTimeZone(TimeZone.getTimeZone("UTC"));
cal.set(2014, 7, 6, 21, 42, 12);
// don't run this test when we are using older Xerces as it triggers an XML Parser backwards compatibility issue
// in the xmlsec jar file
String additionalJar = System.getProperty("additionaljar");
//System.out.println("Having: " + additionalJar);
Assume.assumeTrue("Not running TestSignatureInfo because we are testing with additionaljar set to " + additionalJar,
additionalJar == null || additionalJar.trim().length() == 0);
}
@Test
public void office2007prettyPrintedRels() throws Exception {
OPCPackage pkg = OPCPackage.open(testdata.getFile("office2007prettyPrintedRels.docx"), PackageAccess.READ);
try {
SignatureConfig sic = new SignatureConfig();
sic.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(sic);
boolean isValid = si.verifySignature();
assertTrue(isValid);
} finally {
pkg.close();
}
}
@Test
public void getSignerUnsigned() throws Exception {
String testFiles[] = {
"hello-world-unsigned.docx",
"hello-world-unsigned.pptx",
"hello-world-unsigned.xlsx",
"hello-world-office-2010-technical-preview-unsigned.docx"
};
for (String testFile : testFiles) {
OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ);
SignatureConfig sic = new SignatureConfig();
sic.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(sic);
List<X509Certificate> result = new ArrayList<X509Certificate>();
for (SignaturePart sp : si.getSignatureParts()) {
if (sp.validate()) {
result.add(sp.getSigner());
}
}
pkg.revert();
pkg.close();
assertNotNull(result);
assertTrue(result.isEmpty());
}
}
@Test
public void getSigner() throws Exception {
String testFiles[] = {
"hyperlink-example-signed.docx",
"hello-world-signed.docx",
"hello-world-signed.pptx",
"hello-world-signed.xlsx",
"hello-world-office-2010-technical-preview.docx",
"ms-office-2010-signed.docx",
"ms-office-2010-signed.pptx",
"ms-office-2010-signed.xlsx",
"Office2010-SP1-XAdES-X-L.docx",
"signed.docx",
};
for (String testFile : testFiles) {
OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ);
try {
SignatureConfig sic = new SignatureConfig();
sic.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(sic);
List<X509Certificate> result = new ArrayList<X509Certificate>();
for (SignaturePart sp : si.getSignatureParts()) {
if (sp.validate()) {
result.add(sp.getSigner());
}
}
assertNotNull(result);
assertEquals("test-file: "+testFile, 1, result.size());
X509Certificate signer = result.get(0);
LOG.log(POILogger.DEBUG, "signer: " + signer.getSubjectX500Principal());
boolean b = si.verifySignature();
assertTrue("test-file: "+testFile, b);
pkg.revert();
} finally {
pkg.close();
}
}
}
@Test
public void getMultiSigners() throws Exception {
String testFile = "hello-world-signed-twice.docx";
OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ);
try {
SignatureConfig sic = new SignatureConfig();
sic.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(sic);
List<X509Certificate> result = new ArrayList<X509Certificate>();
for (SignaturePart sp : si.getSignatureParts()) {
if (sp.validate()) {
result.add(sp.getSigner());
}
}
assertNotNull(result);
assertEquals("test-file: "+testFile, 2, result.size());
X509Certificate signer1 = result.get(0);
X509Certificate signer2 = result.get(1);
LOG.log(POILogger.DEBUG, "signer 1: " + signer1.getSubjectX500Principal());
LOG.log(POILogger.DEBUG, "signer 2: " + signer2.getSubjectX500Principal());
boolean b = si.verifySignature();
assertTrue("test-file: "+testFile, b);
pkg.revert();
} finally {
pkg.close();
}
}
@Test
public void testSignSpreadsheet() throws Exception {
String testFile = "hello-world-unsigned.xlsx";
OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE);
sign(pkg, "Test", "CN=Test", 1);
pkg.close();
}
@Test
public void testManipulation() throws Exception {
// sign & validate
String testFile = "hello-world-unsigned.xlsx";
OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE);
sign(pkg, "Test", "CN=Test", 1);
// manipulate
XSSFWorkbook wb = new XSSFWorkbook(pkg);
wb.setSheetName(0, "manipulated");
// ... I don't know, why commit is protected ...
Method m = XSSFWorkbook.class.getDeclaredMethod("commit");
m.setAccessible(true);
m.invoke(wb);
// todo: test a manipulation on a package part, which is not signed
// ... maybe in combination with #56164
// validate
SignatureConfig sic = new SignatureConfig();
sic.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(sic);
boolean b = si.verifySignature();
assertFalse("signature should be broken", b);
wb.close();
}
@Test
public void testSignSpreadsheetWithSignatureInfo() throws Exception {
initKeyPair("Test", "CN=Test");
String testFile = "hello-world-unsigned.xlsx";
OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE);
SignatureConfig sic = new SignatureConfig();
sic.setOpcPackage(pkg);
sic.setKey(keyPair.getPrivate());
sic.setSigningCertificateChain(Collections.singletonList(x509));
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(sic);
// hash > sha1 doesn't work in excel viewer ...
si.confirmSignature();
List<X509Certificate> result = new ArrayList<X509Certificate>();
for (SignaturePart sp : si.getSignatureParts()) {
if (sp.validate()) {
result.add(sp.getSigner());
}
}
assertEquals(1, result.size());
pkg.close();
}
@Test
public void testSignEnvelopingDocument() throws Exception {
String testFile = "hello-world-unsigned.xlsx";
OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE);
initKeyPair("Test", "CN=Test");
final X509CRL crl = PkiTestUtils.generateCrl(x509, keyPair.getPrivate());
// setup
SignatureConfig signatureConfig = new SignatureConfig();
signatureConfig.setOpcPackage(pkg);
signatureConfig.setKey(keyPair.getPrivate());
/*
* We need at least 2 certificates for the XAdES-C complete certificate
* refs construction.
*/
List<X509Certificate> certificateChain = new ArrayList<X509Certificate>();
certificateChain.add(x509);
certificateChain.add(x509);
signatureConfig.setSigningCertificateChain(certificateChain);
signatureConfig.addSignatureFacet(new EnvelopedSignatureFacet());
signatureConfig.addSignatureFacet(new KeyInfoSignatureFacet());
signatureConfig.addSignatureFacet(new XAdESSignatureFacet());
signatureConfig.addSignatureFacet(new XAdESXLSignatureFacet());
// check for internet, no error means it works
boolean mockTsp = (getAccessError("http://timestamp.comodoca.com/rfc3161", true, 10000) == null);
// http://timestamping.edelweb.fr/service/tsp
// http://tsa.belgium.be/connect
// http://timestamp.comodoca.com/authenticode
// http://timestamp.comodoca.com/rfc3161
// http://services.globaltrustfinder.com/adss/tsa
signatureConfig.setTspUrl("http://timestamp.comodoca.com/rfc3161");
signatureConfig.setTspRequestPolicy(null); // comodoca request fails, if default policy is set ...
signatureConfig.setTspOldProtocol(false);
//set proxy info if any
String proxy = System.getProperty("http_proxy");
if (proxy != null && proxy.trim().length() > 0) {
signatureConfig.setProxyUrl(proxy);
}
if (mockTsp) {
TimeStampService tspService = new TimeStampService(){
@Override
public byte[] timeStamp(byte[] data, RevocationData revocationData) throws Exception {
revocationData.addCRL(crl);
return "time-stamp-token".getBytes();
}
@Override
public void setSignatureConfig(SignatureConfig config) {
// empty on purpose
}
};
signatureConfig.setTspService(tspService);
} else {
TimeStampServiceValidator tspValidator = new TimeStampServiceValidator() {
@Override
public void validate(List<X509Certificate> certificateChain,
RevocationData revocationData) throws Exception {
for (X509Certificate certificate : certificateChain) {
LOG.log(POILogger.DEBUG, "certificate: " + certificate.getSubjectX500Principal());
LOG.log(POILogger.DEBUG, "validity: " + certificate.getNotBefore() + " - " + certificate.getNotAfter());
}
}
};
signatureConfig.setTspValidator(tspValidator);
signatureConfig.setTspOldProtocol(signatureConfig.getTspUrl().contains("edelweb"));
}
final RevocationData revocationData = new RevocationData();
revocationData.addCRL(crl);
OCSPResp ocspResp = PkiTestUtils.createOcspResp(x509, false,
x509, x509, keyPair.getPrivate(), "SHA1withRSA", cal.getTimeInMillis());
revocationData.addOCSP(ocspResp.getEncoded());
RevocationDataService revocationDataService = new RevocationDataService(){
@Override
public RevocationData getRevocationData(List<X509Certificate> certificateChain) {
return revocationData;
}
};
signatureConfig.setRevocationDataService(revocationDataService);
// operate
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(signatureConfig);
try {
si.confirmSignature();
} catch (RuntimeException e) {
// only allow a ConnectException because of timeout, we see this in Jenkins from time to time...
assertNotNull("Only allowing ConnectException here, but had: " + e, e.getCause());
if(!(e.getCause() instanceof ConnectException)) {
throw e;
}
assertTrue("Only allowing ConnectException here, but had: " + e, e.getCause().getMessage().contains("timed out"));
}
// verify
Iterator<SignaturePart> spIter = si.getSignatureParts().iterator();
assertTrue(spIter.hasNext());
SignaturePart sp = spIter.next();
boolean valid = sp.validate();
assertTrue(valid);
SignatureDocument sigDoc = sp.getSignatureDocument();
String declareNS =
"declare namespace xades='http://uri.etsi.org/01903/v1.3.2#'; "
+ "declare namespace ds='http://www.w3.org/2000/09/xmldsig#'; ";
String digestValXQuery = declareNS +
"$this/ds:Signature/ds:SignedInfo/ds:Reference";
for (ReferenceType rt : (ReferenceType[])sigDoc.selectPath(digestValXQuery)) {
assertNotNull(rt.getDigestValue());
assertEquals(signatureConfig.getDigestMethodUri(), rt.getDigestMethod().getAlgorithm());
}
String certDigestXQuery = declareNS +
"$this//xades:SigningCertificate/xades:Cert/xades:CertDigest";
XmlObject xoList[] = sigDoc.selectPath(certDigestXQuery);
assertEquals(xoList.length, 1);
DigestAlgAndValueType certDigest = (DigestAlgAndValueType)xoList[0];
assertNotNull(certDigest.getDigestValue());
String qualPropXQuery = declareNS +
"$this/ds:Signature/ds:Object/xades:QualifyingProperties";
xoList = sigDoc.selectPath(qualPropXQuery);
assertEquals(xoList.length, 1);
QualifyingPropertiesType qualProp = (QualifyingPropertiesType)xoList[0];
boolean qualPropXsdOk = qualProp.validate();
assertTrue(qualPropXsdOk);
pkg.close();
}
public static String getAccessError(String destinationUrl, boolean fireRequest, int timeout) {
URL url;
try {
url = new URL(destinationUrl);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid destination URL", e);
}
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
// set specified timeout if non-zero
if(timeout != 0) {
conn.setConnectTimeout(timeout);
conn.setReadTimeout(timeout);
}
conn.setDoOutput(false);
conn.setDoInput(true);
/* if connecting is not possible this will throw a connection refused exception */
conn.connect();
if (fireRequest) {
InputStream is = null;
try {
is = conn.getInputStream();
} finally {
IOUtils.closeQuietly(is);
}
}
/* if connecting is possible we return true here */
return null;
} catch (IOException e) {
/* exception is thrown -> server not available */
return e.getClass().getName() + ": " + e.getMessage();
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
@Test
public void testCertChain() throws Exception {
KeyStore keystore = KeyStore.getInstance("PKCS12");
String password = "test";
InputStream is = testdata.openResourceAsStream("chaintest.pfx");
keystore.load(is, password.toCharArray());
is.close();
Key key = keystore.getKey("poitest", password.toCharArray());
Certificate chainList[] = keystore.getCertificateChain("poitest");
List<X509Certificate> certChain = new ArrayList<X509Certificate>();
for (Certificate c : chainList) {
certChain.add((X509Certificate)c);
}
x509 = certChain.get(0);
keyPair = new KeyPair(x509.getPublicKey(), (PrivateKey)key);
String testFile = "hello-world-unsigned.xlsx";
OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE);
SignatureConfig signatureConfig = new SignatureConfig();
signatureConfig.setKey(keyPair.getPrivate());
signatureConfig.setSigningCertificateChain(certChain);
Calendar cal = Calendar.getInstance();
cal.set(2007, 7, 1);
signatureConfig.setExecutionTime(cal.getTime());
signatureConfig.setDigestAlgo(HashAlgorithm.sha1);
signatureConfig.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(signatureConfig);
si.confirmSignature();
for (SignaturePart sp : si.getSignatureParts()){
assertTrue("Could not validate", sp.validate());
X509Certificate signer = sp.getSigner();
assertNotNull("signer undefined?!", signer);
List<X509Certificate> certChainRes = sp.getCertChain();
assertEquals(3, certChainRes.size());
}
pkg.close();
}
@Test
public void testNonSha1() throws Exception {
String testFile = "hello-world-unsigned.xlsx";
initKeyPair("Test", "CN=Test");
SignatureConfig signatureConfig = new SignatureConfig();
signatureConfig.setKey(keyPair.getPrivate());
signatureConfig.setSigningCertificateChain(Collections.singletonList(x509));
HashAlgorithm testAlgo[] = { HashAlgorithm.sha224, HashAlgorithm.sha256
, HashAlgorithm.sha384, HashAlgorithm.sha512, HashAlgorithm.ripemd160 };
for (HashAlgorithm ha : testAlgo) {
OPCPackage pkg = null;
try {
signatureConfig.setDigestAlgo(ha);
pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE);
signatureConfig.setOpcPackage(pkg);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(signatureConfig);
si.confirmSignature();
boolean b = si.verifySignature();
assertTrue("Signature not correctly calculated for " + ha, b);
} finally {
if (pkg != null) pkg.close();
}
}
}
private void sign(OPCPackage pkgCopy, String alias, String signerDn, int signerCount) throws Exception {
initKeyPair(alias, signerDn);
SignatureConfig signatureConfig = new SignatureConfig();
signatureConfig.setKey(keyPair.getPrivate());
signatureConfig.setSigningCertificateChain(Collections.singletonList(x509));
signatureConfig.setExecutionTime(cal.getTime());
signatureConfig.setDigestAlgo(HashAlgorithm.sha1);
signatureConfig.setOpcPackage(pkgCopy);
SignatureInfo si = new SignatureInfo();
si.setSignatureConfig(signatureConfig);
Document document = DocumentHelper.createDocument();
// operate
DigestInfo digestInfo = si.preSign(document, null);
// verify
assertNotNull(digestInfo);
LOG.log(POILogger.DEBUG, "digest algo: " + digestInfo.hashAlgo);
LOG.log(POILogger.DEBUG, "digest description: " + digestInfo.description);
assertEquals("Office OpenXML Document", digestInfo.description);
assertNotNull(digestInfo.hashAlgo);
assertNotNull(digestInfo.digestValue);
// setup: key material, signature value
byte[] signatureValue = si.signDigest(digestInfo.digestValue);
// operate: postSign
si.postSign(document, signatureValue);
// verify: signature
si.getSignatureConfig().setOpcPackage(pkgCopy);
List<X509Certificate> result = new ArrayList<X509Certificate>();
for (SignaturePart sp : si.getSignatureParts()) {
if (sp.validate()) {
result.add(sp.getSigner());
}
}
assertEquals(signerCount, result.size());
}
private void initKeyPair(String alias, String subjectDN) throws Exception {
final char password[] = "test".toCharArray();
File file = new File("build/test.pfx");
KeyStore keystore = KeyStore.getInstance("PKCS12");
if (file.exists()) {
FileInputStream fis = new FileInputStream(file);
keystore.load(fis, password);
fis.close();
} else {
keystore.load(null, password);
}
if (keystore.isKeyEntry(alias)) {
Key key = keystore.getKey(alias, password);
x509 = (X509Certificate)keystore.getCertificate(alias);
keyPair = new KeyPair(x509.getPublicKey(), (PrivateKey)key);
} else {
keyPair = PkiTestUtils.generateKeyPair();
Calendar cal = Calendar.getInstance();
Date notBefore = cal.getTime();
cal.add(Calendar.YEAR, 1);
Date notAfter = cal.getTime();
KeyUsage keyUsage = new KeyUsage(KeyUsage.digitalSignature);
x509 = PkiTestUtils.generateCertificate(keyPair.getPublic(), subjectDN
, notBefore, notAfter, null, keyPair.getPrivate(), true, 0, null, null, keyUsage);
keystore.setKeyEntry(alias, keyPair.getPrivate(), password, new Certificate[]{x509});
FileOutputStream fos = new FileOutputStream(file);
keystore.store(fos, password);
fos.close();
}
}
private static File copy(File input) throws IOException {
String extension = input.getName().replaceAll(".*?(\\.[^.]+)?$", "$1");
if (extension == null || "".equals(extension)) extension = ".zip";
File tmpFile = new File("build", "sigtest"+extension);
FileOutputStream fos = new FileOutputStream(tmpFile);
FileInputStream fis = new FileInputStream(input);
IOUtils.copy(fis, fos);
fis.close();
fos.close();
return tmpFile;
}
}