UNPKG

@relaycorp/webcrypto-kms

Version:

WebCrypto-compatible client for Key Management Services like GCP KMS

182 lines 8.62 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.retrieveKMSPublicKey = exports.GcpKmsRsaPssProvider = void 0; const fast_crc32c_1 = require("fast-crc32c"); const uuid4_1 = __importDefault(require("uuid4")); const buffer_1 = require("../utils/buffer"); const KmsError_1 = require("../KmsError"); const GcpKmsRsaPssPrivateKey_1 = require("./GcpKmsRsaPssPrivateKey"); const kmsUtils_1 = require("./kmsUtils"); const timing_1 = require("../utils/timing"); const crypto_1 = require("../utils/crypto"); const KmsRsaPssProvider_1 = require("../KmsRsaPssProvider"); // See: https://cloud.google.com/kms/docs/algorithms#rsa_signing_algorithms const SUPPORTED_MODULUS_LENGTHS = [2048, 3072, 4096]; // See: https://cloud.google.com/kms/docs/algorithms#rsa_signing_algorithms const SUPPORTED_SALT_LENGTHS = [ 256 / 8, // SHA-256 512 / 8, // SHA-512 ]; const DEFAULT_DESTROY_SCHEDULED_DURATION_SECONDS = 86_400; // One day; the minimum allowed by GCP /** * The official KMS library will often try to make API requests before the authentication with the * Application Default Credentials is complete, which will result in errors like "Exceeded * maximum number of retries before any response was received". We're working around that by * retrying a few times. */ const REQUEST_OPTIONS = { timeout: 3_000, maxRetries: 10 }; class GcpKmsRsaPssProvider extends KmsRsaPssProvider_1.KmsRsaPssProvider { constructor(client, config) { super(); this.client = client; this.config = config; // See: https://cloud.google.com/kms/docs/algorithms#rsa_signing_algorithms this.hashAlgorithms = ['SHA-256', 'SHA-512']; } async onGenerateKey(algorithm) { if (!SUPPORTED_MODULUS_LENGTHS.includes(algorithm.modulusLength)) { throw new KmsError_1.KmsError(`Unsupported RSA modulus (${algorithm.modulusLength})`); } const projectId = await this.getGCPProjectId(); const cryptoKeyId = (0, uuid4_1.default)(); await this.createCryptoKey(algorithm, projectId, cryptoKeyId); const kmsKeyVersionPath = this.client.cryptoKeyVersionPath(projectId, this.config.location, this.config.keyRing, cryptoKeyId, '1'); const privateKey = new GcpKmsRsaPssPrivateKey_1.GcpKmsRsaPssPrivateKey(kmsKeyVersionPath, algorithm.hash.name, this); const publicKey = await this.getPublicKeyFromPrivate(privateKey); return { privateKey, publicKey }; } async onImportKey(format, keyData, algorithm) { if (format !== 'raw') { throw new KmsError_1.KmsError('Private key can only be exported to raw format'); } const kmsKeyVersionPath = Buffer.from(keyData).toString(); return new GcpKmsRsaPssPrivateKey_1.GcpKmsRsaPssPrivateKey(kmsKeyVersionPath, algorithm.hash.name, this); } async onExportKey(format, key) { requireGcpKmsKey(key); let keySerialised; if (format === 'spki') { keySerialised = await retrieveKMSPublicKey(key.kmsKeyVersionPath, this.client); } else if (format === 'raw') { const pathEncoded = Buffer.from(key.kmsKeyVersionPath); keySerialised = (0, buffer_1.bufferToArrayBuffer)(pathEncoded); } else { throw new KmsError_1.KmsError(`Private key cannot be exported as ${format}`); } return keySerialised; } async onSign(algorithm, key, data) { requireGcpKmsKey(key); if (!SUPPORTED_SALT_LENGTHS.includes(algorithm.saltLength)) { throw new KmsError_1.KmsError(`Unsupported salt length of ${algorithm.saltLength} octets`); } return this.kmsSign(Buffer.from(data), key); } async onVerify() { throw new KmsError_1.KmsError('Signature verification is unsupported'); } async destroyKey(key) { requireGcpKmsKey(key); await (0, kmsUtils_1.wrapGCPCallError)(this.client.destroyCryptoKeyVersion({ name: key.kmsKeyVersionPath }, REQUEST_OPTIONS), 'Key destruction failed'); } async close() { await this.client.close(); } async getGCPProjectId() { // GCP client library already caches the project id. return this.client.getProjectId(); } async createCryptoKey(algorithm, projectId, cryptoKeyId) { const kmsAlgorithm = getKmsAlgorithm(algorithm); const keyRingName = this.client.keyRingPath(projectId, this.config.location, this.config.keyRing); const destroyScheduledDuration = { seconds: this.config.destroyScheduledDurationSeconds ?? DEFAULT_DESTROY_SCHEDULED_DURATION_SECONDS, }; const creationOptions = { cryptoKey: { destroyScheduledDuration, purpose: 'ASYMMETRIC_SIGN', versionTemplate: { algorithm: kmsAlgorithm, protectionLevel: this.config.protectionLevel, }, }, cryptoKeyId, parent: keyRingName, skipInitialVersionCreation: false, }; await (0, kmsUtils_1.wrapGCPCallError)(this.client.createCryptoKey(creationOptions, REQUEST_OPTIONS), 'Failed to create key'); } async getPublicKeyFromPrivate(privateKey) { const publicKeySerialized = (await this.exportKey('spki', privateKey)); return (0, crypto_1.derDeserialisePublicKey)(publicKeySerialized, privateKey.algorithm); } async kmsSign(plaintext, key) { const plaintextChecksum = (0, fast_crc32c_1.calculate)(plaintext); const [response] = await (0, kmsUtils_1.wrapGCPCallError)(this.client.asymmetricSign({ data: plaintext, dataCrc32c: { value: plaintextChecksum }, name: key.kmsKeyVersionPath }, REQUEST_OPTIONS), 'KMS signature request failed'); if (response.name !== key.kmsKeyVersionPath) { throw new KmsError_1.KmsError(`KMS used the wrong key version (${response.name})`); } if (!response.verifiedDataCrc32c) { throw new KmsError_1.KmsError('KMS failed to verify plaintext CRC32C checksum'); } const signature = response.signature; if ((0, fast_crc32c_1.calculate)(signature) !== Number(response.signatureCrc32c.value)) { throw new KmsError_1.KmsError('Signature CRC32C checksum does not match one received from KMS'); } return (0, buffer_1.bufferToArrayBuffer)(signature); } } exports.GcpKmsRsaPssProvider = GcpKmsRsaPssProvider; function requireGcpKmsKey(key) { if (!(key instanceof GcpKmsRsaPssPrivateKey_1.GcpKmsRsaPssPrivateKey)) { throw new KmsError_1.KmsError(`Only GCP KMS keys are supported (got ${key.constructor.name})`); } } function getKmsAlgorithm(algorithm) { const hash = algorithm.hash.name === 'SHA-256' ? 'SHA256' : 'SHA512'; return `RSA_SIGN_PSS_${algorithm.modulusLength}_${hash}`; } async function retrieveKMSPublicKey(kmsKeyVersionName, kmsClient) { const retrieveWhenReady = async () => { let key; try { key = await _retrieveKMSPublicKey(kmsKeyVersionName, kmsClient); } catch (err) { if (!isKeyPendingCreation(err)) { throw err; } // Let's give KMS a bit more time to generate the key await (0, timing_1.sleep)(500); key = await _retrieveKMSPublicKey(kmsKeyVersionName, kmsClient); } return key; }; const publicKeyPEM = await (0, kmsUtils_1.wrapGCPCallError)(retrieveWhenReady(), 'Failed to retrieve public key'); const publicKeyDer = pemToDer(publicKeyPEM); return (0, buffer_1.bufferToArrayBuffer)(publicKeyDer); } exports.retrieveKMSPublicKey = retrieveKMSPublicKey; async function _retrieveKMSPublicKey(kmsKeyVersionName, kmsClient) { const [response] = await kmsClient.getPublicKey({ name: kmsKeyVersionName }, { maxRetries: 3, timeout: 300, }); return response.pem; } function isKeyPendingCreation(err) { const statusDetails = err.statusDetails ?? []; const pendingCreationViolations = statusDetails.filter((d) => 0 < d.violations.filter((v) => v.type === 'KEY_PENDING_GENERATION').length); return !!pendingCreationViolations.length; } function pemToDer(pemBuffer) { const oneliner = pemBuffer.toString().replace(/(-----[\w ]*-----|\n)/g, ''); return Buffer.from(oneliner, 'base64'); } //# sourceMappingURL=GcpKmsRsaPssProvider.js.map