core: SpiffeUtil API for extracting Spiffe URI and loading TrustBundles (#11575)

Additional API for SpiffeUtil:
 - extract Spiffe URI from certificate chain
 - load Spiffe Trust Bundle from filesystem [json spec][] [JWK spec][]

JsonParser was changed to reject duplicate keys in objects.

[json spec]: https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md
[JWK spec]: https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#61-publishing-spiffe-bundle-elements
This commit is contained in:
erm-g 2024-10-17 14:11:07 -04:00 committed by GitHub
parent 1e0928fb79
commit 4be69e3f8a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 725 additions and 7 deletions

View File

@ -16,6 +16,7 @@
package io.grpc.internal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import com.google.gson.stream.JsonReader;
@ -41,7 +42,8 @@ public final class JsonParser {
/**
* Parses a json string, returning either a {@code Map<String, ?>}, {@code List<?>},
* {@code String}, {@code Double}, {@code Boolean}, or {@code null}.
* {@code String}, {@code Double}, {@code Boolean}, or {@code null}. Fails if duplicate names
* found.
*/
public static Object parse(String raw) throws IOException {
JsonReader jr = new JsonReader(new StringReader(raw));
@ -81,6 +83,7 @@ public final class JsonParser {
Map<String, Object> obj = new LinkedHashMap<>();
while (jr.hasNext()) {
String name = jr.nextName();
checkArgument(!obj.containsKey(name), "Duplicate key found: %s", name);
Object value = parseRecursive(jr);
obj.put(name, value);
}

View File

@ -19,15 +19,42 @@ package io.grpc.internal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Helper utility to work with SPIFFE URIs.
* Provides utilities to manage SPIFFE bundles, extract SPIFFE IDs from X.509 certificate chains,
* and parse SPIFFE IDs.
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
*/
public final class SpiffeUtil {
private static final Integer URI_SAN_TYPE = 6;
private static final String USE_PARAMETER_VALUE = "x509-svid";
private static final String KTY_PARAMETER_VALUE = "RSA";
private static final String CERTIFICATE_PREFIX = "-----BEGIN CERTIFICATE-----\n";
private static final String CERTIFICATE_SUFFIX = "-----END CERTIFICATE-----";
private static final String PREFIX = "spiffe://";
private SpiffeUtil() {}
@ -96,6 +123,137 @@ public final class SpiffeUtil {
+ " ([a-zA-Z0-9.-_])");
}
/**
* Returns the SPIFFE ID from the leaf certificate, if present.
*
* @param certChain certificate chain to extract SPIFFE ID from
*/
public static Optional<SpiffeId> extractSpiffeId(X509Certificate[] certChain)
throws CertificateParsingException {
checkArgument(checkNotNull(certChain, "certChain").length > 0, "certChain can't be empty");
Collection<List<?>> subjectAltNames = certChain[0].getSubjectAlternativeNames();
if (subjectAltNames == null) {
return Optional.absent();
}
String uri = null;
// Search for the unique URI SAN.
for (List<?> altName : subjectAltNames) {
if (altName.size() < 2 ) {
continue;
}
if (URI_SAN_TYPE.equals(altName.get(0))) {
if (uri != null) {
throw new IllegalArgumentException("Multiple URI SAN values found in the leaf cert.");
}
uri = (String) altName.get(1);
}
}
if (uri == null) {
return Optional.absent();
}
return Optional.of(parse(uri));
}
/**
* Loads a SPIFFE trust bundle from a file, parsing it from the JSON format.
* In case of success, returns {@link SpiffeBundle}.
* If any element of the JSON content is invalid or unsupported, an
* {@link IllegalArgumentException} is thrown and the entire Bundle is considered invalid.
*
* @param trustBundleFile the file path to the JSON file containing the trust bundle
* @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md">JSON format</a>
* @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md#61-publishing-spiffe-bundle-elements">JWK entry format</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7517#appendix-B">x5c (certificate) parameter</a>
*/
public static SpiffeBundle loadTrustBundleFromFile(String trustBundleFile) throws IOException {
Map<String, ?> trustDomainsNode = readTrustDomainsFromFile(trustBundleFile);
Map<String, List<X509Certificate>> trustBundleMap = new HashMap<>();
Map<String, Long> sequenceNumbers = new HashMap<>();
for (String trustDomainName : trustDomainsNode.keySet()) {
Map<String, ?> domainNode = JsonUtil.getObject(trustDomainsNode, trustDomainName);
if (domainNode.size() == 0) {
trustBundleMap.put(trustDomainName, Collections.emptyList());
continue;
}
Long sequenceNumber = JsonUtil.getNumberAsLong(domainNode, "spiffe_sequence");
sequenceNumbers.put(trustDomainName, sequenceNumber == null ? -1L : sequenceNumber);
List<Map<String, ?>> keysNode = JsonUtil.getListOfObjects(domainNode, "keys");
if (keysNode == null || keysNode.size() == 0) {
trustBundleMap.put(trustDomainName, Collections.emptyList());
continue;
}
trustBundleMap.put(trustDomainName, extractCert(keysNode, trustDomainName));
}
return new SpiffeBundle(sequenceNumbers, trustBundleMap);
}
private static Map<String, ?> readTrustDomainsFromFile(String filePath) throws IOException {
Path path = Paths.get(checkNotNull(filePath, "trustBundleFile"));
String json = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
Object jsonObject = JsonParser.parse(json);
if (!(jsonObject instanceof Map)) {
throw new IllegalArgumentException(
"SPIFFE Trust Bundle should be a JSON object. Found: "
+ (jsonObject == null ? null : jsonObject.getClass()));
}
@SuppressWarnings("unchecked")
Map<String, ?> root = (Map<String, ?>)jsonObject;
Map<String, ?> trustDomainsNode = JsonUtil.getObject(root, "trust_domains");
checkNotNull(trustDomainsNode, "Mandatory trust_domains element is missing");
checkArgument(trustDomainsNode.size() > 0, "Mandatory trust_domains element is missing");
return trustDomainsNode;
}
private static void checkJwkEntry(Map<String, ?> jwkNode, String trustDomainName) {
String kty = JsonUtil.getString(jwkNode, "kty");
if (kty == null || !kty.equals(KTY_PARAMETER_VALUE)) {
throw new IllegalArgumentException(String.format("'kty' parameter must be '%s' but '%s' "
+ "found. Certificate loading for trust domain '%s' failed.", KTY_PARAMETER_VALUE,
kty, trustDomainName));
}
if (jwkNode.containsKey("kid")) {
throw new IllegalArgumentException(String.format("'kid' parameter must not be set. "
+ "Certificate loading for trust domain '%s' failed.", trustDomainName));
}
String use = JsonUtil.getString(jwkNode, "use");
if (use == null || !use.equals(USE_PARAMETER_VALUE)) {
throw new IllegalArgumentException(String.format("'use' parameter must be '%s' but '%s' "
+ "found. Certificate loading for trust domain '%s' failed.", USE_PARAMETER_VALUE,
use, trustDomainName));
}
}
private static List<X509Certificate> extractCert(List<Map<String, ?>> keysNode,
String trustDomainName) {
List<X509Certificate> result = new ArrayList<>();
for (Map<String, ?> keyNode : keysNode) {
checkJwkEntry(keyNode, trustDomainName);
List<String> rawCerts = JsonUtil.getListOfStrings(keyNode, "x5c");
if (rawCerts == null) {
break;
}
if (rawCerts.size() != 1) {
throw new IllegalArgumentException(String.format("Exactly 1 certificate is expected, but "
+ "%s found. Certificate loading for trust domain '%s' failed.", rawCerts.size(),
trustDomainName));
}
InputStream stream = new ByteArrayInputStream((CERTIFICATE_PREFIX + rawCerts.get(0) + "\n"
+ CERTIFICATE_SUFFIX)
.getBytes(StandardCharsets.UTF_8));
try {
Collection<? extends Certificate> certs = CertificateFactory.getInstance("X509")
.generateCertificates(stream);
X509Certificate[] certsArray = certs.toArray(new X509Certificate[0]);
assert certsArray.length == 1;
result.add(certsArray[0]);
} catch (CertificateException e) {
throw new IllegalArgumentException(String.format("Certificate can't be parsed. Certificate "
+ "loading for trust domain '%s' failed.", trustDomainName), e);
}
}
return result;
}
/**
* Represents a SPIFFE ID as defined in the SPIFFE standard.
* @see <a href="https://github.com/spiffe/spiffe/blob/master/standards/SPIFFE-ID.md">Standard</a>
@ -119,4 +277,34 @@ public final class SpiffeUtil {
}
}
/**
* Represents a SPIFFE trust bundle; that is, a map from trust domain to set of trusted
* certificates. Only trust domain's sequence numbers and x509 certificates are supported.
* @see <a href="https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Trust_Domain_and_Bundle.md#4-spiffe-bundle-format">Standard</a>
*/
public static final class SpiffeBundle {
private final ImmutableMap<String, Long> sequenceNumbers;
private final ImmutableMap<String, ImmutableList<X509Certificate>> bundleMap;
private SpiffeBundle(Map<String, Long> sequenceNumbers,
Map<String, List<X509Certificate>> trustDomainMap) {
this.sequenceNumbers = ImmutableMap.copyOf(sequenceNumbers);
ImmutableMap.Builder<String, ImmutableList<X509Certificate>> builder = ImmutableMap.builder();
for (Map.Entry<String, List<X509Certificate>> entry : trustDomainMap.entrySet()) {
builder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
}
this.bundleMap = builder.build();
}
public ImmutableMap<String, Long> getSequenceNumbers() {
return sequenceNumbers;
}
public ImmutableMap<String, ImmutableList<X509Certificate>> getBundleMap() {
return bundleMap;
}
}
}

View File

@ -123,4 +123,11 @@ public class JsonParserTest {
assertEquals(expected, JsonParser.parse("{\"hi\": 2}"));
}
@Test
public void duplicate() throws IOException {
thrown.expect(IllegalArgumentException.class);
JsonParser.parse("{\"hi\": 2, \"hi\": 3}");
}
}

View File

@ -17,12 +17,28 @@
package io.grpc.internal;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import com.google.common.base.Optional;
import io.grpc.internal.SpiffeUtil.SpiffeBundle;
import io.grpc.internal.SpiffeUtil.SpiffeId;
import io.grpc.testing.TlsTesting;
import io.grpc.util.CertificateUtils;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
@ -159,7 +175,8 @@ public class SpiffeUtilTest {
SpiffeUtil.parse(longTrustDomain.toString()));
assertEquals("Trust Domain maximum length is 255 characters", iae.getMessage());
StringBuilder longSpiffe = new StringBuilder(String.format("spiffe://mydomain%scom/", "%21"));
@SuppressWarnings("OrphanedFormatString")
StringBuilder longSpiffe = new StringBuilder("spiffe://mydomain%21com/");
for (int i = 0; i < 405; i++) {
longSpiffe.append("qwert");
}
@ -193,4 +210,157 @@ public class SpiffeUtilTest {
+ "underscores ([a-zA-Z0-9.-_])", iae.getMessage());
}
}
public static class CertificateApiTest {
private static final String SPIFFE_PEM_FILE = "spiffe_cert.pem";
private static final String MULTI_URI_SAN_PEM_FILE = "spiffe_multi_uri_san_cert.pem";
private static final String SERVER_0_PEM_FILE = "server0.pem";
private static final String TEST_DIRECTORY_PREFIX = "io/grpc/internal/";
private static final String SPIFFE_TRUST_BUNDLE = "spiffebundle.json";
private static final String SPIFFE_TRUST_BUNDLE_MALFORMED = "spiffebundle_malformed.json";
private static final String SPIFFE_TRUST_BUNDLE_CORRUPTED_CERT =
"spiffebundle_corrupted_cert.json";
private static final String SPIFFE_TRUST_BUNDLE_WRONG_KTY = "spiffebundle_wrong_kty.json";
private static final String SPIFFE_TRUST_BUNDLE_WRONG_KID = "spiffebundle_wrong_kid.json";
private static final String SPIFFE_TRUST_BUNDLE_WRONG_USE = "spiffebundle_wrong_use.json";
private static final String SPIFFE_TRUST_BUNDLE_WRONG_MULTI_CERTS =
"spiffebundle_wrong_multi_certs.json";
private static final String SPIFFE_TRUST_BUNDLE_DUPLICATES = "spiffebundle_duplicates.json";
private static final String SPIFFE_TRUST_BUNDLE_WRONG_ROOT = "spiffebundle_wrong_root.json";
private static final String SPIFFE_TRUST_BUNDLE_WRONG_SEQ = "spiffebundle_wrong_seq_type.json";
private static final String DOMAIN_ERROR_MESSAGE =
" Certificate loading for trust domain 'google.com' failed.";
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();
private X509Certificate[] spiffeCert;
private X509Certificate[] multipleUriSanCert;
private X509Certificate[] serverCert0;
@Before
public void setUp() throws Exception {
spiffeCert = CertificateUtils.getX509Certificates(TlsTesting.loadCert(SPIFFE_PEM_FILE));
multipleUriSanCert = CertificateUtils.getX509Certificates(TlsTesting
.loadCert(MULTI_URI_SAN_PEM_FILE));
serverCert0 = CertificateUtils.getX509Certificates(TlsTesting.loadCert(SERVER_0_PEM_FILE));
}
private String copyFileToTmp(String fileName) throws Exception {
Path tempFilePath = tempFolder.newFile(fileName).toPath();
try (InputStream resourceStream = SpiffeUtilTest.class.getClassLoader()
.getResourceAsStream(TEST_DIRECTORY_PREFIX + fileName)) {
Files.copy(resourceStream, tempFilePath, StandardCopyOption.REPLACE_EXISTING);
}
return tempFilePath.toString();
}
@Test
public void extractSpiffeIdSuccessTest() throws Exception {
Optional<SpiffeId> spiffeId = SpiffeUtil.extractSpiffeId(spiffeCert);
assertTrue(spiffeId.isPresent());
assertEquals("foo.bar.com", spiffeId.get().getTrustDomain());
assertEquals("/client/workload/1", spiffeId.get().getPath());
}
@Test
public void extractSpiffeIdFailureTest() throws Exception {
Optional<SpiffeUtil.SpiffeId> spiffeId = SpiffeUtil.extractSpiffeId(serverCert0);
assertFalse(spiffeId.isPresent());
IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.extractSpiffeId(multipleUriSanCert));
assertEquals("Multiple URI SAN values found in the leaf cert.", iae.getMessage());
}
@Test
public void extractSpiffeIdFromChainTest() throws Exception {
// Check that the SPIFFE ID is extracted only from the leaf cert in the chain (spiffeCert
// contains it, but serverCert0 does not).
X509Certificate[] leafWithSpiffeChain = new X509Certificate[]{spiffeCert[0], serverCert0[0]};
assertTrue(SpiffeUtil.extractSpiffeId(leafWithSpiffeChain).isPresent());
X509Certificate[] leafWithoutSpiffeChain =
new X509Certificate[]{serverCert0[0], spiffeCert[0]};
assertFalse(SpiffeUtil.extractSpiffeId(leafWithoutSpiffeChain).isPresent());
}
@Test
public void extractSpiffeIdParameterValidityTest() {
NullPointerException npe = assertThrows(NullPointerException.class, () -> SpiffeUtil
.extractSpiffeId(null));
assertEquals("certChain", npe.getMessage());
IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.extractSpiffeId(new X509Certificate[]{}));
assertEquals("certChain can't be empty", iae.getMessage());
}
@Test
public void loadTrustBundleFromFileSuccessTest() throws Exception {
SpiffeBundle tb = SpiffeUtil.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE));
assertEquals(2, tb.getSequenceNumbers().size());
assertEquals(12035488L, (long) tb.getSequenceNumbers().get("example.com"));
assertEquals(-1L, (long) tb.getSequenceNumbers().get("test.example.com"));
assertEquals(3, tb.getBundleMap().size());
assertEquals(0, tb.getBundleMap().get("test.google.com.au").size());
assertEquals(1, tb.getBundleMap().get("example.com").size());
assertEquals(2, tb.getBundleMap().get("test.example.com").size());
Optional<SpiffeId> spiffeId = SpiffeUtil.extractSpiffeId(tb.getBundleMap().get("example.com")
.toArray(new X509Certificate[0]));
assertTrue(spiffeId.isPresent());
assertEquals("foo.bar.com", spiffeId.get().getTrustDomain());
}
@Test
public void loadTrustBundleFromFileFailureTest() {
// Check the exception if JSON root element is different from 'trust_domains'
NullPointerException npe = assertThrows(NullPointerException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_ROOT)));
assertEquals("Mandatory trust_domains element is missing", npe.getMessage());
// Check the exception if JSON root element is different from 'trust_domains'
ClassCastException cce = assertThrows(ClassCastException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_SEQ)));
assertTrue(cce.getMessage().contains("Number expected to be long"));
// Check the exception if JSON file doesn't contain an object
IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_MALFORMED)));
assertTrue(iae.getMessage().contains("SPIFFE Trust Bundle should be a JSON object."));
// Check the exception if JSON contains duplicates
iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_DUPLICATES)));
assertEquals("Duplicate key found: google.com", iae.getMessage());
// Check the exception if 'x5c' value cannot be parsed
iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_CORRUPTED_CERT)));
assertEquals("Certificate can't be parsed." + DOMAIN_ERROR_MESSAGE, iae.getMessage());
// Check the exception if 'kty' value differs from 'RSA'
iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_KTY)));
assertEquals("'kty' parameter must be 'RSA' but 'null' found." + DOMAIN_ERROR_MESSAGE,
iae.getMessage());
// Check the exception if 'kid' has a value
iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_KID)));
assertEquals("'kid' parameter must not be set." + DOMAIN_ERROR_MESSAGE, iae.getMessage());
// Check the exception if 'use' value differs from 'x509-svid'
iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_USE)));
assertEquals("'use' parameter must be 'x509-svid' but 'i_am_not_x509-svid' found."
+ DOMAIN_ERROR_MESSAGE, iae.getMessage());
// Check the exception if multiple certs are provided for 'x5c'
iae = assertThrows(IllegalArgumentException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(copyFileToTmp(SPIFFE_TRUST_BUNDLE_WRONG_MULTI_CERTS)));
assertEquals("Exactly 1 certificate is expected, but 2 found." + DOMAIN_ERROR_MESSAGE,
iae.getMessage());
}
@Test
public void loadTrustBundleFromFileParameterValidityTest() {
NullPointerException npe = assertThrows(NullPointerException.class, () -> SpiffeUtil
.loadTrustBundleFromFile(null));
assertEquals("trustBundleFile", npe.getMessage());
NoSuchFileException nsfe = assertThrows(NoSuchFileException.class, () -> SpiffeUtil
.loadTrustBundleFromFile("i_do_not_exist"));
assertEquals("i_do_not_exist", nsfe.getMessage());
}
}
}

View File

@ -0,0 +1,115 @@
{
"trust_domains": {
"test.google.com.au": {},
"example.com": {
"spiffe_sequence": 12035488,
"keys": [
{
"kty": "RSA",
"use": "x509-svid",
"x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL
BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy
NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM
MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu
dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ
LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z
G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO
a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z
JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV
m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75
7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc
msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc
DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN
zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs
vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI
sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH
HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP
BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t
L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/
aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3
4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0
IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+
PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV
+j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D
vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq
yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV
z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx
x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U
0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX
GA91fn0891b5eEW8BJHXX0jri0aN8g=="],
"n": "<base64urlUint-encoded value>",
"e": "AQAB"
}
]
},
"test.example.com": {
"keys": [
{
"kty": "RSA",
"use": "x509-svid",
"x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL
BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy
NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM
MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu
dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ
LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z
G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO
a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z
JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV
m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75
7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc
msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc
DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN
zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs
vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI
sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH
HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP
BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t
L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/
aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3
4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0
IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+
PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV
+j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D
vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq
yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV
z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx
x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U
0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX
GA91fn0891b5eEW8BJHXX0jri0aN8g=="],
"n": "<base64urlUint-encoded value>",
"e": "AQAB"
},
{
"kty": "RSA",
"use": "x509-svid",
"x5c": ["MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0
MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV
BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl
c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS
SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv
FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5
Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y
yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8
iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3
d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u
YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v
Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N
MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0
YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM
BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB
AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw
UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt
GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s
UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4
WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox
jGL772hQMbwtFCOFXu5VP0s="]
}
]
}
}
}

View File

@ -0,0 +1,14 @@
{
"trust_domains": {
"google.com": {
"spiffe_sequence": 123,
"keys": [
{
"kty": "RSA",
"use": "x509-svid",
"x5c": ["UNPARSABLE_CERTIFICATE"]
}
]
}
}
}

View File

@ -0,0 +1,23 @@
{
"trust_domains": {
"google.com": {
"spiffe_sequence": 123,
"keys": [
{
"x5c": "VALUE_DOESN'T_MATTER"
}
]
},
"google.com": {
"spiffe_sequence": 123,
"keys": [
{
"use": "x509-svid",
"kid": "some_value",
"x5c": "VALUE_DOESN'T_MATTER"
}
]
},
"test.google.com.au": {}
}
}

View File

@ -0,0 +1,4 @@
[
"test.google.com",
"test.google.com.au"
]

View File

@ -0,0 +1,15 @@
{
"trust_domains": {
"google.com": {
"spiffe_sequence": 123,
"keys": [
{
"kty": "RSA",
"use": "x509-svid",
"kid": "some_value",
"x5c": "VALUE_DOESN'T_MATTER"
}
]
}
}
}

View File

@ -0,0 +1,12 @@
{
"trust_domains": {
"google.com": {
"spiffe_sequence": 123,
"keys": [
{
"x5c": "VALUE_DOESN'T_MATTER"
}
]
}
}
}

View File

@ -0,0 +1,67 @@
{
"trust_domains": {
"google.com": {
"spiffe_sequence": 123,
"keys": [
{
"kty": "RSA",
"use": "x509-svid",
"x5c": ["MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL
BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy
NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM
MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu
dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ
LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z
G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO
a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z
JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV
m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75
7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc
msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc
DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN
zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs
vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI
sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH
HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP
BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t
L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/
aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3
4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0
IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+
PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV
+j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D
vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq
yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV
z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx
x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U
0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX
GA91fn0891b5eEW8BJHXX0jri0aN8g==",
"MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0
MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV
BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl
c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS
SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv
FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5
Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y
yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8
iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3
d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u
YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v
Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N
MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0
YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM
BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB
AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw
UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt
GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s
UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4
WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox
jGL772hQMbwtFCOFXu5VP0s="]
}
]
}
}
}

View File

@ -0,0 +1,6 @@
{
"trustDomains": {
"test.google.com": {},
"test.google.com.au": {}
}
}

View File

@ -0,0 +1,12 @@
{
"trust_domains": {
"google.com": {
"spiffe_sequence": 123.5,
"keys": [
{
"x5c": "VALUE_DOESN'T_MATTER"
}
]
}
}
}

View File

@ -0,0 +1,13 @@
{
"trust_domains": {
"google.com": {
"keys": [
{
"kty": "RSA",
"use": "i_am_not_x509-svid",
"x5c": "VALUE_DOESN'T_MATTER"
}
]
}
}
}

View File

@ -108,8 +108,7 @@ public class ObservabilityConfigImplTest {
private static final String PROJECT_ID = "{\n"
+ " \"project_id\": \"grpc-testing\",\n"
+ " \"cloud_logging\": {},\n"
+ " \"project_id\": \"grpc-testing\"\n"
+ " \"cloud_logging\": {}\n"
+ "}";
private static final String EMPTY_CONFIG = "{}";

View File

@ -0,0 +1,12 @@
[spiffe_client]
subjectAltName = @alt_names
[spiffe_client_multi]
subjectAltName = @alt_names_multi
[alt_names]
URI = spiffe://foo.bar.com/client/workload/1
[alt_names_multi]
URI.1 = spiffe://foo.bar.com/client/workload/1
URI.2 = spiffe://foo.bar.com/client/workload/2

View File

@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFsjCCA5qgAwIBAgIURygVMMzdr+Q7rsUaz189JozyHMwwDQYJKoZIhvcNAQEL
BQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQwwCgYDVQQHDANTVkwxDTAL
BgNVBAoMBGdSUEMxFTATBgNVBAMMDHRlc3QtY2xpZW50MTAeFw0yMTEyMjMxODQy
NTJaFw0zMTEyMjExODQyNTJaME4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEM
MAoGA1UEBwwDU1ZMMQ0wCwYDVQQKDARnUlBDMRUwEwYDVQQDDAx0ZXN0LWNsaWVu
dDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ4AqpGetyVSqGUuBJ
LVFla+7bEfca7UYzfVSSZLZ/X+JDmWIVN8UIPuFib5jhMEc3XaUnFXUmM7zEtz/Z
G5hapwLwOb2C3ZxOP6PQjYCJxbkLie+b43UQrFu1xxd3vMhVJgcj/AIxEpmszuqO
a6kUrkYifjJADQ+64kZgl66bsTdXMCzpxyFl9xUfff59L8OX+HUfAcoZz3emjg3Z
JPYURQEmjdZTOau1EjFilwHgd989Jt7NKgx30NXoHmw7nusVBIY94fL2VKN3f1XV
m0dHu5NI279Q6zr0ZBU7k5T3IeHnzsUesQS4NGlklDWoVTKk73Uv9Pna8yQsSW75
7PEbHOGp9Knu4bnoGPOlsG81yIPipO6hTgGFK24pF97M9kpGbWqYX4+2vLlrCAfc
msHqaUPmQlYeRVTT6vw7ctYo2kyUYGtnODXk76LqewRBVvkzx75QUhfjAyb740Yc
DmIenc56Tq6gebJHjhEmVSehR6xIpXP7SVeurTyhPsEQnpJHtgs4dcwWOZp7BvPN
zHXmJqfr7vsshie3vS5kQ0u1e1yqAqXgyDjqKXOkx+dpgUTehSJHhPNHvTc5LXRs
vvXKYz6FrwR/DZ8t7BNEvPeLjFgxpH7QVJFLCvCbXs5K6yYbsnLfxFIBPRnrbJkI
sK+sQwnRdnsiUdPsTkG5B2lQfQIDAQABo4GHMIGEMB0GA1UdDgQWBBQ2lBp0PiRH
HvQ5IRURm8aHsj4RETAfBgNVHSMEGDAWgBQ2lBp0PiRHHvQ5IRURm8aHsj4RETAP
BgNVHRMBAf8EBTADAQH/MDEGA1UdEQQqMCiGJnNwaWZmZTovL2Zvby5iYXIuY29t
L2NsaWVudC93b3JrbG9hZC8xMA0GCSqGSIb3DQEBCwUAA4ICAQA1mSkgRclAl+E/
aS9zJ7t8+Y4n3T24nOKKveSIjxXm/zjhWqVsLYBI6kglWtih2+PELvU8JdPqNZK3
4Kl0Q6FWpVSGDdWN1i6NyORt2ocggL3ke3iXxRk3UpUKJmqwz81VhA2KUHnMlyE0
IufFfZNwNWWHBv13uJfRbjeQpKPhU+yf4DeXrsWcvrZlGvAET+mcplafUzCp7Iv+
PcISJtUerbxbVtuHVeZCLlgDXWkLAWJN8rf0dIG4x060LJ+j6j9uRVhb9sZn1HJV
+j4XdIYm1VKilluhOtNwP2d3Ox/JuTBxf7hFHXZPfMagQE5k5PzmxRaCAEMJ1l2D
vUbZw+shJfSNoWcBo2qadnUaWT3BmmJRBDh7ZReib/RQ1Rd4ygOyzP3E0vkV4/gq
yjLdApXh5PZP8KLQZ+1JN/sdWt7VfIt9wYOpkIqujdll51ESHzwQeAK9WVCB4UvV
z6zdhItB9CRbXPreWC+wCB1xDovIzFKOVsLs5+Gqs1m7VinG2LxbDqaKyo/FB0Hx
x0acBNzezLWoDwXYQrN0T0S4pnqhKD1CYPpdArBkNezUYAjS725FkApuK+mnBX3U
0msBffEaUEOkcyar1EW2m/33vpetD/k3eQQkmvQf4Hbiu9AF+9cNDm/hMuXEw5EX
GA91fn0891b5eEW8BJHXX0jri0aN8g==
-----END CERTIFICATE-----

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIELTCCAxWgAwIBAgIUVXGlXjNENtOZbI12epjgIhMaShEwDQYJKoZIhvcNAQEL
BQAwVjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGdGVzdGNhMB4XDTI0
MDkxNzE2MTk0NFoXDTM0MDkxNTE2MTk0NFowTjELMAkGA1UEBhMCVVMxCzAJBgNV
BAgMAkNBMQwwCgYDVQQHDANTVkwxDTALBgNVBAoMBGdSUEMxFTATBgNVBAMMDHRl
c3QtY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOcTjjcS
SfG/EGrr6G+f+3T2GXyHHfroQFi9mZUz80L7uKBdECOImID+YhoK8vcxLQjPmEEv
FIYgJT5amugDcYIgUhMjBx/8RPJaP/nGmBngAqsuuNCaZfyaHBRqN8XdS/AwmsI5
Wo+nru0+0/7aQFdqqtd2+e9dHjUWwgHxXvMgC4hkHpsdCGIZWVzWyBliwTYQYb1Y
yYe1LzqqQA5OMbZfKOY9MYDCEYOliRiunOn30iIOHj9V5qLzWGfSyxCRuvLRdEP8
iDeNweHbdaKuI80nQmxuBdRIspE9k5sD1WA4vLZpeg3zggxp4rfLL5zBJgb/33D3
d9Rkm14xfDPihhkCAwEAAaOB+jCB9zBZBgNVHREEUjBQhiZzcGlmZmU6Ly9mb28u
YmFyLmNvbS9jbGllbnQvd29ya2xvYWQvMYYmc3BpZmZlOi8vZm9vLmJhci5jb20v
Y2xpZW50L3dvcmtsb2FkLzIwHQYDVR0OBBYEFG9GkBgdBg/p0U9/lXv8zIJ+2c2N
MHsGA1UdIwR0MHKhWqRYMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0
YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM
BnRlc3RjYYIUWrP0VvHcy+LP6UuYNtiL9gBhD5owDQYJKoZIhvcNAQELBQADggEB
AJ4Cbxv+02SpUgkEu4hP/1+8DtSBXUxNxI0VG4e3Ap2+Rhjm3YiFeS/UeaZhNrrw
UEjkSTPFODyXR7wI7UO9OO1StyD6CMkp3SEvevU5JsZtGL6mTiTLTi3Qkywa91Bt
GlyZdVMghA1bBJLBMwiD5VT5noqoJBD7hDy6v9yNmt1Sw2iYBJPqI3Gnf5bMjR3s
UICaxmFyqaMCZsPkfJh0DmZpInGJys3m4QqGz6ZE2DWgcSr1r/ML7/5bSPjjr8j4
WFFSqFR3dMu8CbGnfZTCTXa4GTX/rARXbAO67Z/oJbJBK7VKayskL+PzKuohb9ox
jGL772hQMbwtFCOFXu5VP0s=
-----END CERTIFICATE-----