Skip to content

Commit 3a028c1

Browse files
committed
Added comments for clarity, and moved the exception handling of leaf certificate to the private helper method.
1 parent 33a3a01 commit 3a028c1

File tree

2 files changed

+80
-56
lines changed

2 files changed

+80
-56
lines changed

oauth2_http/java/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplier.java

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ public class CertificateIdentityPoolSubjectTokenSupplier
6262

6363
private final IdentityPoolCredentialSource credentialSource;
6464

65+
private static final Pattern PEM_CERT_PATTERN =
66+
Pattern.compile("-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", Pattern.DOTALL);
67+
6568
CertificateIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) {
6669
this.credentialSource = checkNotNull(credentialSource, "credentialSource cannot be null");
6770
// This check ensures that the credential source was intended for certificate usage.
@@ -72,10 +75,22 @@ public class CertificateIdentityPoolSubjectTokenSupplier
7275
+ " CertificateIdentityPoolSubjectTokenSupplier");
7376
}
7477

75-
private static X509Certificate loadLeafCertificate(String path)
76-
throws IOException, CertificateException {
77-
byte[] leafCertBytes = Files.readAllBytes(Paths.get(path));
78-
return parseCertificate(leafCertBytes);
78+
private static String loadAndEncodeLeafCertificate(String path) throws IOException {
79+
try {
80+
byte[] leafCertBytes = Files.readAllBytes(Paths.get(path));
81+
X509Certificate leafCert = parseCertificate(leafCertBytes);
82+
return encodeCert(leafCert);
83+
} catch (NoSuchFileException e) {
84+
throw new IOException(String.format("Leaf certificate file not found: %s", path), e);
85+
} catch (CertificateException e) {
86+
// This catches parsing errors if the leaf certificate file is invalid.
87+
throw new IOException(
88+
String.format("Failed to parse leaf certificate from file: %s", path), e);
89+
} catch (IOException e) {
90+
// This catches any other general I/O errors during leaf certificate file reading (e.g.,
91+
// permissions).
92+
throw new IOException(String.format("Failed to read leaf certificate file: %s", path), e);
93+
}
7994
}
8095

8196
@VisibleForTesting
@@ -123,25 +138,33 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
123138
trustChainPath = credentialSource.getCertificateConfig().getTrustChainPath();
124139
}
125140

126-
try {
127-
// Load the leaf certificate.
128-
X509Certificate leafCert = loadLeafCertificate(leafCertPath);
129-
String encodedLeafCert = encodeCert(leafCert);
141+
// Load and encode the leaf certificate.
142+
String encodedLeafCert = loadAndEncodeLeafCertificate(leafCertPath);
130143

131-
// Add the leaf certificate first.
132-
java.util.List<String> certChain = new java.util.ArrayList<>();
133-
certChain.add(encodedLeafCert);
144+
// Initialize the certificate chain for the subject token. The Security Token Service (STS)
145+
// requires that the leaf certificate (the one used for authenticating this workload) must be
146+
// the first certificate in this chain.
147+
java.util.List<String> certChain = new java.util.ArrayList<>();
148+
certChain.add(encodedLeafCert);
134149

150+
// Handle trust chain loading and processing.
151+
try {
135152
// Read the trust chain.
136153
List<X509Certificate> trustChainCerts = readTrustChain(trustChainPath);
137154

138155
// Process the trust chain certificates read from the file.
139156
if (!trustChainCerts.isEmpty()) {
140-
// Check the first certificate in the trust chain file.
157+
// Get the first certificate from the user-provided trust chain file.
141158
X509Certificate firstTrustCert = trustChainCerts.get(0);
142159
String encodedFirstTrustCert = encodeCert(firstTrustCert);
143160

144-
// Add the first certificate only if it is not the same as the leaf certificate.
161+
// If the first certificate in the user-provided trust chain file is *not* the leaf
162+
// certificate (which has already been added as the first element to `certChain`), then add
163+
// this certificate to `certChain`. This handles cases where the user's trust chain file
164+
// starts with an intermediate certificate. If the first certificate in the trust chain file
165+
// *is* the leaf certificate, this means the user has explicitly included the leaf in their
166+
// trust chain file. In this case, we skip adding it again to prevent duplication, as the
167+
// leaf is already at the beginning of `certChain`.
145168
if (!encodedFirstTrustCert.equals(encodedLeafCert)) {
146169
certChain.add(encodedFirstTrustCert);
147170
}
@@ -151,7 +174,11 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
151174
X509Certificate currentCert = trustChainCerts.get(i);
152175
String encodedCurrentCert = encodeCert(currentCert);
153176

154-
// Throw an error if the current certificate is the same as the leaf certificate.
177+
// Throw an error if the current certificate (from the user-provided trust chain file,
178+
// at an index beyond the first) is the same as the leaf certificate.
179+
// This enforces that if the leaf certificate is included in the trust chain file by the
180+
// user, it must be the very first certificate in that file. It should not appear
181+
// elsewhere in the chain.
155182
if (encodedCurrentCert.equals(encodedLeafCert)) {
156183
throw new IllegalArgumentException(
157184
"The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely.");
@@ -161,25 +188,24 @@ public String getSubjectToken(ExternalAccountSupplierContext context) throws IOE
161188
certChain.add(encodedCurrentCert);
162189
}
163190
}
164-
165-
return OAuth2Utils.JSON_FACTORY.toString(certChain);
166-
}
167-
// The following catch blocks handle specific exceptions that can occur during
168-
// certificate loading and parsing. These exceptions are wrapped in a new IOException,
169-
// as declared by this method's signature.
170-
catch (NoSuchFileException e) {
171-
// Handles the case where the leaf certificate file itself cannot be found.
172-
throw new IOException(String.format("Leaf certificate file not found: %s", leafCertPath), e);
191+
} catch (IllegalArgumentException e) {
192+
// This catches the specific error for misconfigured trust chain (e.g., leaf in wrong place).
193+
throw new IOException("Trust chain misconfiguration: " + e.getMessage(), e);
194+
} catch (NoSuchFileException e) {
195+
throw new IOException(String.format("Trust chain file not found: %s", trustChainPath), e);
173196
} catch (CertificateException e) {
174-
// Handles errors during the parsing of certificate data, which could stem from
175-
// issues in either the leaf certificate or the trust chain. The message includes
176-
// paths to both for comprehensive error reporting.
197+
// This catches parsing errors if certificates in the trust chain file are invalid.
177198
throw new IOException(
178-
"Failed to read certificate file(s). Leaf path: "
179-
+ leafCertPath
180-
+ (trustChainPath != null ? "\nTrust chain path: " + trustChainPath : ""),
199+
String.format("Failed to parse certificate(s) from trust chain file: %s", trustChainPath),
181200
e);
201+
} catch (IOException e) {
202+
// This catches any other general I/O errors during trust chain file reading (e.g.,
203+
// permissions).
204+
throw new IOException(
205+
String.format("Failed to read trust chain file: %s", trustChainPath), e);
182206
}
207+
208+
return OAuth2Utils.JSON_FACTORY.toString(certChain);
183209
}
184210

185211
/**
@@ -207,19 +233,12 @@ static List<X509Certificate> readTrustChain(String trustChainPath)
207233

208234
// Read the trust chain file.
209235
byte[] trustChainData;
210-
try {
211-
trustChainData = Files.readAllBytes(Paths.get(trustChainPath));
212-
} catch (NoSuchFileException e) {
213-
throw new IOException("Trust chain file not found: " + trustChainPath, e);
214-
} catch (IOException e) {
215-
throw new IOException("Failed to read trust chain file: " + trustChainPath, e);
216-
}
236+
trustChainData = Files.readAllBytes(Paths.get(trustChainPath));
217237

218238
// Split the file content into PEM certificate blocks.
219239
String content = new String(trustChainData);
220-
Pattern pemCertPattern =
221-
Pattern.compile("-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", Pattern.DOTALL);
222-
Matcher matcher = pemCertPattern.matcher(content);
240+
241+
Matcher matcher = PEM_CERT_PATTERN.matcher(content);
223242

224243
while (matcher.find()) {
225244
String pemCertBlock = matcher.group(0);

oauth2_http/javatests/com/google/auth/oauth2/CertificateIdentityPoolSubjectTokenSupplierTest.java

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -235,12 +235,23 @@ public void getSubjectToken_trustChainWrongOrder_throwsIllegalArgumentException(
235235
String trustChainPath = new File(trustChainUrl.getFile()).getAbsolutePath();
236236
when(mockCertificateConfig.getTrustChainPath()).thenReturn(trustChainPath);
237237

238+
String expectedRootErrorMessage =
239+
"The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely.";
240+
String expectedOuterExceptionMessage =
241+
"Trust chain misconfiguration: " + expectedRootErrorMessage;
242+
238243
// Execute & Verify
239-
Exception exception =
240-
assertThrows(IllegalArgumentException.class, () -> supplier.getSubjectToken(mockContext));
241-
assertEquals(
242-
"The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely.",
243-
exception.getMessage());
244+
IOException exception =
245+
assertThrows(IOException.class, () -> supplier.getSubjectToken(mockContext));
246+
247+
assertEquals(expectedOuterExceptionMessage, exception.getMessage());
248+
249+
Throwable cause = exception.getCause();
250+
assertNotNull("Exception cause should not be null", cause);
251+
assertTrue(
252+
"Exception cause should be an IllegalArgumentException",
253+
cause instanceof IllegalArgumentException);
254+
assertEquals(expectedRootErrorMessage, cause.getMessage());
244255
}
245256

246257
@Test
@@ -285,7 +296,7 @@ public void getSubjectToken_trustChainFileNotFound_throwsIOException() {
285296
assertTrue(exception.getCause() instanceof NoSuchFileException);
286297

287298
// Check the outer exception message added in getSubjectToken.
288-
assertEquals(exception.getMessage(), "Trust chain file not found: " + nonExistentPath);
299+
assertEquals("Trust chain file not found: " + nonExistentPath, exception.getMessage());
289300
}
290301

291302
@Test
@@ -303,16 +314,10 @@ public void getSubjectToken_trustChainInvalidFormat_throwsIOException() throws E
303314
IOException exception =
304315
assertThrows(IOException.class, () -> supplier.getSubjectToken(mockContext));
305316

306-
// Construct the expected outer exception message
307-
String expectedLeafPath = mockCredentialSource.getCredentialLocation();
308-
String expectedMessage =
309-
"Failed to read certificate file(s). Leaf path: "
310-
+ expectedLeafPath
311-
+ "\nTrust chain path: "
312-
+ invalidPath;
313-
314317
// The final exception is an IOException thrown by getSubjectToken.
315-
assertEquals(expectedMessage, exception.getMessage());
318+
assertEquals(
319+
"Failed to parse certificate(s) from trust chain file: " + invalidPath,
320+
exception.getMessage());
316321

317322
// Check that the cause is CertificateException from readTrustChain
318323
assertTrue(exception.getCause() instanceof CertificateException);
@@ -377,8 +382,8 @@ public void getSubjectToken_leafCertInvalidFormat_throwsIOException() throws Exc
377382

378383
// Check the outer exception message
379384
assertEquals(
380-
exception.getMessage(),
381-
"Failed to read certificate file(s). Leaf path: " + invalidLeafFile.getAbsolutePath());
385+
"Failed to parse leaf certificate from file: " + invalidLeafFile.getAbsolutePath(),
386+
exception.getMessage());
382387
}
383388

384389
@Test

0 commit comments

Comments
 (0)