3434import static com .google .common .base .Preconditions .checkNotNull ;
3535
3636import com .google .common .annotations .VisibleForTesting ;
37+ import com .google .common .base .Strings ;
3738import java .io .ByteArrayInputStream ;
3839import java .io .IOException ;
3940import java .io .InputStream ;
41+ import java .nio .charset .StandardCharsets ;
4042import java .nio .file .Files ;
43+ import java .nio .file .NoSuchFileException ;
4144import java .nio .file .Paths ;
45+ import java .security .cert .Certificate ;
4246import java .security .cert .CertificateEncodingException ;
4347import java .security .cert .CertificateException ;
4448import java .security .cert .CertificateFactory ;
4549import java .security .cert .X509Certificate ;
50+ import java .util .ArrayList ;
4651import java .util .Base64 ;
52+ import java .util .List ;
53+ import java .util .regex .Matcher ;
54+ import java .util .regex .Pattern ;
4755
4856/**
4957 * Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an
@@ -56,6 +64,9 @@ public class CertificateIdentityPoolSubjectTokenSupplier
5664
5765 private final IdentityPoolCredentialSource credentialSource ;
5866
67+ private static final Pattern PEM_CERT_PATTERN =
68+ Pattern .compile ("-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----" , Pattern .DOTALL );
69+
5970 CertificateIdentityPoolSubjectTokenSupplier (IdentityPoolCredentialSource credentialSource ) {
6071 this .credentialSource = checkNotNull (credentialSource , "credentialSource cannot be null" );
6172 // This check ensures that the credential source was intended for certificate usage.
@@ -66,10 +77,21 @@ public class CertificateIdentityPoolSubjectTokenSupplier
6677 + " CertificateIdentityPoolSubjectTokenSupplier" );
6778 }
6879
69- private static X509Certificate loadLeafCertificate (String path )
70- throws IOException , CertificateException {
71- byte [] leafCertBytes = Files .readAllBytes (Paths .get (path ));
72- return parseCertificate (leafCertBytes );
80+ private static String loadAndEncodeLeafCertificate (String path ) throws IOException {
81+ try {
82+ byte [] leafCertBytes = Files .readAllBytes (Paths .get (path ));
83+ X509Certificate leafCert = parseCertificate (leafCertBytes );
84+ return encodeCert (leafCert );
85+ } catch (NoSuchFileException e ) {
86+ throw new IOException (String .format ("Leaf certificate file not found: %s" , path ), e );
87+ } catch (CertificateException e ) {
88+ throw new IOException (
89+ String .format ("Failed to parse leaf certificate from file: %s" , path ), e );
90+ } catch (IOException e ) {
91+ // This catches any other general I/O errors during leaf certificate file reading (e.g.,
92+ // permissions).
93+ throw new IOException (String .format ("Failed to read leaf certificate file: %s" , path ), e );
94+ }
7395 }
7496
7597 @ VisibleForTesting
@@ -97,33 +119,177 @@ private static String encodeCert(X509Certificate certificate)
97119
98120 /**
99121 * Retrieves the X509 subject token. This method loads the leaf certificate specified by the
100- * {@code credentialSource.credentialLocation}. The subject token is constructed as a JSON array
101- * containing the base64-encoded (DER format) leaf certificate. This JSON array serves as the
102- * subject token for mTLS authentication.
122+ * {@code credentialSource.credentialLocation}. If a trust chain path is configured in the {@code
123+ * credentialSource.certificateConfig}, it also loads and includes the trust chain certificates.
124+ * The subject token is constructed as a JSON array containing the base64-encoded (DER format)
125+ * leaf certificate, followed by the base64-encoded (DER format) certificates in the trust chain.
126+ * This JSON array serves as the subject token for mTLS authentication.
103127 *
104128 * @param context The external account supplier context. This parameter is currently not used in
105129 * this implementation.
106- * @return The JSON string representation of the base64-encoded leaf certificate in a JSON array.
107- * @throws IOException If an I/O error occurs while reading the certificate file.
130+ * @return The JSON string representation of the base64-encoded certificate chain (leaf
131+ * certificate followed by the trust chain, if present).
132+ * @throws IOException If an I/O error occurs while reading the certificate file(s).
108133 */
109134 @ Override
110135 public String getSubjectToken (ExternalAccountSupplierContext context ) throws IOException {
111- try {
112- // credentialSource.credentialLocation is expected to be non-null here,
113- // set during IdentityPoolCredentials construction for certificate type.
114- X509Certificate leafCert = loadLeafCertificate (credentialSource .getCredentialLocation ());
115- String encodedLeafCert = encodeCert (leafCert );
136+ String leafCertPath = credentialSource .getCredentialLocation ();
137+ String trustChainPath = null ;
138+ if (credentialSource .getCertificateConfig () != null ) {
139+ trustChainPath = credentialSource .getCertificateConfig ().getTrustChainPath ();
140+ }
141+
142+ // Load and encode the leaf certificate.
143+ String encodedLeafCert = loadAndEncodeLeafCertificate (leafCertPath );
116144
117- java .util .List <String > certChain = new java .util .ArrayList <>();
118- certChain .add (encodedLeafCert );
145+ // Initialize the certificate chain for the subject token. The Security Token Service (STS)
146+ // requires that the leaf certificate (the one used for authenticating this workload) must be
147+ // the first certificate in this chain.
148+ List <String > certChain = new ArrayList <>();
149+ certChain .add (encodedLeafCert );
150+
151+ // Handle trust chain loading and processing.
152+ try {
153+ // Read the trust chain.
154+ List <X509Certificate > trustChainCerts = readTrustChain (trustChainPath );
119155
120- return OAuth2Utils .JSON_FACTORY .toString (certChain );
156+ // Process the trust chain certificates read from the file.
157+ if (!trustChainCerts .isEmpty ()) {
158+ populateCertChainFromTrustChain (certChain , trustChainCerts , encodedLeafCert );
159+ }
160+ } catch (IllegalArgumentException e ) {
161+ // This catches the specific error for misconfigured trust chain (e.g., leaf in wrong place).
162+ throw new IOException ("Trust chain misconfiguration: " + e .getMessage (), e );
163+ } catch (NoSuchFileException e ) {
164+ throw new IOException (String .format ("Trust chain file not found: %s" , trustChainPath ), e );
121165 } catch (CertificateException e ) {
122- // Catch CertificateException to provide a more specific error message including
123- // the path of the file that failed to parse, and re-throw as IOException
124- // as expected by the getSubjectToken method signature for I/O related issues.
125166 throw new IOException (
126- "Failed to parse certificate(s) from: " + credentialSource .getCredentialLocation (), e );
167+ String .format ("Failed to parse certificate(s) from trust chain file: %s" , trustChainPath ),
168+ e );
169+ } catch (IOException e ) {
170+ // This catches any other general I/O errors during trust chain file reading (e.g.,
171+ // permissions).
172+ throw new IOException (
173+ String .format ("Failed to read trust chain file: %s" , trustChainPath ), e );
174+ }
175+
176+ return OAuth2Utils .JSON_FACTORY .toString (certChain );
177+ }
178+
179+ /**
180+ * Extends {@code certChainToPopulate} with encoded certificates from {@code trustChainCerts},
181+ * applying validation rules for the leaf certificate's presence and order within the trust chain.
182+ *
183+ * @param certChainToPopulate The list of encoded certificate strings to populate.
184+ * @param trustChainCerts The list of X509Certificates from the trust chain file (non-empty).
185+ * @param encodedLeafCert The Base64-encoded leaf certificate.
186+ * @throws CertificateEncodingException If an error occurs during certificate encoding.
187+ * @throws IllegalArgumentException If the leaf certificate is found in an invalid position in the
188+ * trust chain.
189+ */
190+ private void populateCertChainFromTrustChain (
191+ List <String > certChainToPopulate ,
192+ List <X509Certificate > trustChainCerts ,
193+ String encodedLeafCert )
194+ throws CertificateEncodingException , IllegalArgumentException {
195+
196+ // Get the first certificate from the user-provided trust chain file.
197+ X509Certificate firstTrustCert = trustChainCerts .get (0 );
198+ String encodedFirstTrustCert = encodeCert (firstTrustCert );
199+
200+ // If the first certificate in the user-provided trust chain file is *not* the leaf
201+ // certificate (which has already been added as the first element to `certChainToPopulate`),
202+ // then add this certificate. This handles cases where the user's trust chain file
203+ // starts with an intermediate certificate. If the first certificate in the trust chain file
204+ // *is* the leaf certificate, this means the user has explicitly included the leaf in their
205+ // trust chain file. In this case, we skip adding it again to prevent duplication, as the
206+ // leaf is already at the beginning of `certChainToPopulate`.
207+ if (!encodedFirstTrustCert .equals (encodedLeafCert )) {
208+ certChainToPopulate .add (encodedFirstTrustCert );
209+ }
210+
211+ // Iterate over the remaining certificates in the trust chain.
212+ for (int i = 1 ; i < trustChainCerts .size (); i ++) {
213+ X509Certificate currentCert = trustChainCerts .get (i );
214+ String encodedCurrentCert = encodeCert (currentCert );
215+
216+ // Throw an error if the current certificate (from the user-provided trust chain file,
217+ // at an index beyond the first) is the same as the leaf certificate.
218+ // This enforces that if the leaf certificate is included in the trust chain file by the
219+ // user, it must be the very first certificate in that file. It should not appear
220+ // elsewhere in the chain.
221+ if (encodedCurrentCert .equals (encodedLeafCert )) {
222+ throw new IllegalArgumentException (
223+ "The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely." );
224+ }
225+
226+ // Add the current certificate to the chain.
227+ certChainToPopulate .add (encodedCurrentCert );
228+ }
229+ }
230+
231+ /**
232+ * Reads a file containing PEM-encoded X509 certificates and returns a list of parsed
233+ * certificates. It splits the file content based on PEM headers and parses each certificate.
234+ * Returns an empty list if the trust chain path is empty.
235+ *
236+ * @param trustChainPath The path to the trust chain file.
237+ * @return A list of parsed X509 certificates.
238+ * @throws IOException If an error occurs while reading the file.
239+ * @throws CertificateException If an error occurs while parsing a certificate.
240+ */
241+ @ VisibleForTesting
242+ static List <X509Certificate > readTrustChain (String trustChainPath )
243+ throws IOException , CertificateException {
244+ List <X509Certificate > certificateTrustChain = new ArrayList <>();
245+
246+ // If no trust chain path is provided, return an empty list.
247+ if (Strings .isNullOrEmpty (trustChainPath )) {
248+ return certificateTrustChain ;
127249 }
250+
251+ // initialize certificate factory to retrieve x509 certificates.
252+ CertificateFactory cf = CertificateFactory .getInstance ("X.509" );
253+
254+ // Read the trust chain file.
255+ byte [] trustChainData ;
256+ trustChainData = Files .readAllBytes (Paths .get (trustChainPath ));
257+
258+ // Split the file content into PEM certificate blocks.
259+ String content = new String (trustChainData , StandardCharsets .UTF_8 );
260+
261+ Matcher matcher = PEM_CERT_PATTERN .matcher (content );
262+
263+ while (matcher .find ()) {
264+ String pemCertBlock = matcher .group (0 );
265+ try (InputStream certStream =
266+ new ByteArrayInputStream (pemCertBlock .getBytes (StandardCharsets .UTF_8 ))) {
267+ // Parse the certificate data.
268+ Certificate cert = cf .generateCertificate (certStream );
269+
270+ // Append the certificate to the trust chain.
271+ if (cert instanceof X509Certificate ) {
272+ certificateTrustChain .add ((X509Certificate ) cert );
273+ } else {
274+ throw new CertificateException (
275+ "Found non-X.509 certificate in trust chain file: " + trustChainPath );
276+ }
277+ } catch (CertificateException e ) {
278+ // If parsing an individual PEM block fails, re-throw with more context.
279+ throw new CertificateException (
280+ "Error loading PEM certificates from the trust chain file: "
281+ + trustChainPath
282+ + " - "
283+ + e .getMessage (),
284+ e );
285+ }
286+ }
287+
288+ if (trustChainData .length > 0 && certificateTrustChain .isEmpty ()) {
289+ throw new CertificateException (
290+ "Trust chain file was not empty but no PEM certificates were found: " + trustChainPath );
291+ }
292+
293+ return certificateTrustChain ;
128294 }
129295}
0 commit comments