diff --git a/src/main/kotlin/Extension.kt b/src/main/kotlin/Extension.kt index 6d50f67..11ffd9b 100644 --- a/src/main/kotlin/Extension.kt +++ b/src/main/kotlin/Extension.kt @@ -102,7 +102,7 @@ data class ProvisioningInfoMap( ) { fun cborEncode(): ByteArray { val map = CborModelMap() - map.put(UnsignedInteger(1L), certificatesIssued.asDataItem()) + map.put(UnsignedInteger(CERTIFICATES_ISSUED_TAG), certificatesIssued.asDataItem()) return cborEncode(map) } @@ -113,26 +113,49 @@ data class ProvisioningInfoMap( * https://developer.android.com/privacy-and-security/security-key-attestation#provisioning_attestation_ext_schema */ @JvmField val OID = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.30") + private const val CERTIFICATES_ISSUED_TAG = 1L @JvmStatic @JvmOverloads - fun parseFrom(cert: X509Certificate, inputLimits: InputLimits = InputLimits()) = - cert.getExtensionValue(OID.id)?.let { parseFrom(it, inputLimits) } + fun parseFrom( + cert: X509Certificate, + inputLimits: InputLimits = InputLimits(), + logFn: (String) -> Unit = {}, + ): ProvisioningInfoMap? = + cert.getExtensionValue(OID.id)?.let { parseFrom(it, inputLimits, logFn) } @JvmStatic @JvmOverloads - fun parseFrom(bytes: ByteArray?, inputLimits: InputLimits = InputLimits()) = + fun parseFrom( + bytes: ByteArray?, + inputLimits: InputLimits = InputLimits(), + logFn: (String) -> Unit = {}, + ): ProvisioningInfoMap = try { val cborBytes = ASN1OctetString.getInstance(bytes).octets - from(cborDecode(cborBytes, inputLimits).asMap()) + from(cborDecode(cborBytes, inputLimits).asMap(), logFn) } catch (e: CborException) { throw IllegalArgumentException(e) } - private fun from(seq: CborModelMap): ProvisioningInfoMap { + private fun from(seq: CborModelMap, logFn: (String) -> Unit = {}): ProvisioningInfoMap { require(seq.keys.size >= 1) + val certificatesIssued = + seq.get(UnsignedInteger(CERTIFICATES_ISSUED_TAG)).asBigInteger().let { value -> + when { + value < BigInteger.ZERO -> { + logFn("certificatesIssued value ($value) is negative") + 0 + } + value > Int.MAX_VALUE.toBigInteger() -> { + logFn("certificatesIssued value ($value) exceeds Int.MAX_VALUE") + Int.MAX_VALUE + } + else -> value.toInt() + } + } return ProvisioningInfoMap( - certificatesIssued = seq.get(UnsignedInteger(1L)).asInteger(), + certificatesIssued = certificatesIssued, ) } } @@ -955,19 +978,16 @@ private fun BigInteger.toLongExact(): Long { throw CborException("BigInteger out of long range") } -fun DataItem.asInteger(): Int = +fun DataItem.asBigInteger(): BigInteger = when (this) { - is UnsignedInteger -> value.toIntExact() - is NegativeInteger -> value.toIntExact() + is UnsignedInteger -> value + is NegativeInteger -> value else -> throw CborException("Expected a number, got $majorType") } -fun DataItem.asLong(): Long = - when (this) { - is UnsignedInteger -> value.toLongExact() - is NegativeInteger -> value.toLongExact() - else -> throw CborException("Expected a number, got $majorType") - } +fun DataItem.asInteger(): Int = asBigInteger().toIntExact() + +fun DataItem.asLong(): Long = asBigInteger().toLongExact() fun Int.asDataItem() = when { diff --git a/src/main/kotlin/Verifier.kt b/src/main/kotlin/Verifier.kt index 1986655..4ab7b29 100644 --- a/src/main/kotlin/Verifier.kt +++ b/src/main/kotlin/Verifier.kt @@ -256,7 +256,9 @@ constructor( val deviceInformation = if (certPath.provisioningMethod() == ProvisioningMethod.REMOTELY_PROVISIONED) { try { - certPath.attestationCert().provisioningInfo(constraintConfig.inputLimits) + certPath + .attestationCert() + .provisioningInfo(constraintConfig.inputLimits, { msg -> log?.logInfoMessage(msg) }) } catch (e: Exception) { log?.logInfoMessage("Failed to parse provisioning info map: ${e.message}") null diff --git a/src/main/kotlin/X509CertificateExt.kt b/src/main/kotlin/X509CertificateExt.kt index 333717e..44c1fe4 100644 --- a/src/main/kotlin/X509CertificateExt.kt +++ b/src/main/kotlin/X509CertificateExt.kt @@ -47,5 +47,6 @@ fun X509Certificate.keyDescription(): KeyDescription? = KeyDescription.parseFrom * extension is not present in the certificate. */ fun X509Certificate.provisioningInfo( - inputLimits: InputLimits = InputLimits() -): ProvisioningInfoMap? = ProvisioningInfoMap.parseFrom(this, inputLimits) + inputLimits: InputLimits = InputLimits(), + logFn: (String) -> Unit = {}, +): ProvisioningInfoMap? = ProvisioningInfoMap.parseFrom(this, inputLimits, logFn) diff --git a/src/main/kotlin/testing/Certs.kt b/src/main/kotlin/testing/Certs.kt index ffb9e4a..da3a021 100644 --- a/src/main/kotlin/testing/Certs.kt +++ b/src/main/kotlin/testing/Certs.kt @@ -406,6 +406,22 @@ object CertLists { ) } + val maxCertificatesIssued by lazy { + val rkpAttestationCert = + certFactory.generateRkpAttestationCert( + securityLevel = SecurityLevel.TRUSTED_ENVIRONMENT, + serialNumber = TEST_ATTESTATION_SERIAL_NUMBER, + certificatesIssued = Int.MAX_VALUE, + ) + listOf( + certFactory.generateLeafCert(issuer = rkpAttestationCert.subject), + rkpAttestationCert, + certFactory.rkpIntermediate, + Certs.remoteIntermediate, + certFactory.root, + ) + } + /* Different revoked serial numbers for testing. */ @JvmField val REVOKED_SERIAL_NUMBER = 42.toBigInteger() @JvmField val REVOKED_SERIAL_NUMBER_BIG = 8000000000000.toBigInteger() @@ -471,6 +487,9 @@ object Chains { KeyAttestationCertPath(CertLists.validRemotelyProvisioned) } + @JvmStatic + val maxCertificatesIssued by lazy { KeyAttestationCertPath(CertLists.maxCertificatesIssued) } + @JvmStatic val wrongTrustAnchor by lazy { KeyAttestationCertPath(CertLists.wrongTrustAnchor) } /* A chain where the intermediate certificate has a mismatched subject. */ diff --git a/src/main/kotlin/testing/KeyAttestationCertFactory.kt b/src/main/kotlin/testing/KeyAttestationCertFactory.kt index 7e5a639..e10513b 100644 --- a/src/main/kotlin/testing/KeyAttestationCertFactory.kt +++ b/src/main/kotlin/testing/KeyAttestationCertFactory.kt @@ -123,6 +123,7 @@ internal class KeyAttestationCertFactory( serialNumber: BigInteger, notBefore: Date = fakeCalendar.lastWeek(), notAfter: Date = fakeCalendar.nextWeek(), + certificatesIssued: Int = 1, ) = generateAttestationCert( signingKey = rkpKey.private, @@ -136,7 +137,7 @@ internal class KeyAttestationCertFactory( ProvisioningInfoMap.OID, /* critical= */ false, ProvisioningInfoMap( - certificatesIssued = 1, + certificatesIssued = certificatesIssued, ) .cborEncode(), ), diff --git a/src/test/kotlin/ExtensionTest.kt b/src/test/kotlin/ExtensionTest.kt index 3ac1ebe..da2b7b3 100644 --- a/src/test/kotlin/ExtensionTest.kt +++ b/src/test/kotlin/ExtensionTest.kt @@ -67,6 +67,45 @@ class ExtensionTest { assertThat(provisioningInfo).isEqualTo(expectedProvisioningInfo) } + @Test + fun parseFrom_maxCertificatesIssuedFromAttestationCert_success() { + val expectedProvisioningInfo = + ProvisioningInfoMap( + certificatesIssued = Int.MAX_VALUE, + ) + val provisioningInfo = + ProvisioningInfoMap.parseFrom(Chains.maxCertificatesIssued.attestationCert(), InputLimits()) + assertThat(provisioningInfo).isEqualTo(expectedProvisioningInfo) + } + + @Test + fun parseFrom_outOfBoundsCertificatesIssuedFromAttestationCert_mapsToMaxAndLogs() { + val logHook = FakeLogHook() + val provisioningInfo = + ProvisioningInfoMap.parseFrom( + readCertPath("invalid/out_of_bounds_certificates_issued.pem").attestationCert(), + InputLimits(), + logFn = logHook.fakeVerifyRequestLog::logInfoMessage, + ) + assertThat(provisioningInfo?.certificatesIssued).isEqualTo(Int.MAX_VALUE) + assertThat(logHook.fakeVerifyRequestLog.infoMessages) + .contains("certificatesIssued value (${Int.MAX_VALUE.toLong() + 1}) exceeds Int.MAX_VALUE") + } + + @Test + fun parseFrom_negativeCertificatesIssuedFromAttestationCert_mapsToZeroAndLogs() { + val logHook = FakeLogHook() + val provisioningInfo = + ProvisioningInfoMap.parseFrom( + readCertPath("invalid/negative_certificates_issued.pem").attestationCert(), + InputLimits(), + logFn = logHook.fakeVerifyRequestLog::logInfoMessage, + ) + assertThat(provisioningInfo?.certificatesIssued).isEqualTo(0) + assertThat(logHook.fakeVerifyRequestLog.infoMessages) + .contains("certificatesIssued value (${Int.MIN_VALUE.toLong() - 1}) is negative") + } + @Test @TestParameters(valuesProvider = TestCaseProvider::class) fun parseFrom_success(model: String, sdk: Int) { diff --git a/src/test/kotlin/VerifierTest.kt b/src/test/kotlin/VerifierTest.kt index 8bc506e..df93e0a 100644 --- a/src/test/kotlin/VerifierTest.kt +++ b/src/test/kotlin/VerifierTest.kt @@ -371,6 +371,40 @@ class VerifierTest { assertIs(verifier.verify(CertLists.validRemotelyProvisioned)) } + @Test + fun verify_maxCertificatesIssued_succeedsWithMaxDeviceInformation() { + val result = + assertIs(verifier.verify(CertLists.maxCertificatesIssued)) + assertThat(result.deviceInformation?.certificatesIssued).isEqualTo(Int.MAX_VALUE) + } + + @Test + fun verify_outOfBoundsCertificatesIssued_succeedsWithMaxDeviceInformationAndLogs() { + val logHook = FakeLogHook() + val result = + assertIs( + verifier.verify( + readCertList("invalid/out_of_bounds_certificates_issued.pem"), + log = logHook, + ) + ) + assertThat(result.deviceInformation?.certificatesIssued).isEqualTo(Int.MAX_VALUE) + assertThat(logHook.fakeVerifyRequestLog.infoMessages) + .contains("certificatesIssued value (${Int.MAX_VALUE.toLong() + 1}) exceeds Int.MAX_VALUE") + } + + @Test + fun verify_negativeCertificatesIssued_succeedsWithZeroDeviceInformationAndLogs() { + val logHook = FakeLogHook() + val result = + assertIs( + verifier.verify(readCertList("invalid/negative_certificates_issued.pem"), log = logHook) + ) + assertThat(result.deviceInformation?.certificatesIssued).isEqualTo(0) + assertThat(logHook.fakeVerifyRequestLog.infoMessages) + .contains("certificatesIssued value (${Int.MIN_VALUE.toLong() - 1}) is negative") + } + @Test fun importedOrigins_customConfig_succeeds() { val verifier = diff --git a/testdata/invalid/negative_certificates_issued.pem b/testdata/invalid/negative_certificates_issued.pem new file mode 100644 index 0000000..6a96f77 --- /dev/null +++ b/testdata/invalid/negative_certificates_issued.pem @@ -0,0 +1,54 @@ +-----BEGIN CERTIFICATE----- +MIIBoTCCAUegAwIBAgIBATAKBggqhkjOPQQDAjAdMQwwCgYDVQQKDANURUUxDTAL +BgNVBAMMBGYwMGQwHhcNMjQxMDEzMDAwMDAwWhcNMjQxMDI3MDAwMDAwWjAfMR0w +GwYDVQQDDBRBbmRyb2lkIEtleXN0b3JlIEtleTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABACNF0j+6wil7+xzF0SWG2AV4GbskaCwH8AcjFH1EGdZKpUHERUctaoy +Bo2OkTByrwUgKv4bNXG47tCU5RGwF8ajdjB0MHIGCisGAQQB1nkCAREEZDBiAgEB +CgEBAgEBCgEBBChBIHJhbmRvbSA0MC1ieXRlIGNoYWxsZW5nZSBmb3Igbm8gcmVh +c29uBAAwADAmv4U+AwIBAL+FQBswGQQHYm9vdEtleQEBAAoBAAQIYm9vdEhhc2gw +CgYIKoZIzj0EAwIDSAAwRQIhAKQb8/s/cUriMv+pyFdV6BxCh0TVoegsmY0IJzmV +E64hAiBH9UgxTck0Fv1m+e8O4gfcgME37cyEx8rrxzVH3zBlyg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBYDCCAQegAwIBAgIDAPANMAoGCCqGSM49BAMCMCkxEzARBgNVBAoMCkdvb2ds +ZSBMTEMxEjAQBgNVBAMMCURyb2lkIENBMzAeFw0yNDEwMTMwMDAwMDBaFw0yNDEw +MjcwMDAwMDBaMB0xDDAKBgNVBAoMA1RFRTENMAsGA1UEAwwEZjAwZDBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABJP1VWopfZpFZfPNgCg93F+o/SEJdnDFpI2lFhXv +Ltx0NRfVADqTmn3Bkzb26trbOVIhmtmVl8fhvLtisdiPC5ijKjAoMA8GA1UdEwEB +/wQFMAMBAf8wFQYKKwYBBAHWeQIBHgQHoQE6gAAAADAKBggqhkjOPQQDAgNHADBE +AiBpVILsPUQ0/O/0gjQ3SIcEn2IfxrEQ1YPPuqWCrNXIYAIgILsjJCwndPeIOiur ++VGFHHTSwnZ6bD+faqz1I8L/zf0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBWDCB/qADAgECAgUSNFZ4kDAKBggqhkjOPQQDAjApMRIwEAYDVQQDDAlEcm9p +ZCBDQTIxEzARBgNVBAoMCkdvb2dsZSBMTEMwHhcNMjQxMDEzMDAwMDAwWhcNMjQx +MDI3MDAwMDAwWjApMRMwEQYDVQQKDApHb29nbGUgTExDMRIwEAYDVQQDDAlEcm9p +ZCBDQTMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASFAYC5KfEoqWOiJ40NGsyb +TdHvH4b3Su1yD6MN1rVKZVD0nocYcfpla5FUiUsBqyXAlv1Fgh7At64HiLgf2juL +oxMwETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQD44etCPd9D +5vVdGQR9RceHvTcM7DRAQGcxUDsCHZtiMAIhAP+PFJTXRH+F3ZojDa1vzriduVRZ +gba7xl4AhMVDT2U8 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBhjCCASygAwIBAgIFEjRWeJAwCgYIKoZIzj0EAwIwVzEhMB8GA1UEAwwYVGVz +dCBLZXkgQXR0ZXN0YXRpb24gQ0ExMRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQK +DApHb29nbGUgTExDMQswCQYDVQQGEwJVUzAeFw0yNDEwMTMwMDAwMDBaFw0yNDEw +MjcwMDAwMDBaMCkxEjAQBgNVBAMMCURyb2lkIENBMjETMBEGA1UECgwKR29vZ2xl +IExMQzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHQNKzafWVr0buMm4agL4BM+ +aFg68Vix9Q1Fj3vldgM3lKtDfuW+L2hFClERektST3SZhiRKZgVdE4x+zGv+3Guj +EzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgKqGAeEFpF4bE +ZHTi05RPaG6/vkMeLLBYGXJuB4go5fsCIQCldOgxt8k5FmtJ1YdOb13bucBp7JJi +LgKe74j8w2PDaQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtDCCAVqgAwIBAgIFAMoRyv4wCgYIKoZIzj0EAwIwVzEhMB8GA1UEAwwYVGVz +dCBLZXkgQXR0ZXN0YXRpb24gQ0ExMRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQK +DApHb29nbGUgTExDMQswCQYDVQQGEwJVUzAeFw0xOTEwMjAwMDAwMDBaFw0yOTEw +MjAwMDAwMDBaMFcxITAfBgNVBAMMGFRlc3QgS2V5IEF0dGVzdGF0aW9uIENBMTEQ +MA4GA1UECwwHQW5kcm9pZDETMBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMC +VVMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR6t/ih6fnJ+JrhbK7DL8xGFoQk +ZaE0rTKOnRkP9kYx54ipy7h4qRzvwTtWXRAFjUIN85yH0inTD4OXo+/rnBKjoxMw +ETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIHeYBxfApuRJxfJ3 +QRp0fi2nodFoTo8moviIbr0rhR+2AiEA3viHUa49KKowAHJhzk6EsMPLMWsXA6GT +BdolXh7l7LQ= +-----END CERTIFICATE----- diff --git a/testdata/invalid/out_of_bounds_certificates_issued.pem b/testdata/invalid/out_of_bounds_certificates_issued.pem new file mode 100644 index 0000000..61a8ecc --- /dev/null +++ b/testdata/invalid/out_of_bounds_certificates_issued.pem @@ -0,0 +1,54 @@ +-----BEGIN CERTIFICATE----- +MIIBojCCAUegAwIBAgIBATAKBggqhkjOPQQDAjAdMQwwCgYDVQQKDANURUUxDTAL +BgNVBAMMBGYwMGQwHhcNMjQxMDEzMDAwMDAwWhcNMjQxMDI3MDAwMDAwWjAfMR0w +GwYDVQQDDBRBbmRyb2lkIEtleXN0b3JlIEtleTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABACNF0j+6wil7+xzF0SWG2AV4GbskaCwH8AcjFH1EGdZKpUHERUctaoy +Bo2OkTByrwUgKv4bNXG47tCU5RGwF8ajdjB0MHIGCisGAQQB1nkCAREEZDBiAgEB +CgEBAgEBCgEBBChBIHJhbmRvbSA0MC1ieXRlIGNoYWxsZW5nZSBmb3Igbm8gcmVh +c29uBAAwADAmv4U+AwIBAL+FQBswGQQHYm9vdEtleQEBAAoBAAQIYm9vdEhhc2gw +CgYIKoZIzj0EAwIDSQAwRgIhAIfmefwOi3jFgpLuROykKKhrhFZhYCsfBPmXWKKg +7HNVAiEAr+rwunCq2Y1906Qb+TtGIb1af3rUgqxwXW2eUuWMvEY= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBYDCCAQegAwIBAgIDAPANMAoGCCqGSM49BAMCMCkxEzARBgNVBAoMCkdvb2ds +ZSBMTEMxEjAQBgNVBAMMCURyb2lkIENBMzAeFw0yNDEwMTMwMDAwMDBaFw0yNDEw +MjcwMDAwMDBaMB0xDDAKBgNVBAoMA1RFRTENMAsGA1UEAwwEZjAwZDBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABJP1VWopfZpFZfPNgCg93F+o/SEJdnDFpI2lFhXv +Ltx0NRfVADqTmn3Bkzb26trbOVIhmtmVl8fhvLtisdiPC5ijKjAoMA8GA1UdEwEB +/wQFMAMBAf8wFQYKKwYBBAHWeQIBHgQHoQEagAAAADAKBggqhkjOPQQDAgNHADBE +AiBXgdL8dXWN13k746Gf+JWqrViLMmYFLfx0R9T9/mrirQIgIsEGQvKL9znRKtJt +4+UnxZ2QiHRUHz8oJ2DuI2kqoyI= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBWDCB/qADAgECAgUSNFZ4kDAKBggqhkjOPQQDAjApMRIwEAYDVQQDDAlEcm9p +ZCBDQTIxEzARBgNVBAoMCkdvb2dsZSBMTEMwHhcNMjQxMDEzMDAwMDAwWhcNMjQx +MDI3MDAwMDAwWjApMRMwEQYDVQQKDApHb29nbGUgTExDMRIwEAYDVQQDDAlEcm9p +ZCBDQTMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASFAYC5KfEoqWOiJ40NGsyb +TdHvH4b3Su1yD6MN1rVKZVD0nocYcfpla5FUiUsBqyXAlv1Fgh7At64HiLgf2juL +oxMwETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQCvkOlh46xj +kNVeActSG/RWxnY9iOYt/5Y2pHt/vEkU4QIhAIBE8e7nEHcqbYnT/Brqd3QiBEJH +2fH8xmjLWSFxUdU/ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBhjCCASygAwIBAgIFEjRWeJAwCgYIKoZIzj0EAwIwVzEhMB8GA1UEAwwYVGVz +dCBLZXkgQXR0ZXN0YXRpb24gQ0ExMRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQK +DApHb29nbGUgTExDMQswCQYDVQQGEwJVUzAeFw0yNDEwMTMwMDAwMDBaFw0yNDEw +MjcwMDAwMDBaMCkxEjAQBgNVBAMMCURyb2lkIENBMjETMBEGA1UECgwKR29vZ2xl +IExMQzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHQNKzafWVr0buMm4agL4BM+ +aFg68Vix9Q1Fj3vldgM3lKtDfuW+L2hFClERektST3SZhiRKZgVdE4x+zGv+3Guj +EzARMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgErXtZO1cLlOX +AqC9XD2VOyGilv7z+cNo+sbWZzjN04ICIQD2jhe83tEPs40nHcJ0cDJfYMM/sqLQ +MUKe67JXKoIFSA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBtDCCAVqgAwIBAgIFAMoRyv4wCgYIKoZIzj0EAwIwVzEhMB8GA1UEAwwYVGVz +dCBLZXkgQXR0ZXN0YXRpb24gQ0ExMRAwDgYDVQQLDAdBbmRyb2lkMRMwEQYDVQQK +DApHb29nbGUgTExDMQswCQYDVQQGEwJVUzAeFw0xOTEwMjAwMDAwMDBaFw0yOTEw +MjAwMDAwMDBaMFcxITAfBgNVBAMMGFRlc3QgS2V5IEF0dGVzdGF0aW9uIENBMTEQ +MA4GA1UECwwHQW5kcm9pZDETMBEGA1UECgwKR29vZ2xlIExMQzELMAkGA1UEBhMC +VVMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR6t/ih6fnJ+JrhbK7DL8xGFoQk +ZaE0rTKOnRkP9kYx54ipy7h4qRzvwTtWXRAFjUIN85yH0inTD4OXo+/rnBKjoxMw +ETAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0gAMEUCIHneRRUngiYul5mT +5FHVjPECGZ8/z0vINrQn1Pd2pU98AiEAvZFYm8LMp8ZAVujJvio/7FLL+Bsc/yst +O9VteCsT8zg= +-----END CERTIFICATE-----