Skip to content

Commit acb026f

Browse files
committed
feat: Return X.509 certificate chain as subject token.
The CertificateIdentityPoolSubjectTokenSupplier's subjectToken function now returns the full X.509 certificate chain, including the leaf certificate and any provided trust chain certificates, as a JSON array of base64-encoded strings. This chain is used as the subject token for mTLS authentication.
1 parent b347603 commit acb026f

File tree

6 files changed

+585
-14
lines changed

6 files changed

+585
-14
lines changed

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

Lines changed: 138 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,18 @@
3838
import java.io.IOException;
3939
import java.io.InputStream;
4040
import java.nio.file.Files;
41+
import java.nio.file.NoSuchFileException;
4142
import java.nio.file.Paths;
43+
import java.security.cert.Certificate;
4244
import java.security.cert.CertificateEncodingException;
4345
import java.security.cert.CertificateException;
4446
import java.security.cert.CertificateFactory;
4547
import java.security.cert.X509Certificate;
48+
import java.util.ArrayList;
4649
import java.util.Base64;
50+
import java.util.List;
51+
import java.util.regex.Matcher;
52+
import java.util.regex.Pattern;
4753

4854
/**
4955
* Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an
@@ -97,33 +103,153 @@ private static String encodeCert(X509Certificate certificate)
97103

98104
/**
99105
* 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.
106+
* {@code credentialSource.credentialLocation}. If a trust chain path is configured in the {@code
107+
* credentialSource.certificateConfig}, it also loads and includes the trust chain certificates.
108+
* The subject token is constructed as a JSON array containing the base64-encoded (DER format)
109+
* leaf certificate, followed by the base64-encoded (DER format) certificates in the trust chain.
110+
* This JSON array serves as the subject token for mTLS authentication.
103111
*
104112
* @param context The external account supplier context. This parameter is currently not used in
105113
* 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.
114+
* @return The JSON string representation of the base64-encoded certificate chain (leaf
115+
* certificate followed by the trust chain, if present).
116+
* @throws IOException If an I/O error occurs while reading the certificate file(s).
108117
*/
109118
@Override
110119
public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
120+
String leafCertPath = credentialSource.getCredentialLocation();
121+
String trustChainPath = null;
122+
if (credentialSource.getCertificateConfig() != null) {
123+
trustChainPath = credentialSource.getCertificateConfig().getTrustChainPath();
124+
}
125+
111126
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());
127+
// Load the leaf certificate.
128+
X509Certificate leafCert = loadLeafCertificate(leafCertPath);
115129
String encodedLeafCert = encodeCert(leafCert);
116130

131+
// Add the leaf certificate first.
117132
java.util.List<String> certChain = new java.util.ArrayList<>();
118133
certChain.add(encodedLeafCert);
119134

135+
// Read the trust chain.
136+
List<X509Certificate> trustChainCerts = readTrustChain(trustChainPath);
137+
138+
// Process the trust chain certificates read from the file.
139+
if (!trustChainCerts.isEmpty()) {
140+
// Check the first certificate in the trust chain file.
141+
X509Certificate firstTrustCert = trustChainCerts.get(0);
142+
String encodedFirstTrustCert = encodeCert(firstTrustCert);
143+
144+
// Add the first certificate only if it is not the same as the leaf certificate.
145+
if (!encodedFirstTrustCert.equals(encodedLeafCert)) {
146+
certChain.add(encodedFirstTrustCert);
147+
}
148+
149+
// Iterate over the remaining certificates in the trust chain.
150+
for (int i = 1; i < trustChainCerts.size(); i++) {
151+
X509Certificate currentCert = trustChainCerts.get(i);
152+
String encodedCurrentCert = encodeCert(currentCert);
153+
154+
// Throw an error if the current certificate is the same as the leaf certificate.
155+
if (encodedCurrentCert.equals(encodedLeafCert)) {
156+
throw new IllegalArgumentException(
157+
"The leaf certificate should only appear at the beginning of the trust chain file, or be omitted entirely.");
158+
}
159+
160+
// Add the current certificate to the chain.
161+
certChain.add(encodedCurrentCert);
162+
}
163+
}
164+
120165
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);
121173
} 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.
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.
125177
throw new IOException(
126-
"Failed to parse certificate(s) from: " + credentialSource.getCredentialLocation(), e);
178+
"Failed to read certificate file(s). Leaf path: "
179+
+ leafCertPath
180+
+ (trustChainPath != null ? "\nTrust chain path: " + trustChainPath : ""),
181+
e);
127182
}
128183
}
184+
185+
/**
186+
* Reads a file containing PEM-encoded X509 certificates and returns a list of parsed
187+
* certificates. It splits the file content based on PEM headers and parses each certificate.
188+
* Returns an empty list if the trust chain path is empty.
189+
*
190+
* @param trustChainPath The path to the trust chain file.
191+
* @return A list of parsed X509 certificates.
192+
* @throws IOException If an error occurs while reading the file.
193+
* @throws CertificateException If an error occurs while parsing a certificate.
194+
*/
195+
@VisibleForTesting
196+
static List<X509Certificate> readTrustChain(String trustChainPath)
197+
throws IOException, CertificateException {
198+
List<X509Certificate> certificateTrustChain = new ArrayList<>();
199+
200+
// If no trust chain path is provided, return an empty list.
201+
if (trustChainPath == null || trustChainPath.isEmpty()) {
202+
return certificateTrustChain;
203+
}
204+
205+
// initialize certificate factory to retrieve x509 certificates.
206+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
207+
208+
// Read the trust chain file.
209+
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+
}
217+
218+
// Split the file content into PEM certificate blocks.
219+
String content = new String(trustChainData);
220+
Pattern pemCertPattern =
221+
Pattern.compile("-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", Pattern.DOTALL);
222+
Matcher matcher = pemCertPattern.matcher(content);
223+
224+
while (matcher.find()) {
225+
String pemCertBlock = matcher.group(0);
226+
try (InputStream certStream = new ByteArrayInputStream(pemCertBlock.getBytes())) {
227+
// Parse the certificate data.
228+
Certificate cert = cf.generateCertificate(certStream);
229+
230+
// Append the certificate to the trust chain.
231+
if (cert instanceof X509Certificate) {
232+
certificateTrustChain.add((X509Certificate) cert);
233+
} else {
234+
throw new CertificateException(
235+
"Found non-X.509 certificate in trust chain file: " + trustChainPath);
236+
}
237+
} catch (CertificateException e) {
238+
// If parsing an individual PEM block fails, re-throw with more context.
239+
throw new CertificateException(
240+
"Error loading PEM certificates from the trust chain file: "
241+
+ trustChainPath
242+
+ " - "
243+
+ e.getMessage(),
244+
e);
245+
}
246+
}
247+
248+
if (trustChainData.length > 0 && certificateTrustChain.isEmpty()) {
249+
throw new CertificateException(
250+
"Trust chain file was not empty but no PEM certificates were found: " + trustChainPath);
251+
}
252+
253+
return certificateTrustChain;
254+
}
129255
}

0 commit comments

Comments
 (0)