From: Andreas Beeker Date: Tue, 6 Mar 2018 00:07:20 +0000 (+0000) Subject: #62159 - Support XML signature over windows certificate store X-Git-Tag: REL_4_0_0_FINAL~217 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=228ed497065c207d376c3c1512926465507eabe8;p=poi.git #62159 - Support XML signature over windows certificate store git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1825948 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java index 0440c78c04..b4618dc0b5 100644 --- a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java +++ b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java @@ -20,55 +20,72 @@ package org.apache.poi.poifs.crypt; import org.apache.poi.EncryptedDocumentException; public enum HashAlgorithm { - 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), + none ( "", 0x0000, "", 0, "", false, ""), + sha1 ( "SHA-1", 0x8004, "SHA1", 20, "HmacSHA1", false, "1.3.14.3.2.26"), + sha256 ( "SHA-256", 0x800C, "SHA256", 32, "HmacSHA256", false, "2.16.840.1.101.3.4.2.1"), + sha384 ( "SHA-384", 0x800D, "SHA384", 48, "HmacSHA384", false, "2.16.840.1.101.3.4.2.2"), + sha512 ( "SHA-512", 0x800E, "SHA512", 64, "HmacSHA512", false, "2.16.840.1.101.3.4.2.3"), /* only for agile encryption */ - md5 ( "MD5", -1, "MD5", 16, "HmacMD5", false), + md5 ( "MD5", -1, "MD5", 16, "HmacMD5", false, "1.2.840.113549.2.5" ), // although sunjc2 supports md2, hmac-md2 is only supported by bouncycastle - 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), + md2 ( "MD2", -1, "MD2", 16, "Hmac-MD2", true, "1.2.840.113549.2.2" ), + md4 ( "MD4", -1, "MD4", 16, "Hmac-MD4", true, "1.2.840.113549.2.4" ), + ripemd128("RipeMD128", -1, "RIPEMD-128", 16, "HMac-RipeMD128", true, "1.3.36.3.2.2"), + ripemd160("RipeMD160", -1, "RIPEMD-160", 20, "HMac-RipeMD160", true, "1.3.36.3.2.1"), + whirlpool("Whirlpool", -1, "WHIRLPOOL", 64, "HMac-Whirlpool", true, "1.0.10118.3.0.55"), // only for xml signing - sha224 ( "SHA-224", -1, "SHA224", 28, "HmacSHA224", true); + sha224 ( "SHA-224", -1, "SHA224", 28, "HmacSHA224", true, "2.16.840.1.101.3.4.2.4"), + ripemd256("RipeMD256", -1, "RIPEMD-256", 32, "HMac-RipeMD256", true, "1.3.36.3.2.3") + ; - public final String jceId; - public final int ecmaId; + /** the id used for initializing the JCE message digest **/ + public final String jceId; + /** the id used for the BIFF encryption info header **/ + public final int ecmaId; + /** the id used for OOXML encryption info header **/ public final String ecmaString; + /** the length of the digest byte array **/ public final int hashSize; + /** the id used for the integrity algorithm in agile encryption **/ public final String jceHmacId; + /** is bouncycastle necessary for calculating the digest **/ public final boolean needsBouncyCastle; + /** ASN1 object identifier of the digest value in combination with the RSA cipher */ + public final String rsaOid; - HashAlgorithm(String jceId, int ecmaId, String ecmaString, int hashSize, String jceHmacId, boolean needsBouncyCastle) { + HashAlgorithm(String jceId, int ecmaId, String ecmaString, int hashSize, String jceHmacId, boolean needsBouncyCastle, String rsaOid) { this.jceId = jceId; this.ecmaId = ecmaId; this.ecmaString = ecmaString; this.hashSize = hashSize; this.jceHmacId = jceHmacId; this.needsBouncyCastle = needsBouncyCastle; + this.rsaOid = rsaOid; } public static HashAlgorithm fromEcmaId(int ecmaId) { for (HashAlgorithm ha : values()) { - if (ha.ecmaId == ecmaId) return ha; + if (ha.ecmaId == ecmaId) { + return ha; + } } throw new EncryptedDocumentException("hash algorithm not found"); } public static HashAlgorithm fromEcmaId(String ecmaString) { for (HashAlgorithm ha : values()) { - if (ha.ecmaString.equals(ecmaString)) return ha; + if (ha.ecmaString.equals(ecmaString)) { + return ha; + } } throw new EncryptedDocumentException("hash algorithm not found"); } public static HashAlgorithm fromString(String string) { for (HashAlgorithm ha : values()) { - if (ha.ecmaString.equalsIgnoreCase(string) || ha.jceId.equalsIgnoreCase(string)) return ha; + if (ha.ecmaString.equalsIgnoreCase(string) || ha.jceId.equalsIgnoreCase(string)) { + return ha; + } } throw new EncryptedDocumentException("hash algorithm not found"); } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/DigestInfo.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/DigestInfo.java deleted file mode 100644 index 3771053a05..0000000000 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/DigestInfo.java +++ /dev/null @@ -1,56 +0,0 @@ -/* ==================================================================== - Licensed to the Apache Software Foundation (ASF) under one or more - contributor license agreements. See the NOTICE file distributed with - this work for additional information regarding copyright ownership. - The ASF licenses this file to You under the Apache License, Version 2.0 - (the "License"); you may not use this file except in compliance with - the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -==================================================================== */ - -/* ==================================================================== - This product contains an ASLv2 licensed version of the OOXML signer - package from the eID Applet project - http://code.google.com/p/eid-applet/source/browse/trunk/README.txt - Copyright (C) 2008-2014 FedICT. - ================================================================= */ - -package org.apache.poi.poifs.crypt.dsig; - -import java.io.Serializable; - -import org.apache.poi.poifs.crypt.HashAlgorithm; - -/** - * Digest Information data transfer class. - */ -public class DigestInfo implements Serializable { - - private static final long serialVersionUID = 1L; - - /** - * Main constructor. - * - * @param digestValue - * @param hashAlgo - * @param description - */ - public DigestInfo(byte[] digestValue, HashAlgorithm hashAlgo, String description) { - this.digestValue = digestValue.clone(); - this.hashAlgo = hashAlgo; - this.description = description; - } - - public final byte[] digestValue; - - public final String description; - - public final HashAlgorithm hashAlgo; -} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/DigestOutputStream.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/DigestOutputStream.java new file mode 100644 index 0000000000..f8ad4a33e5 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/DigestOutputStream.java @@ -0,0 +1,111 @@ +/* ==================================================================== + 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. +==================================================================== */ + +package org.apache.poi.poifs.crypt.dsig; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.PrivateKey; + +import javax.crypto.Cipher; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.poifs.crypt.ChainingMode; +import org.apache.poi.poifs.crypt.CipherAlgorithm; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.Oid; + +/* package */ class DigestOutputStream extends OutputStream { + final HashAlgorithm algo; + final PrivateKey key; + private MessageDigest md; + + DigestOutputStream(final HashAlgorithm algo, final PrivateKey key) { + this.algo = algo; + this.key = key; + } + + public void init() throws GeneralSecurityException { + if (isMSCapi(key)) { + // see https://stackoverflow.com/questions/39196145 for problems with SunMSCAPI + // and why we can't sign the calculated digest + throw new EncryptedDocumentException( + "Windows keystore entries can't be signed with the "+algo+" hash. Please "+ + "use one digest algorithm of sha1 / sha256 / sha384 / sha512."); + } + md = CryptoFunctions.getMessageDigest(algo); + } + + @Override + public void write(final int b) throws IOException { + md.update((byte)b); + } + + @Override + public void write(final byte[] data, final int off, final int len) throws IOException { + md.update(data, off, len); + } + + public byte[] sign() throws IOException, GeneralSecurityException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(getHashMagic()); + bos.write(md.digest()); + + final Cipher cipher = CryptoFunctions.getCipher(key, CipherAlgorithm.rsa + , ChainingMode.ecb, null, Cipher.ENCRYPT_MODE, "PKCS1Padding"); + return cipher.doFinal(bos.toByteArray()); + } + + static boolean isMSCapi(final PrivateKey key) { + return key != null && key.getClass().getName().contains("mscapi"); + } + + + /** + * Each digest method has its own ASN1 header + * + * @return the ASN1 header bytes for the signatureValue / digestInfo + * + * @see Data encoding + */ + byte[] getHashMagic() { + // in an earlier release the hashMagic (aka DigestAlgorithmIdentifier) contained only + // an object identifier, but to conform with the header generated by the + // javax-signature API, the empty are also included + try { + final byte[] oidBytes = new Oid(algo.rsaOid).getDER(); + + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(0x30); + bos.write(algo.hashSize+oidBytes.length+6); + bos.write(0x30); + bos.write(oidBytes.length+2); + bos.write(oidBytes); + bos.write(new byte[] {5,0,4}); + bos.write(algo.hashSize); + + return bos.toByteArray(); + } catch (GSSException|IOException e) { + throw new IllegalStateException(e); + } + } +} 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 85df30b3fd..f3184ab09f 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 @@ -186,7 +186,9 @@ public class SignatureConfig { namespacePrefixes.put(XADES_132_NS, "xd"); } - if (onlyValidation) return; + if (onlyValidation) { + return; + } if (signatureMarshalListener == null) { signatureMarshalListener = new SignatureMarshalListener(); @@ -711,55 +713,6 @@ public class SignatureConfig { return value == null ? defaultValue : value; } - /** - * Each digest method has its own IV (initial vector) - * - * @return the IV depending on the main digest method - */ - 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; - } - /** * @return the uri for the signature method, i.e. currently only rsa is * supported, so it's the rsa variant of the main digest @@ -785,7 +738,10 @@ public class SignatureConfig { } /** - * @param digestAlgo the digest algo, currently only sha* and ripemd160 is supported + * Sets the digest algorithm - currently only sha* and ripemd160 is supported. + * MS Office only supports sha1, sha256, sha384, sha512. + * + * @param digestAlgo the digest algorithm * @return the uri for the given digest */ public static String getDigestMethodUri(HashAlgorithm digestAlgo) { @@ -857,11 +813,15 @@ public class SignatureConfig { if (prov == null) { String dsigProviderNames[] = { System.getProperty("jsr105Provider"), - "org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI", // Santuario xmlsec - "org.jcp.xml.dsig.internal.dom.XMLDSigRI" // JDK xmlsec + // Santuario xmlsec + "org.apache.jcp.xml.dsig.internal.dom.XMLDSigRI", + // JDK xmlsec + "org.jcp.xml.dsig.internal.dom.XMLDSigRI" }; for (String pn : dsigProviderNames) { - if (pn == null) continue; + if (pn == null) { + continue; + } try { prov = (Provider)Class.forName(pn).newInstance(); break; 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 9ea1104700..7c794071cf 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 @@ -18,16 +18,27 @@ /* ==================================================================== 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 + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt Copyright (C) 2008-2014 FedICT. - ================================================================= */ + ================================================================= */ package org.apache.poi.poifs.crypt.dsig; import static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_DIGSIG_NS; -import javax.crypto.Cipher; +import java.io.IOException; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +import javax.xml.bind.DatatypeConverter; import javax.xml.crypto.MarshalException; import javax.xml.crypto.URIDereferencer; import javax.xml.crypto.XMLStructure; @@ -36,38 +47,16 @@ 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.TransformException; import javax.xml.crypto.dsig.XMLObject; -import javax.xml.crypto.dsig.XMLSignContext; -import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignatureException; import javax.xml.crypto.dsig.XMLSignatureFactory; -import javax.xml.crypto.dsig.XMLValidateContext; import javax.xml.crypto.dsig.dom.DOMSignContext; -import javax.xml.crypto.dsig.dom.DOMValidateContext; import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.security.Provider; -import java.security.Security; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; import org.apache.jcp.xml.dsig.internal.dom.DOMReference; import org.apache.jcp.xml.dsig.internal.dom.DOMSignedInfo; +import org.apache.jcp.xml.dsig.internal.dom.DOMSubTreeData; import org.apache.poi.EncryptedDocumentException; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.opc.ContentTypes; @@ -79,9 +68,8 @@ import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; import org.apache.poi.openxml4j.opc.PackagingURIHelper; import org.apache.poi.openxml4j.opc.TargetMode; -import org.apache.poi.poifs.crypt.ChainingMode; -import org.apache.poi.poifs.crypt.CipherAlgorithm; import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable; import org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet; import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService; @@ -90,7 +78,7 @@ import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.xml.security.Init; import org.apache.xml.security.utils.Base64; -import org.apache.xmlbeans.XmlException; +import org.apache.xml.security.utils.XMLUtils; import org.apache.xmlbeans.XmlOptions; import org.w3.x2000.x09.xmldsig.SignatureDocument; import org.w3c.dom.Document; @@ -98,15 +86,14 @@ import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.w3c.dom.events.EventListener; import org.w3c.dom.events.EventTarget; -import org.xml.sax.SAXException; /** *

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

- * + * *

Validating a signed office document

- * + * *
  * OPCPackage pkg = OPCPackage.open(..., PackageAccess.READ);
  * SignatureConfig sic = new SignatureConfig();
@@ -116,9 +103,9 @@ import org.xml.sax.SAXException;
  * boolean isValid = si.validate();
  * ...
  * 
- * + * *

Signing an office document

- * + * *
  * // loading the keystore - pkcs12 is used here, but of course jks & co are also valid
  * // the keystore needs to contain a private key and it's certificate having a
@@ -129,19 +116,19 @@ import org.xml.sax.SAXException;
  * FileInputStream fis = new FileInputStream(file);
  * keystore.load(fis, password);
  * fis.close();
- * 
+ *
  * // extracting private key and certificate
  * String alias = "xyz"; // alias of the keystore entry
  * Key key = keystore.getKey(alias, password);
  * X509Certificate x509 = (X509Certificate)keystore.getCertificate(alias);
- * 
+ *
  * // filling the SignatureConfig entries (minimum fields, more options are available ...)
  * SignatureConfig signatureConfig = new SignatureConfig();
  * signatureConfig.setKey(keyPair.getPrivate());
  * signatureConfig.setSigningCertificateChain(Collections.singletonList(x509));
  * OPCPackage pkg = OPCPackage.open(..., PackageAccess.READ_WRITE);
  * signatureConfig.setOpcPackage(pkg);
- * 
+ *
  * // adding the signature document to the package
  * SignatureInfo si = new SignatureInfo();
  * si.setSignatureConfig(signatureConfig);
@@ -152,14 +139,14 @@ import org.xml.sax.SAXException;
  * // write the changes back to disc
  * pkg.close();
  * 
- * + * *

Implementation notes:

- * + * *

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

- * + * *

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

*