git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1825948 13f79535-47bb-0310-9956-ffa450edef68pull/103/merge
@@ -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"); | |||
} |
@@ -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; | |||
} |
@@ -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 <a href="https://tools.ietf.org/html/rfc2313#section-10.1.2">Data encoding</a> | |||
*/ | |||
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 <associated parameters> 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); | |||
} | |||
} | |||
} |
@@ -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; |
@@ -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; | |||
/** | |||
* <p>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.</p> | |||
* | |||
* | |||
* <p><b>Validating a signed office document</b></p> | |||
* | |||
* | |||
* <pre> | |||
* OPCPackage pkg = OPCPackage.open(..., PackageAccess.READ); | |||
* SignatureConfig sic = new SignatureConfig(); | |||
@@ -116,9 +103,9 @@ import org.xml.sax.SAXException; | |||
* boolean isValid = si.validate(); | |||
* ... | |||
* </pre> | |||
* | |||
* | |||
* <p><b>Signing an office document</b></p> | |||
* | |||
* | |||
* <pre> | |||
* // 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(); | |||
* </pre> | |||
* | |||
* | |||
* <p><b>Implementation notes:</b></p> | |||
* | |||
* | |||
* <p>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.</p> | |||
* | |||
* | |||
* <p>To use SignatureInfo and its sibling classes, you'll need to have the following libs | |||
* in the classpath:</p> | |||
* <ul> | |||
@@ -172,130 +159,17 @@ public class SignatureInfo implements SignatureConfigurable { | |||
private static final POILogger LOG = POILogFactory.getLogger(SignatureInfo.class); | |||
private static boolean isInitialized; | |||
private SignatureConfig signatureConfig; | |||
public class SignaturePart { | |||
private final PackagePart signaturePart; | |||
private X509Certificate signer; | |||
private List<X509Certificate> certChain; | |||
private SignaturePart(PackagePart signaturePart) { | |||
this.signaturePart = signaturePart; | |||
} | |||
/** | |||
* @return the package part containing the signature | |||
*/ | |||
public PackagePart getPackagePart() { | |||
return signaturePart; | |||
} | |||
/** | |||
* @return the signer certificate | |||
*/ | |||
public X509Certificate getSigner() { | |||
return signer; | |||
} | |||
/** | |||
* @return the certificate chain of the signer | |||
*/ | |||
public List<X509Certificate> getCertChain() { | |||
return certChain; | |||
} | |||
/** | |||
* Helper method for examining the xml signature | |||
* | |||
* @return the xml signature document | |||
* @throws IOException if the xml signature doesn't exist or can't be read | |||
* @throws XmlException if the xml signature is malformed | |||
*/ | |||
public SignatureDocument getSignatureDocument() throws IOException, XmlException { | |||
// TODO: check for XXE | |||
return SignatureDocument.Factory.parse(signaturePart.getInputStream(), DEFAULT_XML_OPTIONS); | |||
} | |||
/** | |||
* @return true, when the xml signature is valid, false otherwise | |||
* | |||
* @throws EncryptedDocumentException if the signature can't be extracted or if its malformed | |||
*/ | |||
@SuppressWarnings("unchecked") | |||
public boolean validate() { | |||
KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); | |||
try { | |||
Document doc = DocumentHelper.readDocument(signaturePart.getInputStream()); | |||
XPath xpath = XPathFactory.newInstance().newXPath(); | |||
NodeList nl = (NodeList)xpath.compile("//*[@Id]").evaluate(doc, XPathConstants.NODESET); | |||
final int length = nl.getLength(); | |||
for (int i=0; i<length; i++) { | |||
((Element)nl.item(i)).setIdAttribute("Id", true); | |||
} | |||
DOMValidateContext domValidateContext = new DOMValidateContext(keySelector, doc); | |||
domValidateContext.setProperty("org.jcp.xml.dsig.validateManifests", Boolean.TRUE); | |||
domValidateContext.setURIDereferencer(signatureConfig.getUriDereferencer()); | |||
brokenJvmWorkaround(domValidateContext); | |||
XMLSignatureFactory xmlSignatureFactory = signatureConfig.getSignatureFactory(); | |||
XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); | |||
// TODO: replace with property when xml-sec patch is applied | |||
// workaround added in r1637283 2014-11-07 | |||
for (Reference ref : (List<Reference>)xmlSignature.getSignedInfo().getReferences()) { | |||
SignatureFacet.brokenJvmWorkaround(ref); | |||
} | |||
for (XMLObject xo : (List<XMLObject>)xmlSignature.getObjects()) { | |||
for (XMLStructure xs : (List<XMLStructure>)xo.getContent()) { | |||
if (xs instanceof Manifest) { | |||
for (Reference ref : (List<Reference>)((Manifest)xs).getReferences()) { | |||
SignatureFacet.brokenJvmWorkaround(ref); | |||
} | |||
} | |||
} | |||
} | |||
boolean valid = xmlSignature.validate(domValidateContext); | |||
if (valid) { | |||
signer = keySelector.getSigner(); | |||
certChain = keySelector.getCertChain(); | |||
} | |||
return valid; | |||
} catch (IOException e) { | |||
String s = "error in reading document"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (SAXException e) { | |||
String s = "error in parsing document"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (XPathExpressionException e) { | |||
String s = "error in searching document with xpath expression"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (MarshalException e) { | |||
String s = "error in unmarshalling the signature"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (XMLSignatureException e) { | |||
String s = "error in validating the signature"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} | |||
} | |||
} | |||
/** | |||
* Constructor initializes xml signature environment, if it hasn't been initialized before | |||
*/ | |||
public SignatureInfo() { | |||
initXmlProvider(); | |||
initXmlProvider(); | |||
} | |||
/** | |||
* @return the signature config | |||
*/ | |||
@@ -306,6 +180,7 @@ public class SignatureInfo implements SignatureConfigurable { | |||
/** | |||
* @param signatureConfig the signature config, needs to be set before a SignatureInfo object is used | |||
*/ | |||
@Override | |||
public void setSignatureConfig(SignatureConfig signatureConfig) { | |||
this.signatureConfig = signatureConfig; | |||
} | |||
@@ -329,18 +204,31 @@ public class SignatureInfo implements SignatureConfigurable { | |||
* @throws MarshalException | |||
*/ | |||
public void confirmSignature() throws XMLSignatureException, MarshalException { | |||
Document document = DocumentHelper.createDocument(); | |||
final Document document = DocumentHelper.createDocument(); | |||
final DOMSignContext xmlSignContext = createXMLSignContext(document); | |||
// operate | |||
DigestInfo digestInfo = preSign(document, null); | |||
final DOMSignedInfo signedInfo = preSign(xmlSignContext); | |||
// setup: key material, signature value | |||
byte[] signatureValue = signDigest(digestInfo.digestValue); | |||
final String signatureValue = signDigest(xmlSignContext, signedInfo); | |||
// operate: postSign | |||
postSign(document, signatureValue); | |||
postSign(xmlSignContext, signatureValue); | |||
} | |||
/** | |||
* Convenience method for creating the signature context | |||
* | |||
* @param document the document the signature is based on | |||
* | |||
* @return the initialized signature context | |||
*/ | |||
public DOMSignContext createXMLSignContext(final Document document) { | |||
return new DOMSignContext(signatureConfig.getKey(), document); | |||
} | |||
/** | |||
* Sign (encrypt) the digest with the private key. | |||
* Currently only rsa is supported. | |||
@@ -348,21 +236,40 @@ public class SignatureInfo implements SignatureConfigurable { | |||
* @param digest the hashed input | |||
* @return the encrypted hash | |||
*/ | |||
public byte[] signDigest(byte digest[]) { | |||
Cipher cipher = CryptoFunctions.getCipher(signatureConfig.getKey(), CipherAlgorithm.rsa | |||
, ChainingMode.ecb, null, Cipher.ENCRYPT_MODE, "PKCS1Padding"); | |||
public String signDigest(final DOMSignContext xmlSignContext, final DOMSignedInfo signedInfo) { | |||
final PrivateKey key = signatureConfig.getKey(); | |||
final HashAlgorithm algo = signatureConfig.getDigestAlgo(); | |||
if (algo.hashSize*4/3 > Base64.BASE64DEFAULTLENGTH && !XMLUtils.ignoreLineBreaks()) { | |||
throw new EncryptedDocumentException("The hash size of the choosen hash algorithm ("+algo+" = "+algo.hashSize+" bytes), "+ | |||
"will motivate XmlSec to add linebreaks to the generated digest, which results in an invalid signature (... at least "+ | |||
"for Office) - please persuade it otherwise by adding '-Dorg.apache.xml.security.ignoreLineBreaks=true' to the JVM "+ | |||
"system properties."); | |||
} | |||
try { | |||
ByteArrayOutputStream digestInfoValueBuf = new ByteArrayOutputStream(); | |||
digestInfoValueBuf.write(signatureConfig.getHashMagic()); | |||
digestInfoValueBuf.write(digest); | |||
byte[] digestInfoValue = digestInfoValueBuf.toByteArray(); | |||
return cipher.doFinal(digestInfoValue); | |||
} catch (Exception e) { | |||
final DigestOutputStream dos; | |||
switch (algo) { | |||
case md2: case md5: case sha1: case sha256: case sha384: case sha512: | |||
dos = new SignatureOutputStream(algo, key); | |||
break; | |||
default: | |||
dos = new DigestOutputStream(algo, key); | |||
break; | |||
} | |||
dos.init(); | |||
final Document document = (Document)xmlSignContext.getParent(); | |||
final Element el = getDsigElement(document, "SignedInfo"); | |||
final DOMSubTreeData subTree = new DOMSubTreeData(el, true); | |||
signedInfo.getCanonicalizationMethod().transform(subTree, xmlSignContext, dos); | |||
return DatatypeConverter.printBase64Binary(dos.sign()); | |||
} catch (GeneralSecurityException|IOException|TransformException e) { | |||
throw new EncryptedDocumentException(e); | |||
} | |||
} | |||
/** | |||
* @return a signature part for each signature document. | |||
* the parts can be validated independently. | |||
@@ -370,17 +277,21 @@ public class SignatureInfo implements SignatureConfigurable { | |||
public Iterable<SignaturePart> getSignatureParts() { | |||
signatureConfig.init(true); | |||
return new Iterable<SignaturePart>() { | |||
@Override | |||
public Iterator<SignaturePart> iterator() { | |||
return new Iterator<SignaturePart>() { | |||
OPCPackage pkg = signatureConfig.getOpcPackage(); | |||
Iterator<PackageRelationship> sigOrigRels = | |||
Iterator<PackageRelationship> sigOrigRels = | |||
pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN).iterator(); | |||
Iterator<PackageRelationship> sigRels; | |||
PackagePart sigPart; | |||
@Override | |||
public boolean hasNext() { | |||
while (sigRels == null || !sigRels.hasNext()) { | |||
if (!sigOrigRels.hasNext()) return false; | |||
if (!sigOrigRels.hasNext()) { | |||
return false; | |||
} | |||
sigPart = pkg.getPart(sigOrigRels.next()); | |||
LOG.log(POILogger.DEBUG, "Digital Signature Origin part", sigPart); | |||
try { | |||
@@ -391,21 +302,25 @@ public class SignatureInfo implements SignatureConfigurable { | |||
} | |||
return true; | |||
} | |||
@Override | |||
public SignaturePart next() { | |||
PackagePart sigRelPart = null; | |||
do { | |||
try { | |||
if (!hasNext()) throw new NoSuchElementException(); | |||
sigRelPart = sigPart.getRelatedPart(sigRels.next()); | |||
if (!hasNext()) { | |||
throw new NoSuchElementException(); | |||
} | |||
sigRelPart = sigPart.getRelatedPart(sigRels.next()); | |||
LOG.log(POILogger.DEBUG, "XML Signature part", sigRelPart); | |||
} catch (InvalidFormatException e) { | |||
LOG.log(POILogger.WARN, "Reference to signature is invalid.", e); | |||
} | |||
} while (sigPart == null); | |||
return new SignaturePart(sigRelPart); | |||
return new SignaturePart(sigRelPart, signatureConfig); | |||
} | |||
@Override | |||
public void remove() { | |||
throw new UnsupportedOperationException(); | |||
} | |||
@@ -413,14 +328,16 @@ public class SignatureInfo implements SignatureConfigurable { | |||
} | |||
}; | |||
} | |||
/** | |||
* Initialize the xml signing environment and the bouncycastle provider | |||
* Initialize the xml signing environment and the bouncycastle provider | |||
*/ | |||
protected static synchronized void initXmlProvider() { | |||
if (isInitialized) return; | |||
if (isInitialized) { | |||
return; | |||
} | |||
isInitialized = true; | |||
try { | |||
Init.init(); | |||
RelationshipTransformService.registerDsigProvider(); | |||
@@ -429,16 +346,18 @@ public class SignatureInfo implements SignatureConfigurable { | |||
throw new RuntimeException("Xml & BouncyCastle-Provider initialization failed", e); | |||
} | |||
} | |||
/** | |||
* Helper method for adding informations before the signing. | |||
* Normally {@link #confirmSignature()} is sufficient to be used. | |||
*/ | |||
@SuppressWarnings("unchecked") | |||
public DigestInfo preSign(Document document, List<DigestInfo> digestInfos) | |||
public DOMSignedInfo preSign(final DOMSignContext xmlSignContext) | |||
throws XMLSignatureException, MarshalException { | |||
signatureConfig.init(false); | |||
final Document document = (Document)xmlSignContext.getParent(); | |||
// it's necessary to explicitly set the mdssi namespace, but the sign() method has no | |||
// normal way to interfere with, so we need to add the namespace under the hand ... | |||
EventTarget target = (EventTarget)document; | |||
@@ -449,11 +368,10 @@ public class SignatureInfo implements SignatureConfigurable { | |||
} | |||
SignatureMarshalListener.setListener(target, creationListener, true); | |||
} | |||
/* | |||
* Signature context construction. | |||
*/ | |||
XMLSignContext xmlSignContext = new DOMSignContext(signatureConfig.getKey(), document); | |||
URIDereferencer uriDereferencer = signatureConfig.getUriDereferencer(); | |||
if (null != uriDereferencer) { | |||
xmlSignContext.setURIDereferencer(uriDereferencer); | |||
@@ -464,23 +382,13 @@ public class SignatureInfo implements SignatureConfigurable { | |||
} | |||
xmlSignContext.setDefaultNamespacePrefix(""); | |||
// signatureConfig.getNamespacePrefixes().get(XML_DIGSIG_NS)); | |||
brokenJvmWorkaround(xmlSignContext); | |||
XMLSignatureFactory signatureFactory = signatureConfig.getSignatureFactory(); | |||
/* | |||
* Add ds:References that come from signing client local files. | |||
*/ | |||
List<Reference> references = new ArrayList<>(); | |||
for (DigestInfo digestInfo : safe(digestInfos)) { | |||
byte[] documentDigestValue = digestInfo.digestValue; | |||
String uri = new File(digestInfo.description).getName(); | |||
Reference reference = SignatureFacet.newReference | |||
(uri, null, null, null, documentDigestValue, signatureConfig); | |||
references.add(reference); | |||
} | |||
/* | |||
* Invoke the signature facets. | |||
@@ -528,11 +436,15 @@ public class SignatureInfo implements SignatureConfigurable { | |||
List<XMLStructure> objectContentList = object.getContent(); | |||
for (XMLStructure objectContent : objectContentList) { | |||
LOG.log(POILogger.DEBUG, "object content java type: " + objectContent.getClass().getName()); | |||
if (!(objectContent instanceof Manifest)) continue; | |||
if (!(objectContent instanceof Manifest)) { | |||
continue; | |||
} | |||
Manifest manifest = (Manifest) objectContent; | |||
List<Reference> manifestReferences = manifest.getReferences(); | |||
for (Reference manifestReference : manifestReferences) { | |||
if (manifestReference.getDigestValue() != null) continue; | |||
if (manifestReference.getDigestValue() != null) { | |||
continue; | |||
} | |||
DOMReference manifestDOMReference = (DOMReference)manifestReference; | |||
manifestDOMReference.digest(xmlSignContext); | |||
@@ -548,40 +460,26 @@ public class SignatureInfo implements SignatureConfigurable { | |||
DOMReference domReference = (DOMReference)signedInfoReference; | |||
// ds:Reference with external digest value | |||
if (domReference.getDigestValue() != null) continue; | |||
if (domReference.getDigestValue() != null) { | |||
continue; | |||
} | |||
domReference.digest(xmlSignContext); | |||
} | |||
/* | |||
* Calculation of XML signature digest value. | |||
*/ | |||
DOMSignedInfo domSignedInfo = (DOMSignedInfo)signedInfo; | |||
ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); | |||
domSignedInfo.canonicalize(xmlSignContext, dataStream); | |||
byte[] octets = dataStream.toByteArray(); | |||
/* | |||
* TODO: we could be using DigestOutputStream here to optimize memory | |||
* usage. | |||
*/ | |||
MessageDigest md = CryptoFunctions.getMessageDigest(signatureConfig.getDigestAlgo()); | |||
byte[] digestValue = md.digest(octets); | |||
String description = signatureConfig.getSignatureDescription(); | |||
return new DigestInfo(digestValue, signatureConfig.getDigestAlgo(), description); | |||
return (DOMSignedInfo)signedInfo; | |||
} | |||
/** | |||
* Helper method for adding informations after the signing. | |||
* Normally {@link #confirmSignature()} is sufficient to be used. | |||
*/ | |||
public void postSign(Document document, byte[] signatureValue) | |||
public void postSign(final DOMSignContext xmlSignContext, final String signatureValue) | |||
throws MarshalException { | |||
LOG.log(POILogger.DEBUG, "postSign"); | |||
final Document document = (Document)xmlSignContext.getParent(); | |||
/* | |||
* Check ds:Signature node. | |||
*/ | |||
@@ -593,11 +491,11 @@ public class SignatureInfo implements SignatureConfigurable { | |||
/* | |||
* Insert signature value into the ds:SignatureValue element | |||
*/ | |||
NodeList sigValNl = document.getElementsByTagNameNS(XML_DIGSIG_NS, "SignatureValue"); | |||
if (sigValNl.getLength() != 1) { | |||
final Element signatureNode = getDsigElement(document, "SignatureValue"); | |||
if (signatureNode == null) { | |||
throw new RuntimeException("preSign has to be called before postSign"); | |||
} | |||
sigValNl.item(0).setTextContent(Base64.encode(signatureValue)); | |||
signatureNode.setTextContent(signatureValue); | |||
/* | |||
* Allow signature facets to inject their own stuff. | |||
@@ -620,7 +518,7 @@ public class SignatureInfo implements SignatureConfigurable { | |||
Map<String,String> namespaceMap = new HashMap<>(); | |||
for(Map.Entry<String,String> entry : signatureConfig.getNamespacePrefixes().entrySet()){ | |||
namespaceMap.put(entry.getValue(), entry.getKey()); | |||
} | |||
} | |||
xo.setSaveSuggestedPrefixes(namespaceMap); | |||
xo.setUseDefaultNamespace(); | |||
@@ -641,12 +539,12 @@ public class SignatureInfo implements SignatureConfigurable { | |||
} catch (InvalidFormatException e) { | |||
throw new MarshalException(e); | |||
} | |||
PackagePart sigPart = pkg.getPart(sigPartName); | |||
if (sigPart == null) { | |||
sigPart = pkg.createPart(sigPartName, ContentTypes.DIGITAL_SIGNATURE_XML_SIGNATURE_PART); | |||
} | |||
try { | |||
OutputStream os = sigPart.getOutputStream(); | |||
SignatureDocument sigDoc = SignatureDocument.Factory.parse(document, DEFAULT_XML_OPTIONS); | |||
@@ -655,46 +553,30 @@ public class SignatureInfo implements SignatureConfigurable { | |||
} catch (Exception e) { | |||
throw new MarshalException("Unable to write signature document", e); | |||
} | |||
PackagePart sigsPart = pkg.getPart(sigsPartName); | |||
if (sigsPart == null) { | |||
// touch empty marker file | |||
sigsPart = pkg.createPart(sigsPartName, ContentTypes.DIGITAL_SIGNATURE_ORIGIN_PART); | |||
} | |||
PackageRelationshipCollection relCol = pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); | |||
for (PackageRelationship pr : relCol) { | |||
pkg.removeRelationship(pr.getId()); | |||
} | |||
pkg.addRelationship(sigsPartName, TargetMode.INTERNAL, PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); | |||
sigsPart.addRelationship(sigPartName, TargetMode.INTERNAL, PackageRelationshipTypes.DIGITAL_SIGNATURE); | |||
} | |||
/** | |||
* Helper method for null lists, which are converted to empty lists | |||
* | |||
* @param other the reference to wrap, if null | |||
* @return if other is null, an empty lists is returned, otherwise other is returned | |||
*/ | |||
private static <T> List<T> safe(List<T> other) { | |||
List<T> emptyList = Collections.emptyList(); | |||
return other == null ? emptyList : other; | |||
} | |||
private void brokenJvmWorkaround(XMLSignContext context) { | |||
// workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1155012 | |||
Provider bcProv = Security.getProvider("BC"); | |||
if (bcProv != null) { | |||
context.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", bcProv); | |||
} | |||
} | |||
private Element getDsigElement(final Document document, final String localName) { | |||
NodeList sigValNl = document.getElementsByTagNameNS(XML_DIGSIG_NS, localName); | |||
if (sigValNl.getLength() == 1) { | |||
return (Element)sigValNl.item(0); | |||
} | |||
private void brokenJvmWorkaround(XMLValidateContext context) { | |||
// workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1155012 | |||
Provider bcProv = Security.getProvider("BC"); | |||
if (bcProv != null) { | |||
context.setProperty("org.jcp.xml.dsig.internal.dom.SignatureProvider", bcProv); | |||
} | |||
LOG.log(POILogger.WARN, "Signature element '"+localName+"' was "+(sigValNl.getLength() == 0 ? "not found" : "multiple times")); | |||
return null; | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* ==================================================================== | |||
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.IOException; | |||
import java.security.GeneralSecurityException; | |||
import java.security.PrivateKey; | |||
import java.security.Signature; | |||
import java.security.SignatureException; | |||
import org.apache.poi.poifs.crypt.HashAlgorithm; | |||
/* package */ class SignatureOutputStream extends DigestOutputStream { | |||
Signature signature; | |||
SignatureOutputStream(final HashAlgorithm algo, PrivateKey key) { | |||
super(algo, key); | |||
} | |||
@Override | |||
public void init() throws GeneralSecurityException { | |||
final String provider = isMSCapi(key) ? "SunMSCAPI" : "SunRsaSign"; | |||
signature = Signature.getInstance(algo.ecmaString+"withRSA", provider); | |||
signature.initSign(key); | |||
} | |||
@Override | |||
public byte[] sign() throws SignatureException { | |||
return signature.sign(); | |||
} | |||
@Override | |||
public void write(final int b) throws IOException { | |||
try { | |||
signature.update((byte)b); | |||
} catch (final SignatureException e) { | |||
throw new IOException(e); | |||
} | |||
} | |||
@Override | |||
public void write(final byte[] data, final int off, final int len) throws IOException { | |||
try { | |||
signature.update(data, off, len); | |||
} catch (final SignatureException e) { | |||
throw new IOException(e); | |||
} | |||
} | |||
} |
@@ -0,0 +1,149 @@ | |||
/* ==================================================================== | |||
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 static org.apache.poi.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; | |||
import java.io.IOException; | |||
import java.security.cert.X509Certificate; | |||
import java.util.List; | |||
import javax.xml.crypto.MarshalException; | |||
import javax.xml.crypto.dsig.XMLSignature; | |||
import javax.xml.crypto.dsig.XMLSignatureException; | |||
import javax.xml.crypto.dsig.XMLSignatureFactory; | |||
import javax.xml.crypto.dsig.dom.DOMValidateContext; | |||
import javax.xml.xpath.XPath; | |||
import javax.xml.xpath.XPathConstants; | |||
import javax.xml.xpath.XPathExpressionException; | |||
import javax.xml.xpath.XPathFactory; | |||
import org.apache.poi.EncryptedDocumentException; | |||
import org.apache.poi.openxml4j.opc.PackagePart; | |||
import org.apache.poi.util.DocumentHelper; | |||
import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
import org.apache.xmlbeans.XmlException; | |||
import org.w3.x2000.x09.xmldsig.SignatureDocument; | |||
import org.w3c.dom.Document; | |||
import org.w3c.dom.Element; | |||
import org.w3c.dom.NodeList; | |||
import org.xml.sax.SAXException; | |||
public class SignaturePart { | |||
private static final POILogger LOG = POILogFactory.getLogger(SignaturePart.class); | |||
private static final String XMLSEC_VALIDATE_MANIFEST = "org.jcp.xml.dsig.validateManifests"; | |||
private final PackagePart signaturePart; | |||
private final SignatureConfig signatureConfig; | |||
private X509Certificate signer; | |||
private List<X509Certificate> certChain; | |||
/* package */ SignaturePart(final PackagePart signaturePart, final SignatureConfig signatureConfig) { | |||
this.signaturePart = signaturePart; | |||
this.signatureConfig = signatureConfig; | |||
} | |||
/** | |||
* @return the package part containing the signature | |||
*/ | |||
public PackagePart getPackagePart() { | |||
return signaturePart; | |||
} | |||
/** | |||
* @return the signer certificate | |||
*/ | |||
public X509Certificate getSigner() { | |||
return signer; | |||
} | |||
/** | |||
* @return the certificate chain of the signer | |||
*/ | |||
public List<X509Certificate> getCertChain() { | |||
return certChain; | |||
} | |||
/** | |||
* Helper method for examining the xml signature | |||
* | |||
* @return the xml signature document | |||
* @throws IOException if the xml signature doesn't exist or can't be read | |||
* @throws XmlException if the xml signature is malformed | |||
*/ | |||
public SignatureDocument getSignatureDocument() throws IOException, XmlException { | |||
// TODO: check for XXE | |||
return SignatureDocument.Factory.parse(signaturePart.getInputStream(), DEFAULT_XML_OPTIONS); | |||
} | |||
/** | |||
* @return true, when the xml signature is valid, false otherwise | |||
* | |||
* @throws EncryptedDocumentException if the signature can't be extracted or if its malformed | |||
*/ | |||
public boolean validate() { | |||
KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); | |||
try { | |||
Document doc = DocumentHelper.readDocument(signaturePart.getInputStream()); | |||
XPath xpath = XPathFactory.newInstance().newXPath(); | |||
NodeList nl = (NodeList)xpath.compile("//*[@Id]").evaluate(doc, XPathConstants.NODESET); | |||
final int length = nl.getLength(); | |||
for (int i=0; i<length; i++) { | |||
((Element)nl.item(i)).setIdAttribute("Id", true); | |||
} | |||
DOMValidateContext domValidateContext = new DOMValidateContext(keySelector, doc); | |||
domValidateContext.setProperty(XMLSEC_VALIDATE_MANIFEST, Boolean.TRUE); | |||
domValidateContext.setURIDereferencer(signatureConfig.getUriDereferencer()); | |||
XMLSignatureFactory xmlSignatureFactory = signatureConfig.getSignatureFactory(); | |||
XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); | |||
boolean valid = xmlSignature.validate(domValidateContext); | |||
if (valid) { | |||
signer = keySelector.getSigner(); | |||
certChain = keySelector.getCertChain(); | |||
} | |||
return valid; | |||
} catch (IOException e) { | |||
String s = "error in reading document"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (SAXException e) { | |||
String s = "error in parsing document"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (XPathExpressionException e) { | |||
String s = "error in searching document with xpath expression"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (MarshalException e) { | |||
String s = "error in unmarshalling the signature"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} catch (XMLSignatureException e) { | |||
String s = "error in validating the signature"; | |||
LOG.log(POILogger.ERROR, s, e); | |||
throw new EncryptedDocumentException(s, e); | |||
} | |||
} | |||
} |
@@ -24,14 +24,7 @@ | |||
package org.apache.poi.poifs.crypt.dsig.facets; | |||
import java.lang.reflect.Field; | |||
import java.lang.reflect.Method; | |||
import java.security.AccessController; | |||
import java.security.GeneralSecurityException; | |||
import java.security.MessageDigest; | |||
import java.security.PrivilegedAction; | |||
import java.security.Provider; | |||
import java.security.Security; | |||
import java.util.List; | |||
import javax.xml.XMLConstants; | |||
@@ -45,14 +38,11 @@ import javax.xml.crypto.dsig.XMLSignatureException; | |||
import javax.xml.crypto.dsig.XMLSignatureFactory; | |||
import javax.xml.crypto.dsig.spec.TransformParameterSpec; | |||
import org.apache.jcp.xml.dsig.internal.dom.DOMDigestMethod; | |||
import org.apache.jcp.xml.dsig.internal.dom.DOMReference; | |||
import org.apache.poi.openxml4j.opc.PackageNamespaces; | |||
import org.apache.poi.poifs.crypt.dsig.SignatureConfig; | |||
import org.apache.poi.poifs.crypt.dsig.SignatureConfig.SignatureConfigurable; | |||
import org.apache.poi.util.POILogFactory; | |||
import org.apache.poi.util.POILogger; | |||
import org.apache.poi.util.SuppressForbidden; | |||
import org.w3c.dom.Document; | |||
/** | |||
@@ -71,6 +61,7 @@ public abstract class SignatureFacet implements SignatureConfigurable { | |||
protected SignatureConfig signatureConfig; | |||
@Override | |||
public void setSignatureConfig(SignatureConfig signatureConfig) { | |||
this.signatureConfig = signatureConfig; | |||
} | |||
@@ -153,38 +144,7 @@ public abstract class SignatureFacet implements SignatureConfigurable { | |||
reference = sigFac.newReference(uri, digestMethod, transforms, type, id, digestValue); | |||
} | |||
brokenJvmWorkaround(reference); | |||
return reference; | |||
} | |||
// helper method ... will be removed soon | |||
public static void brokenJvmWorkaround(final Reference reference) { | |||
final DigestMethod digestMethod = reference.getDigestMethod(); | |||
final String digestMethodUri = digestMethod.getAlgorithm(); | |||
final Provider bcProv = Security.getProvider("BC"); | |||
if (bcProv != null && !DigestMethod.SHA1.equals(digestMethodUri)) { | |||
// workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1155012 | |||
// overwrite standard message digest, if a digest <> SHA1 is used | |||
AccessController.doPrivileged(new PrivilegedAction<Void>() { | |||
@Override | |||
@SuppressForbidden("Workaround for a bug, needs access to private JDK members (may fail in Java 9): https://bugzilla.redhat.com/show_bug.cgi?id=1155012") | |||
public Void run() { | |||
try { | |||
Method m = DOMDigestMethod.class.getDeclaredMethod("getMessageDigestAlgorithm"); | |||
m.setAccessible(true); | |||
String mdAlgo = (String)m.invoke(digestMethod); | |||
MessageDigest md = MessageDigest.getInstance(mdAlgo, bcProv); | |||
Field f = DOMReference.class.getDeclaredField("md"); | |||
f.setAccessible(true); | |||
f.set(reference, md); | |||
} catch (Exception e) { | |||
LOG.log(POILogger.WARN, "Can't overwrite message digest (workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1155012)", e); | |||
} | |||
return null; // Void | |||
} | |||
}); | |||
} | |||
} | |||
} |
@@ -56,15 +56,17 @@ import java.util.Date; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import javax.xml.crypto.dsig.dom.DOMSignContext; | |||
import org.apache.jcp.xml.dsig.internal.dom.DOMSignedInfo; | |||
import org.apache.poi.POIDataSamples; | |||
import org.apache.poi.POITestCase; | |||
import org.apache.poi.openxml4j.opc.OPCPackage; | |||
import org.apache.poi.openxml4j.opc.PackageAccess; | |||
import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; | |||
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.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; | |||
@@ -120,8 +122,6 @@ public class TestSignatureInfo { | |||
cal = LocaleUtil.getLocaleCalendar(LocaleUtil.TIMEZONE_UTC); | |||
assertNotNull(cal); | |||
// cal.set(2014, 7, 6, 21, 42, 12); | |||
// cal.clear(Calendar.MILLISECOND); | |||
// 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 | |||
@@ -129,6 +129,11 @@ public class TestSignatureInfo { | |||
//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); | |||
System.setProperty("org.apache.xml.security.ignoreLineBreaks", "true"); | |||
// Set line.separator for bug61182 | |||
// System.setProperty("line.separator", "\n"); | |||
} | |||
@Ignore("This test is very sensitive, it breaks with every little change to the produced XML") | |||
@@ -198,18 +203,18 @@ public class TestSignatureInfo { | |||
if (sep == null || "\n".equals(sep)) { | |||
// Unix | |||
signExp = | |||
"HDdvgXblLMiE6gZSoRSQUof6+aedrhK9i51we1n+4Q/ioqrQCeh5UkfQ8lD63nV4ZDbM4/pIVFi6VpMpN/HMnA"+ | |||
"UHeVdVUCVTgpn3Iz21Ymcd9/aerNov2BjHLhS8X3oUE+XTu2TbJLNmms0I9G4lfg6HWP9t7ZCXBXy6vyCMArc="; | |||
"QkqTFQZjXagjRAoOWKpAGa8AR0rKqkSfBtfSWqtjBmTgyjarn+t2POHkpySIpheHAbg+90GKSH88ACMtPHbG7q"+ | |||
"FL4gtgAD9Kjew6j16j0IRBwy145UlPrSLFMfF7YF7UlU1k1LBkIlRJ6Fv4MAJl6XspuzZOZIUmHZrWrdxycUQ="; | |||
} else if ("\r\n".equals(sep)){ | |||
// Windows | |||
signExp = | |||
"jVW6EPMywZ8jr4+I4alDosXzqrVuDG4wTdrr+la8QVbXfLm6HOh9AUFlo5yUZuWo/1gXrrkc34UTYNzuslyrOx"+ | |||
"KqadPOIRKUssJzdCh/hKeTxs/YtyWkpGHggrUjrF/vUUIeIXRHo+1DCAh6ptoicviH/I/Dtoa5NgkEHVuOHk8="; | |||
"GmAlL7+bT1r3FsMHJOp3pKg8betblYieZTjhMIrPZPRBbSzjO7KsYRGNtr0aOE3qr8xzyYJN6/8QdF5X7pUEUc"+ | |||
"2m8ctrm7s5o2vZTkAqk9ENJGDjBPXX7TnuVOiVeL1cJdtjHC2QpjtRwkFR+B54G6b1OXLOFuQpP3vqR3+/XXE="; | |||
} else { | |||
// Mac | |||
signExp = | |||
"GSaOQp2eVRkQl2GJgWxoxFdCadJJnmmKeoQtIwGrP3zzk+BnLeytGLN3bqmwCTjvtG7DyxENS+92e2xq/MiC9b"+ | |||
"CtNUfXfCdM0M8fzAny/Ewn9HckIsxjBztmsryt/OZQaKu52VU0ohQu7bG+cGPzcM+qTEss+GUbD0sVAoC34HM="; | |||
"NZedY/LNTYU4nAUEUhIOg5+fKdgVtzRXKmdD3v+47E7Mb84oeiUGv9cCEE91DU3StF/JFIhjOJqavOzKnCsNcz"+ | |||
"NJ4j/inggUl1OJUsicqIGQnA7E8vzWnN1kf5lINgJLv+0PyrrX9sQZbItzxUpgqyOFYcD0trid+31nRt4wtaA="; | |||
} | |||
String signAct = si.getSignatureParts().iterator().next(). | |||
@@ -721,24 +726,21 @@ public class TestSignatureInfo { | |||
SignatureInfo si = new SignatureInfo(); | |||
si.setSignatureConfig(signatureConfig); | |||
Document document = DocumentHelper.createDocument(); | |||
final Document document = DocumentHelper.createDocument(); | |||
final DOMSignContext xmlSignContext = si.createXMLSignContext(document); | |||
// operate | |||
DigestInfo digestInfo = si.preSign(document, null); | |||
final DOMSignedInfo signedInfo = si.preSign(xmlSignContext); | |||
// 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); | |||
assertNotNull(signedInfo); | |||
assertEquals("Office OpenXML Document", signatureConfig.getSignatureDescription()); | |||
// setup: key material, signature value | |||
byte[] signatureValue = si.signDigest(digestInfo.digestValue); | |||
final String signatureValue = si.signDigest(xmlSignContext, signedInfo); | |||
// operate: postSign | |||
si.postSign(document, signatureValue); | |||
si.postSign(xmlSignContext, signatureValue); | |||
// verify: signature | |||
si.getSignatureConfig().setOpcPackage(pkgCopy); |
@@ -16,20 +16,46 @@ | |||
==================================================================== */ | |||
package org.apache.poi.poifs.crypt.dsig; | |||
import static org.junit.Assert.assertEquals; | |||
import java.io.IOException; | |||
import javax.xml.bind.DatatypeConverter; | |||
import org.apache.poi.poifs.crypt.HashAlgorithm; | |||
import org.junit.Ignore; | |||
import org.junit.Test; | |||
import static org.junit.Assert.assertEquals; | |||
public class TestSignatureConfig { | |||
@Ignore("failing in automated builds, due to issues loading security classes") | |||
@Test | |||
@Ignore("failing in automated builds, due to issues loading security classes") | |||
public void testDigestAlgo() throws Exception { | |||
SignatureConfig sc = new SignatureConfig(); | |||
assertEquals(HashAlgorithm.sha256, sc.getDigestAlgo()); | |||
sc.setDigestAlgo(HashAlgorithm.sha1); | |||
assertEquals(HashAlgorithm.sha1, sc.getDigestAlgo()); | |||
} | |||
@Test | |||
public void testHashOids() throws IOException { | |||
final String[][] checks = { | |||
{ "sha1", "MCEwCQYFKw4DAhoFAAQU" }, | |||
{ "sha224", "MC0wDQYJYIZIAWUDBAIEBQAEHA==" }, | |||
{ "sha256", "MDEwDQYJYIZIAWUDBAIBBQAEIA==" }, | |||
{ "sha384", "MEEwDQYJYIZIAWUDBAICBQAEMA==" }, | |||
{ "sha512", "MFEwDQYJYIZIAWUDBAIDBQAEQA==" }, | |||
{ "ripemd128", "MB0wCQYFKyQDAgIFAAQQ" }, | |||
{ "ripemd160", "MCEwCQYFKyQDAgEFAAQU" }, | |||
{ "ripemd256", "MC0wCQYFKyQDAgMFAAQg" }, | |||
}; | |||
for (final String[] check : checks) { | |||
final HashAlgorithm ha = HashAlgorithm.valueOf(check[0]); | |||
try (final DigestOutputStream dos = new DigestOutputStream(ha, null)) { | |||
final String magic = DatatypeConverter.printBase64Binary(dos.getHashMagic()); | |||
assertEquals("hash digest magic mismatches", check[1], magic); | |||
} | |||
} | |||
} | |||
} |