diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c5bf52bd..2982ac594 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -419,7 +419,7 @@ jobs: # path: resources/ffdec.exe sign_and_msi: - name: Generate MSI, Sign EXE+MSI + name: Code signing, MSI installer runs-on: windows-latest needs: - compute-version @@ -444,13 +444,30 @@ jobs: with: name: dist path: dist/ + + - name: Download lib_dist artifact + uses: actions/download-artifact@v4 + with: + name: dist_lib + path: libsrc/ffdec_lib/dist/ - name: Download unsigned EXE artifact uses: actions/download-artifact@v4 with: name: unsigned_exe path: dist/ - + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: adopt + architecture: x64 + java-version: 23 + + - name: Build alt signer + working-directory: altsigner + run: mvn clean package + - id: auth uses: google-github-actions/auth@v2 with: @@ -501,6 +518,41 @@ jobs: Start-Process msiexec.exe -Wait -ArgumentList "/i `"$($msi.FullName)`" /qn /norestart" + - name: Sign ffdec.jar + shell: pwsh + run: | + $kc = "projects/$env:GCP_PROJECT_ID/locations/$env:GCP_LOCATION/keyRings/$env:KMS_KEYRING/cryptoKeys/$env:KMS_KEY/cryptoKeyVersions/$env:KMS_KEY_VERSION" + java -cp altsigner\target\kms-jarsigner-1.0.jar com.jpexs.kmsjarsigner.SignJar dist/ffdec.jar dist/ffdec-signed.jar cert/cert-chain.pem $kc http://timestamp.sectigo.com + move dist/ffdec-signed.jar dist/ffdec.jar + + - name: Verify ffdec.jar signature + shell: pwsh + run: jarsigner.exe -verify -strict dist/ffdec-signed.jar + + - name: Upload signed JAR artifact + uses: actions/upload-artifact@v4 + with: + name: signed_jar + path: dist/ffdec.jar + + - name: Sign ffdec_lib.jar + shell: pwsh + run: | + $kc = "projects/$env:GCP_PROJECT_ID/locations/$env:GCP_LOCATION/keyRings/$env:KMS_KEYRING/cryptoKeys/$env:KMS_KEY/cryptoKeyVersions/$env:KMS_KEY_VERSION" + java -cp altsigner\target\kms-jarsigner-1.0.jar com.jpexs.kmsjarsigner.SignJar libsrc/ffdec_lib/dist/ffdec_lib.jar libsrc/ffdec_lib/dist/ffdec_lib-signed.jar cert/cert-chain.pem $kc http://timestamp.sectigo.com + move libsrc/ffdec_lib/dist/ffdec_lib-signed.jar libsrc/ffdec_lib/dist/ffdec_lib.jar + + - name: Verify ffdec_lib.jar signature + shell: pwsh + run: jarsigner.exe -verify -strict libsrc/ffdec_lib/dist/ffdec_lib.jar + + - name: Upload signed lib JAR artifact + uses: actions/upload-artifact@v4 + with: + name: signed_lib_jar + path: libsrc/ffdec_lib/dist/ffdec_lib.jar + + - name: Locate signtool id: signtool shell: pwsh @@ -727,11 +779,26 @@ jobs: name: dist path: dist/ + - name: Download signed jar artifact + uses: actions/download-artifact@v4 + with: + name: signed_jar + path: dist/ + - name: Download lib dist artifact uses: actions/download-artifact@v4 with: name: lib_dist path: libsrc/ffdec_lib/dist/ + + - name: Download signed lib jar artifact + uses: actions/download-artifact@v4 + with: + name: signed_lib_jar + path: libsrc/ffdec_lib/dist/ + + - name: Copy signed ffdec_lib.jar to lib main dir + run: cp libsrc/ffdec_lib/dist/ffdec_lib.jar lib/ffdec_lib.jar - name: Download signed EXE artifact uses: actions/download-artifact@v4 diff --git a/.gitignore b/.gitignore index 78ee66db7..79aa4e583 100644 --- a/.gitignore +++ b/.gitignore @@ -117,4 +117,5 @@ exported1.all.bin wix/bin/ wix/obj/ *.msi -*.wixpdb \ No newline at end of file +*.wixpdb +/altsigner/target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 24eb1eb58..d9914038e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. - Slovak translation (AI used) - APNG (animated PNG) export for frames, sprites and morphshapes - Context menu association icon -- Windows installer (MSI) and ffdec.exe are signed +- Windows installer (MSI), ffdec.exe, ffdec.jar and ffdec_lib.jar are signed - ffdec.exe contains version information (+ on SplashScreen) ### Fixed diff --git a/altsigner/pom.xml b/altsigner/pom.xml new file mode 100644 index 000000000..6d24cdd2c --- /dev/null +++ b/altsigner/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + + com.jpexs + kms-jarsigner + 1.0 + + + 17 + 1.83 + 2.41.0 + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.3 + + + package + shade + + false + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + META-INF/SIG-* + + + + + + com.jpexs.kmsjarsigner.SignJar + + + + + + + + + + + + + com.google.cloud + google-cloud-kms + 2.86.0 + + + + + org.bouncycastle + bcpkix-jdk18on + 1.83 + + + org.bouncycastle + bcprov-jdk18on + 1.83 + + + \ No newline at end of file diff --git a/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsPrivateKey.java b/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsPrivateKey.java new file mode 100644 index 000000000..3fa7884af --- /dev/null +++ b/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsPrivateKey.java @@ -0,0 +1,19 @@ +package com.jpexs.kmsjarsigner; + +import java.security.PrivateKey; + +public final class KmsPrivateKey implements PrivateKey { + private final String cryptoKeyVersion; // projects/.../cryptoKeyVersions/1 + + public KmsPrivateKey(String cryptoKeyVersion) { + this.cryptoKeyVersion = cryptoKeyVersion; + } + + public String cryptoKeyVersion() { + return cryptoKeyVersion; + } + + @Override public String getAlgorithm() { return "RSA"; } + @Override public String getFormat() { return null; } // not exportable + @Override public byte[] getEncoded() { return null; } // not exportable +} \ No newline at end of file diff --git a/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsProvider.java b/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsProvider.java new file mode 100644 index 000000000..39810b8d9 --- /dev/null +++ b/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsProvider.java @@ -0,0 +1,14 @@ +package com.jpexs.kmsjarsigner; + +import java.security.Provider; + +public final class KmsProvider extends Provider { + public static final String NAME = "KMS"; + + public KmsProvider() { + super(NAME, 1.0, "Google Cloud KMS Signature Provider"); + // We implement exactly what you need: + // RSA 3072 + PKCS#1 v1.5 + SHA-256 = "SHA256withRSA" + put("Signature.SHA256withRSA", KmsSha256WithRsaSignatureSpi.class.getName()); + } +} \ No newline at end of file diff --git a/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsSha256WithRsaSignatureSpi.java b/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsSha256WithRsaSignatureSpi.java new file mode 100644 index 000000000..16c8e4c71 --- /dev/null +++ b/altsigner/src/main/java/com/jpexs/kmsjarsigner/KmsSha256WithRsaSignatureSpi.java @@ -0,0 +1,80 @@ +package com.jpexs.kmsjarsigner; + +import com.google.cloud.kms.v1.AsymmetricSignRequest; +import com.google.cloud.kms.v1.CryptoKeyVersionName; +import com.google.cloud.kms.v1.Digest; +import com.google.cloud.kms.v1.KeyManagementServiceClient; +import com.google.protobuf.ByteString; + +import java.io.ByteArrayOutputStream; +import java.security.*; +import java.security.spec.AlgorithmParameterSpec; + +public final class KmsSha256WithRsaSignatureSpi extends SignatureSpi { + private final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + private KmsPrivateKey key; + + @Override + protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { + throw new InvalidKeyException("Verify not implemented in this provider (JarSigner does not need it)."); + } + + @Override + protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { + if (!(privateKey instanceof KmsPrivateKey)) { + throw new InvalidKeyException("Expected KmsPrivateKey, got: " + privateKey.getClass()); + } + this.key = (KmsPrivateKey) privateKey; + buf.reset(); + } + + @Override protected void engineUpdate(byte b) { buf.write(b); } + + @Override + protected void engineUpdate(byte[] b, int off, int len) { + buf.write(b, off, len); + } + + @Override + protected byte[] engineSign() throws SignatureException { + if (key == null) throw new SignatureException("Not initialized for signing"); + + try { + byte[] data = buf.toByteArray(); + byte[] digestBytes = MessageDigest.getInstance("SHA-256").digest(data); + + CryptoKeyVersionName kv = CryptoKeyVersionName.parse(key.cryptoKeyVersion()); + Digest digest = Digest.newBuilder().setSha256(ByteString.copyFrom(digestBytes)).build(); + + AsymmetricSignRequest req = AsymmetricSignRequest.newBuilder() + .setName(kv.toString()) + .setDigest(digest) + .build(); + + try (KeyManagementServiceClient kms = KeyManagementServiceClient.create()) { + return kms.asymmetricSign(req).getSignature().toByteArray(); + } + } catch (Exception e) { + throw new SignatureException("KMS AsymmetricSign failed: " + e.getMessage(), e); + } + } + + @Override + protected boolean engineVerify(byte[] sigBytes) throws SignatureException { + throw new SignatureException("Verify not implemented"); + } + + @Override + protected void engineSetParameter(String param, Object value) { /* ignored */ } + + @Override + protected Object engineGetParameter(String param) { return null; } + + @Override + protected void engineSetParameter(AlgorithmParameterSpec params) { + // For SHA256withRSA (PKCS#1 v1.5) no params expected + } + + @Override + protected AlgorithmParameters engineGetParameters() { return null; } +} \ No newline at end of file diff --git a/altsigner/src/main/java/com/jpexs/kmsjarsigner/SignJar.java b/altsigner/src/main/java/com/jpexs/kmsjarsigner/SignJar.java new file mode 100644 index 000000000..2f53c665b --- /dev/null +++ b/altsigner/src/main/java/com/jpexs/kmsjarsigner/SignJar.java @@ -0,0 +1,60 @@ +package com.jpexs.kmsjarsigner; + +import jdk.security.jarsigner.JarSigner; + +import java.io.*; +import java.net.URI; +import java.nio.file.*; +import java.security.*; +import java.security.cert.*; +import java.util.*; +import java.util.zip.ZipFile; + +public final class SignJar { + + // Usage: + // java -jar target/kms-jar-signer-1.0.0.jar input.jar output.jar chain.pem KMS_KEY_VERSION TSA_URL + // + // chain.pem: PEM with leaf + intermediate(s). Root optional (usually omit). + public static void main(String[] args) throws Exception { + if (args.length < 5) { + System.err.println("Usage: SignJar "); + System.exit(2); + } + Path inJar = Path.of(args[0]); + Path outJar = Path.of(args[1]); + Path chainPath = Path.of(args[2]); + String kmsKeyVersion = args[3]; + URI tsa = URI.create(args[4]); + + Provider kmsProvider = new KmsProvider(); + Security.addProvider(kmsProvider); + + PrivateKey kmsKey = new KmsPrivateKey(kmsKeyVersion); + CertPath certPath = loadPemCertPath(chainPath); + + JarSigner signer = new JarSigner.Builder(kmsKey, certPath) + .digestAlgorithm("SHA-256") + .signatureAlgorithm("SHA256withRSA", kmsProvider) + .tsa(tsa) // timestamp via RFC3161 TSA + .build(); + + try (ZipFile zip = new ZipFile(inJar.toFile()); + OutputStream os = Files.newOutputStream(outJar, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + signer.sign(zip, os); + } + + System.out.println("Signed: " + outJar); + } + + private static CertPath loadPemCertPath(Path pemPath) throws IOException, CertificateException { + byte[] pem = Files.readAllBytes(pemPath); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Collection certs = cf.generateCertificates(new ByteArrayInputStream(pem)); + if (certs.isEmpty()) throw new CertificateException("No certificates found in " + pemPath); + + List ordered = new ArrayList<>(certs); + // JarSigner expects a CertPath; order typically leaf->intermediate... works fine. + return cf.generateCertPath(ordered); + } +} \ No newline at end of file diff --git a/cert/cert-chain.pem b/cert/cert-chain.pem new file mode 100644 index 000000000..0d2d53bba --- /dev/null +++ b/cert/cert-chain.pem @@ -0,0 +1,106 @@ +-----BEGIN CERTIFICATE----- +MIIFyDCCBDCgAwIBAgIQHPG2s4v6Su+MOOTrbq79yDANBgkqhkiG9w0BAQwFADBU +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQD +EyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgQ0EgUjM2MB4XDTI2MDIwOTAw +MDAwMFoXDTI5MDIwMzIzNTk1OVowXzELMAkGA1UEBhMCQ1oxHDAaBgNVBAgME1N0 +xZllZG/EjWVza8O9IGtyYWoxGDAWBgNVBAoMD0ppbmRyaWNoIFBldHJpazEYMBYG +A1UEAwwPSmluZHJpY2ggUGV0cmlrMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB +igKCAYEAi4YPqddfVVpqhRCnxrSgg4dIHTlefcwjIEZXUooNbEJIAHb28OBbRfrU +lILsET2x2eOUMCqiM22NfMFzA5KiOMQzffD+pxJS0M/6tVXpoI+3q94sybIV1OS+ +QiRw/+FtPsLwAsrzykbCOYj2YpgqrUfRqMMPcBpPN4kUf92CGfQGq9IqbAKtQkW+ +a9PTBvSo0MOytx5Vp7T5oe/0am/rxr3nwJPHbsTq9Efw33x/g/dzQn7aR1vGEdrL +zeG0gFFdPDYTWQiSwkiSaoH3exltc4zkc4ynvfsoFFiazKafUUYSvsx7x7GZUAO1 +i1MIiYkzK5DaYOqGIN/zFO9Wc38qi9qkH4vrdYmKHqZXxNSFN4x1LT0wDrbzFWef +dayrQT5ReR9iOFpoTwSCpktZXcqndw9P0eyTtIhB8AC9/peXYJZrFOuf1o5fSYM0 +7WaMTaaeQjS8zIH0K7Jyp8N0jzlNITH1MfkWh2CXblf4jvGvtiab8TrLTCOJzT0O +lnxCBeIDAgMBAAGjggGJMIIBhTAfBgNVHSMEGDAWgBQPKssghyi47G9IritUpimq +F6TNDDAdBgNVHQ4EFgQUEr4x6TulT8qxylRFTFjogiP/fOcwDgYDVR0PAQH/BAQD +AgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwSgYDVR0gBEMw +QTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdv +LmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwu +c2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkG +CCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0cDovL2NydC5zZWN0aWdvLmNv +bS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGG +F2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQBOkf7v +1vCt7of0tqW0tTnUm9r26fD8xg5wasGAfQRZcgcbdxuCoEsXz1wFC/YHkU6JvVIy +mRW2zlTbr5DI1MSMDh97WSpwDHeHVtMdZFZOzhzoYNT6wrMAvh1FY8gdZiA11dnY +YDVQev21N0Ym277Y6lnqu6bxi6KsDfwMvSCFWzUJYTAn6YDIcrH+C0Zh7FNfn4nH +qlrXjezXRM2VGf7cD5l3WvEtT1PKhFK9SFCbM9f6B2MkQVfF5d+kyg9EL7x3M+ku +Nt/pPzcSIKwJe5J/rN0cBiT9TNi/KCo8igyiudeRIWbkU3RF8bNM5ociEGhpNvEi +LZEU2KqpN3uSdN8KxKVVdbJmyYARnctNxOPq7S6WsTGOSu2QffSfB+wT8bkT6ovd +azOa34IXcY6haH11yK0oTHUvIqjCnwDJ1100Rw2vWuMOMxYOi12H3nbQsD7IEipa +bZsfWJyzui5xNDSrlrq7tXo3CgFBn3nRYusQ7B/sPFFx7zMsO+VEBOrSiQo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGGjCCBAKgAwIBAgIQYh1tDFIBnjuQeRUgiSEcCjANBgkqhkiG9w0BAQwFADBW +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQD +EyRTZWN0aWdvIFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwHhcNMjEwMzIy +MDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBUMQswCQYDVQQGEwJHQjEYMBYGA1UEChMP +U2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDEyJTZWN0aWdvIFB1YmxpYyBDb2RlIFNp +Z25pbmcgQ0EgUjM2MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmyud +U/o1P45gBkNqwM/1f/bIU1MYyM7TbH78WAeVF3llMwsRHgBGRmxDeEDIArCS2VCo +Vk4Y/8j6stIkmYV5Gej4NgNjVQ4BYoDjGMwdjioXan1hlaGFt4Wk9vT0k2oWJMJj +L9G//N523hAm4jF4UjrW2pvv9+hdPX8tbbAfI3v0VdJiJPFy/7XwiunD7mBxNtec +M6ytIdUlh08T2z7mJEXZD9OWcJkZk5wDuf2q52PN43jc4T9OkoXZ0arWZVeffvMr +/iiIROSCzKoDmWABDRzV/UiQ5vqsaeFaqQdzFf4ed8peNWh1OaZXnYvZQgWx/SXi +JDRSAolRzZEZquE6cbcH747FHncs/Kzcn0Ccv2jrOW+LPmnOyB+tAfiWu01TPhCr +9VrkxsHC5qFNxaThTG5j4/Kc+ODD2dX/fmBECELcvzUHf9shoFvrn35XGf2RPaNT +O2uSZ6n9otv7jElspkfK9qEATHZcodp+R4q2OIypxR//YEb3fkDn3UayWW9bAgMB +AAGjggFkMIIBYDAfBgNVHSMEGDAWgBQy65Ka/zWWSC8oQEJwIDaRXBeF5jAdBgNV +HQ4EFgQUDyrLIIcouOxvSK4rVKYpqhekzQwwDgYDVR0PAQH/BAQDAgGGMBIGA1Ud +EwEB/wQIMAYBAf8CAQAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwGwYDVR0gBBQwEjAG +BgRVHSAAMAgGBmeBDAEEATBLBgNVHR8ERDBCMECgPqA8hjpodHRwOi8vY3JsLnNl +Y3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYuY3JsMHsG +CCsGAQUFBwEBBG8wbTBGBggrBgEFBQcwAoY6aHR0cDovL2NydC5zZWN0aWdvLmNv +bS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdSb290UjQ2LnA3YzAjBggrBgEFBQcw +AYYXaHR0cDovL29jc3Auc2VjdGlnby5jb20wDQYJKoZIhvcNAQEMBQADggIBAAb/ +guF3YzZue6EVIJsT/wT+mHVEYcNWlXHRkT+FoetAQLHI1uBy/YXKZDk8+Y1LoNqH +rp22AKMGxQtgCivnDHFyAQ9GXTmlk7MjcgQbDCx6mn7yIawsppWkvfPkKaAQsiqa +T9DnMWBHVNIabGqgQSGTrQWo43MOfsPynhbz2Hyxf5XWKZpRvr3dMapandPfYgoZ +8iDL2OR3sYztgJrbG6VZ9DoTXFm1g0Rf97Aaen1l4c+w3DC+IkwFkvjFV3jS49ZS +c4lShKK6BrPTJYs4NG1DGzmpToTnwoqZ8fAmi2XlZnuchC4NPSZaPATHvNIzt+z1 +PHo35D/f7j2pO1S8BCysQDHCbM5Mnomnq5aYcKCsdbh0czchOm8bkinLrYrKpii+ +Tk7pwL7TjRKLXkomm5D1Umds++pip8wH2cQpf93at3VDcOK4N7EwoIJB0kak6pSz +Eu4I64U6gZs7tS/dGNSljf2OSSnRr7KWzq03zl8l75jy+hOds9TWSenLbjBQUGR9 +6cFr6lEUfAIEHVC1L68Y1GGxx4/eRI82ut83axHMViw1+sVpbPxg51Tbnio1lB93 +079WPFnYaOvfGAA0e0zcfF/M9gXr+korwQTh2Prqooq2bYNMvUoUKD85gnJ+t0sm +rWrb8dee2CvYZXD5laGtaAxOfy/VKNmwuWuAh9kc +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIGgTCCBGmgAwIBAgIQAnw5AQynWsM6te4NVA755TANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjEw +MzIyMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjBWMQswCQYDVQQGEwJHQjEYMBYGA1UE +ChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdvIFB1YmxpYyBDb2Rl +IFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+shJHjUoq14pbe0Idj +JImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCDJ9qaDStQ6Utbs7hk +NqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7P2bSlDFp+m2zNKzB +enjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extmeme/G3h+pDHazJyCh +1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUzT2MuuC3hv2WnBGsY +2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6qRT5uWl+PoVvLnTCG +MOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mczmrYI4IAFSEDu9oJk +Rqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEcQNYWFyn8XJwYK+pF +9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2TOglmmVhcKaO5DKYw +ODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/AZwQsRb8zG4Y3G9i +/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QIDAQABo4IBFjCCARIw +HwYDVR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFDLrkpr/ +NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/ +MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBEGA1UdIAQKMAgwBgYEVR0gADBQBgNVHR8E +STBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNB +Q2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwNQYIKwYBBQUHAQEEKTAnMCUGCCsG +AQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUA +A4ICAQBfHYHOUvth+43SceEtI4YqgnlKuPoaE+1ucOskvqnva/yLCD4BZT4dYLEt +04YG3m+hef+ZXdEKeFscAZpe4EesqsPB+X7IfHhTjS2yO15VwFtGdB56WX7HgFyL +MmaEgJ9NKjFVaOFZb4mIStcdamlS5iDbFXFUGGtIlIdtgy+nhdpPXR4TL+z16QY4 +PHD7+aZ5J6/z8uD9wpnzI1jF7eF+7I/ekvCCiLw5vFYVcqvlOViI9VZmnYtDg1HA +dTCOqPbPhVqzS+KRftx8+VGmJCTpVTxOmkW7uXbdDDOSG572ZPDWUU4lcHcwnfaR +1zKob5u4uvbgigqe+pp+bmiW628Wqx1775G9LqiW26foBCmeHLq7AYlrt33KAW0/ +oocWV8FF0/BSRY5kiq9IHh/CTt+tAjXjAwy0RLtsXyfvEjiKzaQW8W2QU1tlLJVX +VmLmfNxGlJLG65RvdR9cpZE10B8KWleHm6KfNWfcYmdTFbg1TpV8Bh9FhJcXxOjb +rZpQOTaab9gTxyqOzOeD3mqUmHjb++lg6k9gyp2qEOaqY+mfJ1/wc4intu3qCRFR +iEQF5mjhrovhe0S2NYgwjDWjlctIO1wZ13Cwq5xjy0W7tiy3kHigxZBF0MuqHkrt +E1k3jTbYZdt6mifshQ0uiP/7C1Up/gZMhGvcAfKxxdnE05oTJA== +-----END CERTIFICATE----- +