From 5169036f66ce31a75b33e0056d468428ceabf098 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Wed, 13 Jun 2018 20:06:08 +0000 Subject: [PATCH] #62452 - Extract configuration while verifying XML signatures git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1833477 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/poifs/crypt/dsig/SignatureConfig.java | 192 +++++++++-- .../poi/poifs/crypt/dsig/SignaturePart.java | 62 +++- .../dsig/facets/OOXMLSignatureFacet.java | 13 +- .../poi/poifs/crypt/TestSignatureInfo.java | 307 +++++++++++------- test-data/xmldsign/PPT2016withComment.pptx | Bin 0 -> 36193 bytes 5 files changed, 420 insertions(+), 154 deletions(-) create mode 100644 test-data/xmldsign/PPT2016withComment.pptx diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java index f3184ab09..9fa93f7a0 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java @@ -23,10 +23,14 @@ import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XADES_132_NS import java.security.PrivateKey; import java.security.Provider; import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.UUID; @@ -49,6 +53,7 @@ import org.apache.poi.poifs.crypt.dsig.services.SignaturePolicyService; import org.apache.poi.poifs.crypt.dsig.services.TSPTimeStampService; import org.apache.poi.poifs.crypt.dsig.services.TimeStampService; import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator; +import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.xml.security.signature.XMLSignature; @@ -60,10 +65,16 @@ import org.w3c.dom.events.EventListener; * Apart of the thread local members (e.g. opc-package) most values will probably be constant, so * it might be configured centrally (e.g. by spring) */ +@SuppressWarnings({"unused","WeakerAccess"}) public class SignatureConfig { + public static final String SIGNATURE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final POILogger LOG = POILogFactory.getLogger(SignatureConfig.class); - + private static final String DigestMethod_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#sha224"; + private static final String DigestMethod_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"; + + public interface SignatureConfigurable { void setSignatureConfig(SignatureConfig signatureConfig); } @@ -150,14 +161,20 @@ public class SignatureConfig { * with certain namespaces, so this EventListener is used to interfere * with the marshalling process. */ - EventListener signatureMarshalListener; + private EventListener signatureMarshalListener; /** * Map of namespace uris to prefix * If a mapping is specified, the corresponding elements will be prefixed */ - Map namespacePrefixes = new HashMap<>(); - + private final Map namespacePrefixes = new HashMap<>(); + + /** + * if true, the signature config is updated based on the validated document + */ + private boolean updateConfigOnValidate = false; + + /** * Inits and checks the config object. * If not set previously, complex configuration properties also get @@ -308,7 +325,36 @@ public class SignatureConfig { public void setExecutionTime(Date executionTime) { this.executionTime = executionTime; } - + + /** + * @return the formatted execution time ({@link #SIGNATURE_TIME_FORMAT}) + * + * @since POI 4.0.0 + */ + public String formatExecutionTime() { + final DateFormat fmt = new SimpleDateFormat(SIGNATURE_TIME_FORMAT, Locale.ROOT); + fmt.setTimeZone(LocaleUtil.TIMEZONE_UTC); + return fmt.format(getExecutionTime()); + } + + /** + * Sets the executionTime which is in standard format ({@link #SIGNATURE_TIME_FORMAT}) + * @param executionTime the execution time + * + * @since POI 4.0.0 + */ + public void setExecutionTime(String executionTime) { + if (executionTime != null && !"".equals(executionTime)){ + final DateFormat fmt = new SimpleDateFormat(SIGNATURE_TIME_FORMAT, Locale.ROOT); + fmt.setTimeZone(LocaleUtil.TIMEZONE_UTC); + try { + this.executionTime = fmt.parse(executionTime); + } catch (ParseException e) { + LOG.log(POILogger.WARN, "Illegal execution time: "+executionTime); + } + } + } + /** * @return the service to be used for XAdES-EPES properties. There's no default implementation */ @@ -364,7 +410,24 @@ public class SignatureConfig { * @param canonicalizationMethod the default canonicalization method */ public void setCanonicalizationMethod(String canonicalizationMethod) { - this.canonicalizationMethod = canonicalizationMethod; + this.canonicalizationMethod = verifyCanonicalizationMethod(canonicalizationMethod, CanonicalizationMethod.INCLUSIVE); + } + + private static String verifyCanonicalizationMethod(String canonicalizationMethod, String defaultMethod) { + if (canonicalizationMethod == null || canonicalizationMethod.isEmpty()) { + return defaultMethod; + } + + switch (canonicalizationMethod) { + case CanonicalizationMethod.INCLUSIVE: + case CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS: + case CanonicalizationMethod.ENVELOPED: + case CanonicalizationMethod.EXCLUSIVE: + case CanonicalizationMethod.EXCLUSIVE_WITH_COMMENTS: + return canonicalizationMethod; + } + + throw new EncryptedDocumentException("Unknown CanonicalizationMethod: "+canonicalizationMethod); } /** @@ -532,6 +595,16 @@ public class SignatureConfig { this.xadesDigestAlgo = xadesDigestAlgo; } + /** + * @param xadesDigestAlgo hash algorithm used for XAdES. + * When null, defaults to {@link #getDigestAlgo()} + * + * @since POI 4.0.0 + */ + public void setXadesDigestAlgo(String xadesDigestAlgo) { + this.xadesDigestAlgo = getDigestMethodAlgo(xadesDigestAlgo); + } + /** * @return the user agent used for http communication (e.g. to the TSP) */ @@ -700,16 +773,17 @@ public class SignatureConfig { * @param namespacePrefixes the map of namespace uri (key) to prefix (value) */ public void setNamespacePrefixes(Map namespacePrefixes) { - this.namespacePrefixes = namespacePrefixes; + this.namespacePrefixes.clear(); + this.namespacePrefixes.putAll(namespacePrefixes); } /** * helper method for null/default value handling - * @param value - * @param defaultValue + * @param value the value to be tested + * @param defaultValue the default value * @return if value is not null, return value otherwise defaultValue */ - protected static T nvl(T value, T defaultValue) { + private static T nvl(T value, T defaultValue) { return value == null ? defaultValue : value; } @@ -729,34 +803,92 @@ public class SignatureConfig { +getDigestAlgo()+" not supported for signing."); } } - + /** * @return the uri for the main digest */ public String getDigestMethodUri() { return getDigestMethodUri(getDigestAlgo()); } - + /** - * Sets the digest algorithm - currently only sha* and ripemd160 is supported. + * Converts the digest algorithm - currently only sha* and ripemd160 is supported. * MS Office only supports sha1, sha256, sha384, sha512. * - * @param digestAlgo the digest algorithm + * @param digestAlgo the digest algorithm * @return the uri for the given digest */ public static String getDigestMethodUri(HashAlgorithm digestAlgo) { switch (digestAlgo) { case sha1: return DigestMethod.SHA1; - case sha224: return "http://www.w3.org/2001/04/xmldsig-more#sha224"; + case sha224: return DigestMethod_SHA224; case sha256: return DigestMethod.SHA256; - case sha384: return "http://www.w3.org/2001/04/xmldsig-more#sha384"; + case sha384: return DigestMethod_SHA384; case sha512: return DigestMethod.SHA512; case ripemd160: return DigestMethod.RIPEMD160; default: throw new EncryptedDocumentException("Hash algorithm " +digestAlgo+" not supported for signing."); } } - + + /** + * Converts the digest algorithm ur - currently only sha* and ripemd160 is supported. + * MS Office only supports sha1, sha256, sha384, sha512. + * + * @param digestAlgo the digest algorithm uri + * @return the hash algorithm for the given digest + */ + private static HashAlgorithm getDigestMethodAlgo(String digestMethodUri) { + if (digestMethodUri == null || digestMethodUri.isEmpty()) { + return null; + } + switch (digestMethodUri) { + case DigestMethod.SHA1: return HashAlgorithm.sha1; + case DigestMethod_SHA224: return HashAlgorithm.sha224; + case DigestMethod.SHA256: return HashAlgorithm.sha256; + case DigestMethod_SHA384: return HashAlgorithm.sha384; + case DigestMethod.SHA512: return HashAlgorithm.sha512; + case DigestMethod.RIPEMD160: return HashAlgorithm.ripemd160; + default: throw new EncryptedDocumentException("Hash algorithm " + +digestMethodUri+" not supported for signing."); + } + } + + /** + * Set the digest algorithm based on the method uri. + * This is used when a signature was successful validated and the signature + * configuration is updated + * + * @param signatureMethodUri the method uri + * + * @since POI 4.0.0 + */ + public void setSignatureMethodFromUri(final String signatureMethodUri) { + switch (signatureMethodUri) { + case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1: + setDigestAlgo(HashAlgorithm.sha1); + break; + case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA224: + setDigestAlgo(HashAlgorithm.sha224); + break; + case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256: + setDigestAlgo(HashAlgorithm.sha256); + break; + case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA384: + setDigestAlgo(HashAlgorithm.sha384); + break; + case XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512: + setDigestAlgo(HashAlgorithm.sha512); + break; + case XMLSignature.ALGO_ID_SIGNATURE_RSA_RIPEMD160: + setDigestAlgo(HashAlgorithm.ripemd160); + break; + default: throw new EncryptedDocumentException("Hash algorithm " + +signatureMethodUri+" not supported."); + } + } + + /** * @param signatureFactory the xml signature factory, saved as thread-local */ @@ -852,6 +984,28 @@ public class SignatureConfig { * @see javax.xml.crypto.dsig.CanonicalizationMethod */ public void setXadesCanonicalizationMethod(String xadesCanonicalizationMethod) { - this.xadesCanonicalizationMethod = xadesCanonicalizationMethod; + this.xadesCanonicalizationMethod = verifyCanonicalizationMethod(xadesCanonicalizationMethod, CanonicalizationMethod.EXCLUSIVE); } -} + + /** + * @return true, if the signature config is to be updated based on the successful validated document + * + * @since POI 4.0.0 + */ + public boolean isUpdateConfigOnValidate() { + return updateConfigOnValidate; + } + + /** + * The signature config can be updated if a document is succesful validated. + * This flag is used for activating this modifications. + * Defaults to {@code false}

+ * + * @param updateConfigOnValidate if true, update config on validate + * + * @since POI 4.0.0 + */ + public void setUpdateConfigOnValidate(boolean updateConfigOnValidate) { + this.updateConfigOnValidate = updateConfigOnValidate; + } +} \ No newline at end of file diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java index cff1e1d9a..81e5b2197 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignaturePart.java @@ -18,24 +18,31 @@ package org.apache.poi.poifs.crypt.dsig; import static org.apache.poi.ooxml.POIXMLTypeLoader.DEFAULT_XML_OPTIONS; +import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.MS_DIGSIG_NS; +import static org.apache.poi.poifs.crypt.dsig.facets.SignatureFacet.XML_DIGSIG_NS; import java.io.IOException; import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; import javax.xml.crypto.MarshalException; import javax.xml.crypto.dsig.XMLSignature; import javax.xml.crypto.dsig.XMLSignatureException; import javax.xml.crypto.dsig.XMLSignatureFactory; import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.namespace.NamespaceContext; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.apache.poi.EncryptedDocumentException; -import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.ooxml.util.DocumentHelper; +import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; import org.apache.xmlbeans.XmlException; @@ -92,7 +99,7 @@ public class SignaturePart { // TODO: check for XXE return SignatureDocument.Factory.parse(signaturePart.getInputStream(), DEFAULT_XML_OPTIONS); } - + /** * @return true, when the xml signature is valid, false otherwise * @@ -100,9 +107,11 @@ public class SignaturePart { */ public boolean validate() { KeyInfoKeySelector keySelector = new KeyInfoKeySelector(); + XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setNamespaceContext(new XPathNSContext()); + try { Document doc = DocumentHelper.readDocument(signaturePart.getInputStream()); - XPath xpath = XPathFactory.newInstance().newXPath(); NodeList nl = (NodeList)xpath.compile("//*[@Id]").evaluate(doc, XPathConstants.NODESET); final int length = nl.getLength(); for (int i=0; i> m = new HashMap(); + m.put("//mdssi:SignatureTime/mdssi:Value", signatureConfig::setExecutionTime); + m.put("//xd:ClaimedRole", signatureConfig::setXadesRole); + m.put("//dsss:SignatureComments", signatureConfig::setSignatureDescription); + m.put("//xd:QualifyingProperties//xd:SignedSignatureProperties//ds:DigestMethod/@Algorithm", signatureConfig::setXadesDigestAlgo); + m.put("//ds:CanonicalizationMethod", signatureConfig::setCanonicalizationMethod); + + for (Map.Entry> me : m.entrySet()) { + String val = (String)xpath.compile(me.getKey()).evaluate(doc, XPathConstants.STRING); + me.getValue().accept(val); + } + } + + private class XPathNSContext implements NamespaceContext { + final Map nsMap = new HashMap<>(); + + { + for (Map.Entry me : signatureConfig.getNamespacePrefixes().entrySet()) { + nsMap.put(me.getValue(), me.getKey()); + } + nsMap.put("dsss", MS_DIGSIG_NS); + nsMap.put("ds", XML_DIGSIG_NS); + } + + public String getNamespaceURI(String prefix) { + return nsMap.get(prefix); + } + public Iterator getPrefixes(String val) { + return null; + } + public String getPrefix(String uri) { + return null; + } + } } diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java index ee61027f7..5d4ccbd86 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java @@ -221,15 +221,11 @@ public class OOXMLSignatureFacet extends SignatureFacet { /* * SignatureTime */ - DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT); - fmt.setTimeZone(LocaleUtil.TIMEZONE_UTC); - String nowStr = fmt.format(signatureConfig.getExecutionTime()); - 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); + ctTime.setValue(signatureConfig.formatExecutionTime()); + LOG.log(POILogger.DEBUG, "execution time: " + ctTime.getValue()); Element n = (Element)document.importNode(ctTime.getDomNode(),true); List signatureTimeContent = new ArrayList<>(); @@ -253,6 +249,11 @@ public class OOXMLSignatureFacet extends SignatureFacet { SignatureInfoV1Document sigV1 = SignatureInfoV1Document.Factory.newInstance(); CTSignatureInfoV1 ctSigV1 = sigV1.addNewSignatureInfoV1(); ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri()); + + if (signatureConfig.getSignatureDescription() != null) { + ctSigV1.setSignatureComments(signatureConfig.getSignatureDescription()); + } + Element n = (Element)document.importNode(ctSigV1.getDomNode(), true); n.setAttributeNS(XML_NS, XMLConstants.XMLNS_ATTRIBUTE, MS_DIGSIG_NS); diff --git a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java index 7bb4f6ac8..dd2bc1705 100644 --- a/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java +++ b/src/ooxml/testcases/org/apache/poi/poifs/crypt/TestSignatureInfo.java @@ -56,11 +56,14 @@ import java.util.Date; import java.util.Iterator; import java.util.List; +import javax.xml.crypto.dsig.CanonicalizationMethod; import javax.xml.crypto.dsig.dom.DOMSignContext; import org.apache.jcp.xml.dsig.internal.dom.DOMSignedInfo; import org.apache.poi.POIDataSamples; import org.apache.poi.POITestCase; +import org.apache.poi.ooxml.util.DocumentHelper; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackageAccess; import org.apache.poi.openxml4j.opc.PackageRelationshipTypes; @@ -69,6 +72,7 @@ import org.apache.poi.poifs.crypt.dsig.SignatureInfo; import org.apache.poi.poifs.crypt.dsig.SignaturePart; import org.apache.poi.poifs.crypt.dsig.facets.EnvelopedSignatureFacet; import org.apache.poi.poifs.crypt.dsig.facets.KeyInfoSignatureFacet; +import org.apache.poi.poifs.crypt.dsig.facets.OOXMLSignatureFacet; import org.apache.poi.poifs.crypt.dsig.facets.XAdESSignatureFacet; import org.apache.poi.poifs.crypt.dsig.facets.XAdESXLSignatureFacet; import org.apache.poi.poifs.crypt.dsig.services.RevocationData; @@ -77,7 +81,6 @@ import org.apache.poi.poifs.crypt.dsig.services.TimeStampService; import org.apache.poi.poifs.crypt.dsig.services.TimeStampServiceValidator; import org.apache.poi.poifs.storage.RawDataUtil; import org.apache.poi.ss.usermodel.WorkbookFactory; -import org.apache.poi.ooxml.util.DocumentHelper; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.POILogFactory; @@ -186,7 +189,7 @@ public class TestSignatureInfo { bos.reset(); pkg1.save(bos); pkg1.close(); - + XSSFWorkbook wb2 = new XSSFWorkbook(new ByteArrayInputStream(bos.toByteArray())); assertEquals("Test", wb2.getSheetAt(0).getRow(1).getCell(1).getStringCellValue()); OPCPackage pkg2 = wb2.getPackage(); @@ -395,142 +398,173 @@ public class TestSignatureInfo { @Test public void testSignEnvelopingDocument() throws Exception { String testFile = "hello-world-unsigned.xlsx"; - OPCPackage pkg = OPCPackage.open(copy(testdata.getFile(testFile)), PackageAccess.READ_WRITE); + File sigCopy = testdata.getFile(testFile); + ByteArrayOutputStream bos = new ByteArrayOutputStream(50000); - initKeyPair("Test", "CN=Test"); - final X509CRL crl = PkiTestUtils.generateCrl(x509, keyPair.getPrivate()); - - // setup - SignatureConfig signatureConfig = new SignatureConfig(); - signatureConfig.setOpcPackage(pkg); - signatureConfig.setKey(keyPair.getPrivate()); + final String execTimestr; - /* - * We need at least 2 certificates for the XAdES-C complete certificate - * refs construction. - */ - List certificateChain = new ArrayList<>(); - certificateChain.add(x509); - certificateChain.add(x509); - signatureConfig.setSigningCertificateChain(certificateChain); - - signatureConfig.addSignatureFacet(new EnvelopedSignatureFacet()); - signatureConfig.addSignatureFacet(new KeyInfoSignatureFacet()); - signatureConfig.addSignatureFacet(new XAdESSignatureFacet()); - signatureConfig.addSignatureFacet(new XAdESXLSignatureFacet()); - - // check for internet, no error means it works - boolean mockTsp = (getAccessError("http://timestamp.comodoca.com/rfc3161", true, 10000) != null); - - // http://timestamping.edelweb.fr/service/tsp - // http://tsa.belgium.be/connect - // http://timestamp.comodoca.com/authenticode - // http://timestamp.comodoca.com/rfc3161 - // http://services.globaltrustfinder.com/adss/tsa - signatureConfig.setTspUrl("http://timestamp.comodoca.com/rfc3161"); - signatureConfig.setTspRequestPolicy(null); // comodoca request fails, if default policy is set ... - signatureConfig.setTspOldProtocol(false); - - //set proxy info if any - String proxy = System.getProperty("http_proxy"); - if (proxy != null && proxy.trim().length() > 0) { - signatureConfig.setProxyUrl(proxy); - } - if (mockTsp) { - TimeStampService tspService = new TimeStampService(){ - @Override - public byte[] timeStamp(byte[] data, RevocationData revocationData) { - revocationData.addCRL(crl); - return "time-stamp-token".getBytes(LocaleUtil.CHARSET_1252); + try (OPCPackage pkg = OPCPackage.open(copy(sigCopy), PackageAccess.READ_WRITE)) { + + initKeyPair("Test", "CN=Test"); + final X509CRL crl = PkiTestUtils.generateCrl(x509, keyPair.getPrivate()); + + // setup + SignatureConfig signatureConfig = new SignatureConfig(); + signatureConfig.setOpcPackage(pkg); + signatureConfig.setKey(keyPair.getPrivate()); + + /* + * We need at least 2 certificates for the XAdES-C complete certificate + * refs construction. + */ + List certificateChain = new ArrayList<>(); + certificateChain.add(x509); + certificateChain.add(x509); + signatureConfig.setSigningCertificateChain(certificateChain); + + signatureConfig.addSignatureFacet(new OOXMLSignatureFacet()); + signatureConfig.addSignatureFacet(new EnvelopedSignatureFacet()); + signatureConfig.addSignatureFacet(new KeyInfoSignatureFacet()); + signatureConfig.addSignatureFacet(new XAdESSignatureFacet()); + signatureConfig.addSignatureFacet(new XAdESXLSignatureFacet()); + + // check for internet, no error means it works + boolean mockTsp = (getAccessError("http://timestamp.comodoca.com/rfc3161", true, 10000) != null); + + // http://timestamping.edelweb.fr/service/tsp + // http://tsa.belgium.be/connect + // http://timestamp.comodoca.com/authenticode + // http://timestamp.comodoca.com/rfc3161 + // http://services.globaltrustfinder.com/adss/tsa + signatureConfig.setTspUrl("http://timestamp.comodoca.com/rfc3161"); + signatureConfig.setTspRequestPolicy(null); // comodoca request fails, if default policy is set ... + signatureConfig.setTspOldProtocol(false); + + signatureConfig.setXadesDigestAlgo(HashAlgorithm.sha512); + signatureConfig.setXadesRole("Xades Reviewer"); + signatureConfig.setSignatureDescription("test xades signature"); + + execTimestr = signatureConfig.formatExecutionTime(); + + //set proxy info if any + String proxy = System.getProperty("http_proxy"); + if (proxy != null && proxy.trim().length() > 0) { + signatureConfig.setProxyUrl(proxy); + } + + if (mockTsp) { + TimeStampService tspService = new TimeStampService() { + @Override + public byte[] timeStamp(byte[] data, RevocationData revocationData) { + revocationData.addCRL(crl); + return "time-stamp-token".getBytes(LocaleUtil.CHARSET_1252); + } + + @Override + public void setSignatureConfig(SignatureConfig config) { + // empty on purpose + } + }; + signatureConfig.setTspService(tspService); + } else { + TimeStampServiceValidator tspValidator = (validateChain, revocationData) -> { + for (X509Certificate certificate : validateChain) { + LOG.log(POILogger.DEBUG, "certificate: " + certificate.getSubjectX500Principal()); + LOG.log(POILogger.DEBUG, "validity: " + certificate.getNotBefore() + " - " + certificate.getNotAfter()); + } + }; + signatureConfig.setTspValidator(tspValidator); + signatureConfig.setTspOldProtocol(signatureConfig.getTspUrl().contains("edelweb")); + } + + final RevocationData revocationData = new RevocationData(); + revocationData.addCRL(crl); + OCSPResp ocspResp = PkiTestUtils.createOcspResp(x509, false, + x509, x509, keyPair.getPrivate(), "SHA1withRSA", cal.getTimeInMillis()); + revocationData.addOCSP(ocspResp.getEncoded()); + + RevocationDataService revocationDataService = revocationChain -> revocationData; + signatureConfig.setRevocationDataService(revocationDataService); + + // operate + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(signatureConfig); + try { + si.confirmSignature(); + } catch (RuntimeException e) { + pkg.close(); + // only allow a ConnectException because of timeout, we see this in Jenkins from time to time... + if (e.getCause() == null) { + throw e; } - @Override - public void setSignatureConfig(SignatureConfig config) { - // empty on purpose + if ((e.getCause() instanceof ConnectException) || (e.getCause() instanceof SocketTimeoutException)) { + Assume.assumeFalse("Only allowing ConnectException with 'timed out' as message here, but had: " + e, + e.getCause().getMessage().contains("timed out")); + } else if (e.getCause() instanceof IOException) { + Assume.assumeFalse("Only allowing IOException with 'Error contacting TSP server' as message here, but had: " + e, + e.getCause().getMessage().contains("Error contacting TSP server")); + } else if (e.getCause() instanceof RuntimeException) { + Assume.assumeFalse("Only allowing RuntimeException with 'This site is cur' as message here, but had: " + e, + e.getCause().getMessage().contains("This site is cur")); } - }; - signatureConfig.setTspService(tspService); - } else { - TimeStampServiceValidator tspValidator = (validateChain, revocationData) -> { - for (X509Certificate certificate : validateChain) { - LOG.log(POILogger.DEBUG, "certificate: " + certificate.getSubjectX500Principal()); - LOG.log(POILogger.DEBUG, "validity: " + certificate.getNotBefore() + " - " + certificate.getNotAfter()); - } - }; - signatureConfig.setTspValidator(tspValidator); - signatureConfig.setTspOldProtocol(signatureConfig.getTspUrl().contains("edelweb")); - } - - final RevocationData revocationData = new RevocationData(); - revocationData.addCRL(crl); - OCSPResp ocspResp = PkiTestUtils.createOcspResp(x509, false, - x509, x509, keyPair.getPrivate(), "SHA1withRSA", cal.getTimeInMillis()); - revocationData.addOCSP(ocspResp.getEncoded()); - - RevocationDataService revocationDataService = revocationChain -> revocationData; - signatureConfig.setRevocationDataService(revocationDataService); - - // operate - SignatureInfo si = new SignatureInfo(); - si.setSignatureConfig(signatureConfig); - try { - si.confirmSignature(); - } catch (RuntimeException e) { - pkg.close(); - // only allow a ConnectException because of timeout, we see this in Jenkins from time to time... - if(e.getCause() == null) { throw e; } - if((e.getCause() instanceof ConnectException) || (e.getCause() instanceof SocketTimeoutException)) { - Assume.assumeFalse("Only allowing ConnectException with 'timed out' as message here, but had: " + e, - e.getCause().getMessage().contains("timed out")); - } else if (e.getCause() instanceof IOException) { - Assume.assumeFalse("Only allowing IOException with 'Error contacting TSP server' as message here, but had: " + e, - e.getCause().getMessage().contains("Error contacting TSP server")); - } else if (e.getCause() instanceof RuntimeException) { - Assume.assumeFalse("Only allowing RuntimeException with 'This site is cur' as message here, but had: " + e, - e.getCause().getMessage().contains("This site is cur")); + + // verify + Iterator spIter = si.getSignatureParts().iterator(); + assertTrue("Had: " + si.getSignatureConfig().getOpcPackage(). + getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN), + spIter.hasNext()); + SignaturePart sp = spIter.next(); + boolean valid = sp.validate(); + assertTrue(valid); + + SignatureDocument sigDoc = sp.getSignatureDocument(); + String declareNS = + "declare namespace xades='http://uri.etsi.org/01903/v1.3.2#'; " + + "declare namespace ds='http://www.w3.org/2000/09/xmldsig#'; "; + + String digestValXQuery = declareNS + + "$this/ds:Signature/ds:SignedInfo/ds:Reference"; + for (ReferenceType rt : (ReferenceType[]) sigDoc.selectPath(digestValXQuery)) { + assertNotNull(rt.getDigestValue()); + assertEquals(signatureConfig.getDigestMethodUri(), rt.getDigestMethod().getAlgorithm()); } - throw e; - } - - // verify - Iterator spIter = si.getSignatureParts().iterator(); - assertTrue("Had: " + si.getSignatureConfig().getOpcPackage(). - getRelationshipsByType(PackageRelationshipTypes.DIGITAL_SIGNATURE_ORIGIN), - spIter.hasNext()); - SignaturePart sp = spIter.next(); - boolean valid = sp.validate(); - assertTrue(valid); - - SignatureDocument sigDoc = sp.getSignatureDocument(); - String declareNS = - "declare namespace xades='http://uri.etsi.org/01903/v1.3.2#'; " - + "declare namespace ds='http://www.w3.org/2000/09/xmldsig#'; "; - - String digestValXQuery = declareNS + - "$this/ds:Signature/ds:SignedInfo/ds:Reference"; - for (ReferenceType rt : (ReferenceType[])sigDoc.selectPath(digestValXQuery)) { - assertNotNull(rt.getDigestValue()); - assertEquals(signatureConfig.getDigestMethodUri(), rt.getDigestMethod().getAlgorithm()); + + String certDigestXQuery = declareNS + + "$this//xades:SigningCertificate/xades:Cert/xades:CertDigest"; + XmlObject xoList[] = sigDoc.selectPath(certDigestXQuery); + assertEquals(xoList.length, 1); + DigestAlgAndValueType certDigest = (DigestAlgAndValueType) xoList[0]; + assertNotNull(certDigest.getDigestValue()); + + String qualPropXQuery = declareNS + + "$this/ds:Signature/ds:Object/xades:QualifyingProperties"; + xoList = sigDoc.selectPath(qualPropXQuery); + assertEquals(xoList.length, 1); + QualifyingPropertiesType qualProp = (QualifyingPropertiesType) xoList[0]; + boolean qualPropXsdOk = qualProp.validate(); + assertTrue(qualPropXsdOk); + + pkg.save(bos); } - String certDigestXQuery = declareNS + - "$this//xades:SigningCertificate/xades:Cert/xades:CertDigest"; - XmlObject xoList[] = sigDoc.selectPath(certDigestXQuery); - assertEquals(xoList.length, 1); - DigestAlgAndValueType certDigest = (DigestAlgAndValueType)xoList[0]; - assertNotNull(certDigest.getDigestValue()); + try (OPCPackage pkg = OPCPackage.open(new ByteArrayInputStream(bos.toByteArray()))) { + SignatureConfig signatureConfig = new SignatureConfig(); + signatureConfig.setOpcPackage(pkg); + signatureConfig.setUpdateConfigOnValidate(true); - String qualPropXQuery = declareNS + - "$this/ds:Signature/ds:Object/xades:QualifyingProperties"; - xoList = sigDoc.selectPath(qualPropXQuery); - assertEquals(xoList.length, 1); - QualifyingPropertiesType qualProp = (QualifyingPropertiesType)xoList[0]; - boolean qualPropXsdOk = qualProp.validate(); - assertTrue(qualPropXsdOk); + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(signatureConfig); - pkg.close(); + assertTrue(si.verifySignature()); + + assertEquals(HashAlgorithm.sha512, signatureConfig.getXadesDigestAlgo()); + assertEquals("Xades Reviewer", signatureConfig.getXadesRole()); + assertEquals("test xades signature", signatureConfig.getSignatureDescription()); + assertEquals(execTimestr, signatureConfig.formatExecutionTime()); + } } public static String getAccessError(String destinationUrl, boolean fireRequest, int timeout) { @@ -698,6 +732,27 @@ public class TestSignatureInfo { } } + @Test + public void testRetrieveCertificate() throws InvalidFormatException, IOException { + SignatureConfig sic = new SignatureConfig(); + final File file = testdata.getFile("PPT2016withComment.pptx"); + try (final OPCPackage pkg = OPCPackage.open(file, PackageAccess.READ)) { + sic.setOpcPackage(pkg); + sic.setUpdateConfigOnValidate(true); + SignatureInfo si = new SignatureInfo(); + si.setSignatureConfig(sic); + assertTrue(si.verifySignature()); + } + + final List certs = sic.getSigningCertificateChain(); + assertEquals(1, certs.size()); + assertEquals("CN=Test", certs.get(0).getSubjectDN().getName()); + assertEquals("SuperDuper-Reviewer", sic.getXadesRole()); + assertEquals("Purpose for signing", sic.getSignatureDescription()); + assertEquals("2018-06-10T09:00:54Z", sic.formatExecutionTime()); + assertEquals(CanonicalizationMethod.INCLUSIVE, sic.getCanonicalizationMethod()); + } + private SignatureConfig prepareConfig(String alias, String signerDn, String pfxInput) throws Exception { initKeyPair(alias, signerDn, pfxInput); diff --git a/test-data/xmldsign/PPT2016withComment.pptx b/test-data/xmldsign/PPT2016withComment.pptx new file mode 100644 index 0000000000000000000000000000000000000000..92154f9f0d69763e9402b4d6d67ffbd0ceed4282 GIT binary patch literal 36193 zcmeFYV{~QhvNjy6fI|R4pg>?iKtPB<{D*!Zt%8Dpup@wgpn(8EwM6V}olR_= z^;A9VO`LS--EFK1bC*CVe}aI0tpA_u-^&8y$w~@9M&zMQifcULuH1e)Yxu%KUJx%s z9Mr1_+Yz?=gXJV2x;x-y+;pjkoE99qhm`T=`ulrbrf71{lS)Y*)$`ACDKUZLP#8H= zCzCMMwN0OB1u+1OEaED*>;Sw!Cjq`U_Syt)P)M1aPdu}+5iJqvV$SyP@H1*i zaAyo+ZI|Lt-jjQIV^f5joD^yVCS2_u81l5-GKwr)$={r`qRum<>bfP0CS;1xd!yLV z-0j7nhxMc1Iq8eYeT6;?sE}!E90t@lZpWZ$;+9Ib33=%#4n!HZPy>EMY4m`|@eVHD zCX_^cyKeDFG;#*d`X)S5I?EL|X{4X6$)ge7-n$YdPTjX=8XqSUXHuj5&LJzUXxgb| z!zgrXY;ODZSXiRgMBE&dnmNrP!E$s7U4*iURk|cpyo<<;=W>0;pLPrlv19jiqc0#E z`B3?C*Wz#XSl};9foJ@@DpWX!$eigzh~|J3G;x1kjGY8Hl4B%j^MEt>C?6ggtqpfz5n_3J;ZHRU1=2FryPogZxf#+lD^%>)mWxD zjW^wW+gi@0J8)A#=ixm&_j#y&#uZug60`dvu z094P>#M+60{?9LA?#};2_rF(E|IKd7;}vAUnUDuA{WgjAHYOT>3+1DWGulY^c)gK42zS{GRgm5DtJp~Jz!L4+ccoMU3LIWKI3Pad#?>(Sl2|>4){#tZT0x`mTr@Vm zfE$q&2O|WS<9oZ+NQljfnN{ExbGGbMpsUH%{pcaKG%M33@@Lisy$^Y>t)O1FrO5l2)Qnzak(-(Vffpa zG14Zf@BVnh@ewzKA9s6uX9g#03u6-}hJS?5Uw@eXoREJ7Qp8k;WgjCl^cCN<8*ZlX*ZB`s9$mVP^eNG9iDcfBrwC7p{g+TWCQ(Ly3LILH8NXZ{H6 zZ?<}u6v*0)oEb)h2sljIpisaQiw&y#JEZ6noM({BqpWec%lPH8M@nI z)KxuPDIeAerF}E-@kv0(T#VO}U$af9Zp|*EyG-^><)^?Kg@BzEp65Zj%7WSy32N+o z?~BVMO$`#kRQdA_Hl&Ze{WD_V(6d3qf9!nv12G6dU_k%u{vX@6cQkSOAP5G|7IwD( zXY+))5PwkQbCivTEK$>t zWe%wtx=ZZ-`@^e8TFsZ!OZhrFPvu3J;Wz+h+N}C0E7(TZP#KRtAipHP5eo87vU-w% zL7k!4Bt&eUT7tF^6LmUh>w>GOc0gX)5HHPLS!;6yxxlVDem~echGEuuAvwo?+gTDdG9R&TbTVhX4f%O%rO-tsp{s5I|T#{R+K2 zqQ{Z$H*6a09tW5v$T^#siIghe&-g0TY2}U3G1wCZbpRMXFDI;Vtr6-XZ-%n`L9cO0YvrQ-9?ow^OStMk;9Q6yxYJCbIMP*h2p?O42@ zzg2Avjw>lDh5FHGLnc$`K;@=OzoG}P=g5GGNK$1S_;XV4+d$W6xP8vkWrq z+G5UwSViT`NtNiStAkdYp~P2$4Zx8YS-mi~bCeBD*1M^+|7#*cVkeMDENiwKX_pa8 zjHotVrOy9zw&!i?Bf%r^P$tG<{u)v9veNZUs21(>=U-^FxKs2zMXb_;p&IVuq|^wb zBb88PX}=7XiyfD1!IyA2j~n=%Ql?T?v?}twJawdcQ&U56p%?^g*mPh89J|EjM)sHs zQv9y*Ceex@*l->ie>a=<7qT{?>dRc~bmCO@XJpySu%0uaM-874GW%?AsOB)nWv6gn z5>5;o?k-kR>+Xg7IkhoDBMLN=N(8Ok9s9k@o7AEDRMxz~2fx$+z$+u;Pvh zVvSy9wg6KHP%q?3;KOtN8LS+&#tDl*e5(lx1O)wm`PTn}>i-MU;|U|SA6z%+67m_* z?;?U-Iw^wlM?*Db47>O_KZwXojiMB(H*RA2tEU{eAm`(HZXKpD{l2) z`iT4*zaEe7S8h9UM|K^u(ihJUEgx-4x6Y0Zw^l?ZdXrGH<^8^rx?RATdW>vNj-x z_eiB@A21!&y^EBsG-yY643>f`UA$o)cC97~r=&3KAy=b08Ya*~n+PTbAN1%Dc@KRY z1TL>0&a4l_%A^@_EsGB- zW$-35PhR^mgwg=W+KTw%YHxezjxx_=c0aP$GPg*-<3Y2anWvF2?}CR}G;^Ymf~ zH#+KaB3{g5;nI+!j3wqzA3qo5b;!EW|7$8gFZ9mSw}|%aN+7TznjtwSS`dN-LVrg> z&`j(nZ7G&0V`(Yn@5k!>-?rZ?!nw0VeuFu$F453lHDN463)wj~n0ls=m$HuE5hOp@IV>kUx90(&mnW?1FkD$TD;yVVWVS+Al6Qab8K7La&`Xh-x5D z%+lg#5orO9vwI@!Xe|aX#_f=jBIFB;DbG+jnhfmu+Az<#FDU-jwHIdMuG>G}{9z%q z|IxK&4Ls~zoc|LS`Nt#n|8ppRFcP@lKa51@3a|H^yuv?()di)eN9+zXYHt+^NV=DM z1Wcdt^WXJ(`>kE)E0^c4Y>3ewq^V9goUWgF$+lKGwD1|i?C$f>mQ-Il=EJ$;jJA2d zc9gq)te6t76{cepPr))gE72J8NU{{hY(Psw$rqFfJTK+b($(TS8$lkHfe}kd3N8QU zPLemH;7X?;#2#Mqno5;N(*fq9ih!hJX2M%6J?&{CWEv-O7$zVXii{{P_eE0(Ezo8W@a`lMNKbYeX^nYQGat2P$CXWA!J^tg-|HXy=1-UyxKFJVc-@CN2YKO?*2?hkft>IJ+JeQtg%x`=-o zRz`$98U4^dz&0kQ!s$pSWh#vASm9Epdvd(Jm%9uZe46)GZJnhgXXFJ>-+uL{lxgDT z%S?oDXUq>G&IT(0tCiF*YlV`WbQ!%eLHB7{xTruM@_S!zS$Fhm<#gQ8@#S2w6kl*i zQcigS862502n-8srawu}oxZIo09hkIT(ue_sI-)8zWHwX%4ww&YkT+%`mEKKJ;$&@M8*{wUq~MV;=x2^>CVY=2ruDQH zwK?GCXK{&S^Uz9|xYvHKmW?g4>KINVE?fPmHSnF@Hyw8Uz9~$ zXzP8NnSjrmS}LD>u4^DOvC7+8G3&0R@lPo3>hv(i z&szyyGk$^b3L_=SL)~Ij*FVBSrV(wl{4yznX~n9s>(+Kg$ARtT?IFy{glI+_u#Tx0 zVR}-tQX1vJ`sR6JTDKmyR2z6^-qwOhda++cG}H}CM*wvTobmQ~3bE;w$FE^8gpA+` zA1#JD#19fFw2z7-jxWS*JsQ|s4N7);lSzx4mRAl_J|eOV0tCX|D@oHb5ZPj*jJ3qp z)v%0lsRX$ojnc9(_tuOXi`St zhJ_r)O~Zo=TwPD3z+QP+ssRQuf2I5gjqPkRzg6g1tnW@Cbtk;iXR4+K+VX|T4dWhb zj>*LA{p@g9)Gy_38u3l=uupkD=q}k`PFa2w6U5yqd5qHr_AS@78-+n1*^;{&Xr1k! zMNfk@%;vRBGLM{qXx$qUJ)o-~WxguOJ1|x`Fw?M+~F-9eT^sAj6^S(-Z1IV z*H%&L`wUJw8&1>$7sxiCHdrTsqz@_L!0ZX6`a*$`xTIh;>sc(7novePw$fSwXCK(E z>>>VH-e$8wF(MOlCdPBv&HIx^R(9+B9WHKca&i%%CziXXWBq(DTd`z6mO}KT^>c%+ za(-sQd?UBDmqI9eOPShnPMoE$LYY5)bdQ>D7L;Cfd!T==p_Y2)cgb%N&DzPY&Vr{y z-!0m<4Skr2`9N#5`QU!7p(_-HMYHr=pts&@P7!7NvWO7*x*WKG2DZeo`Ji<6oY0?y z5vY4}6R0a_@c1rBs?m@p?cEIR-88tsw%-isQt_r~W-c)oDYl*qhmAX=5yhbxrs$Wj zMQ%kwEFX5sOFt%82MNwC*U;3p<7`_S4qwm-M3A0&))Yk_OB1mOce2h(oy5vtZBxVf zb@l35N6}GWL*o|S7M53~>JWoY5y2#GYYZLYSsCt!3@{7IvXO9zgAn?46Q%{rFL`k( zH%R0XfAY<~_BG-pE7{U4*HbAGa&E)!Bw8W>Se0NuhCm4$5{!$jB&sqL*cOlqwk>6@ zDKF}`{LQ0xdk)3%f%5Pq!s{S&&=u8C#p6e9Jd6@ni-bP_GGVZA&0a1I*ELO7dG)(n zkuBq9=kFI_Q`I|P-$icPv5LcJ>q+D)tjH!&EJiN!9AIWS*ijA-r=Ss1DNq+#BXnLq z_*mq;d&^?=cy%hm&8DQYs)lRSEPdn?_(P2ZZF;fOhGM7Oxe&=GEP;A--(;6$EwVVx z^`FFS^HK(gg-ztl564J>x9@+8b9)>?j-DSl*Z3jr{!5(uE7=uE9z@~_LR6(dyuZk;+@oo_!D~{uBQbhDiX`vl_?r_kIW}-0*ig7u z8>Sy3dv=z?0mfTJ3}f)1KYQX36CDyQGCOWRkPPF#GMp=+bbaqC*wfz6`@ ztcDnB?)uNjZnE?6)pV|Yc{;+NEJ0o6}&g&+uDQnjL+1^#sQX z+T!9J*y)ChnySB3be}_^eNcX}wuJz_CznS(`hM@LXbIXBvovxb7BMF|<)mat@h(*A z?p(h^2TlX<;h<$JEqgOe%B=A*1lT$*25hqONNT#LjFYkwERz%0Oia#R8;jn4zuDgZE;M%1CDd-MVExp)6T?97pSpgkQ2K z)KYyJjjgV769#b>W$5AGz7^XDDuE&zSOPWxUu3H{?Rwy%X02r5bIg=!b!DWML$6E$l)$1STN?m63*Lg)m;2LsBWToP<*h0I@cU`rH6{ zwtCY2jo-GnzDm(2_uqR{4UMLUjObj#o^)J*_?<-+XPx-~2Ti3na4Ce^Cv5Gucb_Y= zDXpV&^v&xN7}3L_k7fm`C}?M2cu8PIOnM&52ujHQvsAYV26-wRi%Fb7vBYEGVqWH8 zW4tIVoI{!*-!lZ{^8F0FSAgNrqy3hEFW+ZH-#0yE*s?_xGQ`cXc-9*>Tsr4^2Yn!? zBpx78DMT{Kl+rl^`AO-F!gY_lGkZ;zA`y%;!DPb1qXYfbu?&&$5uKfK;K(XfHG{3G zfNsjhJE((2(#5`r{c8KpGhNOz&!(((z-J&9qm+{qr{CZ^6`UEr7%3qW919MmlrTs5 zD?}g4&J%i5l3~nRwiY?z2X6hIg5JU@<)K^H5WTOZ{$$&}zIfS1?EN4NJ7tai!jF6r zFPMqok12+NpqL_>bDp1tBYyV;8>~k&yb_;rYPS~3Gci7vfpx8uFv)yWM*5>unmCMD zSXowuXOOh*oV6pj9`rKGhEb3I{cS9H>OJFU+mXR@L0WC4S)S_-=UoN_XSfa6ep(QJ zSTTo)^MSCc12UOyVg207UX82%HRrkWxdeS{st`Gk8&kT(MI@ntj5(Gr;qbxx5 zFBsZiY0&uBbz2}ipp&554Y}TGFkke9qF6<;z8*mmsBcRkn0WNH__z3sZEdcgo|RVy z{|F-0i0dlrZ6oIfL_Z0G_{RqvDV5R0p?m)9zLWmQNdZ4 zebG=cRs)8#_|SO1JPkw(@SaY7K-ah}FtWB=VlLav$*V%ujX(YVNpoJqchNQXhh3Ta zVyfR44?Y^>2Ym0Ph4G%{^aYKT<-88w!A_luQEl2)Z+g5idi0%$mk&WCj?ssLLy zV75uqS!F(xXG=$%x`sJLUJW0PKt$iTRUKR83dX^b3JXt{#)zf>5zgYo8q}=q{iW<< zD2~a#h*{_5#8TA-R(e&um-tj6tSNN-NimeYUhuB$c00Wq8Eq!9|8DIUwA@JA!-rs2 z7sI4{K*6*yUPWIk{E6sma|)SlnERe+2sx_|(E)%-S{S>KYLN3cCpieCf&$urb%YzdH*xMI zJ@8e>j*Skvzyr@67>PMNKV{2jDSeC^xf>K+STl9g0f455UGB!ab~tM5(P@M=4e)T! zpMg!;ZTp>UjM&-cc-{M>E-!W{mFvzx_WPY6FL?Vy5T(z{Kb0; zka3PQaUEoxpYiN2x+Y#<)x=W-1;Mip_dpS&o%+@-$&yYfVbnABURrX@7}^!rJ{J#> z6q6yBs**X?0>Zye_@@}N-;u1hiZ=OtFxlw^X&H;Vnn}yoCUjc;F)p=x&l((wWeHwm z+!JrBW6T3-;z( zD*d`8Iim&-Sc#G*KvRcWpQf7uU#vU3An>x6ptBLA?l3FE^=&6TKh8*th zz?W1xmUSJD!sD}8Sf$=+k7WUv* zL3zD;dP%oq?}c#icAql6vo57WdH62m&h&lPNqtpBkH`JY@Jz_kAmdx){=@(YmkF03 zR@k2jHo!Ln7TDLaACTh@(=9lk$LNLI8Zr=0IB<0=1(zVaA})Pet|=Ed`ZLl_o%&$* zUZ2ZAH-c5&{*q;85R{1XD<>sf9x^@Jr)F8fh~a3%?D*mR&M5);$6+u|RTH||8&8sf zvPfpY1v*BLyZC`CI)1`_X#Md6e&pi_uoTe>?t17kbzWR<_y$D=x;u;~ZRS0w z&A*i7l>|vXZmr$Ne*G!rZJC8j3GCl5mJtqaiv)b1UyrLUtJ_kB?uM2UT;{MFz>9ty zUJ;5j%2?vw6cFshKA*=inXn9YialSAebho*aDu} zZmj)6vy;*8Nxzft<*A6{?N~AS@X4kr7D~Uq&!UrYN%^5Di49=2vDYWHbo;rpf_r8U zRd*m#E$kIdpQB-axzUczV4TqRI*O0qL}qz_k81a=q>q zsQ%r_5cvoYzx}-Ooe_ag%6c0;-E7OPrd%d(0td6X)OZ3RBr4Xq0)mv!;HV1Xuyo1R z7yYfa3*fo_a$dk#s=O_GDrfb~C^uJiuI^S&F=lsQ`>j_va=QDE9>vMyge z7FbBJQ({fK7(o(A%MEQw*-jo2kAHF^>&CwSUhnjah%|VZqrJ0A-c@7!xY+L({PI1@ ziNf!9tCK5~R!M!n;k}t+OeR4N8i$a2ZZ5FmoK>pC8q#^3GRgpxWr_YGqA;d=#T{;b zBmRl6pWh@yRrfExjiSuY(9k#C^KW+*B%S3*0*Ew8`bc6Ugx%pP5q`7(io?KD9Z9qE z@Bzua_31BjAwz_A?m`-Fjf^?d? zq9es5eK93RPv~5C67}^aTBbojrK7wo4(F0%;vTKH#sUrN~A?;@c;d`UxA!ZS`;E`}2aZ&`*&euOJC$RHDKagm`&X2#5$QbpOsozqPG zq_pX275Oy2#NhjWb3Y~^QB2>O>Kk`Ze7cdJ$%*Jg0s3Gli%D#O*$ukw72-kfjy^}} zz*3NjAAh*|qo=Qa&Mmw7s7?A^($FhziamID-Hi#Mxa<-ksnOc@5l(~pGN)pot-Wj6 zGL-fCcfwJQ?ucWCr+}CT|2s$a+o!+7r7f(q5~vSq_zm@6MzKxkCpi52SkAPwAIL*vkK{8?O_;bES}Y`)m<6o451hS$^# zN(F~qRq5<r|k~-7^`R|n}?F#iDNTLe-fqQNje5JbEn{Ej8Kt_+QJgk>qO`tv z!olY;<$l6Z!$`KVELs0%Raj&8^y<*hhRh>S@v2U&xV6TZ3R+_-SrLU( zcE;}$B*VN~2`x%R=v*d8b zAK2A25PH_-f@P240~POQ9B>~U!_3#woo-d)V!A`o!cadXJ4gWJM}!JnbCC1GhEDL8 zEu`R?W=M854Ns*?y-SIPC`BNBodoXe?8bPd#%^ z9W3edSzkea)=jDrDKtrX5`T_c zi|y)82R_Aw0mD%XZhbSMSxsehOmnE}X=30;0rTuVD_!=S+nRL}@7W8p;(12HrMg%) zu`UF=Lk}J}l2=VzixgxHAB!>Tp8TNf9Q5t3+Y>|-6fwaZpmboA7t1{ZVO)kDhyhm!@k#3-?@2>Z;WYh- zmdlorXpS50G)?fbdTn`A{*di-+sf!-B+bgs={SvE6b&<+W=t(x(U{98{9FeBNi;H& zgTNNi!+DGor>C3{XmD+@sIqn?k3(xxT5skJQ~{K6 z_Wu2Wtfx$xxRj5c!(AeEl##8{XS4tk4hTd;j3`8+*A^p(Jw}F5;u|zT69mI-+>2GF zt75B^%ezyMd)SF?lBKH;yXyO8M7)cOL%986A}YQ;&-X~GTD~(v>?h2p(0yW>9nkx8 z!mH+6w8=0NM7Fl%+ilUuTKV1Yy?CQG04}EDj37YEyh4nEg)tclA}zKak6aaH1ySn| z_6LN%zdqcLbunyxKqx<&tlCnC5|1DfNe<3W5EXYJQGDo!6o1sApmMM!r1P&9MFV%0 zuKHIFs}||exmM}IzT!QLl-Y8cy&k;|bha$L_cPvwM(L|ok+iD6r<2y(>iO2qV^yE; z2~KjvO6(pm7OcEKe{}&Mb-vy?>Dj@%%v~;HVZks2G z`ujcK2fF{a2XSG#ZTBK4O|^kt{+V_C2tP=W>o_F%QhnV>xpe3gzd!&~>g{HCIW`p*TGDquNG$8BORCNtq-N=be2BR^KNO4k= zB!Jq;-~pc1c(%Onud!gydB6&T6bU9rxA;m#TulpdE;mRPeUWHO-m&)A3r6LzX*hj@UIlWX93&vCF&(OSb^ zpPbJKR(!?Hftjw}{GoQo_S^&)bh`d5>!>m8bt06iL{Xvb7|sIbn%Ao|NBqor2E#sj zuhIA5?>m{V_>x#{=;EtzbCIk%ug@X2ts(d`_(vGAr(Zh!y}enxG`HvzV_2qCx5rcmln;yggAbV*HV7N~5V@ z=SGF`NG(VpdIwZhX_oZzvz99+QSyS_*s=6)`9r>*uE@~%59KA1*5Ls}4PZc*if$z0 zubpm+IvD$P5Ux>~bu)E= zCD`d90&q>2zhYY9B!(c(j@hq<3qx9!g^0s&WlK)2x6>jlH$t*C8d!~utR0i!AXOc~ zNvzcx!fs0L^YKH0)uR{r2k@030`sWu-;}PW3YENR(`&dy1G?J+TZ!iT?E7B$>MHP+ zWJ`tj^^^w0LSm)@92smJ zr2IaXTmA)5rYAQCZ>S1AasT5kS@Dx z$JpB<9MyZfuAXL1hZ^@#5GkA4w@WA~B~P{bA(JhA2A& z>LDak#0k@(hn_@(8>h)BvfYM{ED@ws2RbE6p^uwM*h@b`lQy)145B%uh1Am~oZo^D z4B2pXv{YXT1>>Nl93ay}gt^jdTEcYwJa=FH7iQ$K$Ww@Z8Y2q2y+8|6MItG{ySh$SPL%Gfr zZi?^1#Co-yU78LLr=@|0B3DVp;&p&{KeGMS+AtO4?Bxqs<{8zo^`Zkxsd&M!bULRLRVg~9J$D2y6 zY86~uly`@R9t+G#T)9`kwDk3Cu^`Sw^Gn6)<56x_r6%Ip%mAN`TK~flH=hpVKEBTE zoJ&`pj92T1oAY{BCHLNP-s!z_@aD|@%U8;%3#AP(c?Ddg9~e!g)vTp-3P-NC-0;P6 z$s=bwPG9tZX{B(~GLxe*dSyngPbxwy>%vQ5qu{bhhh@y@wR#%#3@7!O8?(3dWicXN zDnu`EIlM=k>N*&+E|=E#CDcHp6x|Bj%yv`l-#0h&t?Q{5RwYCwsNBJ7!^^LyH<#)} zt*MMHOFeNH`CTnbvFw@W5>~IScUEkteBTXIuUNigHpy(a2N)s>HPmm}$Dl@5mZWP` zL91)`v^R&!ci%#^SH{;`ZY>VF!i*t;eR%^dTx^lVaHt*s)G`ku)GF!T*YDYwRzXgv zv{UnRek!14!GKr2O3*IOozOO$vzCas|JzwnuKmZ_eTn051VMQu*y}GeAPgbR0WBhH zD};zZ4T-^5m?svZFG4)_!(@|qd->2@-DT`562sY$T%Mm?bavK+EkixvrXfC5cS8I$^=6*2=wmot@>wye`oG&AMZyO@+ zusc6fzWkcs^d;?Wpx{{`@OMt*8(!1f?yuv&A68UUXX@?z5R7Wj{tKu0-!0Gop{wGr zrk%47%|8D_v#*+W6TDl+vW8EYA*UHvP@`%K1ne4ebIBzVeKtS^u0E*z?b2bjF`X#G*Ihgx$j)Bxv=pNXl zF14EkKcJtUH%SP*-xa1mx_D&hT{qMN#DP<{B3@)B9N81kh68Es0$X@i3w2}|a`X!o z%_)h*U8>7+d?Hc1Q)%s%=MEjqW_)_|WqxGsUpdMX!9(<2=o8=GN>83)D}c}yXysoR zP48{u_tc8o6O&)}UXg&zxBcRgf-jOSy0j;=mbc;v89I(M2NkM}2TGfnu!GK3>#x+U zVs6yqVUOu525JT3m|s@wu6sBq)*5lH;guLB z7>8An^n(orRdD^_TS&!(K9elHmwyrQ)rB$Te5%N(hkWz$W>+I}pGv;@xivuPqFgb(ZGtfc2f5^B>JV%Hz7G zNwYF!wjHruXX-<bM`I zc;K^GJQR7R@wlCIaBiprDOigZnHbgsr^;AAbW23nKp+7_$fTV4(!}3ja#_2-$8ZuV zf#eZh>nan!>5cww*R^zNY#Z5}9sw`--Ylxd7{41sI3=-hUb-a&=EEms#^cI&)VgOo zi8Sp+Q$6RoGQT9Y&m>mJ5m;=)%zb7D>wpZ@u}2FV>cJ@AvawjO?mOW_o|y|VE0aD$ zI^su2xo(Il>p;224_UYo>OPb=d;QAYR>hjBvWZ}nVnWzYq<);CEQsnS1o%i7e(dJn zy~~>AHD5}b5kMVD@V^fq9!{CrjUJLFpQ$tNen}gj4$hGOl!)>dj8S3` zg_waLWAOE+De$?ZI7$Tss zNm;O?sE#mJ8s4B!VfBXPsAAx+T;3V$Fmdz1-LQ}1jEMU#7?^f8UufYc{VG6Ra-rYS@Ok2XAdgZBh@w8m2*N*)! zzEUsUQvc8sF`dM5&o0}^OXQ0#<=&EPMd%j%D`5pP9aLoGXL>M)lxcV_eqrP;-fbV9 z(iFboCp3zXcU22s0Wo|;?HKox(lSowiF#l4e+&@$)j9As_ z{OQQP@Aa@rf2mRpC+t$Lap;iGn!w#VDRE_G>qB*p$Y78$g7Bn~yvtp#&26)n31As< zQ&O!(T`6x$H=mc`2y@A7c{^u{Hz(S6h4RMmG1kd4WqjF66CMw*U$+W&CvWTu(L1M> zGa7Jt){ndXBkiXrZ^!j=2el!i2+ud9hpF#Jr!f1vu3{r7uF&N zu)NB&jyBdHkrqzFl~0o7wpr!6>9*WfqTUmyzBRVOkEx79+dTE4&U6iGHqR+hUc9kAkj?`s}8xx?B9yHJ?+w zhx63g@vra4t0FGyKK5>NLPp$X8p$#;cG01`#pi<|pXFQ`pGYBYG9{4-=V6e66y4Dw zpCEd9g=HaN=B7N^@?WG#5E!}&@;V7S`Yd9}iF)0F%#Ym9x6eo`Dq^Y)3T;X!3PpPw6BjJ zjm$53@>@z@&M;QJPs6LXc{2>)wnaGg;T0l^PLi)oyZ#h8{~;C}VZcMl{k>6wURF4eJeV%w+)855IITrDJo?wtOD6lw)yYU>cz+`=F;9YUs;^c z)Qu3TC$VWI*7~nhzecik&!W=`XQUx`QI`*5>#tzblC+v;A(quZeeLvHNCB$2BK>*3 z&i3`Bnjlx!=zg~$jq#$KAeVd%cR#wtrQUY7RX(f zYHj{vU=F@i14)Ps)olg_4oc+@ff7WxUVvd$dg#E9lhN!pLcz`CXgy@wRN6Ucu*fBopb}l0Z6_P?3K&!R z`lnxOK8d&bdSbffbZu)*8e47ML$)9=us;rxpxTnzK6sU%=99mt}XP75Zmpx*sD zTg*Zw7wx=z7p-Hf&PGnt{40?JNA-(qzZ&K|&+8lMEZCErG2I;VvW`DhnEAaC1`LB! zey1k{Md{890l{byZsr#zLUi439bSRfM9{Fhx?#o%Y1y66( z9cQgtb}FZbZ9m-)N_2#l&f3;gay|&7A2JOcq}U@XMlHai!fpixfTGk2mM{gduN}kA z#`Bz}y=Z#3%l5m-lY2Z=zNi!>{-(U#4)e5*xmgXE_Bn5y6_>|I$2Xqg}L*x!i z0BVM#!R#IbnmkG$&U7ccu2m-DGX`@|9AWwhor&)e)Ujx``c)33CC$T;pV>ogGYS_H zlBip45Dre0yL_LX_p{CHF`Tg87jWIyBcRTIb;>Ap`)rB0u*XcBbfe9K_V!TIWP5=| z)w^0sURX-3ugUYcxKO3Hj*jbfP2=y?MJ!xjaQB-a$6af=F~q7>r#-4bko;s zfg!lHU4O`y0pV%}QKpgEP5D7)3_A?ZZn^r%bECJ;h64Mee{bz;fqr(kn~GMH&5L?p z>0-u1_~F@QBd~N!dv)2f!Na1!kQT!)5Zk+p_aM?}K4TghjoYzC z{@F7dtW)Nil25}P5g`{{xt4&C5kiAe=b@ubbAW0sTs?zZal#F zilkdwi|y}~5yeu(vM7`&G33BB%$3_wtbTwYo!LJ|B?K@HS%wvok|!3$;~3BUUKn+{ zS+c&Y_fddV`%P`uTKz6LQ%Eh!;jGm{*=j0mL=NBIZ;YxU_C!Xm8^DtNlT}(+)G$4l z5)r016yqf)(?dw`vJgyg{IP>-wmY^?; zGLj2203glJ`lLZ-kkQ&=STwZ9#8dr?*Y)`V4a4WlakiGV!Y|yf2%D;?%SuO(x~wCt z%#LB|uw6<|eP6>l%DZ^YC9&Vm74DU*x^d+c$f+Nn?7qF>{T(!*SEF+IeL%xK?7xPF zzlLhZ)&FCAMx||nC|xb0GaX!WO{^UM_oQZBqPSw^wMdR1KVH>exSrw=sb=JDFs}R& z_QY@_O@o{caFsl79||waNzhcrf+No>w1I{q6X~wa8_NU&sv*mZq{q|KQApNC*~`XV zd@uLO9J`(;X-=Ly&=x|G2AXq`9m$T`Jd>)I8VW_|1S=p@uG)91y%o)_*g^s#=bcX8 zOOY8_js6{b;fH&->y;H6J*Og5P+S-&<$0*j*$?NOsg}D=rICJeisEMPO=KR}pyz55 z!3vE9ef=o7Sw2w1r#4Gp!OweQk74#R29f-jOb~`P2b%F10;7D~+5zgo;x)1%*Cc3} zluo>;ReSt=J)HPTJJ;MVG!fmcE6d&UQaX71MjW%HW@bK@6^|GS{fjR*q2@!@Td&n6 z!fYImm{E3H09EGG+B`PkZKXl4vdAo5rot?%8}j+!Z%wstpOetV(Z9m9VTo&mqNzTC zQoS{gz7e*nt?1TQLe90n&S3g@%#g^xG=BEp;b|%L=nCmGhd;Wz5-zg*MI$BCbR+d6 ztdtTZsuzX~e?&>tFQs1z*;v=3Z5+&#y-a+Yb~+ggQ@-#zV=0^5YO6an*S+wH-ta)J zV)xsOD#>=_Gnk}#lS(8g(#H6A*u_CU&~U*$xxw`K)jK%#^ZZlWI4zHpluoP#6X@A_ zbCw+sdMfV(atl9u*ch};1;q?ghl>Is{Gq1`6i;y6(MB^hFCXNy9YVH1uwp2mKB;L& z)r4s`hTEu_ZF!lSai=e~^?pidY?U77)BXoO=JX{$F9}prp?NJcpc0G{9^O4aMQl({ zk`?msoCE}=3GQ1&gGb$icNrw7`kYYJVWdLSQX>2J5Xj{4vRlAQOPGm1W z66roEq(XW+WF=HgVQB34h;{9wp#An>>dV2#t+P$}T zALnA+#6KRt&NtaQIs$qhZM&x@aQlCfQe=Enc=U3n%c^~f| z)_Qx{X$kj^st4bufQ>>GV5OZvZLw3o?o_)nIPcGPZs)X&?AGmesMN`u-N*GL$o-kV z*-+>Ex6S#yxkkMmAGxII$I1V%&i21XfdA3rirMj}#kJjPFjq2G`6FYzWF)llC-VUj z%QdQaPA6@f3hdsMx2}_j~7nWG%#gmTs?F$Wv^nn zm_!p7HFR~dWjFK&hf^_ivQ;&d=Myp6{0QblHYpucjI0hy1t>pQYcZEjGS#|$k~$;A&oRvPN{6M4vbN2c{`oZ_%JS z;9U09Kp(%mP)@gxV%q;TQ&zAU=ZPnAslSwCcH6MEGi`G7yu0w$9NM$gj&Z%Y1b1P& z<)yFY&Yu3Z6FDe@av{2|a*0o~{%K@S1y`#RZvZf$NM!j8ctUYevPPk(b;tc0;FDCr z#3QejQ~yLVhu?RWRcVsd=<;RiIj^2x!rwgmM}A6F+^Fuiuws)Je6x_i2tvyic#q}0 z5HX@qUV&~}SN>=;5T8W}wywyIiyUb1;T`Rm+AA*Od(X!6W(`f42S0Z z2)?C;d}BOht|4Q15*89Rh^8Gh><*zODEuKIsTNToa#YX<2hDmE`Uyv+w=$dv0JZ zSd#b2NHQ~5=19g5nMA74_T48`M^;O8b&s)Sw3kx@AYpj@od>c&4i9aM(+71x$`&l9 z8)3#XFSCduB8aHY@3reiJ+RT7ZrXQD1a+M(n#3F4Rr@C5^x{5`@b&@=6n;JDG)`6t z393vchZ&M$IC*tmKPgARMl1IxCU0q`aaVj?Jl{V`krM6L?_|&9dK*mctcoC zvUD*7zqq|7U^ftGdNz0Y*7v{Fms+eq619j$z;dTGsjfy=28v9HhR zf)+NnGhF@IbOfA*b6z@-qx*__B&jK%Yn{SIOe^Z6X;TJ*VwT(m$DK9tCWjx82k#5R zE(8iaudy@^afFlaugABZoFIRBIt9X(n0P9Q2H| zGKNR6$&XC%Q0iBj6pcBDEmA7w4)yrobieWBz+8BL-`>R9pUta~)Z5gr$s3bnT;m99 zflxRlqSkxY$G+~Ejm^&Kcd{6;TGESuttLgf$I!w((DkM`%r)MUHTV-0LBG57j#8+a zGrmB54S%qA>?Q-W3)t#3RVFm2BJ3_PR9$Rhgj^*aUBBRd1eq?IM>fR^m$91`u^PK< zUvc^`LR4-U5yk?^&JhMgQD;!{_^2C^Z33xyjxD0A;Bf2PVUxwJ82OG!cS!+-OKk*$ zympzzjMSA{)qI^42Q9Q4A5()2(L{P{Xq04m(jaGvfw$WY?z9L&;lDMZS-CeGb~Or@ zk*IMSE4hZ09(cVZnJPKCQ82|%IDpEXSLTMfIX;)cs z{_HiA)fOTU!T<)(Gl+S1ILn^4Y*p2QM{{{eWTdrMy?@cf?iRM6`o_34r-kBF3ey5( zb1HV_zz60_Ww;IVX+)119P%g3)4@@J&%s&*+ugNB_ueC%>Jjb9hm)=&ONa8}I#p1n z!_Y8?P_>0D>Ae~FN-5)`!GZ@#EDh3$G7>5RM+oroWg7Q)hx>64sn69X1w6o~s!9vT zxLb~xs4W5O>adBi8_Kss6GF6;+VWj{V8uw9Cp4Xxxh+o^j#$2mj>Hq^bkg5`AQKtD{%cB*?#MmcqZpD!80vIV3rxH0?n@?_%gH)hS= z`w9jC#(w;NF!uk51v03l3fO;&c1NIj@!YTlD+p2k``tnt2L5@7C|lq{lAf1eu>nFn?ywjv{M4I`(cxP;wl5?gp0;93&T*ayakZdCvr{{%(!CVFD>y=`gl>;G& zK5Cc(<5VPg6LmA{sYP#^Z<Q^jeXX#GwwCDaJ;< zmx+B+XqN}DK})atAdvY?2N!iYRNGhEkWl{`?i$%fUMk<;9~IL3RMwbfE6*s-#QeUO zEJVxQZw3&xG}mJ5l*Fu1{(02!`&WY^5{j_|g1F9;l~^zd>%{2tkk>L`l%pK!5yJ7? ztFr50{KFFpZy9m+@nF;ZcNT)k+?H|dK!qsZK`yZ&gC^|F(TS*wvx@3`KF%^XHMo8k z2yTZ*$l@?)&tLpaS0N7NWs0ZJAgxL8Y>qX*x$Ne=MWrh14IYnYN45+Gd~urtsQ%ml zwUg>rLo?M$yj<+agZIk4h2sLc8*2JIH>m2@j2=6Ne5vT14OV9t)+WJJNWF*_c3qtf zWx|lR;`C?3qc)(apwD3})A_SD?CcY41Nt?A2CUIUbWlo4Eea*$Zp_-p#mgs9T41I@!?C!l>+h zKKVD7X@}xvX6GmldJA47S@{PNM3%~vF8W;5We&F;DJq1iwD?L(huwr$+C`aWbboa~ z!bfg&gwgHHQsu#5wr5BU{JcULjvQuo!eAE-iVJ0U2tpN&Jh}voCX~;G51cP2!ruZ; zB4qjO0*c><351LLJ;KBwFNj=5@Oy38{80pQJ;d+-e?fR&mm(UODfi`aW<8N;;b6^} z_X`)1TW)AC2WdwYM}!!$#`y%%NZacd)v zU*}u-w3WX?sA)5EJ*~wu&+AnPo)?yWw}f@h7;7vWi%n+z5Z;%%j~8$iFBJWJ&kwf( zbD`Hpo4|?g5*)SVIgO*UE2vB3EGxr^m$QIRRLxm_T*$(dNDu@)`AsV;(6_$qk_D8Xsf=e`Ls}vQ63D-bIG*CFlB2)y zEI?Nk7C`7(?zqmbt2c^{b$Dj*LZJ)X3KJ5ulywpYWTg!u0>>Gc2m#&_3F(W<{RFi+ zU88`t2W=EWuKBA)2RhCNQM;a;`GouysvsKncl1RL8py+Gl&i>Lrf*B?I8FsS)$For zXqCJ=t7!49_m0yn!wv`Fw!Yt?v$1!Ho|xq!VzZ$D4CJqeK_KEB=%?cxsC~aRc4dZ4 zoCl6$S8`8kwz%GJ*U_gv@P26A={Y~zWZ7?EHSW1omjPKNCaA)!7_|$>u)*nK^C@&) z1!s9RZ^YPGgtx;Z#cg&*cr>3o>T#xR5!nRaLc+LhD1KA!=0qdu3VWEjayU8lcmJW z6xoD>3PQ^H^%X^SPTWfi-so|DQ26HaFldtMU4k+4kVCAw#4Qt45Ow<5$gRs%cju$s z-IA@292Z8rP(}=IQoK6n(lss)tO`c)j!>6 ziEwly^pdaf?A1p^SDkHEVzx7&Y|p}xADebK29wx2DdF4Lf;j7_lqyn%SO<%(aX|+P z9ES9H2bbIAziz;I+YgD>fIP8W9*k0W# zX%OjSP;U~g8SU6iJnK5Q_>Cqq8Ap%LYEyZQ9GBY;C@~ zJ0E)S!gcrdZXv7;-@~LQi06KjUZeSTedOU4J-o^?uhaE;U(iwO{r+|jx?@~c7SC;X zrxM4^Nq|jMS2_Ox*p+{4N~e~$*t^UmZxB+kKBZAmEUcYR!hJo!gO0?`JY+fxa+`xCE=t*h&w@)OGMcA4Z)y;X9ya=q!A0_l<)*!|LR%D1aj} z9sQiQVvV*2CMn9RqYzB1?EuAi&>$+349ylXjJI}wx7`O!)-RsPfT8>4 ztgiX7kH95%Hdf=pBgj!?){Kp{70pf3_*g_Pk%A8Ro{W@j-l-?B97YWsonp+6+ zZpWY!FpF+Unx7WSmKW-t!_R>V@g~?T#I_B74uj`-!=d2JBwZO;DXbxf& zCN~VOu-OlUAJO+6Vjh1>+)KqhtDjWV8>~1bTmD`}{24oX4~mxe0Wg9xa9VSh$eMCG ziAa;!+1COrCQyz&ZIh<)u}QHU?;>dvax1~k?i-kzFQUCr)KeH2j_Q`5JS-}E__w}r z_pa2%dUoD-*K!I?#y^LR7X(xErCt2szj`kvWJXTQR@W~*D4p~%qe=d}lf=mkyjuR- z6+zy`wWi|8?hLmurCwD(Wrx``8}Pc$>24gG8;k4(V=|dR%#gC)nMbi{;Ki9Tug#m0 z83abQDX^8m^GbFM9+rC-G>jTZ|KhN7ZBuR$hkM zF04wq3+UP+Wnf7uOfh?F7jU5s2SZb&otG=t(Y0=!;N#?FP3`U3qk@GGc4yqGga}rU zO^k^!H+HKQ%{0qt)RjU}%+(P_Bn^_t*M%FFLTCuSEIW9uR832xBH=~|dslT1gDZpZ z3S+$`j_HG?S2#ZPi=0#Hqtu4HpkO1qMt$XXs#d&svN$BxtNM|>K3ZUYJzK|=-x^Xr zPNwHcRY+&e-5|BR5!WSVAMHHgBro6I+OSFXdwxZ~Hky4f{r& zX&@MOPLxPO>qfreK3;4PPeR-OYZH!Pq*c*$qH#zX#Yg+iiK^uLK5i^oGDi|vkxDuq z$rz=y7IG!KSzIv!p>@=4|KV$CFyGT8MZGpgWNWh_#?_B2!^QOxrLn4Gj{A{j@VF+zX)986~B|pP2dkJtsLOp9pv1tzod4bfi-rQE3Cd1=qqxGc{Ok} zEvdd-_7>8;X8dRh|Gm5}+S@zZFNC*MvKRw(m(H&H@a|1iH6tNPYWfWbbS7GN5UncI z?p{Yo<%4%3X0xSrE+MZPm+)en+{+Y=Aw%93EJG#UX}vgBJzNEy;hfeiQ$=j*PFFO3 z?Ula4N)_l{q;Bk7qcUS+uL@fBP1DFk>3YvW@7>V;TI(;@ei*?2-xDEFfFFq^8sMt; z{QykkfOIW%jAP*QgG&ndk8aymK+W$B`v;iSQ0fhqtfdUf&fgl5eA_G5k0s@Bv z0fPfd`@O*bO28n1GD1K?LBqfTK2U=M1Of~S3IYcDz0`nDy8+$@0z(EzA!HPQK$X{m zB(g$d@{GuUB7RxX{_NG*F$uG_^#^DebPP-^Y*I3E3Q8&#RyKAHPA)+qVG&U=aS4Uj zif`U3DXZw{>ggL88X4Qz+SxleIyrlJ`}q3#2Lwh&MaRU(#U~_YW@YE(=H(X@R#sKl z)YjF1Z0PvZ+11_C+t)unF*!9oGdnlGy0*TtxwXBsyLWPWcK-F^^6L8LyItSy{3HD| z?1^2-0J}iIz(BzuzuN^2;s7{6k-@+T86i*vGm4q0nANWK^_66EnX$ex_|b z27^w*vPydL-LxN;{WHTp{I4wg)38Uo=7HcqfdP#NiVXAu=uROskOJuc+oLJ#BG?1j zPy>;*4E4&To2$-`uDXaVg4Xo#K<|V7XU?dRx`+l`*W%2q`l}F|4sYjP% zuBjS;R+&M98_soJ#(r{na0JnOLjLANuhq8+blT&zAGj? zpAoOACw$%N*%wx!+`=u8MMr;i<7uKr#*lxwOH_2?rl`OT?HMQ|% z^SuIQg^|b=34$}C$93%uGw)|KCg{m48piUbj=riwr!UdH06cGuAbrIa62_@WC9}fB z&-2zn7zj=N|BnAu2ObY|e-(Mj(j_(t0SvTE0BfNw_cl^x| zX~pOPwY--ORPD>&OVI)!4J_(yM>gy{VSiNzelOIp6A<1EG#%e+Ip+F}#;}bftJlpN zxi5C1I54cNRY>mhi#5pF~ni z)t**2c@#wFLsGTlb&PbSv0Mv=`Bi3@fV~%t8rVr7@f-n9iL*fy)~7=Dn{o2WEqN}J z$#JUoxJzdw1$YMj1$Y(9qmblCh^lH&;g9B;A`>WVwAC}~8@ zr#v>ssx`Zo{asQWzU8hWIW!{Za~^7b%mvdgmNr`G;;&+6EW63FTpbR$cS%lC<*e-( zScOk~fMsd+jWpTdq+1SkCpbHl1+>h+FZ-Om3kfR*yi*G}0L$Kgw4JS%wyB7e&~fnW}MQ2 zE(_?x3NT6o>tZ$}M3{@04*#^(qLop#W;qP3Q|)pNkCm*GO%dAL{n1>*Buj`a8=IH@ zW}+GOyz$CAhhO#OKbw7o%T~Q|nTR3r+{O!7i-MR8NNr~?{O~-px-D;D)89x8RLz;C z+v@f7)W?E5DjIkG$`^lY+_|G|us@(zq%P(rdq%u5 zQb518xwb#+#e&d^&M)@-a0Glkad2b2cUt1R=x0Ep$$$Z_;kR;1BJj~yWu?SUGrz8~ z1>G~XMJgJD8>O^pgHF0foVI$Eclp83;w?4p>QuipJee&KJ{58=Pif7kti*3&iD&Og zQ0EL{_0fi_+xCir>TBbyglA~kN3n}H;HPJXX8`u;4R zzCwh~r=4xSB^!}snh@rKhSp*-^4NirnH;ua$}0{Wt6KUiihahrPIN`{-W^=QWsd8E z>R|;;9H6*SZ-H`w4Vy;gZIc~4xtj$;Y%~0*IdXc#54jP_aobBMaetKmTXnWuqEs2G?Qj{-A=|UZQiAmim-T1`*Lma_TGl|B5BHm#e*W3~_ISbkj5CZMaq4cnHHx6uBEu&^L7zTKhJr+CoyrTtE zFy8WeZ8~Uggw=+Mz-zgA7@Y39Vh|qj39L6O37V;^GMGtY{-F}>qB+;KcJxTNg77fa z4)G*lX-bH1J{CdVYy`g^CxBBE_Or~slnK>bs1@fv92mlB=RT>lXZ{u!X$X9z z>6~>GC3)mT$sV|_%~fajP3`JL%>LP`McsTNXDduydF*q1#O;;k@^#t+T?e1&^G>C$ z9=F?m0U@o>1BX4r>v%riI7uOL%7mU>8Zx7V;i_+b!Qo}4WkF};8C-0~1nY|jgB(zX z%O{(vnSVccblO~fy#@5bL%{R^>DOMUWoh|;o$zM|{Qj8zTHa!w0m(hB`a$!$8LRiyppYa40j zR~z^-LqiA~a=P@Mj>iI`oG8k$op$P~3KS&l8{=s0q8dbvlC-n^55DE6-5I?%LZ=`^ zUuDL4LdXb$eU-p~$SauvLVW^eUWG-I%@_eSW_)IqkRdVeJUAjZJb&T-Qj{0!GX_*(<_>2^DxR#D9HuW} zJq#&KpoQTwKEZtdrKliQZ%|CJrZ%zc#dtbhP{Vwzq@@r$C~WUpbyAr_iE?Wl#KP>ZRlaB##DH8X0!Hg- z(I|7OzzR-+3uZybY#r`<&=D(749vC{7yEvZ&eG?)3vcm8PG!eiq2+cacg(!%ausRw zZHYsB<_*;n9V~V0Uqc)?a=KAFj_zjj45q2cCo;(gBr5oP*tYM$B;<-_QyT&wqkwVj zvLZntU6=@64I7&pw#~qw*cQj#D^5H%@5Xj*GP<}ad8MapZtZdJiQ=j?Svwah(>91i z_bDocTw!fGy1rXGuoN9$k`A7|qFrS|23e`8#{5l+FNgAxCa-lK&j6ZRzS(dc-|05N zATE6nY##mdtT{9bLIFzQdtpdpb);?HB(zm3T1e~E%y9vk_cV~{!?g=&AG0X5Um6Kb zl0laYp&nY5m4)W<=TmJVoMMC?u)A0aA?Br&GMnrtfs4Z7z;TigInS&pby%SE<_ieP z3mQvs`;Y;lwyT`qkU|4J^LdjQTx^6CL+xv0i+~E48!5myufv9cl33>o2J?WH zd8mUg`kLog1w8hceIx~1+)L6CnNW(ER9?X`HFOf;Rke6z*w#DN#Vn3^#B6$Y-i@e> zUeo3j{yw>5G!sOEuT_Pe8HLsf-%RtMLq8Kvyx!oS?Z*8Wc-yjiy(z=!ORX0%sucM= zabP!eQW-^NJ!f>@QY!ML)dGQdw&cj$FNSIp2inM!JSSQs-n&HjC29r1POfV5e(>q7+1Ppr{u;xk}YeQjG ztEizX!C)AuU?wu!x%m`zMXIFJHo@`@?7`^B;4lq&{2JB3vdysQ3aAP%bR6F#5>^d$ z3QjjuDquk>W(i>=!A}?#NO+}4+vOOa&sWUC1RO!H(2pfcT4xJFo^QLlwJ;EHB*&Nu zO82fti!rNs->(Bt_@z!Pw^H;VyvNgpqi!rsXpoFGGt{9Tfi{|lX3eQ4gS?MQ;uhQ2 z<7zRlcPEs0geQ2*j84@=!uCrZPGANmQV(t-` zf#qq+ckO33ld4tW9bo~bY7xhBu)F!( z(AZi-J3eD6vF%1FjMME(1<_x7;&W=WCQ`(jG7q z3=Y4@_3{}CI|&ea?Qz|L;p59!)1=DHetF3U#$s0J88MTNe5^6$O9^_CYzMqvi)?7` zW+oRCG&V`oSIRRk<2%AnSpo5ClbR3g`3=L=H<}X5!C5`k3f^Ysl~G)#y=Na~WXU3? z%rTy<1J5Fotb=>Q`mV3z%OlH%PrLS(9bf+S_g~McmR4>3CJt9#1Kvf*TWBwE^_#$x zsq<3UM9AY#HiyjH!;^8~xMNDDBPjp;rD7sr6Vd6|`wVTSs>dgX74)ToPKmXLCjS=L zn#WQ51mXx(i;S4Qhy~xu5Xfc?vP^8Cd%83>H@$sBNRrP+P)7Fb`^i8KU-C+9cCy-L z%!-Xcn8JF2&r~FHDf4Bh$YmihJAnZcW|QyN`(PH)*`Y#cYIbE?8gECC`^?$gQ~YOb zf);DlDwcO8R)vn`f}S_Lr|FILDDv069B7dd^0$^? zMK7Ltn^l1?QxVPiT+Rgag6#V@6rvJ4;j@Ro5{23do!vha8E;$;ylM2;oe9c-wKZKmNlI3lT$s6XR4A|)28jNn43NJw&ADf z9oIv4QC3Soz#JCD4Kx;Zo6VLaC@q!ayO$%es{k?p&i$-U%iZM>ad` zqy0OkMP#zlK!LYL^|{jvgXE+H9d@gWZv#Df*gcUt$?8uGh{=6lkU(_cu0aB4sM=&! z1mKP{4M~4`lbe6M1$p*XCw!13{NbJsjER_Fd^upGc*wZSShc^DM-?7jn;Aws?jU+O z^FPi!^XKPfG=Q z_a&gwcrBHjP{rfU-l@#Q=+G}~QockvLU$4ZA=XkoS)MpMc5o#EVBOK+R*?~ZWu%IrK?I8Fy)r3kuSh+Qfbzb%_|q~#Wn>?kzC9RVl5#x0DS752bu z^v)!yJ|HK{(6LT?8P>t#w0g$A<=kAW_|7-O6x%p?k#0?c3z=uU#iEAUBJKmgC5zhs3^calWHSdCkRLNsOF#El8vhXx@BVpibpY(U$|> z#L zUh>s~`Vtb{gy=lT z{GL7OX@1mcv1CqTjYF5d)V^UYd%kg&V~o^QeSAtp65?E*Ui;IdhQ4~z1b+5gzZq#W z{nhGz_wdr{NQR+-)Ni9PGMi2DEZZs^2tG5HkQO_wiuDBDtzoxTa%G4M6}PXPLpEin zz1FSeAO3zKE+c4=T>+R*Pyi<5r2jsx_$dzj%jDw6;-u`6cEI8!IEaTHNa zmBUFZid&%GtoB_cw9?NmFD^)&FJd1Mub&^Du*&ZBiK`uBCl*38S@Bwt;%C5%I8de^}>LLZED)^|V}JE)|*P%-V>@;-9f= zfk9{i0<{o8fT(5w@2ppkC*nN7VmA1nH)a7Z{*qJ$EzE88%xyJZJ6Y=4sDHm7At(7) zz+a`;f5{891-Kphv(|o8)zgeRPxPlBvtk26v3Y<(nj0k)|AV%!fKf0*?Y@pR4H zBO)CTY4}gXA3oQlT~O>XeFV5F{yX|nckp-QzZSX`AQAoF$v-uf{M8DNbmK?d4c+f?e>>Pdjq~}4d%^Mx?vG~v(HOrp z@YB*g4e|Aei{kkO_s7`%OWXeFiBEA)gXTQq!UcYh`>~ArDeh^sgh$+#=)M}^pRNvnENHOWPx8~X;y(%hd8OSW;e*b9TzB`h7*AJOJVFMHeip+L zaF_j0*Y?xwPaXY_+5JrZWyN3J{!al