UNPKG

google-auth-library

Version:
223 lines 10.6 kB
"use strict"; // Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. Object.defineProperty(exports, "__esModule", { value: true }); exports.CertificateSubjectTokenSupplier = exports.InvalidConfigurationError = exports.CertificateSourceUnavailableError = exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE = void 0; const util_1 = require("../util"); const fs = require("fs"); const crypto_1 = require("crypto"); const https = require("https"); exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE = 'GOOGLE_API_CERTIFICATE_CONFIG'; /** * Thrown when the certificate source cannot be located or accessed. */ class CertificateSourceUnavailableError extends Error { constructor(message) { super(message); this.name = 'CertificateSourceUnavailableError'; } } exports.CertificateSourceUnavailableError = CertificateSourceUnavailableError; /** * Thrown for invalid configuration that is not related to file availability. */ class InvalidConfigurationError extends Error { constructor(message) { super(message); this.name = 'InvalidConfigurationError'; } } exports.InvalidConfigurationError = InvalidConfigurationError; /** * A subject token supplier that uses a client certificate for authentication. * It provides the certificate chain as the subject token for identity federation. */ class CertificateSubjectTokenSupplier { certificateConfigPath; trustChainPath; cert; key; /** * Initializes a new instance of the CertificateSubjectTokenSupplier. * @param opts The configuration options for the supplier. */ constructor(opts) { if (!opts.useDefaultCertificateConfig && !opts.certificateConfigLocation) { throw new InvalidConfigurationError('Either `useDefaultCertificateConfig` must be true or a `certificateConfigLocation` must be provided.'); } if (opts.useDefaultCertificateConfig && opts.certificateConfigLocation) { throw new InvalidConfigurationError('Both `useDefaultCertificateConfig` and `certificateConfigLocation` cannot be provided.'); } this.trustChainPath = opts.trustChainPath; this.certificateConfigPath = opts.certificateConfigLocation ?? ''; } /** * Creates an HTTPS agent configured with the client certificate and private key for mTLS. * @returns An mTLS-configured https.Agent. */ async createMtlsHttpsAgent() { if (!this.key || !this.cert) { throw new InvalidConfigurationError('Cannot create mTLS Agent with missing certificate or key'); } return new https.Agent({ key: this.key, cert: this.cert }); } /** * Constructs the subject token, which is the base64-encoded certificate chain. * @returns A promise that resolves with the subject token. */ async getSubjectToken() { // The "subject token" in this context is the processed certificate chain. this.certificateConfigPath = await this.#resolveCertificateConfigFilePath(); const { certPath, keyPath } = await this.#getCertAndKeyPaths(); ({ cert: this.cert, key: this.key } = await this.#getKeyAndCert(certPath, keyPath)); return await this.#processChainFromPaths(this.cert); } /** * Resolves the absolute path to the certificate configuration file * by checking the "certificate_config_location" provided in the ADC file, * or the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable * or in the default gcloud path. * @param overridePath An optional path to check first. * @returns The resolved file path. */ async #resolveCertificateConfigFilePath() { // 1. Check for the override path from constructor options. const overridePath = this.certificateConfigPath; if (overridePath) { if (await (0, util_1.isValidFile)(overridePath)) { return overridePath; } throw new CertificateSourceUnavailableError(`Provided certificate config path is invalid: ${overridePath}`); } // 2. Check the standard environment variable. const envPath = process.env[exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE]; if (envPath) { if (await (0, util_1.isValidFile)(envPath)) { return envPath; } throw new CertificateSourceUnavailableError(`Path from environment variable "${exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE}" is invalid: ${envPath}`); } // 3. Check the well-known gcloud config location. const wellKnownPath = (0, util_1.getWellKnownCertificateConfigFileLocation)(); if (await (0, util_1.isValidFile)(wellKnownPath)) { return wellKnownPath; } // 4. If none are found, throw an error. throw new CertificateSourceUnavailableError('Could not find certificate configuration file. Searched override path, ' + `the "${exports.CERTIFICATE_CONFIGURATION_ENV_VARIABLE}" env var, and the gcloud path (${wellKnownPath}).`); } /** * Reads and parses the certificate config JSON file to extract the certificate and key paths. * @returns An object containing the certificate and key paths. */ async #getCertAndKeyPaths() { const configPath = this.certificateConfigPath; let fileContents; try { fileContents = await fs.promises.readFile(configPath, 'utf8'); } catch (err) { throw new CertificateSourceUnavailableError(`Failed to read certificate config file at: ${configPath}`); } try { const config = JSON.parse(fileContents); const certPath = config?.cert_configs?.workload?.cert_path; const keyPath = config?.cert_configs?.workload?.key_path; if (!certPath || !keyPath) { throw new InvalidConfigurationError(`Certificate config file (${configPath}) is missing required "cert_path" or "key_path" in the workload config.`); } return { certPath, keyPath }; } catch (e) { if (e instanceof InvalidConfigurationError) throw e; throw new InvalidConfigurationError(`Failed to parse certificate config from ${configPath}: ${e.message}`); } } /** * Reads and parses the cert and key files get their content and check valid format. * @returns An object containing the cert content and key content in buffer format. */ async #getKeyAndCert(certPath, keyPath) { let cert, key; try { cert = await fs.promises.readFile(certPath); new crypto_1.X509Certificate(cert); } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new CertificateSourceUnavailableError(`Failed to read certificate file at ${certPath}: ${message}`); } try { key = await fs.promises.readFile(keyPath); (0, crypto_1.createPrivateKey)(key); } catch (err) { const message = err instanceof Error ? err.message : String(err); throw new CertificateSourceUnavailableError(`Failed to read private key file at ${keyPath}: ${message}`); } return { cert, key }; } /** * Reads the leaf certificate and trust chain, combines them, * and returns a JSON array of base64-encoded certificates. * @returns A stringified JSON array of the certificate chain. */ async #processChainFromPaths(leafCertBuffer) { const leafCert = new crypto_1.X509Certificate(leafCertBuffer); // If no trust chain is provided, just use the successfully parsed leaf certificate. if (!this.trustChainPath) { return JSON.stringify([leafCert.raw.toString('base64')]); } // Handle the trust chain logic. try { const chainPems = await fs.promises.readFile(this.trustChainPath, 'utf8'); const pemBlocks = chainPems.match(/-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----/g) ?? []; const chainCerts = pemBlocks.map((pem, index) => { try { return new crypto_1.X509Certificate(pem); } catch (err) { const message = err instanceof Error ? err.message : String(err); // Throw a more precise error if a single certificate in the chain is invalid. throw new InvalidConfigurationError(`Failed to parse certificate at index ${index} in trust chain file ${this.trustChainPath}: ${message}`); } }); const leafIndex = chainCerts.findIndex(chainCert => leafCert.raw.equals(chainCert.raw)); let finalChain; if (leafIndex === -1) { // Leaf not found, so prepend it to the chain. finalChain = [leafCert, ...chainCerts]; } else if (leafIndex === 0) { // Leaf is already the first element, so the chain is correctly ordered. finalChain = chainCerts; } else { // Leaf is in the chain but not at the top, which is invalid. throw new InvalidConfigurationError(`Leaf certificate exists in the trust chain but is not the first entry (found at index ${leafIndex}).`); } return JSON.stringify(finalChain.map(cert => cert.raw.toString('base64'))); } catch (err) { // Re-throw our specific configuration errors. if (err instanceof InvalidConfigurationError) throw err; const message = err instanceof Error ? err.message : String(err); throw new CertificateSourceUnavailableError(`Failed to process certificate chain from ${this.trustChainPath}: ${message}`); } } } exports.CertificateSubjectTokenSupplier = CertificateSubjectTokenSupplier; //# sourceMappingURL=certificatesubjecttokensupplier.js.map