From da4201c2cd4c7b2cc89a86e7be1b758232c4fa84 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Sun, 28 Sep 2014 22:30:02 +0000 Subject: [PATCH] more tests, some refactoring git-svn-id: https://svn.apache.org/repos/asf/poi/branches/xml_signature@1628107 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/poifs/crypt/HashAlgorithm.java | 30 +++-- .../poifs/crypt/dsig/KeyInfoKeySelector.java | 36 +++--- .../poi/poifs/crypt/dsig/SignatureConfig.java | 96 +++++++++++++-- .../poi/poifs/crypt/dsig/SignatureInfo.java | 77 ++---------- .../dsig/facets/EnvelopedSignatureFacet.java | 13 +- .../dsig/facets/OOXMLSignatureFacet.java | 11 +- .../dsig/facets/XAdESSignatureFacet.java | 8 +- .../poi/poifs/crypt/TestSignatureInfo.java | 114 +++++++++++++++++- test-data/xmldsign/chaintest.pfx | Bin 0 -> 10416 bytes 9 files changed, 261 insertions(+), 124 deletions(-) create mode 100644 test-data/xmldsign/chaintest.pfx diff --git a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java index 8f2efc2f7..6a490b014 100644 --- a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java +++ b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java @@ -17,24 +17,24 @@ package org.apache.poi.poifs.crypt; -import javax.xml.crypto.dsig.DigestMethod; - import org.apache.poi.EncryptedDocumentException; public enum HashAlgorithm { - none ( "", 0x0000, "", 0, "", null, false), - sha1 ( "SHA-1", 0x8004, "SHA1", 20, "HmacSHA1", DigestMethod.SHA1, false), - sha256 ( "SHA-256", 0x800C, "SHA256", 32, "HmacSHA256", DigestMethod.SHA256, false), - sha384 ( "SHA-384", 0x800D, "SHA384", 48, "HmacSHA384", null, false), - sha512 ( "SHA-512", 0x800E, "SHA512", 64, "HmacSHA512", DigestMethod.SHA512, false), + none ( "", 0x0000, "", 0, "", false), + sha1 ( "SHA-1", 0x8004, "SHA1", 20, "HmacSHA1", false), + sha256 ( "SHA-256", 0x800C, "SHA256", 32, "HmacSHA256", false), + sha384 ( "SHA-384", 0x800D, "SHA384", 48, "HmacSHA384", false), + sha512 ( "SHA-512", 0x800E, "SHA512", 64, "HmacSHA512", false), /* only for agile encryption */ - md5 ( "MD5", -1, "MD5", 16, "HmacMD5", null, false), + md5 ( "MD5", -1, "MD5", 16, "HmacMD5", false), // although sunjc2 supports md2, hmac-md2 is only supported by bouncycastle - md2 ( "MD2", -1, "MD2", 16, "Hmac-MD2", null, true), - md4 ( "MD4", -1, "MD4", 16, "Hmac-MD4", null, true), - ripemd128("RipeMD128", -1, "RIPEMD-128", 16, "HMac-RipeMD128", null, true), - ripemd160("RipeMD160", -1, "RIPEMD-160", 20, "HMac-RipeMD160", DigestMethod.RIPEMD160, true), - whirlpool("Whirlpool", -1, "WHIRLPOOL", 64, "HMac-Whirlpool", null, true), + md2 ( "MD2", -1, "MD2", 16, "Hmac-MD2", true), + md4 ( "MD4", -1, "MD4", 16, "Hmac-MD4", true), + ripemd128("RipeMD128", -1, "RIPEMD-128", 16, "HMac-RipeMD128", true), + ripemd160("RipeMD160", -1, "RIPEMD-160", 20, "HMac-RipeMD160", true), + whirlpool("Whirlpool", -1, "WHIRLPOOL", 64, "HMac-Whirlpool", true), + // only for xml signing + sha224 ( "SHA-224", -1, "SHA224", 28, "HmacSHA224", true); ; public final String jceId; @@ -42,16 +42,14 @@ public enum HashAlgorithm { public final String ecmaString; public final int hashSize; public final String jceHmacId; - public final String xmlSignUri; public final boolean needsBouncyCastle; - HashAlgorithm(String jceId, int ecmaId, String ecmaString, int hashSize, String jceHmacId, String xmlSignUri, boolean needsBouncyCastle) { + HashAlgorithm(String jceId, int ecmaId, String ecmaString, int hashSize, String jceHmacId, boolean needsBouncyCastle) { this.jceId = jceId; this.ecmaId = ecmaId; this.ecmaString = ecmaString; this.hashSize = hashSize; this.jceHmacId = jceHmacId; - this.xmlSignUri = xmlSignUri; this.needsBouncyCastle = needsBouncyCastle; } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java index c24c36fc4..61fedcb9e 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java @@ -26,6 +26,7 @@ package org.apache.poi.poifs.crypt.dsig; import java.security.Key; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.List; import javax.xml.crypto.AlgorithmMethod; @@ -48,7 +49,7 @@ public class KeyInfoKeySelector extends KeySelector implements KeySelectorResult private static final POILogger LOG = POILogFactory.getLogger(KeyInfoKeySelector.class); - private X509Certificate certificate; + private List certChain = new ArrayList(); @SuppressWarnings("unchecked") @Override @@ -58,35 +59,31 @@ public class KeyInfoKeySelector extends KeySelector implements KeySelectorResult throw new KeySelectorException("no ds:KeyInfo present"); } List keyInfoContent = keyInfo.getContent(); - this.certificate = null; + certChain.clear(); for (XMLStructure keyInfoStructure : keyInfoContent) { - if (false == (keyInfoStructure instanceof X509Data)) { + if (!(keyInfoStructure instanceof X509Data)) { continue; } X509Data x509Data = (X509Data) keyInfoStructure; List x509DataList = x509Data.getContent(); for (Object x509DataObject : x509DataList) { - if (false == (x509DataObject instanceof X509Certificate)) { + if (!(x509DataObject instanceof X509Certificate)) { continue; } X509Certificate certificate = (X509Certificate) x509DataObject; LOG.log(POILogger.DEBUG, "certificate", certificate.getSubjectX500Principal()); - if (null == this.certificate) { - /* - * The first certificate is presumably the signer. - */ - this.certificate = certificate; - } - } - if (null != this.certificate) { - return this; + certChain.add(certificate); } } - throw new KeySelectorException("No key found!"); + if (certChain.isEmpty()) { + throw new KeySelectorException("No key found!"); + } + return this; } public Key getKey() { - return this.certificate.getPublicKey(); + // The first certificate is presumably the signer. + return certChain.isEmpty() ? null : certChain.get(0).getPublicKey(); } /** @@ -95,7 +92,12 @@ public class KeyInfoKeySelector extends KeySelector implements KeySelectorResult * * @return */ - public X509Certificate getCertificate() { - return this.certificate; + public X509Certificate getSigner() { + // The first certificate is presumably the signer. + return certChain.isEmpty() ? null : certChain.get(0); + } + + public List getCertChain() { + return certChain; } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java index d60753e47..7c59fbcae 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java @@ -31,6 +31,7 @@ import java.util.UUID; import javax.xml.crypto.URIDereferencer; import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.openxml4j.opc.OPCPackage; @@ -87,7 +88,10 @@ public class SignatureConfig { // timestamp service provider URL private String tspUrl; private boolean tspOldProtocol = false; - private HashAlgorithm tspDigestAlgo = HashAlgorithm.sha1; + /** + * if not defined, it's the same as the main digest + */ + private HashAlgorithm tspDigestAlgo = null; private String tspUser; private String tspPass; private TimeStampServiceValidator tspValidator; @@ -103,7 +107,10 @@ public class SignatureConfig { * When null the signature will be limited to XAdES-T only. */ private RevocationDataService revocationDataService; - private HashAlgorithm xadesDigestAlgo = HashAlgorithm.sha1; + /** + * if not defined, it's the same as the main digest + */ + private HashAlgorithm xadesDigestAlgo = null; private String xadesRole = null; private String xadesSignatureId = null; private boolean xadesSignaturePolicyImplied = true; @@ -290,9 +297,7 @@ public class SignatureConfig { return packageSignatureId; } public void setPackageSignatureId(String packageSignatureId) { - this.packageSignatureId = (packageSignatureId != null) - ? packageSignatureId - : "xmldsig-" + UUID.randomUUID(); + this.packageSignatureId = nvl(packageSignatureId,"xmldsig-"+UUID.randomUUID()); } public String getTspUrl() { return tspUrl; @@ -307,7 +312,7 @@ public class SignatureConfig { this.tspOldProtocol = tspOldProtocol; } public HashAlgorithm getTspDigestAlgo() { - return tspDigestAlgo; + return nvl(tspDigestAlgo,digestAlgo); } public void setTspDigestAlgo(HashAlgorithm tspDigestAlgo) { this.tspDigestAlgo = tspDigestAlgo; @@ -349,7 +354,7 @@ public class SignatureConfig { this.revocationDataService = revocationDataService; } public HashAlgorithm getXadesDigestAlgo() { - return xadesDigestAlgo; + return nvl(xadesDigestAlgo,digestAlgo); } public void setXadesDigestAlgo(HashAlgorithm xadesDigestAlgo) { this.xadesDigestAlgo = xadesDigestAlgo; @@ -420,4 +425,81 @@ public class SignatureConfig { public void setNamespacePrefixes(Map namespacePrefixes) { this.namespacePrefixes = namespacePrefixes; } + protected static T nvl(T value, T defaultValue) { + return value == null ? defaultValue : value; + } + public byte[] getHashMagic() { + // see https://www.ietf.org/rfc/rfc3110.txt + // RSA/SHA1 SIG Resource Records + byte result[]; + switch (getDigestAlgo()) { + case sha1: result = new byte[] + { 0x30, 0x1f, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x0e + , 0x03, 0x02, 0x1a, 0x04, 0x14 }; + break; + case sha224: result = new byte[] + { 0x30, 0x2b, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x04, 0x1c }; + break; + case sha256: result = new byte[] + { 0x30, 0x2f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x04, 0x20 }; + break; + case sha384: result = new byte[] + { 0x30, 0x3f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x04, 0x30 }; + break; + case sha512: result = new byte[] + { 0x30, 0x4f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 + , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x04, 0x40 }; + break; + case ripemd128: result = new byte[] + { 0x30, 0x1b, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24 + , 0x03, 0x02, 0x02, 0x04, 0x10 }; + break; + case ripemd160: result = new byte[] + { 0x30, 0x1f, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24 + , 0x03, 0x02, 0x01, 0x04, 0x14 }; + break; + // case ripemd256: result = new byte[] + // { 0x30, 0x2b, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24 + // , 0x03, 0x02, 0x03, 0x04, 0x20 }; + // break; + default: throw new EncryptedDocumentException("Hash algorithm " + +getDigestAlgo()+" not supported for signing."); + } + + return result; + } + + public String getSignatureMethod() { + switch (getDigestAlgo()) { + case sha1: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1; + case sha224: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA224; + case sha256: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256; + case sha384: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA384; + case sha512: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512; + case ripemd160: return org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_RIPEMD160; + default: throw new EncryptedDocumentException("Hash algorithm " + +getDigestAlgo()+" not supported for signing."); + } + } + + public String getDigestMethodUri() { + return getDigestMethodUri(getDigestAlgo()); + } + + public static String getDigestMethodUri(HashAlgorithm digestAlgo) { + switch (digestAlgo) { + case sha1: return DigestMethod.SHA1; + case sha224: return "http://www.w3.org/2001/04/xmldsig-more#sha224"; + case sha256: return DigestMethod.SHA256; + case sha384: return "http://www.w3.org/2001/04/xmldsig-more#sha384"; + case sha512: return DigestMethod.SHA512; + case ripemd160: return DigestMethod.RIPEMD160; + default: throw new EncryptedDocumentException("Hash algorithm " + +digestAlgo+" not supported for signing."); + } + } + } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java index a4006af3a..69a771b40 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java @@ -25,11 +25,6 @@ package org.apache.poi.poifs.crypt.dsig; import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_DIGSIG_NS; -import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_MAC_HMAC_RIPEMD160; -import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1; -import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256; -import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA384; -import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512; import java.io.ByteArrayOutputStream; import java.io.File; @@ -113,36 +108,6 @@ import org.xml.sax.SAXException; public class SignatureInfo implements SignatureConfigurable { - // see https://www.ietf.org/rfc/rfc3110.txt - // RSA/SHA1 SIG Resource Records - public static final byte[] SHA1_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x1f, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x04, 0x14 }; - - public static final byte[] SHA224_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x2b, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 - , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x04, 0x1c }; - - public static final byte[] SHA256_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x2f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 - , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x04, 0x20 }; - - public static final byte[] SHA384_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x3f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 - , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x04, 0x30 }; - - public static final byte[] SHA512_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x4f, 0x30, 0x0b, 0x06, 0x09, 0x60, (byte) 0x86 - , 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x04, 0x40 }; - - public static final byte[] RIPEMD128_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x1b, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24, 0x03, 0x02, 0x02, 0x04, 0x10 }; - - public static final byte[] RIPEMD160_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x1f, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24, 0x03, 0x02, 0x01, 0x04, 0x14 }; - - public static final byte[] RIPEMD256_DIGEST_INFO_PREFIX = new byte[] - { 0x30, 0x2b, 0x30, 0x07, 0x06, 0x05, 0x2b, 0x24, 0x03, 0x02, 0x03, 0x04, 0x20 }; - private static final POILogger LOG = POILogFactory.getLogger(SignatureInfo.class); private static boolean isInitialized = false; @@ -151,6 +116,7 @@ public class SignatureInfo implements SignatureConfigurable { public class SignaturePart { private final PackagePart signaturePart; private X509Certificate signer; + private List certChain; private SignaturePart(PackagePart signaturePart) { this.signaturePart = signaturePart; @@ -164,6 +130,10 @@ public class SignatureInfo implements SignatureConfigurable { return signer; } + public List getCertChain() { + return certChain; + } + public SignatureDocument getSignatureDocument() throws IOException, XmlException { // TODO: check for XXE return SignatureDocument.Factory.parse(signaturePart.getInputStream()); @@ -188,7 +158,8 @@ public class SignatureInfo implements SignatureConfigurable { boolean valid = xmlSignature.validate(domValidateContext); if (valid) { - signer = keySelector.getCertificate(); + signer = keySelector.getSigner(); + certChain = keySelector.getCertChain(); } return valid; @@ -240,7 +211,7 @@ public class SignatureInfo implements SignatureConfigurable { try { ByteArrayOutputStream digestInfoValueBuf = new ByteArrayOutputStream(); - digestInfoValueBuf.write(getHashMagic()); + digestInfoValueBuf.write(signatureConfig.getHashMagic()); digestInfoValueBuf.write(digest); byte[] digestInfoValue = digestInfoValueBuf.toByteArray(); byte[] signatureValue = cipher.doFinal(digestInfoValue); @@ -324,31 +295,6 @@ public class SignatureInfo implements SignatureConfigurable { throw new RuntimeException("JRE doesn't support default xml signature provider - set jsr105Provider system property!"); } - protected byte[] getHashMagic() { - switch (signatureConfig.getDigestAlgo()) { - case sha1: return SHA1_DIGEST_INFO_PREFIX; - // sha224: return SHA224_DIGEST_INFO_PREFIX; - case sha256: return SHA256_DIGEST_INFO_PREFIX; - case sha384: return SHA384_DIGEST_INFO_PREFIX; - case sha512: return SHA512_DIGEST_INFO_PREFIX; - case ripemd128: return RIPEMD128_DIGEST_INFO_PREFIX; - case ripemd160: return RIPEMD160_DIGEST_INFO_PREFIX; - // case ripemd256: return RIPEMD256_DIGEST_INFO_PREFIX; - default: throw new EncryptedDocumentException("Hash algorithm "+signatureConfig.getDigestAlgo()+" not supported for signing."); - } - } - - protected String getSignatureMethod() { - switch (signatureConfig.getDigestAlgo()) { - case sha1: return ALGO_ID_SIGNATURE_RSA_SHA1; - case sha256: return ALGO_ID_SIGNATURE_RSA_SHA256; - case sha384: return ALGO_ID_SIGNATURE_RSA_SHA384; - case sha512: return ALGO_ID_SIGNATURE_RSA_SHA512; - case ripemd160: return ALGO_ID_MAC_HMAC_RIPEMD160; - default: throw new EncryptedDocumentException("Hash algorithm "+signatureConfig.getDigestAlgo()+" not supported for signing."); - } - } - protected static synchronized void initXmlProvider() { if (isInitialized) return; isInitialized = true; @@ -409,8 +355,8 @@ public class SignatureInfo implements SignatureConfigurable { for (DigestInfo digestInfo : safe(digestInfos)) { byte[] documentDigestValue = digestInfo.digestValue; - DigestMethod digestMethod = signatureFactory.newDigestMethod( - digestInfo.hashAlgo.xmlSignUri, null); + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); String uri = new File(digestInfo.description).getName(); @@ -431,7 +377,8 @@ public class SignatureInfo implements SignatureConfigurable { /* * ds:SignedInfo */ - SignatureMethod signatureMethod = signatureFactory.newSignatureMethod(getSignatureMethod(), null); + SignatureMethod signatureMethod = signatureFactory.newSignatureMethod + (signatureConfig.getSignatureMethod(), null); CanonicalizationMethod canonicalizationMethod = signatureFactory .newCanonicalizationMethod(signatureConfig.getCanonicalizationMethod(), (C14NMethodParameterSpec) null); diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/EnvelopedSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/EnvelopedSignatureFacet.java index c96e932a5..b9c743548 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/EnvelopedSignatureFacet.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/EnvelopedSignatureFacet.java @@ -42,16 +42,15 @@ public class EnvelopedSignatureFacet implements SignatureFacet { , List references , List objects) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { - DigestMethod digestMethod = signatureFactory.newDigestMethod(signatureConfig.getDigestAlgo().xmlSignUri, null); + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); List transforms = new ArrayList(); - Transform envelopedTransform = signatureFactory - .newTransform(CanonicalizationMethod.ENVELOPED, - (TransformParameterSpec) null); + Transform envelopedTransform = signatureFactory.newTransform + (CanonicalizationMethod.ENVELOPED, (TransformParameterSpec) null); transforms.add(envelopedTransform); - Transform exclusiveTransform = signatureFactory - .newTransform(CanonicalizationMethod.EXCLUSIVE, - (TransformParameterSpec) null); + Transform exclusiveTransform = signatureFactory.newTransform + (CanonicalizationMethod.EXCLUSIVE, (TransformParameterSpec) null); transforms.add(exclusiveTransform); Reference reference = signatureFactory.newReference("", digestMethod, diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java index b30c07764..28626e827 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java @@ -123,7 +123,8 @@ public class OOXMLSignatureFacet implements SignatureFacet { XMLObject xo = signatureFactory.newXMLObject(objectContent, objectId, null, null); objects.add(xo); - DigestMethod digestMethod = signatureFactory.newDigestMethod(signatureConfig.getDigestAlgo().xmlSignUri, null); + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); Reference reference = signatureFactory.newReference ("#" + objectId, digestMethod, null, XML_DIGSIG_NS+"Object", null); references.add(reference); @@ -136,7 +137,8 @@ public class OOXMLSignatureFacet implements SignatureFacet { OPCPackage ooxml = signatureConfig.getOpcPackage(); List relsEntryNames = ooxml.getPartsByContentType(ContentTypes.RELATIONSHIPS_PART); - DigestMethod digestMethod = signatureFactory.newDigestMethod(signatureConfig.getDigestAlgo().xmlSignUri, null); + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); Set digestedPartNames = new HashSet(); for (PackagePart pp : relsEntryNames) { String baseUri = pp.getPartName().getName().replaceFirst("(.*)/_rels/.*", "$1"); @@ -252,7 +254,7 @@ public class OOXMLSignatureFacet implements SignatureFacet { SignatureInfoV1Document sigV1 = SignatureInfoV1Document.Factory.newInstance(); CTSignatureInfoV1 ctSigV1 = sigV1.addNewSignatureInfoV1(); - ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestAlgo().xmlSignUri); + ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri()); Element n = (Element)document.importNode(ctSigV1.getDomNode(), true); n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS); @@ -271,7 +273,8 @@ public class OOXMLSignatureFacet implements SignatureFacet { String objectId = "idOfficeObject"; objects.add(signatureFactory.newXMLObject(objectContent, objectId, null, null)); - DigestMethod digestMethod = signatureFactory.newDigestMethod(signatureConfig.getDigestAlgo().xmlSignUri, null); + DigestMethod digestMethod = signatureFactory.newDigestMethod + (signatureConfig.getDigestMethodUri(), null); Reference reference = signatureFactory.newReference ("#" + objectId, digestMethod, null, XML_DIGSIG_NS+"Object", null); references.add(reference); diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java index 576fa9f51..d34b367dd 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java @@ -213,7 +213,7 @@ public class XAdESSignatureFacet implements SignatureFacet { objects.add(xadesObject); // add XAdES ds:Reference - DigestMethod digestMethod = signatureFactory.newDigestMethod(signatureConfig.getDigestAlgo().xmlSignUri, null); + DigestMethod digestMethod = signatureFactory.newDigestMethod(signatureConfig.getDigestMethodUri(), null); List transforms = new ArrayList(); Transform exclusiveTransform = signatureFactory .newTransform(CanonicalizationMethod.INCLUSIVE, @@ -236,11 +236,11 @@ public class XAdESSignatureFacet implements SignatureFacet { protected static void setDigestAlgAndValue( DigestAlgAndValueType digestAlgAndValue, byte[] data, - HashAlgorithm hashAlgo) { + HashAlgorithm digestAlgo) { DigestMethodType digestMethod = digestAlgAndValue.addNewDigestMethod(); - digestMethod.setAlgorithm(hashAlgo.xmlSignUri); + digestMethod.setAlgorithm(SignatureConfig.getDigestMethodUri(digestAlgo)); - MessageDigest messageDigest = CryptoFunctions.getMessageDigest(hashAlgo); + MessageDigest messageDigest = CryptoFunctions.getMessageDigest(digestAlgo); byte[] digestValue = messageDigest.digest(data); digestAlgAndValue.setDigestValue(digestValue); } diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java index 77299c937..4444abe89 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java @@ -24,6 +24,7 @@ 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; @@ -31,6 +32,8 @@ 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.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; @@ -68,6 +71,7 @@ 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; @@ -207,6 +211,35 @@ public class TestSignatureInfo { 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"); @@ -321,7 +354,7 @@ public class TestSignatureInfo { "$this/ds:Signature/ds:SignedInfo/ds:Reference"; for (ReferenceType rt : (ReferenceType[])sigDoc.selectPath(digestValXQuery)) { assertNotNull(rt.getDigestValue()); - assertEquals(HashAlgorithm.sha1.xmlSignUri, rt.getDigestMethod().getAlgorithm()); + assertEquals(signatureConfig.getDigestMethodUri(), rt.getDigestMethod().getAlgorithm()); } String certDigestXQuery = declareNS + @@ -341,8 +374,83 @@ public class TestSignatureInfo { pkg.close(); } + + @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 certChain = new ArrayList(); + 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()){ + boolean b = sp.validate(); + assertTrue(b); + X509Certificate signer = sp.getSigner(); + assertNotNull("signer undefined?!", signer); + List 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) { + signatureConfig.setDigestAlgo(ha); + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + signatureConfig.setOpcPackage(pkg); + + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(signatureConfig); - private OPCPackage sign(OPCPackage pkgCopy, String alias, String signerDn, int signerCount) throws Exception { + si.confirmSignature(); + boolean b = si.verifySignature(); + pkg.close(); + + assertTrue(b); + } + } + + + private void sign(OPCPackage pkgCopy, String alias, String signerDn, int signerCount) throws Exception { initKeyPair(alias, signerDn); SignatureConfig signatureConfig = new SignatureConfig(); @@ -383,8 +491,6 @@ public class TestSignatureInfo { } } assertEquals(signerCount, result.size()); - - return pkgCopy; } private void initKeyPair(String alias, String subjectDN) throws Exception { diff --git a/test-data/xmldsign/chaintest.pfx b/test-data/xmldsign/chaintest.pfx new file mode 100644 index 0000000000000000000000000000000000000000..e92106d2bbd6210f333e216d7a24aacb3decc844 GIT binary patch literal 10416 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