|
38 | 38 | import java.io.IOException; |
39 | 39 | import java.io.InputStream; |
40 | 40 | import java.nio.file.Files; |
| 41 | +import java.nio.file.NoSuchFileException; |
41 | 42 | import java.nio.file.Paths; |
| 43 | +import java.security.cert.Certificate; |
42 | 44 | import java.security.cert.CertificateEncodingException; |
43 | 45 | import java.security.cert.CertificateException; |
44 | 46 | import java.security.cert.CertificateFactory; |
45 | 47 | import java.security.cert.X509Certificate; |
| 48 | +import java.util.ArrayList; |
46 | 49 | import java.util.Base64; |
| 50 | +import java.util.List; |
| 51 | +import java.util.regex.Matcher; |
| 52 | +import java.util.regex.Pattern; |
47 | 53 |
|
48 | 54 | /** |
49 | 55 | * Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an |
@@ -97,33 +103,153 @@ private static String encodeCert(X509Certificate certificate) |
97 | 103 |
|
98 | 104 | /** |
99 | 105 | * 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. |
103 | 111 | * |
104 | 112 | * @param context The external account supplier context. This parameter is currently not used in |
105 | 113 | * 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). |
108 | 117 | */ |
109 | 118 | @Override |
110 | 119 | 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 | + |
111 | 126 | 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); |
115 | 129 | String encodedLeafCert = encodeCert(leafCert); |
116 | 130 |
|
| 131 | + // Add the leaf certificate first. |
117 | 132 | java.util.List<String> certChain = new java.util.ArrayList<>(); |
118 | 133 | certChain.add(encodedLeafCert); |
119 | 134 |
|
| 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 | + |
120 | 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); |
121 | 173 | } 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. |
125 | 177 | 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); |
127 | 182 | } |
128 | 183 | } |
| 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 | + } |
129 | 255 | } |
0 commit comments