@relaycorp/webcrypto-kms
Version:
WebCrypto-compatible client for Key Management Services like GCP KMS
182 lines • 8.62 kB
JavaScript
;
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