UNPKG

@relaycorp/webcrypto-kms

Version:

WebCrypto-compatible client for Key Management Services like GCP KMS

176 lines 7.91 kB
import { calculate as calculateCRC32C } from 'fast-crc32c'; import uuid4 from 'uuid4'; import { bufferToArrayBuffer } from '../utils/buffer'; import { KmsError } from '../KmsError'; import { GcpKmsRsaPssPrivateKey } from './GcpKmsRsaPssPrivateKey'; import { wrapGCPCallError } from './kmsUtils'; import { sleep } from '../utils/timing'; import { derDeserialisePublicKey } from '../utils/crypto'; import { KmsRsaPssProvider } from '../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 }; export class GcpKmsRsaPssProvider extends KmsRsaPssProvider { client; config; 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(`Unsupported RSA modulus (${algorithm.modulusLength})`); } const projectId = await this.getGCPProjectId(); const cryptoKeyId = uuid4(); await this.createCryptoKey(algorithm, projectId, cryptoKeyId); const kmsKeyVersionPath = this.client.cryptoKeyVersionPath(projectId, this.config.location, this.config.keyRing, cryptoKeyId, '1'); const privateKey = new 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('Private key can only be exported to raw format'); } const kmsKeyVersionPath = Buffer.from(keyData).toString(); return new 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 = bufferToArrayBuffer(pathEncoded); } else { throw new 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(`Unsupported salt length of ${algorithm.saltLength} octets`); } return this.kmsSign(Buffer.from(data), key); } async onVerify() { throw new KmsError('Signature verification is unsupported'); } async destroyKey(key) { requireGcpKmsKey(key); await 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 wrapGCPCallError(this.client.createCryptoKey(creationOptions, REQUEST_OPTIONS), 'Failed to create key'); } async getPublicKeyFromPrivate(privateKey) { const publicKeySerialized = (await this.exportKey('spki', privateKey)); return derDeserialisePublicKey(publicKeySerialized, privateKey.algorithm); } async kmsSign(plaintext, key) { const plaintextChecksum = calculateCRC32C(plaintext); const [response] = await 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(`KMS used the wrong key version (${response.name})`); } if (!response.verifiedDataCrc32c) { throw new KmsError('KMS failed to verify plaintext CRC32C checksum'); } const signature = response.signature; if (calculateCRC32C(signature) !== Number(response.signatureCrc32c.value)) { throw new KmsError('Signature CRC32C checksum does not match one received from KMS'); } return bufferToArrayBuffer(signature); } } function requireGcpKmsKey(key) { if (!(key instanceof GcpKmsRsaPssPrivateKey)) { throw new 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}`; } export 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 sleep(500); key = await _retrieveKMSPublicKey(kmsKeyVersionName, kmsClient); } return key; }; const publicKeyPEM = await wrapGCPCallError(retrieveWhenReady(), 'Failed to retrieve public key'); const publicKeyDer = pemToDer(publicKeyPEM); return bufferToArrayBuffer(publicKeyDer); } 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