From: Andreas Beeker Date: Sun, 10 Aug 2014 18:25:10 +0000 (+0000) Subject: Xml signature support - version 1 X-Git-Tag: REL_3_11_BETA3~73^2~29 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=9660a04c19304996f30bf8f815daeeb141c5a26f;p=poi.git Xml signature support - version 1 git-svn-id: https://svn.apache.org/repos/asf/poi/branches/xml_signature@1617141 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/.classpath b/.classpath index 286f8badad..8df184abf0 100644 --- a/.classpath +++ b/.classpath @@ -24,7 +24,7 @@ - + diff --git a/build.xml b/build.xml index 6704bd03fd..c16fe44e9e 100644 --- a/build.xml +++ b/build.xml @@ -118,7 +118,6 @@ under the License. - @@ -169,17 +168,28 @@ under the License. - - - + + + + + + + + + + + - - + + + @@ -356,7 +366,7 @@ under the License. - + @@ -458,19 +468,35 @@ under the License. - + - - + + + + + + + + + + + + + + + + + - + + @@ -481,19 +507,10 @@ under the License. - - - - - - - - - + depends="check-jars,fetch-jars,check-compiled-ooxml-xsds" + description="Unpacks the OOXML xsd files, and compiles them into XmlBeans"> @@ -505,11 +522,9 @@ under the License. - - + + + - - + - - - - - - - - - - - + + + + + + + + + + + + + - - + - + @@ -1447,7 +1461,7 @@ under the License. - + diff --git a/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java index be507a6660..68682f496c 100644 --- a/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java +++ b/src/java/org/apache/poi/poifs/crypt/CipherAlgorithm.java @@ -34,6 +34,8 @@ public enum CipherAlgorithm { // need bouncycastle provider for this one ... // see http://stackoverflow.com/questions/4436397/3des-des-encryption-using-the-jce-generating-an-acceptable-key des3_112(null, "DESede", -1, 128, new int[]{128}, 8, 32, "3DES_112", true), + // only for digital signatures + rsa(null, "RSA", -1, 1024, new int[]{1024, 2048, 3072, 4096}, -1, -1, "", false); ; public final CipherProvider provider; diff --git a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java index f9f970ade9..85d3419cbc 100644 --- a/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java +++ b/src/java/org/apache/poi/poifs/crypt/CryptoFunctions.java @@ -19,6 +19,7 @@ package org.apache.poi.poifs.crypt; import java.nio.charset.Charset; import java.security.DigestException; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.MessageDigest; import java.security.Provider; import java.security.Security; @@ -189,7 +190,7 @@ public class CryptoFunctions { * @return the requested cipher * @throws GeneralSecurityException */ - public static Cipher getCipher(SecretKey key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode, String padding) { + public static Cipher getCipher(Key key, CipherAlgorithm cipherAlgorithm, ChainingMode chain, byte[] vec, int cipherMode, String padding) { int keySizeInBytes = key.getEncoded().length; if (padding == null) padding = "NoPadding"; @@ -274,7 +275,7 @@ public class CryptoFunctions { } @SuppressWarnings("unchecked") - private static void registerBouncyCastle() { + public static void registerBouncyCastle() { if (Security.getProvider("BC") != null) return; try { Class clazz = (Class)Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider"); diff --git a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java index 51217184ba..e69f8f0736 100644 --- a/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java +++ b/src/java/org/apache/poi/poifs/crypt/HashAlgorithm.java @@ -17,22 +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, "", 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, "", 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), /* only for agile encryption */ - md5 ( "MD5", -1, "MD5", 16, "HmacMD5", false), + md5 ( "MD5", -1, "MD5", 16, "HmacMD5", null, false), // 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", 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), ; public final String jceId; @@ -40,14 +42,16 @@ 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, boolean needsBouncyCastle) { + HashAlgorithm(String jceId, int ecmaId, String ecmaString, int hashSize, String jceHmacId, String xmlSignUri, 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/openxml4j/opc/PackageRelationship.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationship.java index 2a5ade28d6..7d4741b219 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationship.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationship.java @@ -182,8 +182,6 @@ public final class PackageRelationship { } /** - * public URI getSourceUri(){ } - * * @return the targetMode */ public TargetMode getTargetMode() { diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java index 6adc737926..bef53f4a6e 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/PackageRelationshipCollection.java @@ -306,7 +306,7 @@ public final class PackageRelationshipCollection implements * @throws InvalidFormatException * Throws if the relationship part is invalid. */ - private void parseRelationshipsPart(PackagePart relPart) + public void parseRelationshipsPart(PackagePart relPart) throws InvalidFormatException { try { logger.log(POILogger.DEBUG, "Parsing relationship: " + relPart.getPartName()); diff --git a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java index 2c3b97a7f4..381d26b9c8 100644 --- a/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java +++ b/src/ooxml/java/org/apache/poi/openxml4j/opc/internal/ContentTypeManager.java @@ -148,11 +148,10 @@ public abstract class ContentTypeManager { *

*/ public void addContentType(PackagePartName partName, String contentType) { - boolean defaultCTExists = false; + boolean defaultCTExists = this.defaultContentType.containsValue(contentType); String extension = partName.getExtension().toLowerCase(); if ((extension.length() == 0) - || (this.defaultContentType.containsKey(extension) && !(defaultCTExists = this.defaultContentType - .containsValue(contentType)))) + || (this.defaultContentType.containsKey(extension) && !defaultCTExists)) this.addOverrideContentType(partName, contentType); else if (!defaultCTExists) this.addDefaultContentType(extension, contentType); @@ -461,7 +460,7 @@ public abstract class ContentTypeManager { } /** - * Use to append default types XML elements, use by the save() metid. + * Use to append default types XML elements, use by the save() method. * * @param root * XML parent element use to append this default type element. diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/CertificateSecurityException.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/CertificateSecurityException.java new file mode 100644 index 0000000000..707ad758a8 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/CertificateSecurityException.java @@ -0,0 +1,38 @@ +/* ==================================================================== + 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; + +/** + * Exception thrown in case there is something wrong with the incoming eID + * certificate. + * + * @author Frank Cornelis + * + */ +public class CertificateSecurityException extends SecurityException { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/ExpiredCertificateSecurityException.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/ExpiredCertificateSecurityException.java new file mode 100644 index 0000000000..adbcfdb35e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/ExpiredCertificateSecurityException.java @@ -0,0 +1,38 @@ +/* ==================================================================== + 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; + +/** + * Exception thrown in case the incoming eID certificate is expired. + * + * @author Frank Cornelis + * + */ +public class ExpiredCertificateSecurityException extends + CertificateSecurityException { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/HorribleProxies.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/HorribleProxies.java new file mode 100644 index 0000000000..54a5aad325 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/HorribleProxies.java @@ -0,0 +1,375 @@ +package org.apache.poi.poifs.crypt.dsig; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Date; + +import javax.security.auth.x500.X500Principal; +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.dom.DOMCryptoContext; +import javax.xml.crypto.dsig.XMLSignContext; +import javax.xml.crypto.dsig.XMLSignatureException; + +import org.apache.poi.poifs.crypt.dsig.HorribleProxy.ProxyIf; +import org.w3c.dom.Node; + +public interface HorribleProxies { + public static final String xmlSecBase = "org.jcp.xml.dsig.internal.dom"; + // public static final String xmlSecBase = "org.apache.jcp.xml.dsig.internal.dom"; + + public interface ASN1InputStreamIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.ASN1InputStream"; + + ASN1OctetStringIf readObject$ASNString() throws IOException; + DEROctetStringIf readObject$DERString() throws IOException; + DERIntegerIf readObject$Integer() throws IOException; + ASN1SequenceIf readObject$Sequence() throws IOException; + Object readObject$Object() throws IOException; + } + + public interface ASN1ObjectIdentifierIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.ASN1ObjectIdentifier"; + } + + public interface ASN1OctetStringIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.ASN1OctetString"; + byte[] getOctets(); + } + + public interface ASN1SequenceIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.ASN1Sequence"; + } + + public interface AuthorityInformationAccessIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.AuthorityInformationAccess"; + } + + public interface AuthorityKeyIdentifierIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.AuthorityKeyIdentifier"; + byte[] getKeyIdentifier(); + } + + public interface BasicConstraintsIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.BasicConstraints"; + } + + public interface BasicOCSPRespIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.BasicOCSPResp"; + Date getProducedAt(); + RespIDIf getResponderId(); + } + + public interface BcDigestCalculatorProviderIf extends ProxyIf { + String delegateClass = "org.bouncycastle.operator.bc.BcDigestCalculatorProvider"; + } + + public interface BcRSASignerInfoVerifierBuilderIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cms.bc.BcRSASignerInfoVerifierBuilder"; + SignerInformationVerifierIf build(X509CertificateHolderIf holder); + } + + public interface CanonicalizerIf extends ProxyIf { + String delegateClass = "com.sun.org.apache.xml.internal.security.c14n.Canonicalizer"; + byte[] canonicalizeSubtree(Node node) throws Exception; + } + + public interface CRLNumberIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.CRLNumber"; + } + + public interface DefaultDigestAlgorithmIdentifierFinderIf extends ProxyIf { + String delegateClass = "org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder"; + } + + public interface DistributionPointNameIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.DistributionPointName"; + } + + public interface DistributionPointIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.DistributionPoint"; + } + + public interface DERIA5StringIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.DERIA5String"; + } + + public interface DERIntegerIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.DERInteger"; + BigInteger getPositiveValue(); + } + + public interface DEROctetStringIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.DEROctetString"; + byte[] getOctets(); + } + + public interface DERTaggedObjectIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.DERTaggedObject"; + int getTagNo(); + ASN1OctetStringIf getObject$String(); + Object getObject$Object(); + } + + public interface DERSequenceIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.DERSequence"; + } + + public interface DOMKeyInfoIf extends ProxyIf { + String delegateClass = xmlSecBase+".DOMKeyInfo"; + void marshal(Node parent, Node nextSibling, String dsPrefix, DOMCryptoContext context) throws MarshalException; + } + + public interface DOMReferenceIf extends ProxyIf { + String delegateClass = xmlSecBase+".DOMReference"; + void digest(XMLSignContext paramXMLSignContext) throws XMLSignatureException; + byte[] getDigestValue(); + } + + public interface DOMSignedInfoIf extends ProxyIf { + String delegateClass = xmlSecBase+".DOMSignedInfo"; + void canonicalize(XMLCryptoContext paramXMLCryptoContext, ByteArrayOutputStream paramByteArrayOutputStream); + } + + public interface XMLSignatureIf extends ProxyIf { + String delegateClass = "com.sun.org.apache.xml.internal.security.signature.XMLSignature"; + String ALGO_ID_SIGNATURE_RSA_SHA1(); + String ALGO_ID_SIGNATURE_RSA_SHA256(); + String ALGO_ID_SIGNATURE_RSA_SHA384(); + String ALGO_ID_SIGNATURE_RSA_SHA512(); + String ALGO_ID_MAC_HMAC_RIPEMD160(); + } + + public interface DOMXMLSignatureIf extends ProxyIf { + String delegateClass = xmlSecBase+".DOMXMLSignature"; + void marshal(Node node, String prefix, DOMCryptoContext context) throws MarshalException; + } + + public interface GeneralNameIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.GeneralName"; + + int uniformResourceIdentifier(); + + } + + public interface GeneralNamesIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.GeneralNames"; + } + + public interface InitIf extends ProxyIf { + String delegateClass = "com.sun.org.apache.xml.internal.security.Init"; + void init(); + } + + public interface KeyUsageIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.KeyUsage"; + int digitalSignature(); + } + + public interface OCSPRespIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.OCSPResp"; + BasicOCSPRespIf getResponseObject(); + } + + public interface PKIFailureInfoIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.cmp.PKIFailureInfo"; + int intValue(); + } + + public interface RespIDIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.RespID"; + ResponderIDIf toASN1Object(); + } + + public interface ResponderIDIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.ocsp.ResponderID"; + DERTaggedObjectIf toASN1Object(); + } + + public interface SignerIdIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cms.SignerId"; + BigInteger getSerialNumber(); + X500Principal getIssuer(); + } + + public interface SignerInformationVerifierIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cms.SignerInformationVerifier"; + } + + public interface StoreIf extends ProxyIf { + String delegateClass = "org.bouncycastle.util.Store"; + Collection getMatches(Object selector) throws Exception; + } + + public interface SubjectKeyIdentifierIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.SubjectKeyIdentifier"; + byte[] getKeyIdentifier(); + } + + public interface SubjectPublicKeyInfoIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.SubjectPublicKeyInfo"; + } + + public interface TimeStampRequestGeneratorIf extends ProxyIf { + String delegateClass = "org.bouncycastle.tsp.TimeStampRequestGenerator"; + void setCertReq(boolean certReq); + void setReqPolicy(String reqPolicy); + TimeStampRequestIf generate(String igestAlgorithmOID, byte[] digest, BigInteger nonce); + } + + public interface TimeStampRequestIf extends ProxyIf { + String delegateClass = "org.bouncycastle.tsp.TimeStampRequest"; + byte[] getEncoded() throws IOException; + } + + public interface TimeStampResponseIf extends ProxyIf { + String delegateClass = "org.bouncycastle.tsp.TimeStampResponse"; + void validate(TimeStampRequestIf request) throws Exception; + int getStatus(); + String getStatusString(); + PKIFailureInfoIf getFailInfo(); + TimeStampTokenIf getTimeStampToken(); + } + + public interface TimeStampTokenIf extends ProxyIf { + String delegateClass = "org.bouncycastle.tsp.TimeStampToken"; + SignerIdIf getSID(); + StoreIf getCertificates(); + StoreIf getCRLs(); + TimeStampTokenInfoIf getTimeStampInfo(); + byte[] getEncoded() throws IOException; + void validate(SignerInformationVerifierIf verifier) throws Exception; + } + + public interface TimeStampTokenInfoIf extends ProxyIf { + String delegateClass = "org.bouncycastle.tsp.TimeStampTokenInfo"; + Date getGenTime(); + } + + public interface X509CertificateHolderIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.X509CertificateHolder"; + } + + public interface X509NameIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.X509Name"; + String toString$delegate(); + } + + public interface X509PrincipalIf extends ProxyIf { + String delegateClass = "org.bouncycastle.jce.X509Principal"; + String getName(); + } + + public interface X509V3CertificateGeneratorIf extends ProxyIf { + String delegateClass = "org.bouncycastle.x509.X509V3CertificateGenerator"; + + void reset(); + void setPublicKey(PublicKey key); + void setSignatureAlgorithm(String signatureAlgorithm); + void setNotBefore(Date date); + void setNotAfter(Date date); + void setIssuerDN(X509PrincipalIf issuerDN); + void setSubjectDN(X509PrincipalIf issuerDN); + void setSerialNumber(BigInteger serialNumber); + + void addExtension(ASN1ObjectIdentifierIf oid, boolean critical, SubjectKeyIdentifierIf value); + void addExtension(ASN1ObjectIdentifierIf oid, boolean critical, AuthorityKeyIdentifierIf value); + void addExtension(ASN1ObjectIdentifierIf oid, boolean critical, BasicConstraintsIf value); + void addExtension(ASN1ObjectIdentifierIf oid, boolean critical, DERSequenceIf value); + void addExtension(ASN1ObjectIdentifierIf oid, boolean critical, AuthorityInformationAccessIf value); + void addExtension(ASN1ObjectIdentifierIf oid, boolean critical, KeyUsageIf value); + + X509Certificate generate(PrivateKey issuerPrivateKey); + } + + public interface OCSPReqIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.OCSPReq"; + + ReqIf[] getRequestList(); + } + + public interface OCSPReqGeneratorIf extends ProxyIf { + String delegateClass = "org.bouncycastle.ocsp.OCSPReqGenerator"; + + void addRequest(CertificateIDIf certId); + OCSPReqIf generate(); + } + + public interface BasicOCSPRespGeneratorIf extends ProxyIf { + String delegateClass = "org.bouncycastle.ocsp.BasicOCSPRespGenerator"; + + void addResponse(CertificateIDIf certificateID, CertificateStatusIf certificateStatus); + BasicOCSPRespIf generate(String signatureAlgorithm, PrivateKey ocspResponderPrivateKey, + X509Certificate chain[], Date date, String provider); + } + + public interface CertificateIDIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.CertificateID"; + + String HASH_SHA1(); + } + + public interface X509ExtensionsIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.X509Extensions"; + + ASN1ObjectIdentifierIf AuthorityKeyIdentifier(); + ASN1ObjectIdentifierIf SubjectKeyIdentifier(); + ASN1ObjectIdentifierIf BasicConstraints(); + ASN1ObjectIdentifierIf CRLDistributionPoints(); + ASN1ObjectIdentifierIf AuthorityInfoAccess(); + ASN1ObjectIdentifierIf KeyUsage(); + ASN1ObjectIdentifierIf CRLNumber(); + } + + public interface X509ObjectIdentifiersIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.X509ObjectIdentifiers"; + + ASN1ObjectIdentifierIf ocspAccessMethod(); + } + + public interface X509V2CRLGeneratorIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.X509V2CRLGenerator"; + + void setIssuerDN(X500Principal issuerDN); + void setThisUpdate(Date date); + void setNextUpdate(Date date); + void setSignatureAlgorithm(String algorithm); + + void addExtension(ASN1ObjectIdentifierIf oid, boolean critical, CRLNumberIf value); + X509CRL generate(PrivateKey privateKey); + } + + public interface ReqIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.Req"; + + CertificateIDIf getCertID(); + } + + public interface CertificateStatusIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.CertificateStatus"; + + CertificateStatusIf GOOD(); + } + + public interface RevokedStatusIf extends ProxyIf { + String delegateClass = "org.bouncycastle.cert.ocsp.RevokedStatus"; + } + + public interface CRLReasonIf extends ProxyIf { + String delegateClass = "org.bouncycastle.asn1.x509.CRLReason"; + int unspecified(); + } + + public interface OCSPRespGeneratorIf extends ProxyIf { + String delegateClass = "org.bouncycastle.ocsp.OCSPRespGenerator"; + int SUCCESSFUL(); + OCSPRespIf generate(int status, BasicOCSPRespIf basicOCSPResp); + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/HorribleProxy.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/HorribleProxy.java new file mode 100644 index 0000000000..2ac5128ee6 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/HorribleProxy.java @@ -0,0 +1,249 @@ +package org.apache.poi.poifs.crypt.dsig; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; + +import org.apache.poi.util.MethodUtils; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +public class HorribleProxy implements InvocationHandler { + + private static final POILogger LOG = POILogFactory.getLogger(HorribleProxy.class); + + protected static interface ProxyIf { + Object getDelegate(); + void setInitDeferred(boolean initDeferred); + }; + + private final Class delegateClass; + private Object delegateRef; + private boolean initDeferred = true; + + protected HorribleProxy(Class delegateClass, Object delegateRef) { + this.delegateClass = delegateClass; + // delegateRef can be null, then we have to deal with deferred initialisation + this.delegateRef = delegateRef; + } + + /** + * Create new instance by constructor + * + * @param proxyClass + * @param initargs + * @return + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws InstantiationException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + */ + @SuppressWarnings("unchecked") + public static T newProxy(Class proxyClass, Object ... initargs) + throws InvocationTargetException, IllegalAccessException, InstantiationException + , NoSuchMethodException, ClassNotFoundException, NoSuchFieldException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + Class delegateClass = getDelegateClass(proxyClass); + Object delegateRef; + if (initargs.length == 0) { + delegateRef = null; + } else if (initargs.length == 1 && delegateClass.isAssignableFrom(initargs[0].getClass())) { + delegateRef = initargs[0]; + } else { + Class paramTypes[] = updateMethodArgs(null, initargs); + Constructor cons = null; + try { + cons = delegateClass.getConstructor(paramTypes); + } catch (Exception e) { + // fallback - find constructor with same amount of parameters + // horrible et al. ... + cons = MethodUtils.getMatchingAccessibleConstructor(delegateClass, paramTypes); + + if (cons == null) { + throw new RuntimeException("There's no constructor for the given arguments."); + } + } + + delegateRef = cons.newInstance(initargs); + } + + HorribleProxy hp = new HorribleProxy(delegateClass, delegateRef); + return (T)Proxy.newProxyInstance(cl, new Class[]{proxyClass}, hp); + } + + /** + * Create new instance by factory method + * + * @param proxyClass + * @param factoryMethod + * @param initargs + * @return + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws InstantiationException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + */ + @SuppressWarnings("unchecked") + public static T createProxy(Class proxyClass, String factoryMethod, Object ... initargs) + throws InvocationTargetException, IllegalAccessException, InstantiationException + , NoSuchMethodException, ClassNotFoundException, NoSuchFieldException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + Class delegateClass = getDelegateClass(proxyClass); + Class paramTypes[] = updateMethodArgs(null, initargs); + Method facMethod = delegateClass.getMethod(factoryMethod, paramTypes); + Object delegateRef = facMethod.invoke(null, initargs); + + if (delegateRef == null) { + return null; + } + + HorribleProxy hp = new HorribleProxy(delegateClass, delegateRef); + return (T)Proxy.newProxyInstance(cl, new Class[]{proxyClass}, hp); + } + + @SuppressWarnings("unchecked") + @Override + public Object invoke(Object proxy, Method method, Object[] args) + throws Exception { + String methodName = method.getName().replaceFirst("\\$.*", ""); + if (Object.class == method.getDeclaringClass()) { + if ("equals".equals(methodName)) { + return proxy == args[0]; + } else if ("hashCode".equals(methodName)) { + return System.identityHashCode(proxy); + } else if ("toString".equals(methodName)) { + return proxy.getClass().getName() + "@" + + Integer.toHexString(System.identityHashCode(proxy)) + + ", with InvocationHandler " + this; + } else { + throw new IllegalStateException(String.valueOf(method)); + } + } + + if ("getDelegate".equals(methodName)) { + initDeferred(); + return delegateRef; + } else if ("setInitDeferred".equals(methodName)) { + initDeferred = (Boolean)args[0]; + return null; + } + + Class methodParams[] = updateMethodArgs(method.getParameterTypes(), args); + + Object ret = null; + boolean isStaticField = false; + if (methodParams.length == 0) { + // check for static fields first + try { + Field f = delegateClass.getDeclaredField(methodName); + ret = f.get(delegateRef); + isStaticField = true; + } catch (NoSuchFieldException e) { + LOG.log(POILogger.DEBUG, "No static field '"+methodName+"' in class '"+delegateClass.getCanonicalName()+"' - trying method now."); + } + } + + if (!isStaticField) { + Method methodImpl = null; + try { + methodImpl = delegateClass.getMethod(methodName, methodParams); + } catch (Exception e) { + // fallback - if methodName is distinct, try to use it + // in case we can't provide method declaration in the Proxy interface + // ... and of course, this is horrible ... + methodImpl = MethodUtils.getMatchingAccessibleMethod(delegateClass, methodName, methodParams); + + if (methodImpl == null) { + throw new RuntimeException("There's no method '"+methodName+"' for the given arguments."); + } + } + + if (!Modifier.isStatic(methodImpl.getModifiers())) { + initDeferred(); + } + ret = methodImpl.invoke(delegateRef, args); + } + + Class retType = method.getReturnType(); + if (retType.isArray()) { + if (ProxyIf.class.isAssignableFrom(retType.getComponentType())) { + Class cType = (Class)retType.getComponentType(); + ProxyIf paRet[] = (ProxyIf[])Array.newInstance(cType, ((Object[])ret).length); + for (int i=0; i<((Object[])ret).length; i++) { + paRet[i] = newProxy(cType, ((Object[])ret)[i]); + paRet[i].setInitDeferred(false); + } + ret = paRet; + } + } else if (ProxyIf.class.isAssignableFrom(retType)) { + ProxyIf pRet = newProxy((Class)retType, ret); + pRet.setInitDeferred(false); + ret = pRet; + } + + return ret; + } + + @SuppressWarnings("unchecked") + private static Class[] updateMethodArgs(Class types[], Object args[]) + throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + if (args == null) return new Class[0]; + if (types == null) types = new Class[args.length]; + if (types.length != args.length) { + throw new IllegalArgumentException(); + } + + for (int i=0; i)types[i]); + if (args[i] != null) { + args[i] = ((ProxyIf)args[i]).getDelegate(); + } + } + } + return types; + } + + private void initDeferred() throws Exception { + if (delegateRef != null || !initDeferred) return; + // currently works only for empty constructor + delegateRef = delegateClass.getConstructor().newInstance(); + } + + private static Class getDelegateClass(Class proxyClass) + throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException { + Field delegateField; + try { + delegateField = proxyClass.getDeclaredField("delegateClass"); + } catch (NoSuchFieldException e) { + // sometimes a proxy interface is returned as proxyClass + // this has to be asked for the real ProxyIf interface + Class ifs[] = proxyClass.getInterfaces(); + if (ifs == null || ifs.length != 1) { + throw new IllegalArgumentException(); + } + delegateField = ifs[0].getDeclaredField("delegateClass"); + } + + String delegateClassName = (String)delegateField.get(null); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Class delegateClass = Class.forName(delegateClassName, true, cl); + return delegateClass; + } +} 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 new file mode 100644 index 0000000000..c24c36fc49 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/KeyInfoKeySelector.java @@ -0,0 +1,101 @@ +/* ==================================================================== + 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.security.Key; +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.xml.crypto.AlgorithmMethod; +import javax.xml.crypto.KeySelector; +import javax.xml.crypto.KeySelectorException; +import javax.xml.crypto.KeySelectorResult; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.X509Data; + +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * JSR105 key selector implementation using the ds:KeyInfo data of the signature + * itself. + */ +public class KeyInfoKeySelector extends KeySelector implements KeySelectorResult { + + private static final POILogger LOG = POILogFactory.getLogger(KeyInfoKeySelector.class); + + private X509Certificate certificate; + + @SuppressWarnings("unchecked") + @Override + public KeySelectorResult select(KeyInfo keyInfo, Purpose purpose, AlgorithmMethod method, XMLCryptoContext context) throws KeySelectorException { + LOG.log(POILogger.DEBUG, "select key"); + if (null == keyInfo) { + throw new KeySelectorException("no ds:KeyInfo present"); + } + List keyInfoContent = keyInfo.getContent(); + this.certificate = null; + for (XMLStructure keyInfoStructure : keyInfoContent) { + if (false == (keyInfoStructure instanceof X509Data)) { + continue; + } + X509Data x509Data = (X509Data) keyInfoStructure; + List x509DataList = x509Data.getContent(); + for (Object x509DataObject : x509DataList) { + if (false == (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; + } + } + throw new KeySelectorException("No key found!"); + } + + public Key getKey() { + return this.certificate.getPublicKey(); + } + + /** + * Gives back the X509 certificate used during the last signature + * verification operation. + * + * @return + */ + public X509Certificate getCertificate() { + return this.certificate; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/OOXMLURIDereferencer.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/OOXMLURIDereferencer.java new file mode 100644 index 0000000000..53b2d9fe38 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/OOXMLURIDereferencer.java @@ -0,0 +1,114 @@ +/* ==================================================================== + 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.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import javax.xml.crypto.Data; +import javax.xml.crypto.OctetStreamData; +import javax.xml.crypto.URIDereferencer; +import javax.xml.crypto.URIReference; +import javax.xml.crypto.URIReferenceException; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.dsig.XMLSignatureFactory; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * JSR105 URI dereferencer for Office Open XML documents. + */ +public class OOXMLURIDereferencer implements URIDereferencer { + + private static final POILogger LOG = POILogFactory.getLogger(OOXMLURIDereferencer.class); + + private final OPCPackage pkg; + + private final URIDereferencer baseUriDereferencer; + + public OOXMLURIDereferencer(OPCPackage pkg) { + if (null == pkg) { + throw new IllegalArgumentException("OPCPackage is null"); + } + this.pkg = pkg; + XMLSignatureFactory xmlSignatureFactory = SignatureInfo.getSignatureFactory(); + this.baseUriDereferencer = xmlSignatureFactory.getURIDereferencer(); + } + + public Data dereference(URIReference uriReference, XMLCryptoContext context) throws URIReferenceException { + if (null == uriReference) { + throw new NullPointerException("URIReference cannot be null"); + } + if (null == context) { + throw new NullPointerException("XMLCrytoContext cannot be null"); + } + + URI uri; + try { + uri = new URI(uriReference.getURI()); + } catch (URISyntaxException e) { + throw new URIReferenceException("could not URL decode the uri: "+uriReference.getURI(), e); + } + + PackagePart part = findPart(uri); + if (part == null) { + LOG.log(POILogger.DEBUG, "cannot resolve, delegating to base DOM URI dereferencer", uri); + return this.baseUriDereferencer.dereference(uriReference, context); + } + + try { + return new OctetStreamData(part.getInputStream(), uri.toString(), null); + } catch (IOException e) { + throw new URIReferenceException("I/O error: " + e.getMessage(), e); + } + } + + private PackagePart findPart(URI uri) { + LOG.log(POILogger.DEBUG, "dereference", uri); + + String path = uri.getPath(); + if (path == null || "".equals(path)) { + LOG.log(POILogger.DEBUG, "illegal part name (expected)", uri); + return null; + } + + PackagePartName ppn; + try { + ppn = PackagingURIHelper.createPartName(path); + } catch (InvalidFormatException e) { + LOG.log(POILogger.WARN, "illegal part name (not expected)", uri); + return null; + } + + return pkg.getPart(ppn); + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/RevokedCertificateSecurityException.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/RevokedCertificateSecurityException.java new file mode 100644 index 0000000000..fa4fee7370 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/RevokedCertificateSecurityException.java @@ -0,0 +1,38 @@ +/* ==================================================================== + 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; + +/** + * Exception thrown in case the incoming eID certificate has been revoked. + * + * @author Frank Cornelis + * + */ +public class RevokedCertificateSecurityException extends + CertificateSecurityException { + + private static final long serialVersionUID = 1L; + +} 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 new file mode 100644 index 0000000000..9e945840c3 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureInfo.java @@ -0,0 +1,283 @@ +/* ==================================================================== + 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.ByteArrayOutputStream; +import java.io.IOException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import javax.crypto.Cipher; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; + +import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; +import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; +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.HorribleProxies.InitIf; +import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService; +import org.apache.poi.poifs.crypt.dsig.services.XmlSignatureService; +import org.apache.poi.poifs.crypt.dsig.spi.DigestInfo; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.poi.util.SAXHelper; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlObject; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class SignatureInfo { + + 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; + + private final OPCPackage pkg; + + public SignatureInfo(OPCPackage pkg) { + this.pkg = pkg; + } + + public boolean verifySignature() { + initXmlProvider(); + // http://www.oracle.com/technetwork/articles/javase/dig-signature-api-140772.html + List signers = new LinkedList(); + return getSignersAndValidate(signers, true); + } + + public void confirmSignature(Key key, X509Certificate x509) + throws NoSuchAlgorithmException, IOException { + confirmSignature(key, x509, HashAlgorithm.sha1); + } + + public void confirmSignature(Key key, X509Certificate x509, HashAlgorithm hashAlgo) + throws NoSuchAlgorithmException, IOException { + XmlSignatureService signatureService = createSignatureService(hashAlgo, pkg); + + // operate + List x509Chain = Collections.singletonList(x509); + DigestInfo digestInfo = signatureService.preSign(null, x509Chain, null, null, null); + + // setup: key material, signature value + + Cipher cipher = CryptoFunctions.getCipher(key, CipherAlgorithm.rsa + , ChainingMode.ecb, null, Cipher.ENCRYPT_MODE, "PKCS1Padding"); + + byte[] signatureValue; + try { + ByteArrayOutputStream digestInfoValueBuf = new ByteArrayOutputStream(); + digestInfoValueBuf.write(SHA1_DIGEST_INFO_PREFIX); + digestInfoValueBuf.write(digestInfo.digestValue); + byte[] digestInfoValue = digestInfoValueBuf.toByteArray(); + signatureValue = cipher.doFinal(digestInfoValue); + } catch (Exception e) { + throw new EncryptedDocumentException(e); + } + + + // operate: postSign + signatureService.postSign(signatureValue, Collections.singletonList(x509)); + } + + public XmlSignatureService createSignatureService(HashAlgorithm hashAlgo, OPCPackage pkg) { + XmlSignatureService signatureService = new XmlSignatureService(hashAlgo, pkg); + signatureService.initFacets(new Date()); + return signatureService; + } + + public List getSigners() { + initXmlProvider(); + List signers = new LinkedList(); + getSignersAndValidate(signers, false); + return signers; + } + + protected boolean getSignersAndValidate(List signers, boolean onlyFirst) { + boolean allValid = true; + List signatureParts = getSignatureParts(onlyFirst); + if (signatureParts.isEmpty()) { + LOG.log(POILogger.DEBUG, "no signature resources"); + allValid = false; + } + + for (PackagePart signaturePart : signatureParts) { + KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); + + try { + Document doc = SAXHelper.readSAXDocumentW3C(signaturePart.getInputStream()); + // dummy call to createSignatureService to tweak document afterwards + createSignatureService(HashAlgorithm.sha1, pkg).registerIds(doc); + + DOMValidateContext domValidateContext = new DOMValidateContext(keySelector, doc); + domValidateContext.setProperty("org.jcp.xml.dsig.validateManifests", Boolean.TRUE); + OOXMLURIDereferencer dereferencer = new OOXMLURIDereferencer(pkg); + domValidateContext.setURIDereferencer(dereferencer); + + XMLSignatureFactory xmlSignatureFactory = getSignatureFactory(); + XMLSignature xmlSignature = xmlSignatureFactory.unmarshalXMLSignature(domValidateContext); + boolean validity = xmlSignature.validate(domValidateContext); + allValid &= validity; + if (!validity) continue; + // TODO: check what has been signed. + } catch (Exception e) { + LOG.log(POILogger.ERROR, "error in marshalling and validating the signature", e); + continue; + } + + X509Certificate signer = keySelector.getCertificate(); + signers.add(signer); + } + + return allValid; + } + + protected List getSignatureParts(boolean onlyFirst) { + List packageParts = new LinkedList(); + + PackageRelationshipCollection sigOrigRels = pkg.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN); + for (PackageRelationship rel : sigOrigRels) { + PackagePart sigPart = pkg.getPart(rel); + LOG.log(POILogger.DEBUG, "Digital Signature Origin part", sigPart); + + try { + PackageRelationshipCollection sigRels = sigPart.getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE); + for (PackageRelationship sigRel : sigRels) { + PackagePart sigRelPart = sigPart.getRelatedPart(sigRel); + LOG.log(POILogger.DEBUG, "XML Signature part", sigRelPart); + packageParts.add(sigRelPart); + if (onlyFirst) break; + } + } catch (InvalidFormatException e) { + LOG.log(POILogger.WARN, "Reference to signature is invalid.", e); + } + + if (onlyFirst && !packageParts.isEmpty()) break; + } + + return packageParts; + } + + public static XMLSignatureFactory getSignatureFactory() { + Provider p = Security.getProvider("XMLDSig"); + assert(p != null); + return XMLSignatureFactory.getInstance("DOM", p); + } + + public static KeyInfoFactory getKeyInfoFactory() { + Provider p = Security.getProvider("XMLDSig"); + assert(p != null); + return KeyInfoFactory.getInstance("DOM", p); + } + + public static void insertXChild(XmlObject root, XmlObject child) { + XmlCursor rootCursor = root.newCursor(); + insertXChild(rootCursor, child); + rootCursor.dispose(); + } + + public static void insertXChild(XmlCursor rootCursor, XmlObject child) { + rootCursor.toEndToken(); + XmlCursor childCursor = child.newCursor(); + childCursor.toNextToken(); + childCursor.moveXml(rootCursor); + childCursor.dispose(); + } + + public static void setPrefix(XmlObject xobj, String ns, String prefix) { + for (XmlCursor cur = xobj.newCursor(); cur.hasNextToken(); cur.toNextToken()) { + if (cur.isStart()) { + Element el = (Element)cur.getDomNode(); + if (ns.equals(el.getNamespaceURI())) el.setPrefix(prefix); + } + } + } + + public static synchronized void initXmlProvider() { + if (isInitialized) return; + isInitialized = true; + + try { + InitIf init = HorribleProxy.newProxy(InitIf.class); + init.init(); + + RelationshipTransformService.registerDsigProvider(); + + Provider bcProv = Security.getProvider("BC"); + if (bcProv == null) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + Class c = cl.loadClass("org.bouncycastle.jce.provider.BouncyCastleProvider"); + bcProv = (Provider)c.newInstance(); + Security.addProvider(bcProv); + } + } catch (Exception e) { + throw new RuntimeException("Xml & BouncyCastle-Provider initialization failed", e); + } + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/TrustCertificateSecurityException.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/TrustCertificateSecurityException.java new file mode 100644 index 0000000000..b99b5ebb99 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/TrustCertificateSecurityException.java @@ -0,0 +1,38 @@ +/* ==================================================================== + 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; + +/** + * Exception thrown in case the incoming eID certificate is not trusted. + * + * @author Frank Cornelis + * + */ +public class TrustCertificateSecurityException extends + CertificateSecurityException { + + private static final long serialVersionUID = 1L; + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/KeyInfoSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/KeyInfoSignatureFacet.java new file mode 100644 index 0000000000..4cbcdcc54b --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/KeyInfoSignatureFacet.java @@ -0,0 +1,194 @@ +/* ==================================================================== + 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.facets; + +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.KeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.dom.DOMCryptoContext; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.keyinfo.KeyValue; +import javax.xml.crypto.dsig.keyinfo.X509Data; + +import org.apache.poi.poifs.crypt.dsig.HorribleProxy; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DOMKeyInfoIf; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.w3.x2000.x09.xmldsig.ObjectType; +import org.w3.x2000.x09.xmldsig.SignatureType; +import org.w3c.dom.Node; + +/** + * Signature Facet implementation that adds ds:KeyInfo to the XML signature. + * + * @author Frank Cornelis + * + */ +public class KeyInfoSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(KeyInfoSignatureFacet.class); + + private final boolean includeEntireCertificateChain; + + private final boolean includeIssuerSerial; + + private final boolean includeKeyValue; + + /** + * Main constructor. + * + * @param includeEntireCertificateChain + * @param includeIssuerSerial + * @param includeKeyValue + */ + public KeyInfoSignatureFacet(boolean includeEntireCertificateChain, + boolean includeIssuerSerial, boolean includeKeyValue) { + this.includeEntireCertificateChain = includeEntireCertificateChain; + this.includeIssuerSerial = includeIssuerSerial; + this.includeKeyValue = includeKeyValue; + } + + public void postSign(SignatureType signatureElement, + List signingCertificateChain) { + LOG.log(POILogger.DEBUG, "postSign"); + + List objList = signatureElement.getObjectList(); + + /* + * Make sure we insert right after the ds:SignatureValue element, just + * before the first ds:Object element. + */ + Node nextSibling = (objList.isEmpty()) ? null : objList.get(0).getDomNode(); + + /* + * Construct the ds:KeyInfo element using JSR 105. + */ + String providerName = System.getProperty("jsr105Provider", "org.jcp.xml.dsig.internal.dom.XMLDSigRI"); + Provider xmlDSigProv; + try { + xmlDSigProv = (Provider) Class.forName(providerName).newInstance(); + } catch (Exception e) { + throw new RuntimeException("JRE doesn't support default xml signature provider - set jsr105Provider system property!", e); + } + + KeyInfoFactory keyInfoFactory = KeyInfoFactory.getInstance("DOM", xmlDSigProv); + List x509DataObjects = new LinkedList(); + X509Certificate signingCertificate = signingCertificateChain.get(0); + + List keyInfoContent = new LinkedList(); + + if (this.includeKeyValue) { + KeyValue keyValue; + try { + keyValue = keyInfoFactory.newKeyValue(signingCertificate.getPublicKey()); + } catch (KeyException e) { + throw new RuntimeException("key exception: " + e.getMessage(), e); + } + keyInfoContent.add(keyValue); + } + + if (this.includeIssuerSerial) { + x509DataObjects.add(keyInfoFactory.newX509IssuerSerial( + signingCertificate.getIssuerX500Principal().toString(), + signingCertificate.getSerialNumber())); + } + + if (this.includeEntireCertificateChain) { + for (X509Certificate certificate : signingCertificateChain) { + x509DataObjects.add(certificate); + } + } else { + x509DataObjects.add(signingCertificate); + } + + if (false == x509DataObjects.isEmpty()) { + X509Data x509Data = keyInfoFactory.newX509Data(x509DataObjects); + keyInfoContent.add(x509Data); + } + KeyInfo keyInfo = keyInfoFactory.newKeyInfo(keyInfoContent); + DOMKeyInfoIf domKeyInfo; + try { + domKeyInfo = HorribleProxy.newProxy(DOMKeyInfoIf.class, keyInfo); + } catch (Exception e) { + throw new RuntimeException("DOMKeyInfo instance error: " + e.getMessage(), e); + } + + Key key = new Key() { + private static final long serialVersionUID = 1L; + + public String getAlgorithm() { + return null; + } + + public byte[] getEncoded() { + return null; + } + + public String getFormat() { + return null; + } + }; + + DOMSignContext domSignContext = new DOMSignContext(key, signatureElement.getDomNode()); + DOMCryptoContext domCryptoContext = domSignContext; + String signatureNamespacePrefix = "xd"; + try { + domKeyInfo.marshal(signatureElement.getDomNode(), nextSibling, + signatureNamespacePrefix, domCryptoContext); + } catch (MarshalException e) { + throw new RuntimeException("marshall error: " + e.getMessage(), e); + } + } + + public void preSign(XMLSignatureFactory signatureFactory, + String signatureId, + List signingCertificateChain, + List references, + List objects + ) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + // empty + } + + public Map getNamespacePrefixMapping() { + Map map = new HashMap(); + // map.put("xd", "http://www.w3.org/2000/09/xmldsig#"); + return map; + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..f7978f4e71 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java @@ -0,0 +1,541 @@ +/* ==================================================================== + 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.facets; + +import static org.apache.poi.poifs.crypt.dsig.SignatureInfo.setPrefix; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; + +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Manifest; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureProperties; +import javax.xml.crypto.dsig.SignatureProperty; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.ContentTypes; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageNamespaces; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackageRelationship; +import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; +import org.apache.poi.openxml4j.opc.PackagingURIHelper; +import org.apache.poi.openxml4j.opc.TargetMode; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService; +import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService.RelationshipTransformParameterSpec; +import org.apache.poi.poifs.crypt.dsig.services.XmlSignatureService; +import org.apache.poi.poifs.crypt.dsig.spi.Constants; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xmlbeans.XmlException; +import org.openxmlformats.schemas.xpackage.x2006.digitalSignature.CTSignatureTime; +import org.openxmlformats.schemas.xpackage.x2006.digitalSignature.SignatureTimeDocument; +import org.w3.x2000.x09.xmldsig.SignatureType; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import com.microsoft.schemas.office.x2006.digsig.CTSignatureInfoV1; +import com.microsoft.schemas.office.x2006.digsig.SignatureInfoV1Document; + +/** + * Office OpenXML Signature Facet implementation. + * + * @author fcorneli + * @see http://msdn.microsoft.com/en-us/library/cc313071.aspx + */ +public class OOXMLSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(OOXMLSignatureFacet.class); + + public static final String OOXML_DIGSIG_NS = "http://schemas.openxmlformats.org/package/2006/digital-signature"; + public static final String OFFICE_DIGSIG_NS = "http://schemas.microsoft.com/office/2006/digsig"; + + private final XmlSignatureService signatureService; + + private final Date clock; + + private final HashAlgorithm hashAlgo; + + /** + * Main constructor. + */ + public OOXMLSignatureFacet(XmlSignatureService signatureService, Date clock, HashAlgorithm hashAlgo) { + this.signatureService = signatureService; + this.clock = (clock == null ? new Date() : clock); + this.hashAlgo = (hashAlgo == null ? HashAlgorithm.sha1 : hashAlgo); + } + + public void preSign(XMLSignatureFactory signatureFactory, + String signatureId, + List signingCertificateChain, + List references, List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + LOG.log(POILogger.DEBUG, "pre sign"); + addManifestObject(signatureFactory, signatureId, references, objects); + addSignatureInfo(signatureFactory, signatureId, references, objects); + } + + private void addManifestObject(XMLSignatureFactory signatureFactory, + String signatureId, List references, + List objects) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + Manifest manifest = constructManifest(signatureFactory); + String objectId = "idPackageObject"; // really has to be this value. + List objectContent = new LinkedList(); + objectContent.add(manifest); + + addSignatureTime(signatureFactory, signatureId, objectContent); + + objects.add(signatureFactory.newXMLObject(objectContent, objectId, + null, null)); + + DigestMethod digestMethod = signatureFactory.newDigestMethod(this.hashAlgo.xmlSignUri, null); + Reference reference = signatureFactory.newReference("#" + objectId, + digestMethod, null, "http://www.w3.org/2000/09/xmldsig#Object", + null); + references.add(reference); + } + + private Manifest constructManifest(XMLSignatureFactory signatureFactory) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + List manifestReferences = new ArrayList(); + + try { + addManifestReferences(signatureFactory, manifestReferences); + } catch (Exception e) { + throw new RuntimeException("error: " + e.getMessage(), e); + } + + return signatureFactory.newManifest(manifestReferences); + } + + private void addManifestReferences(XMLSignatureFactory signatureFactory, List manifestReferences) + throws IOException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, URISyntaxException, XmlException { + + OPCPackage ooxml = this.signatureService.getOfficeOpenXMLDocument(); + List relsEntryNames = ooxml.getPartsByContentType(ContentTypes.RELATIONSHIPS_PART); + + + DigestMethod digestMethod = signatureFactory.newDigestMethod(this.hashAlgo.xmlSignUri, null); + Set digestedPartNames = new HashSet(); + for (PackagePart pp : relsEntryNames) { + String baseUri = pp.getPartName().getName().replaceFirst("(.*)/_rels/.*", "$1"); + + PackageRelationshipCollection prc; + try { + prc = new PackageRelationshipCollection(ooxml); + prc.parseRelationshipsPart(pp); + } catch (InvalidFormatException e) { + throw new IOException("Invalid relationship descriptor: "+pp.getPartName().getName(), e); + } + + RelationshipTransformParameterSpec parameterSpec = new RelationshipTransformParameterSpec(); + for (PackageRelationship relationship : prc) { + String relationshipType = relationship.getRelationshipType(); + + if (TargetMode.EXTERNAL == relationship.getTargetMode()) { + /* + * ECMA-376 Part 2 - 3rd edition + * 13.2.4.16 Manifest Element + * "The producer shall not create a Manifest element that references any data outside of the package." + */ + continue; + } + + if (!isSignedRelationship(relationshipType)) continue; + + parameterSpec.addRelationshipReference(relationship.getId()); + + // TODO: find a better way ... + String partName = baseUri + relationship.getTargetURI().toString(); + partName = new URI(partName).normalize().getPath().replace('\\', '/'); + LOG.log(POILogger.DEBUG, "part name: " + partName); + + String contentType; + try { + PackagePartName relName = PackagingURIHelper.createPartName(partName); + PackagePart pp2 = ooxml.getPart(relName); + contentType = pp2.getContentType(); + } catch (InvalidFormatException e) { + throw new IOException(e); + } + if (relationshipType.endsWith("customXml") + && !(contentType.equals("inkml+xml") || contentType.equals("text/xml"))) { + LOG.log(POILogger.DEBUG, "skipping customXml with content type: " + contentType); + continue; + } + + if (!digestedPartNames.contains(partName)) { + // We only digest a part once. + String uri = partName + "?ContentType=" + contentType; + Reference reference = signatureFactory.newReference(uri, digestMethod); + manifestReferences.add(reference); + digestedPartNames.add(partName); + } + } + + if (parameterSpec.hasSourceIds()) { + List transforms = new LinkedList(); + transforms.add(signatureFactory.newTransform( + RelationshipTransformService.TRANSFORM_URI, + parameterSpec)); + transforms.add(signatureFactory.newTransform( + CanonicalizationMethod.INCLUSIVE, + (TransformParameterSpec) null)); + String uri = pp.getPartName().getName() + + "?ContentType=application/vnd.openxmlformats-package.relationships+xml"; + Reference reference = signatureFactory.newReference(uri, digestMethod, transforms, null, null); + manifestReferences.add(reference); + } + } + } + + + private void addSignatureTime(XMLSignatureFactory signatureFactory, + String signatureId, + List objectContent) { + /* + * SignatureTime + */ + DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + fmt.setTimeZone(TimeZone.getTimeZone("UTC")); + String nowStr = fmt.format(this.clock); + LOG.log(POILogger.DEBUG, "now: " + nowStr); + + SignatureTimeDocument sigTime = SignatureTimeDocument.Factory.newInstance(); + CTSignatureTime ctTime = sigTime.addNewSignatureTime(); + ctTime.setFormat("YYYY-MM-DDThh:mm:ssTZD"); + ctTime.setValue(nowStr); + + // TODO: find better method to have xmlbeans + export the prefix + Node n = ctTime.getDomNode(); + setPrefix(ctTime, PackageNamespaces.DIGITAL_SIGNATURE, "mdssi"); + + List signatureTimeContent = new LinkedList(); + signatureTimeContent.add(new DOMStructure(n)); + SignatureProperty signatureTimeSignatureProperty = signatureFactory + .newSignatureProperty(signatureTimeContent, "#" + signatureId, + "idSignatureTime"); + List signaturePropertyContent = new LinkedList(); + signaturePropertyContent.add(signatureTimeSignatureProperty); + SignatureProperties signatureProperties = signatureFactory + .newSignatureProperties(signaturePropertyContent, + "id-signature-time-" + this.clock.getTime()); + objectContent.add(signatureProperties); + } + + private void addSignatureInfo(XMLSignatureFactory signatureFactory, + String signatureId, List references, + List objects) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + List objectContent = new LinkedList(); + + SignatureInfoV1Document sigV1 = SignatureInfoV1Document.Factory.newInstance(); + CTSignatureInfoV1 ctSigV1 = sigV1.addNewSignatureInfoV1(); + ctSigV1.setManifestHashAlgorithm("http://www.w3.org/2000/09/xmldsig#sha1"); + Node n = ctSigV1.getDomNode(); + ((Element)n).setAttributeNS(Constants.NamespaceSpecNS, "xmlns", "http://schemas.microsoft.com/office/2006/digsig"); + + List signatureInfoContent = new LinkedList(); + signatureInfoContent.add(new DOMStructure(n)); + SignatureProperty signatureInfoSignatureProperty = signatureFactory + .newSignatureProperty(signatureInfoContent, "#" + signatureId, + "idOfficeV1Details"); + + List signaturePropertyContent = new LinkedList(); + signaturePropertyContent.add(signatureInfoSignatureProperty); + SignatureProperties signatureProperties = signatureFactory + .newSignatureProperties(signaturePropertyContent, null); + objectContent.add(signatureProperties); + + String objectId = "idOfficeObject"; + objects.add(signatureFactory.newXMLObject(objectContent, objectId, + null, null)); + + DigestMethod digestMethod = signatureFactory.newDigestMethod(this.hashAlgo.xmlSignUri, null); + Reference reference = signatureFactory.newReference("#" + objectId, + digestMethod, null, "http://www.w3.org/2000/09/xmldsig#Object", + null); + references.add(reference); + } + + public void postSign(SignatureType signatureElement, + List signingCertificateChain) { + // empty + } + + public static String getRelationshipReferenceURI(String zipEntryName) { + + return "/" + + zipEntryName + + "?ContentType=application/vnd.openxmlformats-package.relationships+xml"; + } + + public static String getResourceReferenceURI(String resourceName, + String contentType) { + + return "/" + resourceName + "?ContentType=" + contentType; + } + + public static String[] contentTypes = { + + /* + * Word + */ + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml", + "application/vnd.openxmlformats-officedocument.theme+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml", + + /* + * Word 2010 + */ + "application/vnd.ms-word.stylesWithEffects+xml", + + /* + * Excel + */ + "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", + + /* + * Powerpoint + */ + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml", + "application/vnd.openxmlformats-officedocument.presentationml.slide+xml", + "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml", + + /* + * Powerpoint 2010 + */ + "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml", + "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" }; + + public static boolean isSignedRelationship(String relationshipType) { + LOG.log(POILogger.DEBUG, "relationship type: " + relationshipType); + for (String signedTypeExtension : signed) { + if (relationshipType.endsWith(signedTypeExtension)) { + return true; + } + } + if (relationshipType.endsWith("customXml")) { + LOG.log(POILogger.DEBUG, "customXml relationship type"); + return true; + } + return false; + } + + public Map getNamespacePrefixMapping() { + Map m = new HashMap(); + m.put("mdssi", OOXML_DIGSIG_NS); + m.put("xd", "http://uri.etsi.org/01903/v1.3.2#"); + return m; + } + + + /** + * Office 2010 list of signed types (extensions). + */ + public static String[] signed = { "powerPivotData", // + "activeXControlBinary", // + "attachedToolbars", // + "connectorXml", // + "downRev", // + "functionPrototypes", // + "graphicFrameDoc", // + "groupShapeXml", // + "ink", // + "keyMapCustomizations", // + "legacyDiagramText", // + "legacyDocTextInfo", // + "officeDocument", // + "pictureXml", // + "shapeXml", // + "smartTags", // + "ui/altText", // + "ui/buttonSize", // + "ui/controlID", // + "ui/description", // + "ui/enabled", // + "ui/extensibility", // + "ui/helperText", // + "ui/imageID", // + "ui/imageMso", // + "ui/keyTip", // + "ui/label", // + "ui/lcid", // + "ui/loud", // + "ui/pressed", // + "ui/progID", // + "ui/ribbonID", // + "ui/showImage", // + "ui/showLabel", // + "ui/supertip", // + "ui/target", // + "ui/text", // + "ui/title", // + "ui/tooltip", // + "ui/userCustomization", // + "ui/visible", // + "userXmlData", // + "vbaProject", // + "wordVbaData", // + "wsSortMap", // + "xlBinaryIndex", // + "xlExternalLinkPath/xlAlternateStartup", // + "xlExternalLinkPath/xlLibrary", // + "xlExternalLinkPath/xlPathMissing", // + "xlExternalLinkPath/xlStartup", // + "xlIntlMacrosheet", // + "xlMacrosheet", // + "customData", // + "diagramDrawing", // + "hdphoto", // + "inkXml", // + "media", // + "slicer", // + "slicerCache", // + "stylesWithEffects", // + "ui/extensibility", // + "chartColorStyle", // + "chartLayout", // + "chartStyle", // + "dictionary", // + "timeline", // + "timelineCache", // + "aFChunk", // + "attachedTemplate", // + "audio", // + "calcChain", // + "chart", // + "chartsheet", // + "chartUserShapes", // + "commentAuthors", // + "comments", // + "connections", // + "control", // + "customProperty", // + "customXml", // + "diagramColors", // + "diagramData", // + "diagramLayout", // + "diagramQuickStyle", // + "dialogsheet", // + "drawing", // + "endnotes", // + "externalLink", // + "externalLinkPath", // + "font", // + "fontTable", // + "footer", // + "footnotes", // + "glossaryDocument", // + "handoutMaster", // + "header", // + "hyperlink", // + "image", // + "mailMergeHeaderSource", // + "mailMergeRecipientData", // + "mailMergeSource", // + "notesMaster", // + "notesSlide", // + "numbering", // + "officeDocument", // + "oleObject", // + "package", // + "pivotCacheDefinition", // + "pivotCacheRecords", // + "pivotTable", // + "presProps", // + "printerSettings", // + "queryTable", // + "recipientData", // + "settings", // + "sharedStrings", // + "sheetMetadata", // + "slide", // + "slideLayout", // + "slideMaster", // + "slideUpdateInfo", // + "slideUpdateUrl", // + "styles", // + "table", // + "tableSingleCells", // + "tableStyles", // + "tags", // + "theme", // + "themeOverride", // + "transform", // + "video", // + "viewProps", // + "volatileDependencies", // + "webSettings", // + "worksheet", // + "xmlMaps", // + "ctrlProp", // + "customData", // + "diagram", // + "diagramColorsHeader", // + "diagramLayoutHeader", // + "diagramQuickStyleHeader", // + "documentParts", // + "slicer", // + "slicerCache", // + "vmlDrawing" // + }; +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/Office2010SignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/Office2010SignatureFacet.java new file mode 100644 index 0000000000..ead9d2f2ce --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/Office2010SignatureFacet.java @@ -0,0 +1,101 @@ +/* ==================================================================== + 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.facets; + +import static org.apache.poi.poifs.crypt.dsig.facets.XAdESXLSignatureFacet.XADES_NAMESPACE; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.namespace.QName; + +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; +import org.etsi.uri.x01903.v13.QualifyingPropertiesType; +import org.etsi.uri.x01903.v13.UnsignedPropertiesType; +import org.etsi.uri.x01903.v13.UnsignedSignaturePropertiesType; +import org.w3.x2000.x09.xmldsig.ObjectType; +import org.w3.x2000.x09.xmldsig.SignatureType; + +/** + * Work-around for Office2010 to accept the XAdES-BES/EPES signature. + * + * xades:UnsignedProperties/xades:UnsignedSignatureProperties needs to be + * present. + * + * @author Frank Cornelis + * + */ +public class Office2010SignatureFacet implements SignatureFacet { + + public void preSign(XMLSignatureFactory signatureFactory, + String signatureId, + List signingCertificateChain, + List references, + List objects + ) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + } + + public void postSign(SignatureType signatureElement, List signingCertificateChain) { + QualifyingPropertiesType qualProps = null; + + try { + // check for XAdES-BES + for (ObjectType ot : signatureElement.getObjectList()) { + XmlObject xo[] = ot.selectChildren(new QName(XADES_NAMESPACE, "QualifyingProperties")); + if (xo != null && xo.length > 0) { + qualProps = QualifyingPropertiesType.Factory.parse(xo[0].getDomNode()); + break; + } + } + } catch (XmlException e) { + throw new RuntimeException("signature decoding error", e); + } + + if (qualProps == null) { + throw new IllegalArgumentException("no XAdES-BES extension present"); + } + + // create basic XML container structure + UnsignedPropertiesType unsignedProps = qualProps.getUnsignedProperties(); + if (unsignedProps == null) { + unsignedProps = qualProps.addNewUnsignedProperties(); + } + UnsignedSignaturePropertiesType unsignedSigProps = unsignedProps.getUnsignedSignatureProperties(); + if (unsignedSigProps == null) { + unsignedSigProps = unsignedProps.addNewUnsignedSignatureProperties(); + } + } + + public Map getNamespacePrefixMapping() { + return null; + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignatureFacet.java new file mode 100644 index 0000000000..70f7d911a6 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignatureFacet.java @@ -0,0 +1,83 @@ +/* ==================================================================== + 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.facets; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; + +import org.w3.x2000.x09.xmldsig.SignatureType; + +/** + * JSR105 Signature Facet interface. + * + * @author Frank Cornelis + * + */ +public interface SignatureFacet { + + /** + * This method is being invoked by the XML signature service engine during + * pre-sign phase. Via this method a signature facet implementation can add + * signature facets to an XML signature. + * + * @param signatureFactory + * @param document + * @param signatureId + * @param signingCertificateChain + * the optional signing certificate chain + * @param references + * @param objects + * @throws InvalidAlgorithmParameterException + * @throws NoSuchAlgorithmException + */ + void preSign( + XMLSignatureFactory signatureFactory + , String signatureId + , List signingCertificateChain + , List references + , List objects + ) throws NoSuchAlgorithmException, InvalidAlgorithmParameterException; + + /** + * This method is being invoked by the XML signature service engine during + * the post-sign phase. Via this method a signature facet can extend the XML + * signatures with for example key information. + * + * @param signatureElement + * @param signingCertificateChain + */ + void postSign( + SignatureType signatureElement + , List signingCertificateChain); + + Map getNamespacePrefixMapping(); +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignaturePolicyService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignaturePolicyService.java new file mode 100644 index 0000000000..d19fb8dfe2 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/SignaturePolicyService.java @@ -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. +==================================================================== */ + +/* ==================================================================== + 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.facets; + +/** + * Interface for the signature policy service. + * + * @author Frank Cornelis + * + */ +public interface SignaturePolicyService { + + /** + * Gives back the signature policy identifier URI. + * + * @return + */ + String getSignaturePolicyIdentifier(); + + /** + * Gives back the short description of the signature policy or + * null if a description is not available. + * + * @return the description, or null. + */ + String getSignaturePolicyDescription(); + + /** + * Gives back the download URL where the signature policy document can be + * found. Can be null in case such a download location does not + * exist. + * + * @return the download URL, or null. + */ + String getSignaturePolicyDownloadUrl(); + + /** + * Gives back the signature policy document. + * + * @return the bytes of the signature policy document. + */ + byte[] getSignaturePolicyDocument(); +} \ No newline at end of file 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 new file mode 100644 index 0000000000..9a197aa6ee --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESSignatureFacet.java @@ -0,0 +1,385 @@ +/* ==================================================================== + 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.facets; + +import static org.apache.poi.poifs.crypt.dsig.SignatureInfo.setPrefix; + +import java.security.InvalidAlgorithmParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.dsig.SignatureInfo; +import org.apache.poi.poifs.crypt.dsig.spi.Constants; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xmlbeans.XmlString; +import org.etsi.uri.x01903.v13.AnyType; +import org.etsi.uri.x01903.v13.CertIDListType; +import org.etsi.uri.x01903.v13.CertIDType; +import org.etsi.uri.x01903.v13.ClaimedRolesListType; +import org.etsi.uri.x01903.v13.DataObjectFormatType; +import org.etsi.uri.x01903.v13.DigestAlgAndValueType; +import org.etsi.uri.x01903.v13.IdentifierType; +import org.etsi.uri.x01903.v13.ObjectIdentifierType; +import org.etsi.uri.x01903.v13.QualifyingPropertiesDocument; +import org.etsi.uri.x01903.v13.QualifyingPropertiesType; +import org.etsi.uri.x01903.v13.SigPolicyQualifiersListType; +import org.etsi.uri.x01903.v13.SignaturePolicyIdType; +import org.etsi.uri.x01903.v13.SignaturePolicyIdentifierType; +import org.etsi.uri.x01903.v13.SignedDataObjectPropertiesType; +import org.etsi.uri.x01903.v13.SignedPropertiesType; +import org.etsi.uri.x01903.v13.SignedSignaturePropertiesType; +import org.etsi.uri.x01903.v13.SignerRoleType; +import org.w3.x2000.x09.xmldsig.DigestMethodType; +import org.w3.x2000.x09.xmldsig.SignatureType; +import org.w3.x2000.x09.xmldsig.X509IssuerSerialType; +import org.w3c.dom.Element; + +/** + * XAdES Signature Facet. Implements XAdES v1.4.1 which is compatible with XAdES + * v1.3.2. The implemented XAdES format is XAdES-BES/EPES. It's up to another + * part of the signature service to upgrade the XAdES-BES to a XAdES-X-L. + * + * This implementation has been tested against an implementation that + * participated multiple ETSI XAdES plugtests. + * + * @author Frank Cornelis + * @see http://en.wikipedia.org/wiki/XAdES + * + */ +public class XAdESSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(XAdESSignatureFacet.class); + + private static final String XADES_TYPE = "http://uri.etsi.org/01903#SignedProperties"; + + private final Date clock; + + private final HashAlgorithm hashAlgo; + + private final SignaturePolicyService signaturePolicyService; + + private String idSignedProperties; + + private boolean signaturePolicyImplied; + + private String role; + + private boolean issuerNameNoReverseOrder = false; + + private Map dataObjectFormatMimeTypes; + + /** + * Main constructor. + * + * @param clock + * the clock to be used for determining the xades:SigningTime, + * defaults to now when null + * @param hashAlgo + * the digest algorithm to be used for all required XAdES digest + * operations. Possible values: "SHA-1", "SHA-256", or "SHA-512", + * defaults to SHA-1 when null + * @param signaturePolicyService + * the optional signature policy service used for XAdES-EPES. + */ + public XAdESSignatureFacet(Date clock, HashAlgorithm hashAlgo, + SignaturePolicyService signaturePolicyService) { + this.clock = (clock == null ? new Date() : clock); + this.hashAlgo = (hashAlgo == null ? HashAlgorithm.sha1 : hashAlgo); + this.signaturePolicyService = signaturePolicyService; + this.dataObjectFormatMimeTypes = new HashMap(); + } + + public void postSign(SignatureType signatureElement, + List signingCertificateChain) { + LOG.log(POILogger.DEBUG, "postSign"); + } + + public void preSign(XMLSignatureFactory signatureFactory, + String signatureId, + List signingCertificateChain, + List references, List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + LOG.log(POILogger.DEBUG, "preSign"); + + // QualifyingProperties + QualifyingPropertiesDocument qualDoc = QualifyingPropertiesDocument.Factory.newInstance(); + QualifyingPropertiesType qualifyingProperties = qualDoc.addNewQualifyingProperties(); + qualifyingProperties.setTarget("#" + signatureId); + + // SignedProperties + SignedPropertiesType signedProperties = qualifyingProperties.addNewSignedProperties(); + String signedPropertiesId; + if (null != this.idSignedProperties) { + signedPropertiesId = this.idSignedProperties; + } else { + signedPropertiesId = signatureId + "-xades"; + } + signedProperties.setId(signedPropertiesId); + + // SignedSignatureProperties + SignedSignaturePropertiesType signedSignatureProperties = signedProperties.addNewSignedSignatureProperties(); + + // SigningTime + Calendar xmlGregorianCalendar = Calendar.getInstance(); + xmlGregorianCalendar.setTimeZone(TimeZone.getTimeZone("Z")); + xmlGregorianCalendar.setTime(this.clock); + xmlGregorianCalendar.clear(Calendar.MILLISECOND); + signedSignatureProperties.setSigningTime(xmlGregorianCalendar); + + // SigningCertificate + if (null == signingCertificateChain + || signingCertificateChain.isEmpty()) { + throw new RuntimeException("no signing certificate chain available"); + } + CertIDListType signingCertificates = signedSignatureProperties.addNewSigningCertificate(); + CertIDType certId = signingCertificates.addNewCert(); + X509Certificate signingCertificate = signingCertificateChain.get(0); + setCertID(certId, signingCertificate, this.hashAlgo, this.issuerNameNoReverseOrder); + + // ClaimedRole + if (null != this.role && false == this.role.isEmpty()) { + SignerRoleType signerRole = signedSignatureProperties.addNewSignerRole(); + signedSignatureProperties.setSignerRole(signerRole); + ClaimedRolesListType claimedRolesList = signerRole.addNewClaimedRoles(); + AnyType claimedRole = claimedRolesList.addNewClaimedRole(); + XmlString roleString = XmlString.Factory.newInstance(); + roleString.setStringValue(this.role); + SignatureInfo.insertXChild(claimedRole, roleString); + } + + // XAdES-EPES + if (null != this.signaturePolicyService) { + SignaturePolicyIdentifierType signaturePolicyIdentifier = + signedSignatureProperties.addNewSignaturePolicyIdentifier(); + + SignaturePolicyIdType signaturePolicyId = signaturePolicyIdentifier.addNewSignaturePolicyId(); + + ObjectIdentifierType objectIdentifier = signaturePolicyId.addNewSigPolicyId(); + objectIdentifier.setDescription(this.signaturePolicyService.getSignaturePolicyDescription()); + + IdentifierType identifier = objectIdentifier.addNewIdentifier(); + identifier.setStringValue(this.signaturePolicyService.getSignaturePolicyIdentifier()); + + byte[] signaturePolicyDocumentData = this.signaturePolicyService.getSignaturePolicyDocument(); + DigestAlgAndValueType sigPolicyHash = signaturePolicyId.addNewSigPolicyHash(); + setDigestAlgAndValue(sigPolicyHash, signaturePolicyDocumentData, this.hashAlgo); + + String signaturePolicyDownloadUrl = this.signaturePolicyService + .getSignaturePolicyDownloadUrl(); + if (null != signaturePolicyDownloadUrl) { + SigPolicyQualifiersListType sigPolicyQualifiers = signaturePolicyId.addNewSigPolicyQualifiers(); + AnyType sigPolicyQualifier = sigPolicyQualifiers.addNewSigPolicyQualifier(); + XmlString spUriElement = XmlString.Factory.newInstance(); + spUriElement.setStringValue(signaturePolicyDownloadUrl); + SignatureInfo.insertXChild(sigPolicyQualifier, spUriElement); + } + } else if (this.signaturePolicyImplied) { + SignaturePolicyIdentifierType signaturePolicyIdentifier = + signedSignatureProperties.addNewSignaturePolicyIdentifier(); + signaturePolicyIdentifier.addNewSignaturePolicyImplied(); + } + + // DataObjectFormat + if (false == this.dataObjectFormatMimeTypes.isEmpty()) { + SignedDataObjectPropertiesType signedDataObjectProperties = + signedProperties.addNewSignedDataObjectProperties(); + + List dataObjectFormats = signedDataObjectProperties + .getDataObjectFormatList(); + for (Map.Entry dataObjectFormatMimeType : this.dataObjectFormatMimeTypes + .entrySet()) { + DataObjectFormatType dataObjectFormat = DataObjectFormatType.Factory.newInstance(); + dataObjectFormat.setObjectReference("#" + dataObjectFormatMimeType.getKey()); + dataObjectFormat.setMimeType(dataObjectFormatMimeType.getValue()); + dataObjectFormats.add(dataObjectFormat); + } + } + + // marshall XAdES QualifyingProperties + // ((Element)qualifyingProperties.getSignedProperties().getDomNode()).setIdAttribute("Id", true); + + // add XAdES ds:Object + List xadesObjectContent = new LinkedList(); + Element qualDocEl = (Element)qualifyingProperties.getDomNode(); + qualDocEl.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:xd", "http://uri.etsi.org/01903/v1.3.2#"); + setPrefix(qualifyingProperties, "http://uri.etsi.org/01903/v1.3.2#", "xd"); + xadesObjectContent.add(new DOMStructure(qualDocEl)); + XMLObject xadesObject = signatureFactory.newXMLObject(xadesObjectContent, null, null, null); + objects.add(xadesObject); + + // add XAdES ds:Reference + DigestMethod digestMethod = signatureFactory.newDigestMethod(hashAlgo.xmlSignUri, null); + List transforms = new LinkedList(); + Transform exclusiveTransform = signatureFactory + .newTransform(CanonicalizationMethod.INCLUSIVE, + (TransformParameterSpec) null); + transforms.add(exclusiveTransform); + Reference reference = signatureFactory.newReference("#" + + signedPropertiesId, digestMethod, transforms, XADES_TYPE, + null); + references.add(reference); + } + + /** + * Gives back the JAXB DigestAlgAndValue data structure. + * + * @param data + * @param xadesObjectFactory + * @param xmldsigObjectFactory + * @param hashAlgo + * @return + */ + protected static void setDigestAlgAndValue( + DigestAlgAndValueType digestAlgAndValue, + byte[] data, + HashAlgorithm hashAlgo) { + DigestMethodType digestMethod = digestAlgAndValue.addNewDigestMethod(); + digestMethod.setAlgorithm(hashAlgo.xmlSignUri); + + MessageDigest messageDigest = CryptoFunctions.getMessageDigest(hashAlgo); + byte[] digestValue = messageDigest.digest(data); + digestAlgAndValue.setDigestValue(digestValue); + } + + /** + * Gives back the JAXB CertID data structure. + * + * @param certificate + * @param xadesObjectFactory + * @param xmldsigObjectFactory + * @param digestAlgorithm + * @return + */ + protected static void setCertID( + CertIDType certId, + X509Certificate certificate, + HashAlgorithm digestAlgorithm, boolean issuerNameNoReverseOrder) { + X509IssuerSerialType issuerSerial = certId.addNewIssuerSerial(); + String issuerName; + if (issuerNameNoReverseOrder) { + /* + * Make sure the DN is encoded using the same order as present + * within the certificate. This is an Office2010 work-around. + * Should be reverted back. + * + * XXX: not correct according to RFC 4514. + */ + // TODO: check if issuerName is different on getTBSCertificate + // issuerName = PrincipalUtil.getIssuerX509Principal(certificate).getName().replace(",", ", "); + issuerName = certificate.getIssuerDN().getName().replace(",", ", "); + } else { + issuerName = certificate.getIssuerX500Principal().toString(); + } + issuerSerial.setX509IssuerName(issuerName); + issuerSerial.setX509SerialNumber(certificate.getSerialNumber()); + + byte[] encodedCertificate; + try { + encodedCertificate = certificate.getEncoded(); + } catch (CertificateEncodingException e) { + throw new RuntimeException("certificate encoding error: " + + e.getMessage(), e); + } + DigestAlgAndValueType certDigest = certId.addNewCertDigest(); + setDigestAlgAndValue(certDigest, encodedCertificate, digestAlgorithm); + } + + /** + * Adds a mime-type for the given ds:Reference (referred via its @URI). This + * information is added via the xades:DataObjectFormat element. + * + * @param dsReferenceUri + * @param mimetype + */ + public void addMimeType(String dsReferenceUri, String mimetype) { + this.dataObjectFormatMimeTypes.put(dsReferenceUri, mimetype); + } + + /** + * Sets the Id that will be used on the SignedProperties element; + * + * @param idSignedProperties + */ + public void setIdSignedProperties(String idSignedProperties) { + this.idSignedProperties = idSignedProperties; + } + + /** + * Sets the signature policy to implied. + * + * @param signaturePolicyImplied + */ + public void setSignaturePolicyImplied(boolean signaturePolicyImplied) { + this.signaturePolicyImplied = signaturePolicyImplied; + } + + /** + * Sets the XAdES claimed role. + * + * @param role + */ + public void setRole(String role) { + this.role = role; + } + + /** + * Work-around for Office 2010 IssuerName encoding. + * + * @param reverseOrder + */ + public void setIssuerNameNoReverseOrder(boolean reverseOrder) { + this.issuerNameNoReverseOrder = reverseOrder; + } + + + public Map getNamespacePrefixMapping() { + Map map = new HashMap(); + map.put("xd", "http://uri.etsi.org/01903/v1.3.2#"); + return map; + } + +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java new file mode 100644 index 0000000000..ade2494f06 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/XAdESXLSignatureFacet.java @@ -0,0 +1,492 @@ +/* ==================================================================== + 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.facets; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CRLException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.namespace.QName; + +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.dsig.HorribleProxy; +import org.apache.poi.poifs.crypt.dsig.SignatureInfo; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.ASN1InputStreamIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.ASN1OctetStringIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.BasicOCSPRespIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.CanonicalizerIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DERIntegerIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DERTaggedObjectIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.InitIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.OCSPRespIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.RespIDIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.ResponderIDIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.X509NameIf; +import org.apache.poi.poifs.crypt.dsig.services.RevocationData; +import org.apache.poi.poifs.crypt.dsig.services.RevocationDataService; +import org.apache.poi.poifs.crypt.dsig.services.TimeStampService; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; +import org.etsi.uri.x01903.v13.CRLIdentifierType; +import org.etsi.uri.x01903.v13.CRLRefType; +import org.etsi.uri.x01903.v13.CRLRefsType; +import org.etsi.uri.x01903.v13.CRLValuesType; +import org.etsi.uri.x01903.v13.CertIDListType; +import org.etsi.uri.x01903.v13.CertIDType; +import org.etsi.uri.x01903.v13.CertificateValuesType; +import org.etsi.uri.x01903.v13.CompleteCertificateRefsType; +import org.etsi.uri.x01903.v13.CompleteRevocationRefsType; +import org.etsi.uri.x01903.v13.DigestAlgAndValueType; +import org.etsi.uri.x01903.v13.EncapsulatedPKIDataType; +import org.etsi.uri.x01903.v13.OCSPIdentifierType; +import org.etsi.uri.x01903.v13.OCSPRefType; +import org.etsi.uri.x01903.v13.OCSPRefsType; +import org.etsi.uri.x01903.v13.OCSPValuesType; +import org.etsi.uri.x01903.v13.QualifyingPropertiesType; +import org.etsi.uri.x01903.v13.ResponderIDType; +import org.etsi.uri.x01903.v13.RevocationValuesType; +import org.etsi.uri.x01903.v13.UnsignedPropertiesType; +import org.etsi.uri.x01903.v13.UnsignedSignaturePropertiesType; +import org.etsi.uri.x01903.v13.XAdESTimeStampType; +import org.etsi.uri.x01903.v14.ValidationDataType; +import org.w3.x2000.x09.xmldsig.CanonicalizationMethodType; +import org.w3.x2000.x09.xmldsig.ObjectType; +import org.w3.x2000.x09.xmldsig.SignatureType; +import org.w3.x2000.x09.xmldsig.SignatureValueType; +import org.w3c.dom.Node; + +/** + * XAdES-X-L v1.4.1 signature facet. This signature facet implementation will + * upgrade a given XAdES-BES/EPES signature to XAdES-X-L. + * + * We don't inherit from XAdESSignatureFacet as we also want to be able to use + * this facet out of the context of a signature creation. This signature facet + * assumes that the signature is already XAdES-BES/EPES compliant. + * + * This implementation has been tested against an implementation that + * participated multiple ETSI XAdES plugtests. + * + * @author Frank Cornelis + * @see XAdESSignatureFacet + */ +public class XAdESXLSignatureFacet implements SignatureFacet { + + private static final POILogger LOG = POILogFactory.getLogger(XAdESXLSignatureFacet.class); + + public static final String XADES_NAMESPACE = "http://uri.etsi.org/01903/v1.3.2#"; + + public static final String XADES141_NAMESPACE = "http://uri.etsi.org/01903/v1.4.1#"; + + private final TimeStampService timeStampService; + + private String c14nAlgoId; + + private final RevocationDataService revocationDataService; + + private final CertificateFactory certificateFactory; + + private final HashAlgorithm hashAlgo; + + static { + try { + HorribleProxy.createProxy(InitIf.class, "init"); + } catch (Exception e) { + throw new RuntimeException("Can't initialize JDK xml signature classes - feature unsupported by the this JDK?!", e); + } + } + + /** + * Convenience constructor. + * + * @param timeStampService + * the time-stamp service used for XAdES-T and XAdES-X. + * @param revocationDataService + * the optional revocation data service used for XAdES-C and + * XAdES-X-L. When null the signature will be + * limited to XAdES-T only. + */ + public XAdESXLSignatureFacet(TimeStampService timeStampService, + RevocationDataService revocationDataService) { + this(timeStampService, revocationDataService, HashAlgorithm.sha1); + } + + /** + * Main constructor. + * + * @param timeStampService + * the time-stamp service used for XAdES-T and XAdES-X. + * @param revocationDataService + * the optional revocation data service used for XAdES-C and + * XAdES-X-L. When null the signature will be + * limited to XAdES-T only. + * @param digestAlgorithm + * the digest algorithm to be used for construction of the + * XAdES-X-L elements. + */ + public XAdESXLSignatureFacet(TimeStampService timeStampService, + RevocationDataService revocationDataService, + HashAlgorithm digestAlgorithm) { + this.c14nAlgoId = CanonicalizationMethod.EXCLUSIVE; + this.hashAlgo = digestAlgorithm; + this.timeStampService = timeStampService; + this.revocationDataService = revocationDataService; + + try { + this.certificateFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("X509 JCA error: " + e.getMessage(), e); + } + } + + public void setCanonicalizerAlgorithm(String c14nAlgoId) { + this.c14nAlgoId = c14nAlgoId; + } + + public void postSign(SignatureType signatureElement, + List signingCertificateChain) { + LOG.log(POILogger.DEBUG, "XAdES-X-L post sign phase"); + + QualifyingPropertiesType qualProps = null; + + try { + // check for XAdES-BES + for (ObjectType ot : signatureElement.getObjectList()) { + XmlObject xo[] = ot.selectChildren(new QName(XADES_NAMESPACE, "QualifyingProperties")); + if (xo != null && xo.length > 0) { + qualProps = QualifyingPropertiesType.Factory.parse(xo[0].getDomNode()); + break; + } + } + } catch (XmlException e) { + throw new RuntimeException("signature decoding error", e); + } + + if (qualProps == null) { + throw new IllegalArgumentException("no XAdES-BES extension present"); + } + + // create basic XML container structure + UnsignedPropertiesType unsignedProps = qualProps.getUnsignedProperties(); + if (unsignedProps == null) { + unsignedProps = qualProps.addNewUnsignedProperties(); + } + UnsignedSignaturePropertiesType unsignedSigProps = unsignedProps.getUnsignedSignatureProperties(); + if (unsignedSigProps == null) { + unsignedSigProps = unsignedProps.addNewUnsignedSignatureProperties(); + } + + + // create the XAdES-T time-stamp + SignatureValueType svt = signatureElement.getSignatureValue(); + + RevocationData tsaRevocationDataXadesT = new RevocationData(); + LOG.log(POILogger.DEBUG, "creating XAdES-T time-stamp"); + XAdESTimeStampType signatureTimeStamp = createXAdESTimeStamp( + Collections.singletonList(svt.getDomNode()), + tsaRevocationDataXadesT, this.c14nAlgoId, + this.timeStampService); + + // marshal the XAdES-T extension + unsignedSigProps.addNewSignatureTimeStamp().set(signatureTimeStamp); + + // xadesv141::TimeStampValidationData + if (tsaRevocationDataXadesT.hasRevocationDataEntries()) { + ValidationDataType validationData = createValidationData(tsaRevocationDataXadesT); + SignatureInfo.insertXChild(unsignedSigProps, validationData); + } + + if (null == this.revocationDataService) { + /* + * Without revocation data service we cannot construct the XAdES-C + * extension. + */ + return; + } + + // XAdES-C: complete certificate refs + CompleteCertificateRefsType completeCertificateRefs = + unsignedSigProps.addNewCompleteCertificateRefs(); + + CertIDListType certIdList = completeCertificateRefs.addNewCertRefs(); + for (int certIdx = 1; certIdx < signingCertificateChain.size(); certIdx++) { + /* + * We skip the signing certificate itself according to section + * 4.4.3.2 of the XAdES 1.4.1 specification. + */ + X509Certificate certificate = signingCertificateChain.get(certIdx); + CertIDType certId = certIdList.addNewCert(); + XAdESSignatureFacet.setCertID(certId, certificate, this.hashAlgo, false); + } + + // XAdES-C: complete revocation refs + CompleteRevocationRefsType completeRevocationRefs = + unsignedSigProps.addNewCompleteRevocationRefs(); + RevocationData revocationData = this.revocationDataService + .getRevocationData(signingCertificateChain); + if (revocationData.hasCRLs()) { + CRLRefsType crlRefs = completeRevocationRefs.addNewCRLRefs(); + completeRevocationRefs.setCRLRefs(crlRefs); + + for (byte[] encodedCrl : revocationData.getCRLs()) { + CRLRefType crlRef = crlRefs.addNewCRLRef(); + X509CRL crl; + try { + crl = (X509CRL) this.certificateFactory + .generateCRL(new ByteArrayInputStream(encodedCrl)); + } catch (CRLException e) { + throw new RuntimeException("CRL parse error: " + + e.getMessage(), e); + } + + CRLIdentifierType crlIdentifier = crlRef.addNewCRLIdentifier(); + String issuerName = crl.getIssuerDN().getName().replace(",", ", "); + crlIdentifier.setIssuer(issuerName); + Calendar cal = Calendar.getInstance(); + cal.setTime(crl.getThisUpdate()); + crlIdentifier.setIssueTime(cal); + crlIdentifier.setNumber(getCrlNumber(crl)); + + DigestAlgAndValueType digestAlgAndValue = crlRef.addNewDigestAlgAndValue(); + XAdESSignatureFacet.setDigestAlgAndValue(digestAlgAndValue, encodedCrl, this.hashAlgo); + } + } + if (revocationData.hasOCSPs()) { + OCSPRefsType ocspRefs = completeRevocationRefs.addNewOCSPRefs(); + for (byte[] ocsp : revocationData.getOCSPs()) { + try { + OCSPRefType ocspRef = ocspRefs.addNewOCSPRef(); + + DigestAlgAndValueType digestAlgAndValue = ocspRef.addNewDigestAlgAndValue(); + XAdESSignatureFacet.setDigestAlgAndValue(digestAlgAndValue, ocsp, this.hashAlgo); + + OCSPIdentifierType ocspIdentifier = ocspRef.addNewOCSPIdentifier(); + + OCSPRespIf ocspResp = HorribleProxy.newProxy(OCSPRespIf.class, ocsp); + + BasicOCSPRespIf basicOcspResp = ocspResp.getResponseObject(); + + Calendar cal = Calendar.getInstance(); + cal.setTime(basicOcspResp.getProducedAt()); + ocspIdentifier.setProducedAt(cal); + + ResponderIDType responderId = ocspIdentifier.addNewResponderID(); + + RespIDIf respId = basicOcspResp.getResponderId(); + ResponderIDIf ocspResponderId = respId.toASN1Object(); + DERTaggedObjectIf derTaggedObject = ocspResponderId.toASN1Object(); + if (2 == derTaggedObject.getTagNo()) { + ASN1OctetStringIf keyHashOctetString = derTaggedObject.getObject$String(); + byte key[] = keyHashOctetString.getOctets(); + responderId.setByKey(key); + } else { + X509NameIf name = HorribleProxy.createProxy(X509NameIf.class, "getInstance", derTaggedObject.getObject$Object()); + String nameStr = name.toString$delegate(); + responderId.setByName(nameStr); + } + } catch (Exception e) { + throw new RuntimeException("OCSP decoding error: " + e.getMessage(), e); + } + } + } + + // marshal XAdES-C + + // XAdES-X Type 1 timestamp + + + + List timeStampNodesXadesX1 = new LinkedList(); + timeStampNodesXadesX1.add(signatureElement.getDomNode()); + timeStampNodesXadesX1.add(signatureTimeStamp.getDomNode()); + timeStampNodesXadesX1.add(completeCertificateRefs.getDomNode()); + timeStampNodesXadesX1.add(completeRevocationRefs.getDomNode()); + + RevocationData tsaRevocationDataXadesX1 = new RevocationData(); + LOG.log(POILogger.DEBUG, "creating XAdES-X time-stamp"); + XAdESTimeStampType timeStampXadesX1 = createXAdESTimeStamp( + timeStampNodesXadesX1, tsaRevocationDataXadesX1, + this.c14nAlgoId, this.timeStampService); + if (tsaRevocationDataXadesX1.hasRevocationDataEntries()) { + ValidationDataType timeStampXadesX1ValidationData = createValidationData(tsaRevocationDataXadesX1); + SignatureInfo.insertXChild(unsignedSigProps, timeStampXadesX1ValidationData); + } + + // marshal XAdES-X + + // XAdES-X-L + CertificateValuesType certificateValues = unsignedSigProps.addNewCertificateValues(); + for (X509Certificate certificate : signingCertificateChain) { + EncapsulatedPKIDataType encapsulatedPKIDataType = certificateValues.addNewEncapsulatedX509Certificate(); + try { + encapsulatedPKIDataType.setByteArrayValue(certificate.getEncoded()); + } catch (CertificateEncodingException e) { + throw new RuntimeException("certificate encoding error: " + e.getMessage(), e); + } + } + + RevocationValuesType revocationValues = unsignedSigProps.addNewRevocationValues(); + createRevocationValues(revocationValues, revocationData); + + // marshal XAdES-X-L + } + + public static byte[] getC14nValue(List nodeList, String c14nAlgoId) { + byte[] c14nValue = null; + try { + for (Node node : nodeList) { + /* + * Re-initialize the c14n else the namespaces will get cached + * and will be missing from the c14n resulting nodes. + */ + CanonicalizerIf c14n = HorribleProxy.createProxy(CanonicalizerIf.class, "newInstance", c14nAlgoId); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(c14nValue); + bos.write(c14n.canonicalizeSubtree(node)); + c14nValue = bos.toByteArray(); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("c14n error: " + e.getMessage(), e); + } + return c14nValue; + } + + public void preSign(XMLSignatureFactory signatureFactory, + String signatureId, + List signingCertificateChain, + List references, List objects) + throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + // nothing to do here + } + + private BigInteger getCrlNumber(X509CRL crl) { + byte[] crlNumberExtensionValue = crl.getExtensionValue("2.5.29.20" /*CRLNumber*/); + if (null == crlNumberExtensionValue) { + return null; + } + try { + ASN1InputStreamIf asn1InputStream = HorribleProxy.newProxy(ASN1InputStreamIf.class, crlNumberExtensionValue); + ASN1OctetStringIf octetString = asn1InputStream.readObject$ASNString(); + byte[] octets = octetString.getOctets(); + asn1InputStream = HorribleProxy.newProxy(ASN1InputStreamIf.class, octets); + DERIntegerIf integer = asn1InputStream.readObject$Integer(); + BigInteger crlNumber = integer.getPositiveValue(); + return crlNumber; + } catch (Exception e) { + throw new RuntimeException("I/O error: " + e.getMessage(), e); + } + } + + public static XAdESTimeStampType createXAdESTimeStamp( + List nodeList, + RevocationData revocationData, + String c14nAlgoId, + TimeStampService timeStampService) { + byte[] c14nSignatureValueElement = getC14nValue(nodeList, c14nAlgoId); + + return createXAdESTimeStamp(c14nSignatureValueElement, revocationData, + c14nAlgoId, timeStampService); + } + + public static XAdESTimeStampType createXAdESTimeStamp( + byte[] data, + RevocationData revocationData, + String c14nAlgoId, + TimeStampService timeStampService) { + // create the time-stamp + byte[] timeStampToken; + try { + timeStampToken = timeStampService.timeStamp(data, revocationData); + } catch (Exception e) { + throw new RuntimeException("error while creating a time-stamp: " + + e.getMessage(), e); + } + + // create a XAdES time-stamp container + XAdESTimeStampType xadesTimeStamp = XAdESTimeStampType.Factory.newInstance(); + xadesTimeStamp.setId("time-stamp-" + UUID.randomUUID().toString()); + CanonicalizationMethodType c14nMethod = xadesTimeStamp.addNewCanonicalizationMethod(); + c14nMethod.setAlgorithm(c14nAlgoId); + + // embed the time-stamp + EncapsulatedPKIDataType encapsulatedTimeStamp = xadesTimeStamp.addNewEncapsulatedTimeStamp(); + encapsulatedTimeStamp.setByteArrayValue(timeStampToken); + encapsulatedTimeStamp.setId("time-stamp-token-" + UUID.randomUUID().toString()); + + return xadesTimeStamp; + } + + private ValidationDataType createValidationData( + RevocationData revocationData) { + ValidationDataType validationData = ValidationDataType.Factory.newInstance(); + RevocationValuesType revocationValues = validationData.addNewRevocationValues(); + createRevocationValues(revocationValues, revocationData); + return validationData; + } + + private void createRevocationValues( + RevocationValuesType revocationValues, RevocationData revocationData) { + if (revocationData.hasCRLs()) { + CRLValuesType crlValues = revocationValues.addNewCRLValues(); + for (byte[] crl : revocationData.getCRLs()) { + EncapsulatedPKIDataType encapsulatedCrlValue = crlValues.addNewEncapsulatedCRLValue(); + encapsulatedCrlValue.setByteArrayValue(crl); + } + } + if (revocationData.hasOCSPs()) { + OCSPValuesType ocspValues = revocationValues.addNewOCSPValues(); + for (byte[] ocsp : revocationData.getOCSPs()) { + EncapsulatedPKIDataType encapsulatedOcspValue = ocspValues.addNewEncapsulatedOCSPValue(); + encapsulatedOcspValue.setByteArrayValue(ocsp); + } + } + } + + public Map getNamespacePrefixMapping() { + return null; + } + +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RelationshipTransformService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RelationshipTransformService.java new file mode 100644 index 0000000000..7769bb7910 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RelationshipTransformService.java @@ -0,0 +1,249 @@ +/* ==================================================================== + 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.services; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.Provider; +import java.security.Security; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.crypto.Data; +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.OctetStreamData; +import javax.xml.crypto.XMLCryptoContext; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMStructure; +import javax.xml.crypto.dsig.TransformException; +import javax.xml.crypto.dsig.TransformService; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; + +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.poi.util.XmlSort; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; +import org.apache.xmlbeans.XmlOptions; +import org.openxmlformats.schemas.xpackage.x2006.digitalSignature.RelationshipReferenceDocument; +import org.openxmlformats.schemas.xpackage.x2006.relationships.CTRelationship; +import org.openxmlformats.schemas.xpackage.x2006.relationships.CTRelationships; +import org.openxmlformats.schemas.xpackage.x2006.relationships.RelationshipsDocument; +import org.openxmlformats.schemas.xpackage.x2006.relationships.STTargetMode; +import org.w3.x2000.x09.xmldsig.TransformDocument; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * JSR105 implementation of the RelationshipTransform transformation. + * + *

+ * Specs: http://openiso.org/Ecma/376/Part2/12.2.4#26 + *

+ */ +public class RelationshipTransformService extends TransformService { + + public static final String TRANSFORM_URI = "http://schemas.openxmlformats.org/package/2006/RelationshipTransform"; + + private final List sourceIds; + + private static final POILogger LOG = POILogFactory.getLogger(RelationshipTransformService.class); + + /** + * Relationship Transform parameter specification class. + */ + public static class RelationshipTransformParameterSpec implements TransformParameterSpec { + List sourceIds = new LinkedList(); + public void addRelationshipReference(String relationshipId) { + sourceIds.add(relationshipId); + } + public boolean hasSourceIds() { + return !sourceIds.isEmpty(); + } + } + + + public RelationshipTransformService() { + super(); + LOG.log(POILogger.DEBUG, "constructor"); + this.sourceIds = new LinkedList(); + } + + /** + * Register the provider for this TransformService + * + * @see javax.xml.crypto.dsig.TransformService + */ + public static synchronized void registerDsigProvider() { + // the xml signature classes will try to find a special TransformerService, + // which is ofcourse unknown to JCE before ... + final String dsigProvider = "POIXmlDsigProvider"; + if (Security.getProperty(dsigProvider) == null) { + Provider p = new Provider(dsigProvider, 1.0, dsigProvider){ + static final long serialVersionUID = 1L; + }; + p.put("TransformService." + TRANSFORM_URI, RelationshipTransformService.class.getName()); + p.put("TransformService." + TRANSFORM_URI + " MechanismType", "DOM"); + Security.addProvider(p); + } + } + + + @Override + public void init(TransformParameterSpec params) throws InvalidAlgorithmParameterException { + LOG.log(POILogger.DEBUG, "init(params)"); + if (!(params instanceof RelationshipTransformParameterSpec)) { + throw new InvalidAlgorithmParameterException(); + } + RelationshipTransformParameterSpec relParams = (RelationshipTransformParameterSpec) params; + for (String sourceId : relParams.sourceIds) { + this.sourceIds.add(sourceId); + } + } + + @Override + public void init(XMLStructure parent, XMLCryptoContext context) throws InvalidAlgorithmParameterException { + LOG.log(POILogger.DEBUG, "init(parent,context)"); + LOG.log(POILogger.DEBUG, "parent java type: " + parent.getClass().getName()); + DOMStructure domParent = (DOMStructure) parent; + Node parentNode = domParent.getNode(); + + try { + TransformDocument transDoc = TransformDocument.Factory.parse(parentNode); + XmlObject xoList[] = transDoc.getTransform().selectChildren(RelationshipReferenceDocument.type.getDocumentElementName()); + if (xoList.length == 0) { + LOG.log(POILogger.WARN, "no RelationshipReference/@SourceId parameters present"); + } + for (XmlObject xo : xoList) { + RelationshipReferenceDocument refDoc = + RelationshipReferenceDocument.Factory.parse(xo.getDomNode()); + String sourceId = refDoc.getRelationshipReference().getSourceId(); + LOG.log(POILogger.DEBUG, "sourceId: ", sourceId); + this.sourceIds.add(sourceId); + } + } catch (XmlException e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + @Override + public void marshalParams(XMLStructure parent, XMLCryptoContext context) throws MarshalException { + LOG.log(POILogger.DEBUG, "marshallParams(parent,context)"); + DOMStructure domParent = (DOMStructure) parent; + Element parentNode = (Element)domParent.getNode(); + // parentNode.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:mdssi", DIGITAL_SIGNATURE); + Document doc = parentNode.getOwnerDocument(); + + for (String sourceId : this.sourceIds) { + RelationshipReferenceDocument relRef = RelationshipReferenceDocument.Factory.newInstance(); + relRef.addNewRelationshipReference().setSourceId(sourceId); + Node n = relRef.getRelationshipReference().getDomNode(); + // TODO: is there a more elegant way to do this? + n.setPrefix("mdssi"); + n = doc.importNode(n, true); + parentNode.appendChild(n); + } + } + + public AlgorithmParameterSpec getParameterSpec() { + LOG.log(POILogger.DEBUG, "getParameterSpec"); + return null; + } + + public Data transform(Data data, XMLCryptoContext context) throws TransformException { + LOG.log(POILogger.DEBUG, "transform(data,context)"); + LOG.log(POILogger.DEBUG, "data java type: " + data.getClass().getName()); + OctetStreamData octetStreamData = (OctetStreamData) data; + LOG.log(POILogger.DEBUG, "URI: " + octetStreamData.getURI()); + InputStream octetStream = octetStreamData.getOctetStream(); + + RelationshipsDocument relDoc; + try { + relDoc = RelationshipsDocument.Factory.parse(octetStream); + } catch (Exception e) { + throw new TransformException(e.getMessage(), e); + } + LOG.log(POILogger.DEBUG, "relationships document", relDoc); + + CTRelationships rels = relDoc.getRelationships(); + List relList = rels.getRelationshipList(); + Iterator relIter = rels.getRelationshipList().iterator(); + while (relIter.hasNext()) { + CTRelationship rel = relIter.next(); + /* + * See: ISO/IEC 29500-2:2008(E) - 13.2.4.24 Relationships Transform + * Algorithm. + */ + if (!this.sourceIds.contains(rel.getId())) { + LOG.log(POILogger.DEBUG, "removing element: " + rel.getId()); + relIter.remove(); + } else { + if (!rel.isSetTargetMode()) { + rel.setTargetMode(STTargetMode.INTERNAL); + } + } + } + + // TODO: remove non element nodes ??? + LOG.log(POILogger.DEBUG, "# Relationship elements", relList.size()); + + XmlSort.sort(rels, new Comparator(){ + public int compare(XmlCursor c1, XmlCursor c2) { + String id1 = ((CTRelationship)c1.getObject()).getId(); + String id2 = ((CTRelationship)c2.getObject()).getId(); + return id1.compareTo(id2); + } + }); + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + XmlOptions xo = new XmlOptions(); + xo.setSaveNoXmlDecl(); + relDoc.save(bos, xo); + return new OctetStreamData(new ByteArrayInputStream(bos.toByteArray())); + } catch (IOException e) { + throw new TransformException(e.getMessage(), e); + } + } + + public Data transform(Data data, XMLCryptoContext context, OutputStream os) throws TransformException { + LOG.log(POILogger.DEBUG, "transform(data,context,os)"); + return null; + } + + public boolean isFeatureSupported(String feature) { + LOG.log(POILogger.DEBUG, "isFeatureSupported(feature)"); + return false; + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java new file mode 100644 index 0000000000..31595c39e3 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationData.java @@ -0,0 +1,131 @@ +/* ==================================================================== + 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.services; + +import java.security.cert.CRLException; +import java.security.cert.X509CRL; +import java.util.LinkedList; +import java.util.List; + +/** + * Container class for PKI revocation data. + * + * @author Frank Cornelis + * + */ +public class RevocationData { + + private final List crls; + + private final List ocsps; + + /** + * Default constructor. + */ + public RevocationData() { + this.crls = new LinkedList(); + this.ocsps = new LinkedList(); + } + + /** + * Adds a CRL to this revocation data set. + * + * @param encodedCrl + */ + public void addCRL(byte[] encodedCrl) { + this.crls.add(encodedCrl); + } + + /** + * Adds a CRL to this revocation data set. + * + * @param crl + */ + public void addCRL(X509CRL crl) { + byte[] encodedCrl; + try { + encodedCrl = crl.getEncoded(); + } catch (CRLException e) { + throw new IllegalArgumentException("CRL coding error: " + + e.getMessage(), e); + } + addCRL(encodedCrl); + } + + /** + * Adds an OCSP response to this revocation data set. + * + * @param encodedOcsp + */ + public void addOCSP(byte[] encodedOcsp) { + this.ocsps.add(encodedOcsp); + } + + /** + * Gives back a list of all CRLs. + * + * @return + */ + public List getCRLs() { + return this.crls; + } + + /** + * Gives back a list of all OCSP responses. + * + * @return + */ + public List getOCSPs() { + return this.ocsps; + } + + /** + * Returns true if this revocation data set holds OCSP + * responses. + * + * @return + */ + public boolean hasOCSPs() { + return false == this.ocsps.isEmpty(); + } + + /** + * Returns true if this revocation data set holds CRLs. + * + * @return + */ + public boolean hasCRLs() { + return false == this.crls.isEmpty(); + } + + /** + * Returns true if this revocation data is not empty. + * + * @return + */ + public boolean hasRevocationDataEntries() { + return hasOCSPs() || hasCRLs(); + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationDataService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationDataService.java new file mode 100644 index 0000000000..b519c40e3d --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/RevocationDataService.java @@ -0,0 +1,47 @@ +/* ==================================================================== + 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.services; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for a service that retrieves revocation data about some given + * certificate chain. + * + * @author Frank Cornelis + * + */ +public interface RevocationDataService { + + /** + * Gives back the revocation data corresponding with the given certificate + * chain. + * + * @param certificateChain + * @return + */ + RevocationData getRevocationData(List certificateChain); +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/SignatureService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/SignatureService.java new file mode 100644 index 0000000000..4057807634 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/SignatureService.java @@ -0,0 +1,101 @@ +/* ==================================================================== + 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.services; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.List; + +import org.apache.poi.poifs.crypt.dsig.CertificateSecurityException; +import org.apache.poi.poifs.crypt.dsig.ExpiredCertificateSecurityException; +import org.apache.poi.poifs.crypt.dsig.RevokedCertificateSecurityException; +import org.apache.poi.poifs.crypt.dsig.TrustCertificateSecurityException; +import org.apache.poi.poifs.crypt.dsig.spi.AddressDTO; +import org.apache.poi.poifs.crypt.dsig.spi.DigestInfo; +import org.apache.poi.poifs.crypt.dsig.spi.IdentityDTO; + +/** + * Interface for signature service component. + * + * @author Frank Cornelis + * + */ +public interface SignatureService { + + /** + * Gives back the digest algorithm to be used for construction of the digest + * infos of the preSign method. Return a digest algorithm here if you want + * to let the client sign some locally stored files. Return + * null if no pre-sign digest infos are required. + * + * @return the digest algorithm to be used when digesting local files. + * @see #preSign(List, List) + */ + String getFilesDigestAlgorithm(); + + /** + * Pre-sign callback method. Depending on the configuration some parameters + * are passed. The returned value will be signed by the eID Applet. + * + *

+ * TODO: service must be able to throw some exception on failure. + *

+ * + * @param digestInfos + * the optional list of digest infos. + * @param signingCertificateChain + * the optional list of certificates. + * @param identity + * the optional identity. + * @param address + * the optional identity address. + * @param photo + * the optional identity photo. + * @param timestamp + * the optional timestamp, defaults to now + * @return the digest to be signed. + * @throws NoSuchAlgorithmException + */ + DigestInfo preSign(List digestInfos, + List signingCertificateChain, + IdentityDTO identity, AddressDTO address, byte[] photo) + throws NoSuchAlgorithmException; + + /** + * Post-sign callback method. Received the signature value. Depending on the + * configuration the signing certificate chain is also obtained. + * + * @param signatureValue + * @param signingCertificateChain + * the optional chain of signing certificates. + */ + void postSign(byte[] signatureValue, + List signingCertificateChain) + throws ExpiredCertificateSecurityException, + RevokedCertificateSecurityException, + TrustCertificateSecurityException, CertificateSecurityException, + SecurityException, IOException; +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java new file mode 100644 index 0000000000..d0ba961eb9 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TSPTimeStampService.java @@ -0,0 +1,392 @@ +/* ==================================================================== + 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.services; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.security.auth.x500.X500Principal; +import javax.xml.bind.DatatypeConverter; + +import org.apache.commons.codec.binary.Hex; +import org.apache.poi.poifs.crypt.CryptoFunctions; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.ASN1InputStreamIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.ASN1OctetStringIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.AuthorityKeyIdentifierIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.BcDigestCalculatorProviderIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.BcRSASignerInfoVerifierBuilderIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DEROctetStringIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DefaultDigestAlgorithmIdentifierFinderIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.PKIFailureInfoIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.SignerIdIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.SignerInformationVerifierIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.SubjectKeyIdentifierIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.TimeStampRequestGeneratorIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.TimeStampRequestIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.TimeStampResponseIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.TimeStampTokenIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.X509CertificateHolderIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxy; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; + +/** + * A TSP time-stamp service implementation. + * + * @author Frank Cornelis + * + */ +public class TSPTimeStampService implements TimeStampService { + + private static final POILogger LOG = POILogFactory.getLogger(TSPTimeStampService.class); + + static { + CryptoFunctions.registerBouncyCastle(); + } + + public static final String DEFAULT_USER_AGENT = "eID Applet Service TSP Client"; + + private final String tspServiceUrl; + + private String requestPolicy; + + private final String userAgent; + + private final TimeStampServiceValidator validator; + + private String username; + + private String password; + + private String proxyHost; + + private int proxyPort; + + private String digestAlgo; + + private String digestAlgoOid; + + public TSPTimeStampService(String tspServiceUrl, + TimeStampServiceValidator validator) { + this(tspServiceUrl, validator, null, null); + } + + /** + * Main constructor. + * + * @param tspServiceUrl + * the URL of the TSP service. + * @param validator + * the trust validator used to validate incoming TSP response + * signatures. + * @param requestPolicy + * the optional TSP request policy. + * @param userAgent + * the optional User-Agent TSP request header value. + */ + public TSPTimeStampService(String tspServiceUrl, + TimeStampServiceValidator validator, String requestPolicy, + String userAgent) { + if (null == tspServiceUrl) { + throw new IllegalArgumentException("TSP service URL required"); + } + this.tspServiceUrl = tspServiceUrl; + + if (null == validator) { + throw new IllegalArgumentException("TSP validator required"); + } + this.validator = validator; + + this.requestPolicy = requestPolicy; + + if (null != userAgent) { + this.userAgent = userAgent; + } else { + this.userAgent = DEFAULT_USER_AGENT; + } + + this.digestAlgo = "SHA-1"; + this.digestAlgoOid = "1.3.14.3.2.26"; + } + + /** + * Sets the request policy OID. + * + * @param policyOid + */ + public void setRequestPolicy(String policyOid) { + this.requestPolicy = policyOid; + } + + /** + * Sets the credentials used in case the TSP service requires + * authentication. + * + * @param username + * @param password + */ + public void setAuthenticationCredentials(String username, String password) { + this.username = username; + this.password = password; + } + + /** + * Resets the authentication credentials. + */ + public void resetAuthenticationCredentials() { + this.username = null; + this.password = null; + } + + /** + * Sets the digest algorithm used for time-stamping data. Example value: + * "SHA-1". + * + * @param digestAlgo + */ + public void setDigestAlgo(String digestAlgo) { + if ("SHA-1".equals(digestAlgo)) { + this.digestAlgoOid = "1.3.14.3.2.26"; + } else if ("SHA-256".equals(digestAlgo)) { + this.digestAlgoOid = "2.16.840.1.101.3.4.2.1"; + } else if ("SHA-384".equals(digestAlgo)) { + this.digestAlgoOid = "2.16.840.1.101.3.4.2.2"; + } else if ("SHA-512".equals(digestAlgo)) { + this.digestAlgoOid = "2.16.840.1.101.3.4.2.3"; + } else { + throw new IllegalArgumentException("unsupported digest algo: " + digestAlgo); + } + + this.digestAlgo = digestAlgo; + } + + /** + * Configures the HTTP proxy settings to be used to connect to the TSP + * service. + * + * @param proxyHost + * @param proxyPort + */ + public void setProxy(String proxyHost, int proxyPort) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + } + + /** + * Resets the HTTP proxy settings. + */ + public void resetProxy() { + this.proxyHost = null; + this.proxyPort = 0; + } + + public byte[] timeStamp(byte[] data, RevocationData revocationData) + throws Exception { + // digest the message + MessageDigest messageDigest = MessageDigest + .getInstance(this.digestAlgo); + byte[] digest = messageDigest.digest(data); + + // generate the TSP request + BigInteger nonce = new BigInteger(128, new SecureRandom()); + TimeStampRequestGeneratorIf requestGenerator = HorribleProxy.newProxy(TimeStampRequestGeneratorIf.class); + requestGenerator.setCertReq(true); + if (null != this.requestPolicy) { + requestGenerator.setReqPolicy(this.requestPolicy); + } + TimeStampRequestIf request = requestGenerator.generate(this.digestAlgoOid, digest, nonce); + byte[] encodedRequest = request.getEncoded(); + + // create the HTTP POST request + Proxy proxy = (this.proxyHost != null) + ? new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.proxyHost, this.proxyPort)) + : Proxy.NO_PROXY; + HttpURLConnection huc = (HttpURLConnection)new URL(this.tspServiceUrl).openConnection(proxy); + + if (null != this.username) { + String userPassword = this.username + ":" + this.password; + String encoding = DatatypeConverter.printBase64Binary(userPassword.getBytes(Charset.forName("iso-8859-1"))); + huc.setRequestProperty("Authorization", "Basic " + encoding); + } + + huc.setDoOutput(true); // also sets method to POST. + huc.setRequestProperty("User-Agent", this.userAgent); + huc.setRequestProperty("Content-Type", "application/timestamp-query;charset=ISO-8859-1"); + + OutputStream hucOut = huc.getOutputStream(); + hucOut.write(encodedRequest); + + // invoke TSP service + huc.connect(); + + int statusCode = huc.getResponseCode(); + if (statusCode != 200) { + LOG.log(POILogger.ERROR, "Error contacting TSP server ", this.tspServiceUrl); + throw new Exception("Error contacting TSP server " + this.tspServiceUrl); + } + + // HTTP input validation + String contentType = huc.getHeaderField("Content-Type"); + if (null == contentType) { + throw new RuntimeException("missing Content-Type header"); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(huc.getInputStream(), bos); + LOG.log(POILogger.DEBUG, "response content: ", bos.toString()); + + if (!contentType.startsWith("application/timestamp-reply")) { + throw new RuntimeException("invalid Content-Type: " + contentType); + } + + if (bos.size() == 0) { + throw new RuntimeException("Content-Length is zero"); + } + + // TSP response parsing and validation + TimeStampResponseIf timeStampResponse = HorribleProxy.newProxy(TimeStampResponseIf.class, bos.toByteArray()); + timeStampResponse.validate(request); + + if (0 != timeStampResponse.getStatus()) { + LOG.log(POILogger.DEBUG, "status: " + timeStampResponse.getStatus()); + LOG.log(POILogger.DEBUG, "status string: " + timeStampResponse.getStatusString()); + PKIFailureInfoIf failInfo = timeStampResponse.getFailInfo(); + if (null != failInfo) { + LOG.log(POILogger.DEBUG, "fail info int value: " + failInfo.intValue()); + if (/*PKIFailureInfo.unacceptedPolicy*/(1 << 8) == failInfo.intValue()) { + LOG.log(POILogger.DEBUG, "unaccepted policy"); + } + } + throw new RuntimeException("timestamp response status != 0: " + + timeStampResponse.getStatus()); + } + TimeStampTokenIf timeStampToken = timeStampResponse.getTimeStampToken(); + SignerIdIf signerId = timeStampToken.getSID(); + BigInteger signerCertSerialNumber = signerId.getSerialNumber(); + X500Principal signerCertIssuer = signerId.getIssuer(); + LOG.log(POILogger.DEBUG, "signer cert serial number: " + signerCertSerialNumber); + LOG.log(POILogger.DEBUG, "signer cert issuer: " + signerCertIssuer); + + // TSP signer certificates retrieval + Collection certificates = timeStampToken.getCertificates().getMatches(null); + + X509Certificate signerCert = null; + Map certificateMap = new HashMap(); + for (Certificate certificate : certificates) { + X509Certificate x509Certificate = (X509Certificate) certificate; + if (signerCertIssuer.equals(x509Certificate + .getIssuerX500Principal()) + && signerCertSerialNumber.equals(x509Certificate + .getSerialNumber())) { + signerCert = x509Certificate; + } + String ski = Hex.encodeHexString(getSubjectKeyId(x509Certificate)); + certificateMap.put(ski, x509Certificate); + LOG.log(POILogger.DEBUG, "embedded certificate: " + + x509Certificate.getSubjectX500Principal() + "; SKI=" + + ski); + } + + // TSP signer cert path building + if (null == signerCert) { + throw new RuntimeException( + "TSP response token has no signer certificate"); + } + List tspCertificateChain = new LinkedList(); + X509Certificate certificate = signerCert; + do { + LOG.log(POILogger.DEBUG, "adding to certificate chain: " + + certificate.getSubjectX500Principal()); + tspCertificateChain.add(certificate); + if (certificate.getSubjectX500Principal().equals( + certificate.getIssuerX500Principal())) { + break; + } + String aki = Hex.encodeHexString(getAuthorityKeyId(certificate)); + certificate = certificateMap.get(aki); + } while (null != certificate); + + // verify TSP signer signature + X509CertificateHolderIf holder = HorribleProxy.newProxy(X509CertificateHolderIf.class, tspCertificateChain.get(0).getEncoded()); + DefaultDigestAlgorithmIdentifierFinderIf finder = HorribleProxy.newProxy(DefaultDigestAlgorithmIdentifierFinderIf.class); + BcDigestCalculatorProviderIf calculator = HorribleProxy.newProxy(BcDigestCalculatorProviderIf.class); + BcRSASignerInfoVerifierBuilderIf verifierBuilder = HorribleProxy.newProxy(BcRSASignerInfoVerifierBuilderIf.class, finder, calculator); + SignerInformationVerifierIf verifier = verifierBuilder.build(holder); + + timeStampToken.validate(verifier); + + // verify TSP signer certificate + this.validator.validate(tspCertificateChain, revocationData); + + LOG.log(POILogger.DEBUG, "time-stamp token time: " + + timeStampToken.getTimeStampInfo().getGenTime()); + + byte[] timestamp = timeStampToken.getEncoded(); + return timestamp; + } + + private byte[] getSubjectKeyId(X509Certificate cert) throws Exception { + // X509Extensions.SubjectKeyIdentifier.getId() + byte[] extvalue = cert.getExtensionValue("2.5.29.14"); + if (extvalue == null) return null; + + ASN1InputStreamIf keyCntStream = HorribleProxy.newProxy(ASN1InputStreamIf.class, new ByteArrayInputStream(extvalue)); + ASN1OctetStringIf cntStr = HorribleProxy.createProxy(ASN1OctetStringIf.class, "getInstance", keyCntStream.readObject$Object()); + ASN1InputStreamIf keyIdStream = HorribleProxy.newProxy(ASN1InputStreamIf.class, new ByteArrayInputStream(cntStr.getOctets())); + SubjectKeyIdentifierIf keyId = HorribleProxy.createProxy(SubjectKeyIdentifierIf.class, "getInstance", keyIdStream.readObject$Object()); + + return keyId.getKeyIdentifier(); + } + + private byte[] getAuthorityKeyId(X509Certificate cert) throws Exception { + // X509Extensions.AuthorityKeyIdentifier.getId() + byte[] extvalue = cert.getExtensionValue("2.5.29.35"); + if (extvalue == null) return null; + + ASN1InputStreamIf keyCntStream = HorribleProxy.newProxy(ASN1InputStreamIf.class, new ByteArrayInputStream(extvalue)); + DEROctetStringIf cntStr = keyCntStream.readObject$DERString(); + ASN1InputStreamIf keyIdStream = HorribleProxy.newProxy(ASN1InputStreamIf.class, new ByteArrayInputStream(cntStr.getOctets())); + AuthorityKeyIdentifierIf keyId = HorribleProxy.newProxy(AuthorityKeyIdentifierIf.class, keyIdStream.readObject$Sequence()); + + return keyId.getKeyIdentifier(); + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampService.java new file mode 100644 index 0000000000..dd9474e65e --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampService.java @@ -0,0 +1,52 @@ +/* ==================================================================== + 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.services; + + +/** + * Interface for a time-stamp service. + * + * @author Frank Cornelis + * + */ +public interface TimeStampService { + + /** + * Gives back the encoded time-stamp token for the given array of data + * bytes. We assume that the time-stamp token itself contains its full + * certificate chain required for proper validation. + * + * @param data + * the data to be time-stamped. + * @param revocationData + * the optional container that needs to be filled up with the + * revocation data used to validate the TSA certificate chain. + * @return the DER encoded time-stamp token. + * @throws Exception + * in case something went wrong. + */ + byte[] timeStamp(byte[] data, RevocationData revocationData) + throws Exception; +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampServiceValidator.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampServiceValidator.java new file mode 100644 index 0000000000..4d36be9ec5 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/TimeStampServiceValidator.java @@ -0,0 +1,51 @@ +/* ==================================================================== + 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.services; + +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Interface for trust validator of a TSP. + * + * @author Frank Cornelis + * + */ +public interface TimeStampServiceValidator { + + /** + * Validates the given certificate chain. + * + * @param certificateChain + * @param revocationData + * the optional data container that should be filled with + * revocation data that was used to validate the given + * certificate chain. + * @throws Exception + * in case the certificate chain is invalid. + */ + void validate(List certificateChain, + RevocationData revocationData) throws Exception; +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/XmlSignatureService.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/XmlSignatureService.java new file mode 100644 index 0000000000..c09501a4a1 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/services/XmlSignatureService.java @@ -0,0 +1,610 @@ +/* ==================================================================== + 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.services; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.URIDereferencer; +import javax.xml.crypto.XMLStructure; +import javax.xml.crypto.dom.DOMCryptoContext; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Manifest; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignatureMethod; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.XMLObject; +import javax.xml.crypto.dsig.XMLSignContext; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactoryConfigurationError; + +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageNamespaces; +import org.apache.poi.openxml4j.opc.PackagePart; +import org.apache.poi.openxml4j.opc.PackagePartName; +import org.apache.poi.openxml4j.opc.PackageRelationship; +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.CryptoFunctions; +import org.apache.poi.poifs.crypt.HashAlgorithm; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DOMReferenceIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DOMSignedInfoIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DOMXMLSignatureIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.XMLSignatureIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxy; +import org.apache.poi.poifs.crypt.dsig.OOXMLURIDereferencer; +import org.apache.poi.poifs.crypt.dsig.SignatureInfo; +import org.apache.poi.poifs.crypt.dsig.facets.KeyInfoSignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.OOXMLSignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.Office2010SignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet; +import org.apache.poi.poifs.crypt.dsig.spi.AddressDTO; +import org.apache.poi.poifs.crypt.dsig.spi.Constants; +import org.apache.poi.poifs.crypt.dsig.spi.DigestInfo; +import org.apache.poi.poifs.crypt.dsig.spi.IdentityDTO; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlOptions; +import org.w3.x2000.x09.xmldsig.SignatureDocument; +import org.w3.x2000.x09.xmldsig.SignatureType; +import org.w3.x2000.x09.xmldsig.SignatureValueType; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + + +/** + * Abstract base class for an XML Signature Service implementation. + */ +public class XmlSignatureService implements SignatureService { + private static final POILogger LOG = POILogFactory.getLogger(XmlSignatureService.class); + + protected final List signatureFacets; + + private String signatureNamespacePrefix; + private String signatureId; + private final HashAlgorithm hashAlgo; + private final OPCPackage opcPackage; + private SignatureDocument sigDoc; + private XAdESSignatureFacet xadesSignatureFacet; + + /** + * Main constructor. + */ + public XmlSignatureService(HashAlgorithm digestAlgo, OPCPackage opcPackage) { + this.signatureFacets = new LinkedList(); + this.signatureNamespacePrefix = null; + this.signatureId = null; + this.hashAlgo = digestAlgo; + this.opcPackage = opcPackage; + this.sigDoc = null; + } + + public void initFacets(Date clock) { + if (clock == null) clock = new Date(); + addSignatureFacet(new OOXMLSignatureFacet(this, clock, hashAlgo)); + addSignatureFacet(new KeyInfoSignatureFacet(true, false, false)); + + this.xadesSignatureFacet = new XAdESSignatureFacet(clock, hashAlgo, null); + this.xadesSignatureFacet.setIdSignedProperties("idSignedProperties"); + this.xadesSignatureFacet.setSignaturePolicyImplied(true); + /* + * Work-around for Office 2010. + */ + this.xadesSignatureFacet.setIssuerNameNoReverseOrder(true); + setSignatureId("idPackageSignature"); + addSignatureFacet(this.xadesSignatureFacet); + addSignatureFacet(new Office2010SignatureFacet()); + } + + + /** + * Sets the signature Id attribute value used to create the XML signature. A + * null value will trigger an automatically generated signature + * Id. + * + * @param signatureId + */ + protected void setSignatureId(String signatureId) { + this.signatureId = signatureId; + } + + /** + * Sets the XML Signature namespace prefix to be used for signature + * creation. A null value will omit the prefixing. + * + * @param signatureNamespacePrefix + */ + protected void setSignatureNamespacePrefix(String signatureNamespacePrefix) { + this.signatureNamespacePrefix = signatureNamespacePrefix; + } + + /** + * Adds a signature facet to this XML signature service. + * + * @param signatureFacet + */ + protected void addSignatureFacet(SignatureFacet signatureFacet) { + this.signatureFacets.add(signatureFacet); + } + + /** + * Gives back the signature digest algorithm. Allowed values are SHA-1, + * SHA-256, SHA-384, SHA-512, RIPEND160. The default algorithm is SHA-1. + * Override this method to select another signature digest algorithm. + * + * @return + */ + protected HashAlgorithm getSignatureDigestAlgorithm() { + return null != this.hashAlgo ? this.hashAlgo : HashAlgorithm.sha1; + } + + /** + * Override this method to change the URI dereferener used by the signing + * engine. + * + * @return + */ + protected URIDereferencer getURIDereferencer() { + OPCPackage ooxmlDocument = getOfficeOpenXMLDocument(); + return new OOXMLURIDereferencer(ooxmlDocument); + } + + /** + * Gives back the human-readable description of what the citizen will be + * signing. The default value is "XML Document". Override this method to + * provide the citizen with another description. + * + * @return + */ + protected String getSignatureDescription() { + return "Office OpenXML Document"; + } + + /** + * Gives back the URL of the OOXML to be signed. + * + * @return + */ + public OPCPackage getOfficeOpenXMLDocument() { + return opcPackage; + } + + + + /** + * Gives back the output stream to which to write the signed XML document. + * + * @return + */ + // protected abstract OutputStream getSignedDocumentOutputStream(); + + public DigestInfo preSign(List digestInfos, + List signingCertificateChain, + IdentityDTO identity, AddressDTO address, byte[] photo) + throws NoSuchAlgorithmException { + SignatureInfo.initXmlProvider(); + + LOG.log(POILogger.DEBUG, "preSign"); + HashAlgorithm hashAlgo = getSignatureDigestAlgorithm(); + + byte[] digestValue; + try { + digestValue = getXmlSignatureDigestValue(hashAlgo, digestInfos, signingCertificateChain); + } catch (Exception e) { + throw new RuntimeException("XML signature error: " + e.getMessage(), e); + } + + String description = getSignatureDescription(); + return new DigestInfo(digestValue, hashAlgo, description); + } + + public void postSign(byte[] signatureValue, List signingCertificateChain) + throws IOException { + LOG.log(POILogger.DEBUG, "postSign"); + SignatureInfo.initXmlProvider(); + + /* + * Retrieve the intermediate XML signature document from the temporary + * data storage. + */ + SignatureType sigType = sigDoc.getSignature(); + + /* + * Check ds:Signature node. + */ + if (!signatureId.equals(sigType.getId())) { + throw new RuntimeException("ds:Signature not found for @Id: " + signatureId); + } + + /* + * Insert signature value into the ds:SignatureValue element + */ + SignatureValueType sigVal = sigType.getSignatureValue(); + sigVal.setByteArrayValue(signatureValue); + + /* + * Allow signature facets to inject their own stuff. + */ + for (SignatureFacet signatureFacet : this.signatureFacets) { + signatureFacet.postSign(sigType, signingCertificateChain); + } + + writeDocument(); + } + + @SuppressWarnings("unchecked") + private byte[] getXmlSignatureDigestValue(HashAlgorithm hashAlgo, + List digestInfos, + List signingCertificateChain) + throws ParserConfigurationException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, MarshalException, + javax.xml.crypto.dsig.XMLSignatureException, + TransformerFactoryConfigurationError, TransformerException, + IOException, SAXException, NoSuchProviderException, XmlException { + /* + * DOM Document construction. + */ + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + Document doc = dbf.newDocumentBuilder().newDocument(); + + /* + * Signature context construction. + */ + Key key = new Key() { + private static final long serialVersionUID = 1L; + + public String getAlgorithm() { + return null; + } + + public byte[] getEncoded() { + return null; + } + + public String getFormat() { + return null; + } + }; + + // As of JDK 7, can't use sigDoc here directly, because the + // setAttributeID will be called and it's not implemented in xmlbeans + XMLSignContext xmlSignContext = new DOMSignContext(key, doc); + URIDereferencer uriDereferencer = getURIDereferencer(); + if (null != uriDereferencer) { + xmlSignContext.setURIDereferencer(uriDereferencer); + } + + xmlSignContext.putNamespacePrefix( + "http://schemas.openxmlformats.org/package/2006/digital-signature", + "mdssi"); + + if (this.signatureNamespacePrefix != null) { + /* + * OOo doesn't like ds namespaces so per default prefixing is off. + */ + xmlSignContext.putNamespacePrefix( + javax.xml.crypto.dsig.XMLSignature.XMLNS, + this.signatureNamespacePrefix); + } + + XMLSignatureFactory signatureFactory = XMLSignatureFactory.getInstance("DOM", "XMLDSig"); + + /* + * Add ds:References that come from signing client local files. + */ + List references = new LinkedList(); + addDigestInfosAsReferences(digestInfos, signatureFactory, references); + + /* + * Invoke the signature facets. + */ + String localSignatureId; + if (null == this.signatureId) { + localSignatureId = "xmldsig-" + UUID.randomUUID().toString(); + } else { + localSignatureId = this.signatureId; + } + List objects = new LinkedList(); + for (SignatureFacet signatureFacet : this.signatureFacets) { + LOG.log(POILogger.DEBUG, "invoking signature facet: " + + signatureFacet.getClass().getSimpleName()); + signatureFacet.preSign(signatureFactory, localSignatureId, signingCertificateChain, references, objects); + } + + /* + * ds:SignedInfo + */ + SignatureMethod signatureMethod = signatureFactory.newSignatureMethod( + getSignatureMethod(hashAlgo), null); + CanonicalizationMethod canonicalizationMethod = signatureFactory + .newCanonicalizationMethod(getCanonicalizationMethod(), + (C14NMethodParameterSpec) null); + SignedInfo signedInfo = signatureFactory.newSignedInfo( + canonicalizationMethod, signatureMethod, references); + + /* + * JSR105 ds:Signature creation + */ + String signatureValueId = localSignatureId + "-signature-value"; + javax.xml.crypto.dsig.XMLSignature xmlSignature = signatureFactory + .newXMLSignature(signedInfo, null, objects, localSignatureId, + signatureValueId); + + /* + * ds:Signature Marshalling. + */ + DOMXMLSignatureIf domXmlSignature; + try { + domXmlSignature = HorribleProxy.newProxy(DOMXMLSignatureIf.class, xmlSignature); + } catch (Exception e) { + throw new RuntimeException("DomXmlSignature instance error: " + e.getMessage(), e); + } + + domXmlSignature.marshal(doc, this.signatureNamespacePrefix, (DOMCryptoContext) xmlSignContext); + + registerIds(doc); + Element el = doc.getElementById("idPackageObject"); + assert (el != null); + el.setAttributeNS(Constants.NamespaceSpecNS, "xmlns:mdssi", PackageNamespaces.DIGITAL_SIGNATURE); + + + /* + * Completion of undigested ds:References in the ds:Manifests. + */ + for (XMLObject object : objects) { + LOG.log(POILogger.DEBUG, "object java type: " + object.getClass().getName()); + List objectContentList = object.getContent(); + for (XMLStructure objectContent : objectContentList) { + LOG.log(POILogger.DEBUG, "object content java type: " + objectContent.getClass().getName()); + if (!(objectContent instanceof Manifest)) continue; + Manifest manifest = (Manifest) objectContent; + List manifestReferences = manifest.getReferences(); + for (Reference manifestReference : manifestReferences) { + if (manifestReference.getDigestValue() != null) continue; + + DOMReferenceIf manifestDOMReference; + try { + manifestDOMReference = HorribleProxy.newProxy(DOMReferenceIf.class, manifestReference); + } catch (Exception e) { + throw new RuntimeException("DOMReference instance error: " + e.getMessage(), e); + } + manifestDOMReference.digest(xmlSignContext); + } + } + } + + /* + * Completion of undigested ds:References. + */ + List signedInfoReferences = signedInfo.getReferences(); + for (Reference signedInfoReference : signedInfoReferences) { + DOMReferenceIf domReference; + try { + domReference = HorribleProxy.newProxy(DOMReferenceIf.class, signedInfoReference); + } catch (Exception e) { + throw new RuntimeException("DOMReference instance error: " + e.getMessage(), e); + } + + // ds:Reference with external digest value + if (domReference.getDigestValue() != null) continue; + + domReference.digest(xmlSignContext); + } + + /* + * Calculation of XML signature digest value. + */ + DOMSignedInfoIf domSignedInfo; + try { + domSignedInfo = HorribleProxy.newProxy(DOMSignedInfoIf.class, signedInfo); + } catch (Exception e) { + throw new RuntimeException("DOMSignedInfo instance error: " + e.getMessage(), e); + } + + ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); + domSignedInfo.canonicalize(xmlSignContext, dataStream); + byte[] octets = dataStream.toByteArray(); + + sigDoc = SignatureDocument.Factory.parse(doc.getDocumentElement()); + + + /* + * TODO: we could be using DigestOutputStream here to optimize memory + * usage. + */ + + MessageDigest jcaMessageDigest = CryptoFunctions.getMessageDigest(hashAlgo); + byte[] digestValue = jcaMessageDigest.digest(octets); + return digestValue; + } + + /** + * the resulting document needs to be tweaked before it can be digested - + * this applies to the verification and signing step + * + * @param doc + */ + public void registerIds(Document doc) { + NodeList nl = doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "Object"); + registerIdAttribute(nl); + nl = doc.getElementsByTagNameNS("http://uri.etsi.org/01903/v1.3.2#", "SignedProperties"); + registerIdAttribute(nl); + } + + protected void registerIdAttribute(NodeList nl) { + for (int i=0; i digestInfos, + XMLSignatureFactory signatureFactory, List references) + throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException, MalformedURLException { + if (digestInfos == null) return; + for (DigestInfo digestInfo : digestInfos) { + byte[] documentDigestValue = digestInfo.digestValue; + + DigestMethod digestMethod = signatureFactory.newDigestMethod( + digestInfo.hashAlgo.xmlSignUri, null); + + String uri = new File(digestInfo.description).getName(); + + Reference reference = signatureFactory.newReference(uri, + digestMethod, null, null, null, documentDigestValue); + references.add(reference); + } + } + + private String getSignatureMethod(HashAlgorithm hashAlgo) { + if (null == hashAlgo) { + throw new RuntimeException("digest algo is null"); + } + + XMLSignatureIf XmlSignature; + try { + XmlSignature = HorribleProxy.newProxy(XMLSignatureIf.class); + } catch (Exception e) { + throw new RuntimeException("JDK doesn't support XmlSignature", e); + } + + switch (hashAlgo) { + case sha1: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA1(); + case sha256: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA256(); + case sha384: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA384(); + case sha512: return XmlSignature.ALGO_ID_SIGNATURE_RSA_SHA512(); + case ripemd160: return XmlSignature.ALGO_ID_MAC_HMAC_RIPEMD160(); + default: break; + } + + throw new RuntimeException("unsupported sign algo: " + hashAlgo); + } + + /** + * Gives back the used XAdES signature facet. + * + * @return + */ + protected XAdESSignatureFacet getXAdESSignatureFacet() { + return this.xadesSignatureFacet; + } + + public String getFilesDigestAlgorithm() { + return null; + } + + protected String getCanonicalizationMethod() { + return CanonicalizationMethod.INCLUSIVE; + } + + protected void writeDocument() throws IOException { + XmlOptions xo = new XmlOptions(); + Map namespaceMap = new HashMap(); + for (SignatureFacet sf : this.signatureFacets) { + Map sfm = sf.getNamespacePrefixMapping(); + if (sfm != null) { + namespaceMap.putAll(sfm); + } + } + xo.setSaveSuggestedPrefixes(namespaceMap); + xo.setUseDefaultNamespace(); + + LOG.log(POILogger.DEBUG, "output signed Office OpenXML document"); + + /* + * Copy the original OOXML content to the signed OOXML package. During + * copying some files need to changed. + */ + OPCPackage pkg = this.getOfficeOpenXMLDocument(); + + PackagePartName sigPartName, sigsPartName; + try { + // + sigPartName = PackagingURIHelper.createPartName("/_xmlsignatures/sig1.xml"); + // + sigsPartName = PackagingURIHelper.createPartName("/_xmlsignatures/origin.sigs"); + } catch (InvalidFormatException e) { + throw new IOException(e); + } + + String sigContentType = "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"; + PackagePart sigPart = pkg.getPart(sigPartName); + if (sigPart == null) { + sigPart = pkg.createPart(sigPartName, sigContentType); + } + + OutputStream os = sigPart.getOutputStream(); + sigDoc.save(os, xo); + os.close(); + + String sigsContentType = "application/vnd.openxmlformats-package.digital-signature-origin"; + PackagePart sigsPart = pkg.getPart(sigsPartName); + if (sigsPart == null) { + // touch empty marker file + sigsPart = pkg.createPart(sigsPartName, sigsContentType); + } + + 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); + } +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/AddressDTO.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/AddressDTO.java new file mode 100644 index 0000000000..a164046319 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/AddressDTO.java @@ -0,0 +1,51 @@ +/* ==================================================================== + 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.spi; + +import java.io.Serializable; +import java.security.Identity; + +/** + * Address Data Transfer Object. + * + * @author Frank Cornelis + * @see Identity + * + */ +public class AddressDTO implements Serializable { + + /* + * We implement serializable to allow this class to be used in distributed + * containers as defined in the Servlet v2.4 specification. + */ + + private static final long serialVersionUID = 1L; + + public String streetAndNumber; + + public String zip; + + public String city; +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/Constants.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/Constants.java new file mode 100644 index 0000000000..7c2caeb633 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/Constants.java @@ -0,0 +1,30 @@ +/* ==================================================================== + 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.spi; + +public interface Constants { + String NamespaceSpecNS = "http://www.w3.org/2000/xmlns/"; + String SignatureSpecNS = "http://www.w3.org/2000/09/xmldsig#"; +} diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/DigestInfo.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/DigestInfo.java new file mode 100644 index 0000000000..2f7c58c338 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/DigestInfo.java @@ -0,0 +1,56 @@ +/* ==================================================================== + 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.spi; + +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; + 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/spi/IdentityDTO.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/IdentityDTO.java new file mode 100644 index 0000000000..9cfa0aae25 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/spi/IdentityDTO.java @@ -0,0 +1,75 @@ +/* ==================================================================== + 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.spi; + +import java.io.Serializable; +import java.util.GregorianCalendar; + +/** + * Identity Data Transfer Object. + * + * @author Frank Cornelis + * + */ +public class IdentityDTO implements Serializable { + + /* + * We implement serializable to allow this class to be used in distributed + * containers as defined in the Servlet v2.4 specification. + */ + private static final long serialVersionUID = 1L; + + public String cardNumber; + + public String chipNumber; + + public GregorianCalendar cardValidityDateBegin; + + public GregorianCalendar cardValidityDateEnd; + + public String cardDeliveryMunicipality; + + public String nationalNumber; + + public String name; + + public String firstName; + + public String middleName; + + public String nationality; + + public String placeOfBirth; + + public GregorianCalendar dateOfBirth; + + public boolean male; + + public boolean female; + + public String nobleCondition; + + public String duplicate; +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/util/MethodUtils.java b/src/ooxml/java/org/apache/poi/util/MethodUtils.java new file mode 100644 index 0000000000..c006c85462 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/util/MethodUtils.java @@ -0,0 +1,1334 @@ +/* ==================================================================== + 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.util; + + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + +/** + *

Utility reflection methods focussed on methods in general rather than properties in particular.

+ * + *

Known Limitations

+ *

Accessing Public Methods In A Default Access Superclass

+ *

There is an issue when invoking public methods contained in a default access superclass. + * Reflection locates these methods fine and correctly assigns them as public. + * However, an IllegalAccessException is thrown if the method is invoked.

+ * + *

MethodUtils contains a workaround for this situation. + * It will attempt to call setAccessible on this method. + * If this call succeeds, then the method can be invoked as normal. + * This call will only succeed when the application has sufficient security privilages. + * If this call fails then a warning will be logged and the method may fail.

+ * + * @author Craig R. McClanahan + * @author Ralph Schaer + * @author Chris Audley + * @author Rey François + * @author Gregor Raýman + * @author Jan Sorensen + * @author Robert Burrell Donkin + */ + +public class MethodUtils { + + // --------------------------------------------------------- Private Methods + + /** + * Only log warning about accessibility work around once. + *

+ * Note that this is broken when this class is deployed via a shared + * classloader in a container, as the warning message will be emitted + * only once, not once per webapp. However making the warning appear + * once per webapp means having a map keyed by context classloader + * which introduces nasty memory-leak problems. As this warning is + * really optional we can ignore this problem; only one of the webapps + * will get the warning in its logs but that should be good enough. + */ + private static boolean loggedAccessibleWarning = false; + + /** + * Indicates whether methods should be cached for improved performance. + *

+ * Note that when this class is deployed via a shared classloader in + * a container, this will affect all webapps. However making this + * configurable per webapp would mean having a map keyed by context classloader + * which may introduce memory-leak problems. + */ + private static boolean CACHE_METHODS = true; + + /** An empty class array */ + private static final Class[] EMPTY_CLASS_PARAMETERS = new Class[0]; + /** An empty object array */ + private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; + + // --------------------------------------------------------- Public Methods + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than invokeExactMethod(). + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeMethod(Object object,String methodName,Object [] args)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeMethod(object, methodName, args); + + } + + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeMethod(object, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a named method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link + * #invokeExactMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeMethod( + Object object, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + Method method = getMatchingAccessibleMethod( + object.getClass(), + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + object.getClass().getName()); + } + return method.invoke(object, args); + } + + + /** + *

Invoke a method whose parameter type matches exactly the object + * type.

+ * + *

This is a convenient wrapper for + * {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + *

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeExactMethod(object, methodName, args); + + } + + + /** + *

Invoke a method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactMethod(object, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * getAccessibleMethod().

+ * + * @param object invoke method on this object + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactMethod( + Object object, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + + Method method = getAccessibleMethod( + object.getClass(), + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on object: " + object.getClass().getName()); + } + return method.invoke(object, args); + + } + + /** + *

Invoke a static method whose parameter types match exactly the parameter + * types given.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + + Method method = getAccessibleMethod( + objectClass, + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + objectClass.getName()); + } + return method.invoke(null, args); + + } + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object, String, Object[], Class[])}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeStaticMethod(Class objectClass,String methodName,Object [] args)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeStaticMethod (objectClass, methodName, args); + + } + + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link #invokeExactMethod(Object object,String methodName,Object [] args)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + *

This is a convenient wrapper for + * {@link #invokeStaticMethod(Class objectClass,String methodName,Object [] args,Class[] parameterTypes)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeStaticMethod (objectClass, methodName, args, parameterTypes); + + } + + + /** + *

Invoke a named static method whose parameter type matches the object type.

+ * + *

The behaviour of this method is less deterministic + * than {@link + * #invokeExactStaticMethod(Class objectClass,String methodName,Object [] args,Class[] parameterTypes)}. + * It loops through all methods with names that match + * and then executes the first it finds with compatable parameters.

+ * + *

This method supports calls to methods taking primitive parameters + * via passing in wrapping classes. So, for example, a Boolean class + * would match a boolean primitive.

+ * + * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @param parameterTypes match these parameters - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeStaticMethod( + Class objectClass, + String methodName, + Object[] args, + Class[] parameterTypes) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + if (parameterTypes == null) { + parameterTypes = EMPTY_CLASS_PARAMETERS; + } + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + + Method method = getMatchingAccessibleMethod( + objectClass, + methodName, + parameterTypes); + if (method == null) { + throw new NoSuchMethodException("No such accessible method: " + + methodName + "() on class: " + objectClass.getName()); + } + return method.invoke(null, args); + } + + + /** + *

Invoke a static method whose parameter type matches exactly the object + * type.

+ * + *

This is a convenient wrapper for + * {@link #invokeExactStaticMethod(Class objectClass,String methodName,Object [] args)}. + *

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param arg use this argument + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object arg) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + + Object[] args = {arg}; + return invokeExactStaticMethod (objectClass, methodName, args); + + } + + + /** + *

Invoke a static method whose parameter types match exactly the object + * types.

+ * + *

This uses reflection to invoke the method obtained from a call to + * {@link #getAccessibleMethod(Class, String, Class[])}.

+ * + * @param objectClass invoke static method on this class + * @param methodName get method with this name + * @param args use these arguments - treat null as empty array + * @return The value returned by the invoked method + * + * @throws NoSuchMethodException if there is no such accessible method + * @throws InvocationTargetException wraps an exception thrown by the + * method invoked + * @throws IllegalAccessException if the requested method is not accessible + * via reflection + */ + public static Object invokeExactStaticMethod( + Class objectClass, + String methodName, + Object[] args) + throws + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (args == null) { + args = EMPTY_OBJECT_ARRAY; + } + int arguments = args.length; + Class[] parameterTypes = new Class[arguments]; + for (int i = 0; i < arguments; i++) { + parameterTypes[i] = args[i].getClass(); + } + return invokeExactStaticMethod(objectClass, methodName, args, parameterTypes); + + } + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) with given name and a single parameter. If no such method + * can be found, return null. + * Basically, a convenience wrapper that constructs a Class + * array for you.

+ * + * @param clazz get method from this class + * @param methodName get method with this name + * @param parameterType taking this type of parameter + * @return The accessible method + */ + public static Method getAccessibleMethod( + Class clazz, + String methodName, + Class parameterType) { + + Class[] parameterTypes = {parameterType}; + return getAccessibleMethod(clazz, methodName, parameterTypes); + + } + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) with given name and parameters. If no such method + * can be found, return null. + * This is just a convenient wrapper for + * {@link #getAccessibleMethod(Method method)}.

+ * + * @param clazz get method from this class + * @param methodName get method with this name + * @param parameterTypes with these parameters types + * @return The accessible method + */ + public static Method getAccessibleMethod( + Class clazz, + String methodName, + Class[] parameterTypes) { + + try { + MethodDescriptor md = new MethodDescriptor(clazz, methodName, parameterTypes, true); + Method method = getAccessibleMethod + (clazz, clazz.getMethod(methodName, parameterTypes)); + return method; + } catch (NoSuchMethodException e) { + return (null); + } + + } + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified Method. If no such method + * can be found, return null.

+ * + * @param method The method that we wish to call + * @return The accessible method + */ + public static Method getAccessibleMethod(Method method) { + + // Make sure we have a method to check + if (method == null) { + return (null); + } + + return getAccessibleMethod(method.getDeclaringClass(), method); + + } + + + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified Method. If no such method + * can be found, return null.

+ * + * @param clazz The class of the object + * @param method The method that we wish to call + * @return The accessible method + */ + public static Method getAccessibleMethod(Class clazz, Method method) { + + // Make sure we have a method to check + if (method == null) { + return (null); + } + + // If the requested method is not public we cannot call it + if (!Modifier.isPublic(method.getModifiers())) { + return (null); + } + + boolean sameClass = true; + if (clazz == null) { + clazz = method.getDeclaringClass(); + } else { + sameClass = clazz.equals(method.getDeclaringClass()); + if (!method.getDeclaringClass().isAssignableFrom(clazz)) { + throw new IllegalArgumentException(clazz.getName() + + " is not assignable from " + method.getDeclaringClass().getName()); + } + } + + // If the class is public, we are done + if (Modifier.isPublic(clazz.getModifiers())) { + if (!sameClass && !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + setMethodAccessible(method); // Default access superclass workaround + } + return (method); + } + + String methodName = method.getName(); + Class[] parameterTypes = method.getParameterTypes(); + + // Check the implemented interfaces and subinterfaces + method = + getAccessibleMethodFromInterfaceNest(clazz, + methodName, + parameterTypes); + + // Check the superclass chain + if (method == null) { + method = getAccessibleMethodFromSuperclass(clazz, + methodName, + parameterTypes); + } + + return (method); + + } + + + // -------------------------------------------------------- Private Methods + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) by scanning through the superclasses. If no such method + * can be found, return null.

+ * + * @param clazz Class to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + */ + private static Method getAccessibleMethodFromSuperclass + (Class clazz, String methodName, Class[] parameterTypes) { + + Class parentClazz = clazz.getSuperclass(); + while (parentClazz != null) { + if (Modifier.isPublic(parentClazz.getModifiers())) { + try { + return parentClazz.getMethod(methodName, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } + } + parentClazz = parentClazz.getSuperclass(); + } + return null; + } + + /** + *

Return an accessible method (that is, one that can be invoked via + * reflection) that implements the specified method, by scanning through + * all implemented interfaces and subinterfaces. If no such method + * can be found, return null.

+ * + *

There isn't any good reason why this method must be private. + * It is because there doesn't seem any reason why other classes should + * call this rather than the higher level methods.

+ * + * @param clazz Parent class for the interfaces to be checked + * @param methodName Method name of the method we wish to call + * @param parameterTypes The parameter type signatures + */ + private static Method getAccessibleMethodFromInterfaceNest + (Class clazz, String methodName, Class[] parameterTypes) { + + Method method = null; + + // Search up the superclass chain + for (; clazz != null; clazz = clazz.getSuperclass()) { + + // Check the implemented interfaces of the parent class + Class[] interfaces = clazz.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + + // Is this interface public? + if (!Modifier.isPublic(interfaces[i].getModifiers())) { + continue; + } + + // Does the method exist on this interface? + try { + method = interfaces[i].getDeclaredMethod(methodName, + parameterTypes); + } catch (NoSuchMethodException e) { + /* Swallow, if no method is found after the loop then this + * method returns null. + */ + } + if (method != null) { + return method; + } + + // Recursively check our parent interfaces + method = + getAccessibleMethodFromInterfaceNest(interfaces[i], + methodName, + parameterTypes); + if (method != null) { + return method; + } + + } + + } + + // If we found a method return it + if (method != null) { + return (method); + } + + // We did not find anything + return (null); + + } + + /** + *

Find an accessible method that matches the given name and has compatible parameters. + * Compatible parameters mean that every method parameter is assignable from + * the given parameters. + * In other words, it finds a method with the given name + * that will take the parameters given.

+ * + *

This method is slightly undeterminstic since it loops + * through methods names and return the first matching method.

+ * + *

This method is used by + * {@link + * #invokeMethod(Object object,String methodName,Object [] args,Class[] parameterTypes)}. + * + *

This method can match primitive parameter by passing in wrapper classes. + * For example, a Boolean will match a primitive boolean + * parameter. + * + * @param clazz find method in this class + * @param methodName find method with this name + * @param parameterTypes find method with compatible parameters + * @return The accessible method + */ + public static Method getMatchingAccessibleMethod( + Class clazz, + String methodName, + Class[] parameterTypes) { + // trace logging + Log log = LogFactory.getLog(MethodUtils.class); + if (log.isTraceEnabled()) { + log.trace("Matching name=" + methodName + " on " + clazz); + } + MethodDescriptor md = new MethodDescriptor(clazz, methodName, parameterTypes, false); + + // see if we can find the method directly + // most of the time this works and it's much faster + try { + Method method = clazz.getMethod(methodName, parameterTypes); + if (log.isTraceEnabled()) { + log.trace("Found straight match: " + method); + log.trace("isPublic:" + Modifier.isPublic(method.getModifiers())); + } + + setMethodAccessible(method); // Default access superclass workaround + + return method; + + } catch (NoSuchMethodException e) { /* SWALLOW */ } + + // search through all methods + int paramSize = parameterTypes.length; + Method bestMatch = null; + Method[] methods = clazz.getMethods(); + float bestMatchCost = Float.MAX_VALUE; + float myCost = Float.MAX_VALUE; + for (int i = 0, size = methods.length; i < size ; i++) { + if (methods[i].getName().equals(methodName)) { + // log some trace information + if (log.isTraceEnabled()) { + log.trace("Found matching name:"); + log.trace(methods[i]); + } + + // compare parameters + Class[] methodsParams = methods[i].getParameterTypes(); + int methodParamSize = methodsParams.length; + if (methodParamSize == paramSize) { + boolean match = true; + for (int n = 0 ; n < methodParamSize; n++) { + if (log.isTraceEnabled()) { + log.trace("Param=" + parameterTypes[n].getName()); + log.trace("Method=" + methodsParams[n].getName()); + } + if (!isAssignmentCompatible(methodsParams[n], parameterTypes[n])) { + if (log.isTraceEnabled()) { + log.trace(methodsParams[n] + " is not assignable from " + + parameterTypes[n]); + } + match = false; + break; + } + } + + if (match) { + // get accessible version of method + Method method = getAccessibleMethod(clazz, methods[i]); + if (method != null) { + if (log.isTraceEnabled()) { + log.trace(method + " accessible version of " + + methods[i]); + } + setMethodAccessible(method); // Default access superclass workaround + myCost = getTotalTransformationCost(parameterTypes,method.getParameterTypes()); + if ( myCost < bestMatchCost ) { + bestMatch = method; + bestMatchCost = myCost; + } + } + + log.trace("Couldn't find accessible method."); + } + } + } + } + if ( bestMatch == null ){ + // didn't find a match + log.trace("No match found."); + } + + return bestMatch; + } + + public static Constructor getMatchingAccessibleConstructor( + Class clazz, + Class[] parameterTypes) { + // trace logging + Log log = LogFactory.getLog(MethodUtils.class); + MethodDescriptor md = new MethodDescriptor(clazz, "dummy", parameterTypes, false); + + // see if we can find the method directly + // most of the time this works and it's much faster + try { + Constructor constructor = clazz.getConstructor(parameterTypes); + if (log.isTraceEnabled()) { + log.trace("Found straight match: " + constructor); + log.trace("isPublic:" + Modifier.isPublic(constructor.getModifiers())); + } + + setMethodAccessible(constructor); // Default access superclass workaround + + return constructor; + + } catch (NoSuchMethodException e) { /* SWALLOW */ } + + // search through all methods + int paramSize = parameterTypes.length; + Constructor bestMatch = null; + Constructor[] constructors = clazz.getConstructors(); + float bestMatchCost = Float.MAX_VALUE; + float myCost = Float.MAX_VALUE; + for (int i = 0, size = constructors.length; i < size ; i++) { + // compare parameters + Class[] methodsParams = constructors[i].getParameterTypes(); + int methodParamSize = methodsParams.length; + if (methodParamSize == paramSize) { + boolean match = true; + for (int n = 0 ; n < methodParamSize; n++) { + if (log.isTraceEnabled()) { + log.trace("Param=" + parameterTypes[n].getName()); + log.trace("Method=" + methodsParams[n].getName()); + } + if (!isAssignmentCompatible(methodsParams[n], parameterTypes[n])) { + if (log.isTraceEnabled()) { + log.trace(methodsParams[n] + " is not assignable from " + + parameterTypes[n]); + } + match = false; + break; + } + } + + if (match) { + // get accessible version of method + Constructor cons = (Constructor)constructors[i]; + myCost = getTotalTransformationCost(parameterTypes,cons.getParameterTypes()); + if ( myCost < bestMatchCost ) { + bestMatch = cons; + bestMatchCost = myCost; + } + } + } + } + if ( bestMatch == null ){ + // didn't find a match + log.trace("No match found."); + } + + return bestMatch; + } + + /** + * Try to make the method accessible + * @param method The source arguments + */ + private static void setMethodAccessible(Object method) { + try { + // + // XXX Default access superclass workaround + // + // When a public class has a default access superclass + // with public methods, these methods are accessible. + // Calling them from compiled code works fine. + // + // Unfortunately, using reflection to invoke these methods + // seems to (wrongly) to prevent access even when the method + // modifer is public. + // + // The following workaround solves the problem but will only + // work from sufficiently privilages code. + // + // Better workarounds would be greatfully accepted. + // + if (method instanceof Method) { + ((Method)method).setAccessible(true); + } else if (method instanceof Constructor) { + ((Constructor)method).setAccessible(true); + } else { + throw new RuntimeException("invalid parameter"); + } + + } catch (SecurityException se) { + // log but continue just in case the method.invoke works anyway + Log log = LogFactory.getLog(MethodUtils.class); + if (!loggedAccessibleWarning) { + boolean vulnerableJVM = false; + try { + String specVersion = System.getProperty("java.specification.version"); + if (specVersion.charAt(0) == '1' && + (specVersion.charAt(2) == '0' || + specVersion.charAt(2) == '1' || + specVersion.charAt(2) == '2' || + specVersion.charAt(2) == '3')) { + + vulnerableJVM = true; + } + } catch (SecurityException e) { + // don't know - so display warning + vulnerableJVM = true; + } + if (vulnerableJVM) { + log.warn( + "Current Security Manager restricts use of workarounds for reflection bugs " + + " in pre-1.4 JVMs."); + } + loggedAccessibleWarning = true; + } + log.debug("Cannot setAccessible on method. Therefore cannot use jvm access bug workaround.", se); + } + } + + /** + * Returns the sum of the object transformation cost for each class in the source + * argument list. + * @param srcArgs The source arguments + * @param destArgs The destination arguments + * @return The total transformation cost + */ + private static float getTotalTransformationCost(Class[] srcArgs, Class[] destArgs) { + + float totalCost = 0.0f; + for (int i = 0; i < srcArgs.length; i++) { + Class srcClass, destClass; + srcClass = srcArgs[i]; + destClass = destArgs[i]; + totalCost += getObjectTransformationCost(srcClass, destClass); + } + + return totalCost; + } + + /** + * Gets the number of steps required needed to turn the source class into the + * destination class. This represents the number of steps in the object hierarchy + * graph. + * @param srcClass The source class + * @param destClass The destination class + * @return The cost of transforming an object + */ + private static float getObjectTransformationCost(Class srcClass, Class destClass) { + float cost = 0.0f; + while (destClass != null && !destClass.equals(srcClass)) { + if (destClass.isInterface() && isAssignmentCompatible(destClass,srcClass)) { + // slight penalty for interface match. + // we still want an exact match to override an interface match, but + // an interface match should override anything where we have to get a + // superclass. + cost += 0.25f; + break; + } + cost++; + destClass = destClass.getSuperclass(); + } + + /* + * If the destination class is null, we've travelled all the way up to + * an Object match. We'll penalize this by adding 1.5 to the cost. + */ + if (destClass == null) { + cost += 1.5f; + } + + return cost; + } + + + /** + *

Determine whether a type can be used as a parameter in a method invocation. + * This method handles primitive conversions correctly.

+ * + *

In order words, it will match a Boolean to a boolean, + * a Long to a long, + * a Float to a float, + * a Integer to a int, + * and a Double to a double. + * Now logic widening matches are allowed. + * For example, a Long will not match a int. + * + * @param parameterType the type of parameter accepted by the method + * @param parameterization the type of parameter being tested + * + * @return true if the assignement is compatible. + */ + public static final boolean isAssignmentCompatible(Class parameterType, Class parameterization) { + // try plain assignment + if (parameterType.isAssignableFrom(parameterization)) { + return true; + } + + if (parameterType.isPrimitive()) { + // this method does *not* do widening - you must specify exactly + // is this the right behaviour? + Class parameterWrapperClazz = getPrimitiveWrapper(parameterType); + if (parameterWrapperClazz != null) { + return parameterWrapperClazz.equals(parameterization); + } + } + + return false; + } + + /** + * Gets the wrapper object class for the given primitive type class. + * For example, passing boolean.class returns Boolean.class + * @param primitiveType the primitive type class for which a match is to be found + * @return the wrapper type associated with the given primitive + * or null if no match is found + */ + public static Class getPrimitiveWrapper(Class primitiveType) { + // does anyone know a better strategy than comparing names? + if (boolean.class.equals(primitiveType)) { + return Boolean.class; + } else if (float.class.equals(primitiveType)) { + return Float.class; + } else if (long.class.equals(primitiveType)) { + return Long.class; + } else if (int.class.equals(primitiveType)) { + return Integer.class; + } else if (short.class.equals(primitiveType)) { + return Short.class; + } else if (byte.class.equals(primitiveType)) { + return Byte.class; + } else if (double.class.equals(primitiveType)) { + return Double.class; + } else if (char.class.equals(primitiveType)) { + return Character.class; + } else { + + return null; + } + } + + /** + * Gets the class for the primitive type corresponding to the primitive wrapper class given. + * For example, an instance of Boolean.class returns a boolean.class. + * @param wrapperType the + * @return the primitive type class corresponding to the given wrapper class, + * null if no match is found + */ + public static Class getPrimitiveType(Class wrapperType) { + // does anyone know a better strategy than comparing names? + if (Boolean.class.equals(wrapperType)) { + return boolean.class; + } else if (Float.class.equals(wrapperType)) { + return float.class; + } else if (Long.class.equals(wrapperType)) { + return long.class; + } else if (Integer.class.equals(wrapperType)) { + return int.class; + } else if (Short.class.equals(wrapperType)) { + return short.class; + } else if (Byte.class.equals(wrapperType)) { + return byte.class; + } else if (Double.class.equals(wrapperType)) { + return double.class; + } else if (Character.class.equals(wrapperType)) { + return char.class; + } else { + Log log = LogFactory.getLog(MethodUtils.class); + if (log.isDebugEnabled()) { + log.debug("Not a known primitive wrapper class: " + wrapperType); + } + return null; + } + } + + /** + * Find a non primitive representation for given primitive class. + * + * @param clazz the class to find a representation for, not null + * @return the original class if it not a primitive. Otherwise the wrapper class. Not null + */ + public static Class toNonPrimitiveClass(Class clazz) { + if (clazz.isPrimitive()) { + Class primitiveClazz = MethodUtils.getPrimitiveWrapper(clazz); + // the above method returns + if (primitiveClazz != null) { + return primitiveClazz; + } else { + return clazz; + } + } else { + return clazz; + } + } + + + /** + * Represents the key to looking up a Method by reflection. + */ + private static class MethodDescriptor { + private Class cls; + private String methodName; + private Class[] paramTypes; + private boolean exact; + private int hashCode; + + /** + * The sole constructor. + * + * @param cls the class to reflect, must not be null + * @param methodName the method name to obtain + * @param paramTypes the array of classes representing the paramater types + * @param exact whether the match has to be exact. + */ + public MethodDescriptor(Class cls, String methodName, Class[] paramTypes, boolean exact) { + if (cls == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + if (methodName == null) { + throw new IllegalArgumentException("Method Name cannot be null"); + } + if (paramTypes == null) { + paramTypes = EMPTY_CLASS_PARAMETERS; + } + + this.cls = cls; + this.methodName = methodName; + this.paramTypes = paramTypes; + this.exact= exact; + + this.hashCode = methodName.length(); + } + /** + * Checks for equality. + * @param obj object to be tested for equality + * @return true, if the object describes the same Method. + */ + public boolean equals(Object obj) { + if (!(obj instanceof MethodDescriptor)) { + return false; + } + MethodDescriptor md = (MethodDescriptor)obj; + + return ( + exact == md.exact && + methodName.equals(md.methodName) && + cls.equals(md.cls) && + java.util.Arrays.equals(paramTypes, md.paramTypes) + ); + } + /** + * Returns the string length of method name. I.e. if the + * hashcodes are different, the objects are different. If the + * hashcodes are the same, need to use the equals method to + * determine equality. + * @return the string length of method name. + */ + public int hashCode() { + return hashCode; + } + } +} diff --git a/src/ooxml/java/org/apache/poi/util/SAXHelper.java b/src/ooxml/java/org/apache/poi/util/SAXHelper.java index b38b2c2be9..9ee00fb69a 100644 --- a/src/ooxml/java/org/apache/poi/util/SAXHelper.java +++ b/src/ooxml/java/org/apache/poi/util/SAXHelper.java @@ -23,6 +23,9 @@ import java.io.StringReader; import java.lang.reflect.Method; import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import org.dom4j.Document; import org.dom4j.DocumentException; @@ -89,4 +92,72 @@ public final class SAXHelper { public static Document readSAXDocument(InputStream inp) throws DocumentException { return getSAXReader().read(inp); } + + private static final EntityResolver IGNORING_ENTITY_RESOLVER = new EntityResolver() { + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + return new InputSource(new StringReader("")); + } + }; + + private static void trySetSAXFeature(DocumentBuilderFactory documentBuilderFactory, String feature, boolean enabled) { + try { + documentBuilderFactory.setFeature(feature, enabled); + } catch (Exception e) { + logger.log(POILogger.INFO, "SAX Feature unsupported", feature, e); + } + } + private static void trySetXercesSecurityManager(DocumentBuilderFactory documentBuilderFactory) { + // Try built-in JVM one first, standalone if not + for (String securityManagerClassName : new String[] { + "com.sun.org.apache.xerces.internal.util.SecurityManager", + "org.apache.xerces.util.SecurityManager" + }) { + try { + Object mgr = Class.forName(securityManagerClassName).newInstance(); + Method setLimit = mgr.getClass().getMethod("setEntityExpansionLimit", Integer.TYPE); + setLimit.invoke(mgr, 4096); + documentBuilderFactory.setAttribute("http://apache.org/xml/properties/security-manager", mgr); + // Stop once one can be setup without error + return; + } catch (Exception e) { + logger.log(POILogger.INFO, "SAX Security Manager could not be setup", e); + } + } + } + + private static final ThreadLocal documentBuilder = new ThreadLocal() { + @Override + protected DocumentBuilder initialValue() { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + trySetSAXFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + trySetXercesSecurityManager(factory); + try { + return factory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new IllegalStateException("cannot create a DocumentBuilder", e); + } + } + + @Override + public DocumentBuilder get() { + DocumentBuilder documentBuilder = super.get(); + documentBuilder.reset(); + documentBuilder.setEntityResolver(IGNORING_ENTITY_RESOLVER); + return documentBuilder; + } + }; + + /** + * Parses the given stream via the default (sensible) + * SAX Reader + * @param inp Stream to read the XML data from + * @return the SAX processed Document + */ + public static org.w3c.dom.Document readSAXDocumentW3C(InputStream inp) throws IOException, SAXException { + return documentBuilder.get().parse(inp); + } } diff --git a/src/ooxml/java/org/apache/poi/util/XmlSort.java b/src/ooxml/java/org/apache/poi/util/XmlSort.java new file mode 100644 index 0000000000..4e1ffa54f0 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/util/XmlSort.java @@ -0,0 +1,221 @@ +/* ==================================================================== + 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.util; + +import java.io.File; +import java.io.IOException; +import java.util.Comparator; + +import javax.xml.namespace.QName; + +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; + +/** + */ +public final class XmlSort +{ + /** + * Receives an XML element instance and sorts the children of this + * element in lexicographical (by default) order. + * + * @param args An array in which the first item is a + * path to the XML instance file and the second item (optional) is + * an XPath inside the document identifying the element to be sorted + */ + public static void main(String[] args) + { + if (args.length < 1 || args.length > 2) + { + System.out.println(" java XmlSort []"); + return; + } + File f = new File(args[0]); + try + { + XmlObject docInstance = XmlObject.Factory.parse(f); + XmlObject element = null; + if (args.length > 1) + { + String xpath = args[1]; + XmlObject[] result = docInstance.selectPath(xpath); + if (result.length == 0) + { + System.out.println("ERROR: XPath \"" + xpath + "\" did not return any results"); + } + else if (result.length > 1) + { + System.out.println("ERROR: XPath \"" + xpath + "\" returned more than one " + + "node (" + result.length + ")"); + } + else + element = result[0]; + } + else + { + // Navigate to the root element + XmlCursor c = docInstance.newCursor(); + c.toFirstChild(); + element = c.getObject(); + c.dispose(); + } + if (element != null) + sort(element, new QNameComparator(QNameComparator.ASCENDING)); + System.out.println(docInstance.xmlText()); + } + catch (IOException ioe) + { + System.out.println("ERROR: Could not open file: \"" + args[0] + "\": " + + ioe.getMessage()); + } + catch (XmlException xe) + { + System.out.println("ERROR: Could not parse file: \"" + args[0] + "\": " + + xe.getMessage()); + } + } + + /** + * Sorts the children of element according to the order indicated by the + * comparator. + * @param element the element whose content is to be sorted. Only element children are sorted, + * attributes are not touched. When elements are reordered, all the text, comments and PIs + * follow the element that they come immediately after. + * @param comp a comparator that is to be used when comparing the QNames of two + * elements. See {@link org.apache.xmlbeans.samples.cursor.XmlSort.QNameComparator} for a simple + * implementation that compares two elements based on the value of their QName, but more + * complicated implementations are possible, for instance, ones that compare two elements based + * on the value of a specifc attribute etc. + * @throws IllegalArgumentException if the input XmlObject does not represent + * an element + */ + public static void sort(XmlObject element, Comparator comp) + { + XmlCursor headCursor = element.newCursor(); + if (!headCursor.isStart()) + throw new IllegalStateException("The element parameter must point to a STARTDOC"); + // We use insertion sort to minimize the number of swaps, because each swap means + // moving a part of the document + /* headCursor points to the beginning of the list of the already sorted items and + listCursor points to the beginning of the list of unsorted items + At the beginning, headCursor points to the first element and listCursor points to the + second element. The algorithm ends when listCursor cannot be moved to the "next" + element in the unsorted list, i.e. the unsorted list becomes empty */ + boolean moved = headCursor.toFirstChild(); + if (!moved) + { + // Cursor was not moved, which means that the given element has no children and + // therefore there is nothing to sort + return; + } + XmlCursor listCursor = headCursor.newCursor(); + boolean moreElements = listCursor.toNextSibling(); + while (moreElements) + { + moved = false; + // While we can move the head of the unsorted list, it means that there are still + // items (elements) that need to be sorted + while (headCursor.comparePosition(listCursor) < 0) + { + if (comp.compare(headCursor, listCursor) > 0) + { + // We have found the position in the sorted list, insert the element and the + // text following the element in the current position + /* + * Uncomment this code to cause the text before the element to move along + * with the element, rather than the text after the element. Notice that this + * is more difficult to do, because the cursor's "type" refers to the position + * to the right of the cursor, so to get the type of the token to the left, the + * cursor needs to be first moved to the left (previous token) + * + headCursor.toPrevToken(); + while (headCursor.isComment() || headCursor.isProcinst() || headCursor.isText()) + headCursor.toPrevToken(); + headCursor.toNextToken(); + listCursor.toPrevToken(); + while (listCursor.isComment() || listCursor.isProcinst() || listCursor.isText()) + listCursor.toPrevToken(); + listCursor.toNextToken(); + while (!listCursor.isStart()) + listCursor.moveXml(headCursor); + listCursor.moveXml(headCursor); + */ + // Move the element + listCursor.moveXml(headCursor); + // Move the text following the element + while (!listCursor.isStart() && !listCursor.isEnd()) + listCursor.moveXml(headCursor); + moreElements = listCursor.isStart(); + moved = true; + break; + } + headCursor.toNextSibling(); + } + if (!moved) + { + // Because during the move of a fragment of XML, the listCursor is also moved, in + // case we didn't need to move XML (the new element to be inserted happened to + // be the last one in order), we need to move this cursor + moreElements = listCursor.toNextSibling(); + } + // Reposition the head of the sorted list + headCursor.toParent(); + headCursor.toFirstChild(); + } + } + + /** + * Implements a java.util.Comparator for comparing QNamevalues. + * The namespace URIs are compared first and if they are equal, the local parts are compared. + *

+ * The constructor accepts an argument indicating whether the comparison order is the same as + * the lexicographic order of the strings or the reverse. + */ + public static final class QNameComparator implements Comparator + { + public static final int ASCENDING = 1; + public static final int DESCENDING = 2; + + private int order; + + public QNameComparator(int order) + { + this.order = order; + if (order != ASCENDING && order != DESCENDING) + throw new IllegalArgumentException("Please specify one of ASCENDING or DESCENDING "+ + "comparison orders"); + } + + public int compare(Object o, Object o1) + { + XmlCursor cursor1 = (XmlCursor) o; + XmlCursor cursor2 = (XmlCursor) o1; + QName qname1 = cursor1.getName(); + QName qname2 = cursor2.getName(); + int qnameComparisonRes = qname1.getNamespaceURI().compareTo(qname2.getNamespaceURI()); + if (qnameComparisonRes == 0) + return order == ASCENDING ? + qname1.getLocalPart().compareTo(qname2.getLocalPart()) : + -qname1.getLocalPart().compareTo(qname2.getLocalPart()); + else + return order == ASCENDING ? qnameComparisonRes : -qnameComparisonRes; + } + } +} + \ No newline at end of file diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/XAdES.xsd b/src/ooxml/resources/org/apache/poi/poifs/crypt/XAdES.xsd new file mode 100644 index 0000000000..656e721c7c --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/XAdES.xsd @@ -0,0 +1,466 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/XAdESv141.xsd b/src/ooxml/resources/org/apache/poi/poifs/crypt/XAdESv141.xsd new file mode 100644 index 0000000000..cd6614f134 --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/XAdESv141.xsd @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ooxml/resources/org/apache/poi/poifs/crypt/signatureInfo.xsd b/src/ooxml/resources/org/apache/poi/poifs/crypt/signatureInfo.xsd new file mode 100644 index 0000000000..f7019f13f5 --- /dev/null +++ b/src/ooxml/resources/org/apache/poi/poifs/crypt/signatureInfo.xsd @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/PkiTestUtils.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/PkiTestUtils.java new file mode 100644 index 0000000000..6d6592a2a3 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/PkiTestUtils.java @@ -0,0 +1,328 @@ +/* ==================================================================== + 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; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.SignatureException; +import java.security.cert.CRLException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.security.spec.RSAKeyGenParameterSpec; +import java.util.Date; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.poi.poifs.crypt.dsig.HorribleProxy; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.ASN1InputStreamIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.AuthorityInformationAccessIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.AuthorityKeyIdentifierIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.BasicConstraintsIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.BasicOCSPRespGeneratorIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.BasicOCSPRespIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.CRLNumberIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.CRLReasonIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.CertificateIDIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.CertificateStatusIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DERIA5StringIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DERSequenceIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DistributionPointIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.DistributionPointNameIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.GeneralNameIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.GeneralNamesIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.KeyUsageIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.OCSPReqGeneratorIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.OCSPReqIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.OCSPRespGeneratorIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.OCSPRespIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.ReqIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.RevokedStatusIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.SubjectKeyIdentifierIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.SubjectPublicKeyInfoIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.X509ExtensionsIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.X509ObjectIdentifiersIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.X509PrincipalIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.X509V2CRLGeneratorIf; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.X509V3CertificateGeneratorIf; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class PkiTestUtils { + + private PkiTestUtils() { + super(); + } + + static KeyPair generateKeyPair() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + SecureRandom random = new SecureRandom(); + keyPairGenerator.initialize(new RSAKeyGenParameterSpec(1024, + RSAKeyGenParameterSpec.F4), random); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + return keyPair; + } + + private static SubjectKeyIdentifierIf createSubjectKeyId(PublicKey publicKey) + throws IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException + , IllegalAccessException, InvocationTargetException, NoSuchFieldException { + ByteArrayInputStream bais = new ByteArrayInputStream(publicKey.getEncoded()); + ASN1InputStreamIf asnObj = HorribleProxy.newProxy(ASN1InputStreamIf.class, bais); + SubjectPublicKeyInfoIf info = + HorribleProxy.newProxy(SubjectPublicKeyInfoIf.class, asnObj.readObject$Sequence()); + SubjectKeyIdentifierIf keyId = HorribleProxy.newProxy(SubjectKeyIdentifierIf.class, info); + return keyId; + } + + private static AuthorityKeyIdentifierIf createAuthorityKeyId(PublicKey publicKey) + throws IOException, ClassNotFoundException, NoSuchMethodException, InstantiationException + , IllegalAccessException, InvocationTargetException, NoSuchFieldException { + + ByteArrayInputStream bais = new ByteArrayInputStream(publicKey.getEncoded()); + ASN1InputStreamIf asnObj = HorribleProxy.newProxy(ASN1InputStreamIf.class, bais); + SubjectPublicKeyInfoIf info = + HorribleProxy.newProxy(SubjectPublicKeyInfoIf.class, asnObj.readObject$Sequence()); + AuthorityKeyIdentifierIf keyId = HorribleProxy.newProxy(AuthorityKeyIdentifierIf.class, info); + + return keyId; + } + + static X509Certificate generateCertificate(PublicKey subjectPublicKey, + String subjectDn, Date notBefore, Date notAfter, + X509Certificate issuerCertificate, PrivateKey issuerPrivateKey, + boolean caFlag, int pathLength, String crlUri, String ocspUri, + KeyUsageIf keyUsage) + throws IOException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException + , SignatureException, CertificateException, InvocationTargetException, IllegalAccessException + , InstantiationException, NoSuchMethodException, ClassNotFoundException, NoSuchFieldException + { + String signatureAlgorithm = "SHA1withRSA"; + X509V3CertificateGeneratorIf certificateGenerator = HorribleProxy.newProxy(X509V3CertificateGeneratorIf.class); + certificateGenerator.reset(); + certificateGenerator.setPublicKey(subjectPublicKey); + certificateGenerator.setSignatureAlgorithm(signatureAlgorithm); + certificateGenerator.setNotBefore(notBefore); + certificateGenerator.setNotAfter(notAfter); + X509PrincipalIf subjectDN = HorribleProxy.newProxy(X509PrincipalIf.class, subjectDn); + X509PrincipalIf issuerDN; + if (null != issuerCertificate) { + issuerDN = HorribleProxy.newProxy(X509PrincipalIf.class, issuerCertificate + .getSubjectX500Principal().toString()); + } else { + issuerDN = subjectDN; + } + certificateGenerator.setIssuerDN(issuerDN); + certificateGenerator.setSubjectDN(subjectDN); + certificateGenerator.setSerialNumber(new BigInteger(128, + new SecureRandom())); + + X509ExtensionsIf X509Extensions = HorribleProxy.newProxy(X509ExtensionsIf.class); + + certificateGenerator.addExtension(X509Extensions.SubjectKeyIdentifier(), + false, createSubjectKeyId(subjectPublicKey)); + PublicKey issuerPublicKey; + issuerPublicKey = subjectPublicKey; + certificateGenerator.addExtension( + X509Extensions.AuthorityKeyIdentifier(), false, + createAuthorityKeyId(issuerPublicKey)); + + if (caFlag) { + BasicConstraintsIf bc; + + if (-1 == pathLength) { + bc = HorribleProxy.newProxy(BasicConstraintsIf.class, true); + } else { + bc = HorribleProxy.newProxy(BasicConstraintsIf.class, pathLength); + } + certificateGenerator.addExtension(X509Extensions.BasicConstraints(), false, bc); + } + + if (null != crlUri) { + GeneralNameIf gn = HorribleProxy.newProxy(GeneralNameIf.class); + int uri = gn.uniformResourceIdentifier(); + DERIA5StringIf crlUriDer = HorribleProxy.newProxy(DERIA5StringIf.class, crlUri); + gn = HorribleProxy.newProxy(GeneralNameIf.class, uri, crlUriDer); + + DERSequenceIf gnDer = HorribleProxy.newProxy(DERSequenceIf.class, gn); + GeneralNamesIf gns = HorribleProxy.newProxy(GeneralNamesIf.class, gnDer); + + DistributionPointNameIf dpn = HorribleProxy.newProxy(DistributionPointNameIf.class, 0, gns); + DistributionPointIf distp = HorribleProxy.newProxy(DistributionPointIf.class, dpn, null, null); + DERSequenceIf distpDer = HorribleProxy.newProxy(DERSequenceIf.class, distp); + certificateGenerator.addExtension(X509Extensions.CRLDistributionPoints(), false, distpDer); + } + + if (null != ocspUri) { + GeneralNameIf ocspName = HorribleProxy.newProxy(GeneralNameIf.class); + int uri = ocspName.uniformResourceIdentifier(); + ocspName = HorribleProxy.newProxy(GeneralNameIf.class, uri, ocspUri); + + X509ObjectIdentifiersIf X509ObjectIdentifiers = HorribleProxy.newProxy(X509ObjectIdentifiersIf.class); + AuthorityInformationAccessIf authorityInformationAccess = + HorribleProxy.newProxy(AuthorityInformationAccessIf.class + , X509ObjectIdentifiers.ocspAccessMethod(), ocspName); + + certificateGenerator.addExtension( + X509Extensions.AuthorityInfoAccess(), false, + authorityInformationAccess); + } + + if (null != keyUsage) { + certificateGenerator.addExtension(X509Extensions.KeyUsage(), true, keyUsage); + } + + X509Certificate certificate; + certificate = certificateGenerator.generate(issuerPrivateKey); + + /* + * Next certificate factory trick is needed to make sure that the + * certificate delivered to the caller is provided by the default + * security provider instead of BouncyCastle. If we don't do this trick + * we might run into trouble when trying to use the CertPath validator. + */ + CertificateFactory certificateFactory = CertificateFactory + .getInstance("X.509"); + certificate = (X509Certificate) certificateFactory + .generateCertificate(new ByteArrayInputStream(certificate + .getEncoded())); + return certificate; + } + + static Document loadDocument(InputStream documentInputStream) + throws ParserConfigurationException, SAXException, IOException { + InputSource inputSource = new InputSource(documentInputStream); + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory + .newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder documentBuilder = documentBuilderFactory + .newDocumentBuilder(); + Document document = documentBuilder.parse(inputSource); + return document; + } + + static String toString(Node dom) throws TransformerException { + Source source = new DOMSource(dom); + StringWriter stringWriter = new StringWriter(); + Result result = new StreamResult(stringWriter); + TransformerFactory transformerFactory = TransformerFactory + .newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + /* + * We have to omit the ?xml declaration if we want to embed the + * document. + */ + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.transform(source, result); + return stringWriter.getBuffer().toString(); + } + + public static X509CRL generateCrl(X509Certificate issuer, + PrivateKey issuerPrivateKey) throws InvalidKeyException, + CRLException, IllegalStateException, NoSuchAlgorithmException, + SignatureException, InvocationTargetException, IllegalAccessException, + InstantiationException, NoSuchMethodException, ClassNotFoundException, NoSuchFieldException { + X509V2CRLGeneratorIf crlGenerator = HorribleProxy.newProxy(X509V2CRLGeneratorIf.class); + crlGenerator.setIssuerDN(issuer.getSubjectX500Principal()); + Date now = new Date(); + crlGenerator.setThisUpdate(now); + crlGenerator.setNextUpdate(new Date(now.getTime() + 100000)); + crlGenerator.setSignatureAlgorithm("SHA1withRSA"); + + X509ExtensionsIf X509Extensions = HorribleProxy.newProxy(X509ExtensionsIf.class); + CRLNumberIf crlNumber = HorribleProxy.newProxy(CRLNumberIf.class, new BigInteger("1234")); + + crlGenerator.addExtension(X509Extensions.CRLNumber(), false, crlNumber); + X509CRL x509Crl = crlGenerator.generate(issuerPrivateKey); + return x509Crl; + } + + public static OCSPRespIf createOcspResp(X509Certificate certificate, + boolean revoked, X509Certificate issuerCertificate, + X509Certificate ocspResponderCertificate, + PrivateKey ocspResponderPrivateKey, String signatureAlgorithm) + throws Exception { + // request + OCSPReqGeneratorIf ocspReqGenerator = HorribleProxy.newProxy(OCSPReqGeneratorIf.class); + CertificateIDIf certId = HorribleProxy.newProxy(CertificateIDIf.class); + certId = HorribleProxy.newProxy(CertificateIDIf.class, certId.HASH_SHA1(), + issuerCertificate, certificate.getSerialNumber()); + ocspReqGenerator.addRequest(certId); + OCSPReqIf ocspReq = ocspReqGenerator.generate(); + + BasicOCSPRespGeneratorIf basicOCSPRespGenerator = + HorribleProxy.newProxy(BasicOCSPRespGeneratorIf.class, ocspResponderCertificate.getPublicKey()); + + // request processing + ReqIf[] requestList = ocspReq.getRequestList(); + for (ReqIf ocspRequest : requestList) { + CertificateIDIf certificateID = ocspRequest.getCertID(); + CertificateStatusIf certificateStatus; + if (revoked) { + CRLReasonIf crlr = HorribleProxy.newProxy(CRLReasonIf.class); + RevokedStatusIf rs = HorribleProxy.newProxy(RevokedStatusIf.class, new Date(), crlr.unspecified()); + certificateStatus = HorribleProxy.newProxy(CertificateStatusIf.class, rs.getDelegate()); + } else { + CertificateStatusIf cs = HorribleProxy.newProxy(CertificateStatusIf.class); + certificateStatus = cs.GOOD(); + } + basicOCSPRespGenerator + .addResponse(certificateID, certificateStatus); + } + + // basic response generation + X509Certificate[] chain = null; + if (!ocspResponderCertificate.equals(issuerCertificate)) { + chain = new X509Certificate[] { ocspResponderCertificate, + issuerCertificate }; + } + + BasicOCSPRespIf basicOCSPResp = basicOCSPRespGenerator.generate( + signatureAlgorithm, ocspResponderPrivateKey, chain, new Date(), + "BC"); + + // response generation + OCSPRespGeneratorIf ocspRespGenerator = HorribleProxy.newProxy(OCSPRespGeneratorIf.class); + OCSPRespIf ocspResp = ocspRespGenerator.generate( + ocspRespGenerator.SUCCESSFUL(), basicOCSPResp); + + return ocspResp; + } +} diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java new file mode 100644 index 0000000000..f155620e2d --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java @@ -0,0 +1,266 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +/* ==================================================================== + This product contains an ASLv2 licensed version of the OOXML signer + package from the eID Applet project + http://code.google.com/p/eid-applet/source/browse/trunk/README.txt + Copyright (C) 2008-2014 FedICT. + ================================================================= */ +package org.apache.poi.poifs.crypt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; + +import javax.crypto.Cipher; + +import org.apache.poi.POIDataSamples; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.openxml4j.opc.PackageAccess; +import org.apache.poi.poifs.crypt.dsig.HorribleProxy; +import org.apache.poi.poifs.crypt.dsig.SignatureInfo; +import org.apache.poi.poifs.crypt.dsig.HorribleProxies.KeyUsageIf; +import org.apache.poi.poifs.crypt.dsig.services.XmlSignatureService; +import org.apache.poi.poifs.crypt.dsig.spi.DigestInfo; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestSignatureInfo { + private static final POILogger LOG = POILogFactory.getLogger(TestSignatureInfo.class); + private static final POIDataSamples testdata = POIDataSamples.getXmlDSignInstance(); + + private KeyPair keyPair = null; + private X509Certificate x509 = null; + + + + @BeforeClass + public static void initBouncy() throws MalformedURLException { + File bcJar = testdata.getFile("bcprov-ext-jdk15on-1.49.jar"); + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + URLClassLoader ucl = new URLClassLoader(new URL[]{bcJar.toURI().toURL()}, cl); + Thread.currentThread().setContextClassLoader(ucl); + } + + @Test + public void getSignerUnsigned() throws Exception { + String testFiles[] = { + "hello-world-unsigned.docx", + "hello-world-unsigned.pptx", + "hello-world-unsigned.xlsx", + "hello-world-office-2010-technical-preview-unsigned.docx" + }; + + for (String testFile : testFiles) { + OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ); + SignatureInfo si = new SignatureInfo(pkg); + List result = si.getSigners(); + pkg.revert(); + pkg.close(); + assertNotNull(result); + assertTrue(result.isEmpty()); + } + } + + @Test + public void getSigner() throws Exception { + String testFiles[] = { + "hyperlink-example-signed.docx", + "hello-world-signed.docx", + "hello-world-signed.pptx", + "hello-world-signed.xlsx", + "hello-world-office-2010-technical-preview.docx", + "ms-office-2010-signed.docx", + "ms-office-2010-signed.pptx", + "ms-office-2010-signed.xlsx", + "Office2010-SP1-XAdES-X-L.docx", + "signed.docx", + }; + + for (String testFile : testFiles) { + OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ); + SignatureInfo si = new SignatureInfo(pkg); + List result = si.getSigners(); + + assertNotNull(result); + assertEquals("test-file: "+testFile, 1, result.size()); + X509Certificate signer = result.get(0); + LOG.log(POILogger.DEBUG, "signer: " + signer.getSubjectX500Principal()); + + boolean b = si.verifySignature(); + assertTrue("test-file: "+testFile, b); + pkg.revert(); + } + } + + @Test + public void getMultiSigners() throws Exception { + String testFile = "hello-world-signed-twice.docx"; + OPCPackage pkg = OPCPackage.open(testdata.getFile(testFile), PackageAccess.READ); + SignatureInfo si = new SignatureInfo(pkg); + List result = si.getSigners(); + + assertNotNull(result); + assertEquals("test-file: "+testFile, 2, result.size()); + X509Certificate signer1 = result.get(0); + X509Certificate signer2 = result.get(1); + LOG.log(POILogger.DEBUG, "signer 1: " + signer1.getSubjectX500Principal()); + LOG.log(POILogger.DEBUG, "signer 2: " + signer2.getSubjectX500Principal()); + + boolean b = si.verifySignature(); + assertTrue("test-file: "+testFile, b); + pkg.revert(); + } + + @Test + public void testSignSpreadsheet() throws Exception { + String testFile = "hello-world-unsigned.xlsx"; + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + sign(pkg, "Test", "CN=Test", 1); + pkg.close(); + } + + @Test + public void testSignSpreadsheetWithSignatureInfo() throws Exception { + String testFile = "hello-world-unsigned.xlsx"; + OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + SignatureInfo si = new SignatureInfo(pkg); + initKeyPair("Test", "CN=Test"); + si.confirmSignature(keyPair.getPrivate(), x509, HashAlgorithm.sha1); + List signer = si.getSigners(); + assertEquals(1, signer.size()); + pkg.close(); + } + + + private OPCPackage sign(OPCPackage pkgCopy, String alias, String signerDn, int signerCount) throws Exception { + /*** TODO : set cal to now ... only set to fixed date for debugging ... */ + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.setTimeZone(TimeZone.getTimeZone("UTC")); + cal.set(2014, 7, 6, 21, 42, 12); + + XmlSignatureService signatureService = new XmlSignatureService(HashAlgorithm.sha1, pkgCopy); + signatureService.initFacets(cal.getTime()); + initKeyPair(alias, signerDn); + + // operate + List x509Chain = Collections.singletonList(x509); + DigestInfo digestInfo = signatureService.preSign(null, x509Chain, null, null, null); + + // verify + assertNotNull(digestInfo); + LOG.log(POILogger.DEBUG, "digest algo: " + digestInfo.hashAlgo); + LOG.log(POILogger.DEBUG, "digest description: " + digestInfo.description); + assertEquals("Office OpenXML Document", digestInfo.description); + assertNotNull(digestInfo.hashAlgo); + assertNotNull(digestInfo.digestValue); + + // setup: key material, signature value + + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate()); + ByteArrayOutputStream digestInfoValueBuf = new ByteArrayOutputStream(); + digestInfoValueBuf.write(SignatureInfo.SHA1_DIGEST_INFO_PREFIX); + digestInfoValueBuf.write(digestInfo.digestValue); + byte[] digestInfoValue = digestInfoValueBuf.toByteArray(); + byte[] signatureValue = cipher.doFinal(digestInfoValue); + + // operate: postSign + signatureService.postSign(signatureValue, Collections.singletonList(x509)); + + // verify: signature + SignatureInfo si = new SignatureInfo(pkgCopy); + List signers = si.getSigners(); + assertEquals(signerCount, signers.size()); + + return pkgCopy; + } + + private void initKeyPair(String alias, String subjectDN) throws Exception { + final char password[] = "test".toCharArray(); + File file = new File("build/test.pfx"); + + KeyStore keystore = KeyStore.getInstance("PKCS12"); + + if (file.exists()) { + FileInputStream fis = new FileInputStream(file); + keystore.load(fis, password); + fis.close(); + } else { + keystore.load(null, password); + } + + if (keystore.isKeyEntry(alias)) { + Key key = keystore.getKey(alias, password); + x509 = (X509Certificate)keystore.getCertificate(alias); + keyPair = new KeyPair(x509.getPublicKey(), (PrivateKey)key); + } else { + keyPair = PkiTestUtils.generateKeyPair(); + Calendar cal = Calendar.getInstance(); + Date notBefore = cal.getTime(); + cal.add(Calendar.YEAR, 1); + Date notAfter = cal.getTime(); + KeyUsageIf keyUsage = HorribleProxy.newProxy(KeyUsageIf.class); + keyUsage = HorribleProxy.newProxy(KeyUsageIf.class, keyUsage.digitalSignature()); + + x509 = PkiTestUtils.generateCertificate(keyPair.getPublic(), subjectDN + , notBefore, notAfter, null, keyPair.getPrivate(), true, 0, null, null, keyUsage); + + keystore.setKeyEntry(alias, keyPair.getPrivate(), password, new Certificate[]{x509}); + FileOutputStream fos = new FileOutputStream(file); + keystore.store(fos, password); + fos.close(); + } + } + + private static File copy(File input) throws IOException { + String extension = input.getName().replaceAll(".*?(\\.[^.]+)?$", "$1"); + if (extension == null || "".equals(extension)) extension = ".zip"; + File tmpFile = new File("build", "sigtest"+extension); + FileOutputStream fos = new FileOutputStream(tmpFile); + FileInputStream fis = new FileInputStream(input); + IOUtils.copy(fis, fos); + fis.close(); + fos.close(); + return tmpFile; + } +} diff --git a/src/testcases/org/apache/poi/POIDataSamples.java b/src/testcases/org/apache/poi/POIDataSamples.java index b335215390..a62e664e1e 100644 --- a/src/testcases/org/apache/poi/POIDataSamples.java +++ b/src/testcases/org/apache/poi/POIDataSamples.java @@ -44,6 +44,7 @@ public final class POIDataSamples { private static POIDataSamples _instHPSF; private static POIDataSamples _instHPBF; private static POIDataSamples _instHSMF; + private static POIDataSamples _instXmlDSign; private File _resolvedDataDir; /** true if standard system propery is not set, @@ -114,6 +115,12 @@ public final class POIDataSamples { if(_instHSMF == null) _instHSMF = new POIDataSamples("hsmf"); return _instHSMF; } + + public static POIDataSamples getXmlDSignInstance(){ + if(_instXmlDSign == null) _instXmlDSign = new POIDataSamples("xmldsign"); + return _instXmlDSign; + } + /** * Opens a sample file from the test data directory * diff --git a/test-data/xmldsign/Office2010-SP1-XAdES-X-L.docx b/test-data/xmldsign/Office2010-SP1-XAdES-X-L.docx new file mode 100644 index 0000000000..4aaa772a0d Binary files /dev/null and b/test-data/xmldsign/Office2010-SP1-XAdES-X-L.docx differ diff --git a/test-data/xmldsign/bcprov-ext-jdk15on-1.49.jar b/test-data/xmldsign/bcprov-ext-jdk15on-1.49.jar new file mode 100644 index 0000000000..cb0e0be8cd Binary files /dev/null and b/test-data/xmldsign/bcprov-ext-jdk15on-1.49.jar differ diff --git a/test-data/xmldsign/hello-world-office-2010-technical-preview-unsigned.docx b/test-data/xmldsign/hello-world-office-2010-technical-preview-unsigned.docx new file mode 100644 index 0000000000..5162b67c60 Binary files /dev/null and b/test-data/xmldsign/hello-world-office-2010-technical-preview-unsigned.docx differ diff --git a/test-data/xmldsign/hello-world-office-2010-technical-preview.docx b/test-data/xmldsign/hello-world-office-2010-technical-preview.docx new file mode 100644 index 0000000000..cbd4277564 Binary files /dev/null and b/test-data/xmldsign/hello-world-office-2010-technical-preview.docx differ diff --git a/test-data/xmldsign/hello-world-signed-twice.docx b/test-data/xmldsign/hello-world-signed-twice.docx new file mode 100644 index 0000000000..96c91e957e Binary files /dev/null and b/test-data/xmldsign/hello-world-signed-twice.docx differ diff --git a/test-data/xmldsign/hello-world-signed.docx b/test-data/xmldsign/hello-world-signed.docx new file mode 100644 index 0000000000..79a7bbb81f Binary files /dev/null and b/test-data/xmldsign/hello-world-signed.docx differ diff --git a/test-data/xmldsign/hello-world-signed.pptx b/test-data/xmldsign/hello-world-signed.pptx new file mode 100644 index 0000000000..9b37033f54 Binary files /dev/null and b/test-data/xmldsign/hello-world-signed.pptx differ diff --git a/test-data/xmldsign/hello-world-signed.xlsx b/test-data/xmldsign/hello-world-signed.xlsx new file mode 100644 index 0000000000..0d45c53ede Binary files /dev/null and b/test-data/xmldsign/hello-world-signed.xlsx differ diff --git a/test-data/xmldsign/hello-world-unsigned.docx b/test-data/xmldsign/hello-world-unsigned.docx new file mode 100644 index 0000000000..1790c961ce Binary files /dev/null and b/test-data/xmldsign/hello-world-unsigned.docx differ diff --git a/test-data/xmldsign/hello-world-unsigned.pptx b/test-data/xmldsign/hello-world-unsigned.pptx new file mode 100644 index 0000000000..ca42529a9a Binary files /dev/null and b/test-data/xmldsign/hello-world-unsigned.pptx differ diff --git a/test-data/xmldsign/hello-world-unsigned.xlsx b/test-data/xmldsign/hello-world-unsigned.xlsx new file mode 100644 index 0000000000..b99143e92c Binary files /dev/null and b/test-data/xmldsign/hello-world-unsigned.xlsx differ diff --git a/test-data/xmldsign/hyperlink-example-signed.docx b/test-data/xmldsign/hyperlink-example-signed.docx new file mode 100644 index 0000000000..f8698fe4d7 Binary files /dev/null and b/test-data/xmldsign/hyperlink-example-signed.docx differ diff --git a/test-data/xmldsign/ms-office-2010-signed.docx b/test-data/xmldsign/ms-office-2010-signed.docx new file mode 100644 index 0000000000..61e4e2a1b8 Binary files /dev/null and b/test-data/xmldsign/ms-office-2010-signed.docx differ diff --git a/test-data/xmldsign/ms-office-2010-signed.pptx b/test-data/xmldsign/ms-office-2010-signed.pptx new file mode 100644 index 0000000000..c70f153248 Binary files /dev/null and b/test-data/xmldsign/ms-office-2010-signed.pptx differ diff --git a/test-data/xmldsign/ms-office-2010-signed.xlsx b/test-data/xmldsign/ms-office-2010-signed.xlsx new file mode 100644 index 0000000000..d8ba05b1da Binary files /dev/null and b/test-data/xmldsign/ms-office-2010-signed.xlsx differ diff --git a/test-data/xmldsign/signed.docx b/test-data/xmldsign/signed.docx new file mode 100644 index 0000000000..98f3b8d841 Binary files /dev/null and b/test-data/xmldsign/signed.docx differ