diff --git a/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java b/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java index 4a33a6479..49a8ec18d 100644 --- a/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java +++ b/oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java @@ -34,16 +34,24 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; 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.NoSuchFileException; import java.nio.file.Paths; +import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an @@ -56,6 +64,9 @@ public class CertificateIdentityPoolSubjectTokenSupplier private final IdentityPoolCredentialSource credentialSource; + private static final Pattern PEM_CERT_PATTERN = + Pattern.compile("-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", Pattern.DOTALL); + CertificateIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) { this.credentialSource = checkNotNull(credentialSource, "credentialSource cannot be null"); // This check ensures that the credential source was intended for certificate usage. @@ -66,10 +77,21 @@ public class CertificateIdentityPoolSubjectTokenSupplier + " CertificateIdentityPoolSubjectTokenSupplier"); } - private static X509Certificate loadLeafCertificate(String path) - throws IOException, CertificateException { - byte[] leafCertBytes = Files.readAllBytes(Paths.get(path)); - return parseCertificate(leafCertBytes); + private static String loadAndEncodeLeafCertificate(String path) throws IOException { + try { + byte[] leafCertBytes = Files.readAllBytes(Paths.get(path)); + X509Certificate leafCert = parseCertificate(leafCertBytes); + return encodeCert(leafCert); + } catch (NoSuchFileException e) { + throw new IOException(String.format("Leaf certificate file not found: %s", path), e); + } catch (CertificateException e) { + throw new IOException( + String.format("Failed to parse leaf certificate from file: %s", path), e); + } catch (IOException e) { + // This catches any other general I/O errors during leaf certificate file reading (e.g., + // permissions). + throw new IOException(String.format("Failed to read leaf certificate file: %s", path), e); + } } @VisibleForTesting @@ -97,33 +119,177 @@ private static String encodeCert(X509Certificate certificate) /** * Retrieves the X509 subject token. This method loads the leaf certificate specified by the - * {@code credentialSource.credentialLocation}. The subject token is constructed as a JSON array - * containing the base64-encoded (DER format) leaf certificate. This JSON array serves as the - * subject token for mTLS authentication. + * {@code credentialSource.credentialLocation}. If a trust chain path is configured in the {@code + * credentialSource.certificateConfig}, it also loads and includes the trust chain certificates. + * The subject token is constructed as a JSON array containing the base64-encoded (DER format) + * leaf certificate, followed by the base64-encoded (DER format) certificates in the trust chain. + * This JSON array serves as the subject token for mTLS authentication. * * @param context The external account supplier context. This parameter is currently not used in * this implementation. - * @return The JSON string representation of the base64-encoded leaf certificate in a JSON array. - * @throws IOException If an I/O error occurs while reading the certificate file. + * @return The JSON string representation of the base64-encoded certificate chain (leaf + * certificate followed by the trust chain, if present). + * @throws IOException If an I/O error occurs while reading the certificate file(s). */ @Override public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException { - try { - // credentialSource.credentialLocation is expected to be non-null here, - // set during IdentityPoolCredentials construction for certificate type. - X509Certificate leafCert = loadLeafCertificate(credentialSource.getCredentialLocation()); - String encodedLeafCert = encodeCert(leafCert); + String leafCertPath = credentialSource.getCredentialLocation(); + String trustChainPath = null; + if (credentialSource.getCertificateConfig() != null) { + trustChainPath = credentialSource.getCertificateConfig().getTrustChainPath(); + } + + // Load and encode the leaf certificate. + String encodedLeafCert = loadAndEncodeLeafCertificate(leafCertPath); - java.util.List certChain = new java.util.ArrayList<>(); - certChain.add(encodedLeafCert); + // Initialize the certificate chain for the subject token. The Security Token Service (STS) + // requires that the leaf certificate (the one used for authenticating this workload) must be + // the first certificate in this chain. + List certChain = new ArrayList<>(); + certChain.add(encodedLeafCert); + + // Handle trust chain loading and processing. + try { + // Read the trust chain. + List trustChainCerts = readTrustChain(trustChainPath); - return OAuth2Utils.JSON_FACTORY.toString(certChain); + // Process the trust chain certificates read from the file. + if (!trustChainCerts.isEmpty()) { + populateCertChainFromTrustChain(certChain, trustChainCerts, encodedLeafCert); + } + } catch (IllegalArgumentException e) { + // This catches the specific error for misconfigured trust chain (e.g., leaf in wrong place). + throw new IOException("Trust chain misconfiguration: " + e.getMessage(), e); + } catch (NoSuchFileException e) { + throw new IOException(String.format("Trust chain file not found: %s", trustChainPath), e); } catch (CertificateException e) { - // Catch CertificateException to provide a more specific error message including - // the path of the file that failed to parse, and re-throw as IOException - // as expected by the getSubjectToken method signature for I/O related issues. throw new IOException( - "Failed to parse certificate(s) from: " + credentialSource.getCredentialLocation(), e); + String.format("Failed to parse certificate(s) from trust chain file: %s", trustChainPath), + e); + } catch (IOException e) { + // This catches any other general I/O errors during trust chain file reading (e.g., + // permissions). + throw new IOException( + String.format("Failed to read trust chain file: %s", trustChainPath), e); + } + + return OAuth2Utils.JSON_FACTORY.toString(certChain); + } + + /** + * Extends {@code certChainToPopulate} with encoded certificates from {@code trustChainCerts}, + * applying validation rules for the leaf certificate's presence and order within the trust chain. + * + * @param certChainToPopulate The list of encoded certificate strings to populate. + * @param trustChainCerts The list of X509Certificates from the trust chain file (non-empty). + * @param encodedLeafCert The Base64-encoded leaf certificate. + * @throws CertificateEncodingException If an error occurs during certificate encoding. + * @throws IllegalArgumentException If the leaf certificate is found in an invalid position in the + * trust chain. + */ + private void populateCertChainFromTrustChain( + List certChainToPopulate, + List trustChainCerts, + String encodedLeafCert) + throws CertificateEncodingException, IllegalArgumentException { + + // Get the first certificate from the user-provided trust chain file. + X509Certificate firstTrustCert = trustChainCerts.get(0); + String encodedFirstTrustCert = encodeCert(firstTrustCert); + + // If the first certificate in the user-provided trust chain file is *not* the leaf + // certificate (which has already been added as the first element to `certChainToPopulate`), + // then add this certificate. This handles cases where the user's trust chain file + // starts with an intermediate certificate. If the first certificate in the trust chain file + // *is* the leaf certificate, this means the user has explicitly included the leaf in their + // trust chain file. In this case, we skip adding it again to prevent duplication, as the + // leaf is already at the beginning of `certChainToPopulate`. + if (!encodedFirstTrustCert.equals(encodedLeafCert)) { + certChainToPopulate.add(encodedFirstTrustCert); + } + + // Iterate over the remaining certificates in the trust chain. + for (int i = 1; i < trustChainCerts.size(); i++) { + X509Certificate currentCert = trustChainCerts.get(i); + String encodedCurrentCert = encodeCert(currentCert); + + // Throw an error if the current certificate (from the user-provided trust chain file, + // at an index beyond the first) is the same as the leaf certificate. + // This enforces that if the leaf certificate is included in the trust chain file by the + // user, it must be the very first certificate in that file. It should not appear + // elsewhere in the chain. + if (encodedCurrentCert.equals(encodedLeafCert)) { + throw new IllegalArgumentException( + "The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely."); + } + + // Add the current certificate to the chain. + certChainToPopulate.add(encodedCurrentCert); + } + } + + /** + * Reads a file containing PEM-encoded X509 certificates and returns a list of parsed + * certificates. It splits the file content based on PEM headers and parses each certificate. + * Returns an empty list if the trust chain path is empty. + * + * @param trustChainPath The path to the trust chain file. + * @return A list of parsed X509 certificates. + * @throws IOException If an error occurs while reading the file. + * @throws CertificateException If an error occurs while parsing a certificate. + */ + @VisibleForTesting + static List readTrustChain(String trustChainPath) + throws IOException, CertificateException { + List certificateTrustChain = new ArrayList<>(); + + // If no trust chain path is provided, return an empty list. + if (Strings.isNullOrEmpty(trustChainPath)) { + return certificateTrustChain; } + + // initialize certificate factory to retrieve x509 certificates. + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + // Read the trust chain file. + byte[] trustChainData; + trustChainData = Files.readAllBytes(Paths.get(trustChainPath)); + + // Split the file content into PEM certificate blocks. + String content = new String(trustChainData, StandardCharsets.UTF_8); + + Matcher matcher = PEM_CERT_PATTERN.matcher(content); + + while (matcher.find()) { + String pemCertBlock = matcher.group(0); + try (InputStream certStream = + new ByteArrayInputStream(pemCertBlock.getBytes(StandardCharsets.UTF_8))) { + // Parse the certificate data. + Certificate cert = cf.generateCertificate(certStream); + + // Append the certificate to the trust chain. + if (cert instanceof X509Certificate) { + certificateTrustChain.add((X509Certificate) cert); + } else { + throw new CertificateException( + "Found non-X.509 certificate in trust chain file: " + trustChainPath); + } + } catch (CertificateException e) { + // If parsing an individual PEM block fails, re-throw with more context. + throw new CertificateException( + "Error loading PEM certificates from the trust chain file: " + + trustChainPath + + " - " + + e.getMessage(), + e); + } + } + + if (trustChainData.length > 0 && certificateTrustChain.isEmpty()) { + throw new CertificateException( + "Trust chain file was not empty but no PEM certificates were found: " + trustChainPath); + } + + return certificateTrustChain; } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java b/oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java index 18856d23b..bfc7c59cb 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java @@ -31,7 +31,10 @@ package com.google.auth.oauth2; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import com.google.auth.oauth2.IdentityPoolCredentialSource.CertificateConfig; @@ -42,11 +45,14 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.nio.file.Paths; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Base64; +import java.util.List; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -69,9 +75,16 @@ public class CertificateIdentityPoolSubjectTokenSupplierTest { private CertificateIdentityPoolSubjectTokenSupplier supplier; private static final byte[] INVALID_CERT_BYTES = - "invalid certificate data".getBytes(StandardCharsets.UTF_8); + ("-----BEGIN CERTIFICATE-----\n" + // This content is intentionally not valid Base64 or a valid DER encoding. + // The presence of spaces and non-Base64 characters will cause a parsing error. + + "This is not valid base64 encoded certificate data.\n" + + "It will cause a CertificateException during parsing.\n" + + "-----END CERTIFICATE-----\n") + .getBytes(StandardCharsets.UTF_8); private byte[] testCertBytesFromFile; + private byte[] intermediateCertBytesFromFile; @Before public void setUp() throws IOException, URISyntaxException { @@ -80,6 +93,9 @@ public void setUp() throws IOException, URISyntaxException { assertNotNull("Test leaf certificate file not found!", leafCertUrl); File testCertFile = new File(leafCertUrl.getFile()); + URL intermediateCertUrl = classLoader.getResource("x509_intermediate_certificate.pem"); + assertNotNull("Test intermediate certificate file not found!", intermediateCertUrl); + when(mockCertificateConfig.useDefaultCertificateConfig()).thenReturn(false); when(mockCertificateConfig.getCertificateConfigLocation()) .thenReturn(testCertFile.getAbsolutePath()); @@ -89,6 +105,7 @@ public void setUp() throws IOException, URISyntaxException { supplier = new CertificateIdentityPoolSubjectTokenSupplier(mockCredentialSource); testCertBytesFromFile = Files.readAllBytes(Paths.get(leafCertUrl.toURI())); + intermediateCertBytesFromFile = Files.readAllBytes(Paths.get(intermediateCertUrl.toURI())); } @Test @@ -128,7 +145,7 @@ public void parseCertificate_invalidData_throwsCertificateException() { } @Test - public void getSubjectToken_success() throws Exception { + public void getSubjectToken_withoutTrustChain_success() throws Exception { // Calculate expected result based on the file content. CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate expectedCert = @@ -143,4 +160,280 @@ public void getSubjectToken_success() throws Exception { // Verify assertEquals(expectedSubjectToken, actualSubjectToken); } + + @Test + public void getSubjectToken_trustChainWithLeafFirst_success() throws Exception { + // Configure mock to return the path to the trust chain file with leaf. + ClassLoader classLoader = getClass().getClassLoader(); + URL trustChainUrl = classLoader.getResource("trust_chain_with_leaf.pem"); + assertNotNull("Test trust chain file not found!", trustChainUrl); + when(mockCertificateConfig.getTrustChainPath()) + .thenReturn(new File(trustChainUrl.getFile()).getAbsolutePath()); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + // Parse and encode the leaf certificate. + X509Certificate expectedLeafCert = + (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(testCertBytesFromFile)); + String expectedEncodedLeaf = Base64.getEncoder().encodeToString(expectedLeafCert.getEncoded()); + + // Parse and encode the intermediate certificate. + X509Certificate expectedIntermediateCert = + (X509Certificate) + cf.generateCertificate(new ByteArrayInputStream(intermediateCertBytesFromFile)); + String expectedEncodedIntermediate = + Base64.getEncoder().encodeToString(expectedIntermediateCert.getEncoded()); + + // Expected: JSON array with encoded leaf and then encoded intermediate. + String[] expectedCertChain = new String[] {expectedEncodedLeaf, expectedEncodedIntermediate}; + String expectedSubjectToken = OAuth2Utils.JSON_FACTORY.toString(expectedCertChain); + + // Execute + String actualSubjectToken = supplier.getSubjectToken(mockContext); + + // Verify + assertEquals(expectedSubjectToken, actualSubjectToken); + } + + @Test + public void getSubjectToken_trustChainWithoutLeaf_success() throws Exception { + // Configure mock to return the path to the trust chain file WITHOUT leaf. + ClassLoader classLoader = getClass().getClassLoader(); + URL trustChainUrl = classLoader.getResource("trust_chain_without_leaf.pem"); + assertNotNull("Test trust chain file (without leaf) not found!", trustChainUrl); + when(mockCertificateConfig.getTrustChainPath()) + .thenReturn(new File(trustChainUrl.getFile()).getAbsolutePath()); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + // Parse and encode the leaf certificate. + X509Certificate expectedLeafCert = + (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(testCertBytesFromFile)); + String expectedEncodedLeaf = Base64.getEncoder().encodeToString(expectedLeafCert.getEncoded()); + + // Parse and encode the intermediate certificate. + X509Certificate expectedIntermediateCert = + (X509Certificate) + cf.generateCertificate(new ByteArrayInputStream(intermediateCertBytesFromFile)); + String expectedEncodedIntermediate = + Base64.getEncoder().encodeToString(expectedIntermediateCert.getEncoded()); + + // Expected: JSON array with encoded leaf and then encoded intermediate. + String[] expectedCertChainArray = + new String[] {expectedEncodedLeaf, expectedEncodedIntermediate}; + String expectedSubjectToken = OAuth2Utils.JSON_FACTORY.toString(expectedCertChainArray); + + // Execute + String actualSubjectToken = supplier.getSubjectToken(mockContext); + + // Verify + assertEquals(expectedSubjectToken, actualSubjectToken); + } + + // Tests that an IllegalArgumentException (wrapped in IOException) is thrown + // when the trust chain file is provided and contains the leaf certificate, + // but the leaf certificate is not the *first* certificate in that file. + // For example, an intermediate certificate appears before the leaf certificate. + @Test + public void getSubjectToken_trustChainWrongOrder_throwsIllegalArgumentException() { + ClassLoader classLoader = getClass().getClassLoader(); + URL trustChainUrl = classLoader.getResource("trust_chain_wrong_order.pem"); + assertNotNull("Test trust chain file (wrong order) not found!", trustChainUrl); + String trustChainPath = new File(trustChainUrl.getFile()).getAbsolutePath(); + when(mockCertificateConfig.getTrustChainPath()).thenReturn(trustChainPath); + + String expectedRootErrorMessage = + "The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely."; + String expectedOuterExceptionMessage = + "Trust chain misconfiguration: " + expectedRootErrorMessage; + + // Execute & Verify + IOException exception = + assertThrows(IOException.class, () -> supplier.getSubjectToken(mockContext)); + + assertEquals(expectedOuterExceptionMessage, exception.getMessage()); + + Throwable cause = exception.getCause(); + assertNotNull("Exception cause should not be null", cause); + assertTrue( + "Exception cause should be an IllegalArgumentException", + cause instanceof IllegalArgumentException); + assertEquals(expectedRootErrorMessage, cause.getMessage()); + } + + @Test + public void getSubjectToken_trustChainOnlyLeaf_success() throws Exception { + // Configure mock to use the leaf certificate file itself as the trust chain file, + // simulating a scenario where the trust chain file contains only the leaf. + ClassLoader classLoader = getClass().getClassLoader(); + URL trustChainUrl = classLoader.getResource("x509_leaf_certificate.pem"); + assertNotNull( + "Test resource 'x509_leaf_certificate.pem' (used as trust chain) not found!", + trustChainUrl); + when(mockCertificateConfig.getTrustChainPath()) + .thenReturn(new File(trustChainUrl.getFile()).getAbsolutePath()); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + // Parse and encode the leaf certificate + X509Certificate expectedLeafCert = + (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(testCertBytesFromFile)); + String expectedEncodedLeaf = Base64.getEncoder().encodeToString(expectedLeafCert.getEncoded()); + + // Expected: JSON array with only the encoded leaf. + String[] expectedCertChainArray = new String[] {expectedEncodedLeaf}; + String expectedSubjectToken = OAuth2Utils.JSON_FACTORY.toString(expectedCertChainArray); + + // Execute + String actualSubjectToken = supplier.getSubjectToken(mockContext); + + // Verify + assertEquals(expectedSubjectToken, actualSubjectToken); + } + + @Test + public void getSubjectToken_trustChainFileNotFound_throwsIOException() { + // Configure mock to return a non-existent path for the trust chain. + String nonExistentPath = "/path/to/non/existent/trust_chain.pem"; + when(mockCertificateConfig.getTrustChainPath()).thenReturn(nonExistentPath); + + // Execute & Verify + IOException exception = + assertThrows(IOException.class, () -> supplier.getSubjectToken(mockContext)); + + // Check that the cause is NoSuchFileException from readTrustChain. + assertTrue(exception.getCause() instanceof NoSuchFileException); + + // Check the outer exception message added in getSubjectToken. + assertEquals("Trust chain file not found: " + nonExistentPath, exception.getMessage()); + } + + @Test + public void getSubjectToken_trustChainInvalidFormat_throwsIOException() throws Exception { + // Create a temporary file with invalid cert data for the trust chain. + File invalidTrustChainFile = File.createTempFile("invalid_trust_chain", ".pem"); + invalidTrustChainFile.deleteOnExit(); + Files.write(invalidTrustChainFile.toPath(), INVALID_CERT_BYTES); + + // Configure mock to return the path to the temporary invalid trust chain file. + String invalidPath = invalidTrustChainFile.getAbsolutePath(); + when(mockCertificateConfig.getTrustChainPath()).thenReturn(invalidPath); + + // Execute & Verify + IOException exception = + assertThrows(IOException.class, () -> supplier.getSubjectToken(mockContext)); + + // The final exception is an IOException thrown by getSubjectToken. + assertEquals( + "Failed to parse certificate(s) from trust chain file: " + invalidPath, + exception.getMessage()); + + // Check that the cause is CertificateException from readTrustChain + assertTrue(exception.getCause() instanceof CertificateException); + + // Verify the cause's message specifically points to the trust chain parsing failure + // and includes the path of the invalid trust chain file. + assertTrue( + exception + .getCause() + .getMessage() + .startsWith( + "Error loading PEM certificates from the trust chain file: " + invalidPath)); + } + + @Test + public void getSubjectToken_leafCertFileNotFound_throwsIOException() { + // Configure mock to return a non-existent path for the leaf certificate. + String nonExistentPath = "/path/to/non/existent/leaf.pem"; + when(mockCredentialSource.getCredentialLocation()).thenReturn(nonExistentPath); + // Re-initialize supplier with the bad leaf path + supplier = new CertificateIdentityPoolSubjectTokenSupplier(mockCredentialSource); + + // Execute & Verify: Expect the wrapper IOException. + IOException exception = + assertThrows(IOException.class, () -> supplier.getSubjectToken(mockContext)); + + // Check the message of the wrapper IOException. + assertEquals("Leaf certificate file not found: " + nonExistentPath, exception.getMessage()); + + // Check that the cause is the original NoSuchFileException. + assertNotNull("Exception should have a cause", exception.getCause()); + assertTrue( + "Cause should be NoSuchFileException", exception.getCause() instanceof NoSuchFileException); + + // Check the message of the cause (which is the path) in a platform-agnostic way. + Path expectedCausePath = Paths.get(nonExistentPath); + Path actualCausePath = Paths.get(exception.getCause().getMessage()); + assertEquals(expectedCausePath, actualCausePath); + } + + @Test + public void getSubjectToken_leafCertInvalidFormat_throwsIOException() throws Exception { + // Create a temporary file with invalid cert data. + File invalidLeafFile = File.createTempFile("invalid_leaf", ".pem"); + invalidLeafFile.deleteOnExit(); + Files.write(invalidLeafFile.toPath(), INVALID_CERT_BYTES); + + // Configure mock to return the path to the invalid leaf certificate file + when(mockCredentialSource.getCredentialLocation()) + .thenReturn(invalidLeafFile.getAbsolutePath()); + // Re-initialize supplier with the bad leaf path + supplier = new CertificateIdentityPoolSubjectTokenSupplier(mockCredentialSource); + + // Execute & Verify + IOException exception = + assertThrows(IOException.class, () -> supplier.getSubjectToken(mockContext)); + + // Check that the cause is CertificateException from parseCertificate (via + // loadLeafCertificate) + assertTrue(exception.getCause() instanceof CertificateException); + assertEquals("Failed to parse X.509 certificate data.", exception.getCause().getMessage()); + + // Check the outer exception message + assertEquals( + "Failed to parse leaf certificate from file: " + invalidLeafFile.getAbsolutePath(), + exception.getMessage()); + } + + @Test + public void readTrustChain_whenFileIsNotEmptyButContainsNoPemBlocks_throwsCertificateException() + throws IOException { + // Create a temporary file with content that does not match PEM certificate blocks. + File trustChainFile = File.createTempFile("non_pem_content_trust_chain", ".txt"); + trustChainFile.deleteOnExit(); + String fileContent = "This is a test file with some data, but no actual PEM certificates."; + Files.write(trustChainFile.toPath(), fileContent.getBytes(StandardCharsets.UTF_8)); + String trustChainPath = trustChainFile.getAbsolutePath(); + + // Execute & Verify + CertificateException exception = + assertThrows( + CertificateException.class, + () -> CertificateIdentityPoolSubjectTokenSupplier.readTrustChain(trustChainPath)); + + // Verify the exception message. + String expectedMessage = + "Trust chain file was not empty but no PEM certificates were found: " + trustChainPath; + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + public void readTrustChain_nullPath_returnsEmptyList() throws Exception { + List certs = CertificateIdentityPoolSubjectTokenSupplier.readTrustChain(null); + assertNotNull(certs); + assertTrue(certs.isEmpty()); + } + + @Test + public void readTrustChain_emptyFile_returnsEmptyList() throws IOException, CertificateException { + // Create an empty temporary file. + File emptyFile = File.createTempFile("empty_trust_chain", ".pem"); + emptyFile.deleteOnExit(); + + String emptyFilePath = emptyFile.getAbsolutePath(); + List certs = + CertificateIdentityPoolSubjectTokenSupplier.readTrustChain(emptyFilePath); + assertNotNull(certs); + assertTrue(certs.isEmpty()); + } } diff --git a/oauth2_http/testresources/trust_chain_with_leaf.pem b/oauth2_http/testresources/trust_chain_with_leaf.pem new file mode 100644 index 000000000..250387d9d --- /dev/null +++ b/oauth2_http/testresources/trust_chain_with_leaf.pem @@ -0,0 +1,52 @@ +-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx +lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H +EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL +XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU +RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC +oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ +IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW +xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO +ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q +F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3 +uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F +mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw +bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX +riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS +6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh +CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl +sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR +pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N +vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv +/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi +pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7 +6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI +nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/ +lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/oauth2_http/testresources/trust_chain_without_leaf.pem b/oauth2_http/testresources/trust_chain_without_leaf.pem new file mode 100644 index 000000000..9da0f37fe --- /dev/null +++ b/oauth2_http/testresources/trust_chain_without_leaf.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx +lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H +EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL +XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU +RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC +oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ +IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW +xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO +ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q +F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3 +uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F +mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw +bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX +riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS +6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh +CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl +sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR +pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N +vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv +/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi +pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7 +6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI +nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/ +lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/oauth2_http/testresources/trust_chain_wrong_order.pem b/oauth2_http/testresources/trust_chain_wrong_order.pem new file mode 100644 index 000000000..e8dc5d359 --- /dev/null +++ b/oauth2_http/testresources/trust_chain_wrong_order.pem @@ -0,0 +1,52 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx +lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H +EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL +XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU +RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC +oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ +IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW +xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO +ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q +F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3 +uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F +mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw +bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX +riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS +6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh +CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl +sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR +pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N +vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv +/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi +pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7 +6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI +nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/ +lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV +BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV +MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM +7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer +uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp +gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 ++WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 +ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O +gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh +GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr +odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk ++JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 +ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql +ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT +cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB +-----END CERTIFICATE----- \ No newline at end of file diff --git a/oauth2_http/testresources/x509_intermediate_certificate.pem b/oauth2_http/testresources/x509_intermediate_certificate.pem new file mode 100644 index 000000000..6895d1e7b --- /dev/null +++ b/oauth2_http/testresources/x509_intermediate_certificate.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFtTCCA52gAwIBAgIJAPBsLZmNGfKtMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYwOTIxMDI0NTEyWhcNMTYxMDIxMDI0NTEyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAsiMC7mTsmUXwZoYlT4aHY1FLw8bxIXC+z3IqA+TY1WqfbeiZRo8MA5Zx +lTTxYMKPCZUE1XBc7jvD8GJhWIj6pToPYHn73B01IBkLBxq4kF1yV2Z7DVmkvc6H +EcxXXq8zkCx0j6XOfiI4+qkXnuQn8cvrk8xfhtnMMZM7iVm6VSN93iRP/8ey6xuL +XTHrDX7ukoRce1hpT8O+15GXNrY0irhhYQz5xKibNCJF3EjV28WMry8y7I8uYUFU +RWDiQawwK9ec1zhZ94v92+GZDlPevmcFmSERKYQ0NsKcT0Y3lGuGnaExs8GyOpnC +oksu4YJGXQjg7lkv4MxzsNbRqmCkUwxw1Mg6FP0tsCNsw9qTrkvWCRA9zp/aU+sZ +IBGh1t4UGCub8joeQFvHxvr/3F7mH/dyvCjA34u0Lo1VPx+jYUIi9i0odltMspDW +xOpjqdGARZYmlJP5Au9q5cQjPMcwS/EBIb8cwNl32mUE6WnFlep+38mNR/FghIjO +ViAkXuKQmcHe6xppZAoHFsO/t3l4Tjek5vNW7erI1rgrFku/fvkIW/G8V1yIm/+Q +F+CE4maQzCJfhftpkhM/sPC/FuLNBmNE8BHVX8y58xG4is/cQxL4Z9TsFIw0C5+3 +uTrFW9D0agysahMVzPGtCqhDQqJdIJrBQqlS6bztpzBA8zEI0skCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUz/8FmW6TfqXyNJZr7rhc+Tn5sKQwdQYDVR0jBG4wbIAUz/8F +mW6TfqXyNJZr7rhc+Tn5sKShSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDw +bC2ZjRnyrTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQCQmrcfhurX +riR3Q0Y+nq040/3dJIAJXjyI9CEtxaU0nzCNTng7PwgZ0CKmCelQfInuwWFwBSHS +6kBfC1rgJeFnjnTt8a3RCgRlIgUr9NCdPSEccB7TurobwPJ2h6cJjjR8urcb0CXh +CEMvPneyPj0xUFY8vVKXMGWahz/kyfwIiVqcX/OtMZ29fUu1onbWl71g2gVLtUZl +sECdZ+AC/6HDCVpYIVETMl1T7N/XyqXZQiDLDNRDeZhnapz8w9fsW1KVujAZLNQR +pVnw2qa2UK1dSf2FHX+lQU5mFSYM4vtwaMlX/LgfdLZ9I796hFh619WwTVz+LO2N +vHnwBMabld3XSPuZRqlbBulDQ07Vbqdjv8DYSLA2aKI4ZkMMKuFLG/oS28V2ZYmv +/KpGEs5UgKY+P9NulYpTDwCU/6SomuQpP795wbG6sm7Hzq82r2RmB61GupNRGeqi +pXKsy69T388zBxYu6zQrosXiDl5YzaViH7tm0J7opye8dCWjjpnahki0vq2znti7 +6cWla2j8Xz1glvLz+JI/NCOMfxUInb82T7ijo80N0VJ2hzf7p2GxRZXAxAV9knLI +nM4F5TLjSd7ZhOOZ7ni/eZFueTMisWfypt2nc41whGjHMX/Zp1kPfhB4H2bLKIX/ +lSrwNr3qbGTEJX8JqpDBNVAd96XkMvDNyA== +-----END CERTIFICATE-----