LibPDF
Digital Signatures

Google Cloud KMS

Sign PDFs with keys stored in Google Cloud Key Management Service (KMS), including HSM-backed keys.

Google Cloud KMS

Sign PDFs using keys stored in Google Cloud Key Management Service (KMS). This is ideal for enterprise environments where private keys must never leave a hardware security module (HSM) for compliance and security reasons.

The private key never leaves KMS - only the digest is sent for signing.

Installation

The Google Cloud KMS client is an optional peer dependency:

npm install @google-cloud/kms

For loading certificates from Secret Manager:

npm install @google-cloud/secret-manager

Quick Start

import { PDF, GoogleKmsSigner } from "@libpdf/core";
import { readFile, writeFile } from "fs/promises";

// Load your DER-encoded certificate (issued by your CA for the KMS key)
const certificate = await readFile("certificate.der");

// Create signer with KMS key reference
const signer = await GoogleKmsSigner.create({
  keyVersionName:
    "projects/my-project/locations/us-east1/keyRings/my-ring/cryptoKeys/my-key/cryptoKeyVersions/1",
  certificate,
});

// Sign the PDF
const pdf = await PDF.load(await readFile("document.pdf"));
const { bytes } = await pdf.sign({ signer });

await writeFile("signed.pdf", bytes);

Authentication

GoogleKmsSigner uses Application Default Credentials (ADC) by default. Common authentication methods:

MethodEnvironmentSetup
Service Account KeyAnySet GOOGLE_APPLICATION_CREDENTIALS env var
User CredentialsLocal developmentRun gcloud auth application-default login
Workload IdentityGKEConfigure workload identity for your pod
Attached Service AccountGCE/Cloud Run/etc.Automatic (uses instance metadata)

Required Permissions

The authenticating identity needs these IAM permissions:

  • cloudkms.cryptoKeyVersions.useToSign - Sign with the key
  • cloudkms.cryptoKeyVersions.viewPublicKey - Validate certificate matches
  • cloudkms.cryptoKeyVersions.get - Read key metadata

The predefined role roles/cloudkms.signerVerifier includes all required permissions.


GoogleKmsSigner.create(options)

Create a new KMS signer instance.

Full Resource Name

ParamTypeDefaultDescription
optionsobjectrequired
options.keyVersionNamestringrequiredFull KMS key version resource name
options.certificateUint8ArrayrequiredDER-encoded X.509 certificate for this KMS key
[options.certificateChain]Uint8Array[]Intermediate and root certificates
[options.buildChain]booleanfalseFetch chain via AIA extensions
[options.chainTimeout]number15000Timeout for AIA fetching (ms)
[options.client]KeyManagementServiceClientPre-configured KMS client
const signer = await GoogleKmsSigner.create({
  keyVersionName:
    "projects/my-project/locations/us-east1/keyRings/my-ring/cryptoKeys/my-key/cryptoKeyVersions/1",
  certificate: certificateDer,
  buildChain: true, // Automatically fetch intermediate certificates
});

Shorthand Options

Instead of a full resource name, you can use shorthand properties:

ParamTypeDefaultDescription
options.projectIdstringrequiredGCP project ID
options.locationIdstringrequiredKMS location
options.keyRingIdstringrequiredKey ring name
options.keyIdstringrequiredKey name
[options.keyVersion]string"1"Key version number
const signer = await GoogleKmsSigner.create({
  projectId: "my-project",
  locationId: "us-east1",
  keyRingId: "my-ring",
  keyId: "my-key",
  keyVersion: "1",
  certificate: certificateDer,
});

Returns: Promise<GoogleKmsSigner>

Throws: KmsSignerError if:

  • Key is not found or not accessible
  • Key is not enabled
  • Key algorithm is unsupported
  • Certificate public key doesn't match the KMS key

Signer Properties

After creation, inspect the signer's detected configuration:

const signer = await GoogleKmsSigner.create({
  keyVersionName: "projects/.../cryptoKeyVersions/1",
  certificate: certificateDer,
});

signer.keyType; // "RSA" or "EC"
signer.signatureAlgorithm; // "RSASSA-PKCS1-v1_5", "RSA-PSS", or "ECDSA"
signer.digestAlgorithm; // "SHA-256", "SHA-384", or "SHA-512"
signer.keyVersionName; // Full resource name
signer.certificate; // DER-encoded certificate
signer.certificateChain; // Chain certificates (if provided/built)

Supported Algorithms

Algorithm is auto-detected from the KMS key metadata:

KMS AlgorithmKey TypeSignature AlgorithmDigest
RSA_SIGN_PKCS1_2048_SHA256RSARSASSA-PKCS1-v1_5SHA-256
RSA_SIGN_PKCS1_3072_SHA256RSARSASSA-PKCS1-v1_5SHA-256
RSA_SIGN_PKCS1_4096_SHA256RSARSASSA-PKCS1-v1_5SHA-256
RSA_SIGN_PKCS1_4096_SHA512RSARSASSA-PKCS1-v1_5SHA-512
RSA_SIGN_PSS_*RSARSA-PSSSHA-256/512
EC_SIGN_P256_SHA256ECECDSASHA-256
EC_SIGN_P384_SHA384ECECDSASHA-384
EC_SIGN_P521_SHA512ECECDSASHA-512

RSA-PSS compatibility: RSA-PSS signatures may not verify correctly in older PDF readers (Adobe Acrobat before 2020). Use PKCS#1 v1.5 keys for maximum compatibility.

Unsupported: EC_SIGN_SECP256K1_SHA256 (secp256k1 is not suitable for PDF signing)


Certificate from Secret Manager

Store your certificate securely in Google Secret Manager and load it at runtime:

// Load certificate from Secret Manager
const certificate = await GoogleKmsSigner.getCertificateFromSecretManager(
  "projects/my-project/secrets/signing-cert/versions/latest",
);

const signer = await GoogleKmsSigner.create({
  keyVersionName: "projects/my-project/.../cryptoKeyVersions/1",
  certificate,
});

Cross-Project Access

The certificate can be stored in a different project than the KMS key:

// Certificate in shared-certs project, KMS key in app project
const certificate = await GoogleKmsSigner.getCertificateFromSecretManager(
  "projects/shared-certs/secrets/signing-cert/versions/1",
);

const signer = await GoogleKmsSigner.create({
  keyVersionName:
    "projects/my-app/locations/us-east1/keyRings/ring/cryptoKeys/key/cryptoKeyVersions/1",
  certificate,
});

The certificate must be stored as DER-encoded binary, not PEM. Convert before storing:

openssl x509 -in cert.pem -outform DER -out cert.der

Certificate Chain

For trusted signatures, include the full certificate chain (intermediates and root).

Automatic Chain Building (AIA)

If your certificate has Authority Information Access (AIA) extensions (most CA-issued certificates do), the chain can be built automatically:

const signer = await GoogleKmsSigner.create({
  keyVersionName: "projects/.../cryptoKeyVersions/1",
  certificate: certificateDer,
  buildChain: true, // Fetch intermediates via AIA
  chainTimeout: 20000, // Optional: extend timeout (default 15s)
});

console.log(`Chain has ${signer.certificateChain.length} certificates`);

Manual Chain

Provide the chain explicitly if AIA isn't available or you want to avoid network calls:

const signer = await GoogleKmsSigner.create({
  keyVersionName: "projects/.../cryptoKeyVersions/1",
  certificate: certificateDer,
  certificateChain: [intermediateDer, rootDer],
});

Custom KMS Client

Provide your own pre-configured client for custom authentication or testing:

import { KeyManagementServiceClient } from "@google-cloud/kms";

const client = new KeyManagementServiceClient({
  keyFilename: "/path/to/service-account.json",
});

const signer = await GoogleKmsSigner.create({
  client,
  keyVersionName: "projects/.../cryptoKeyVersions/1",
  certificate: certificateDer,
});

Complete Example

import { readFile, writeFile } from "fs/promises";
import { PDF, GoogleKmsSigner, HttpTimestampAuthority } from "@libpdf/core";

async function signWithKms() {
  // Load certificate from Secret Manager
  const certificate = await GoogleKmsSigner.getCertificateFromSecretManager(
    "projects/my-project/secrets/signing-cert/versions/latest",
  );

  // Create KMS signer with automatic chain building
  const signer = await GoogleKmsSigner.create({
    projectId: "my-project",
    locationId: "us-east1",
    keyRingId: "document-signing",
    keyId: "contract-key",
    certificate,
    buildChain: true,
  });

  // Load document
  const pdf = await PDF.load(await readFile("contract.pdf"));

  // Create timestamp authority for long-term validation
  const tsa = new HttpTimestampAuthority("http://timestamp.digicert.com");

  // Sign with timestamp
  const { bytes } = await pdf.sign({
    signer,
    level: "B-LT",
    timestampAuthority: tsa,
    reason: "Contract approval",
    location: "Cloud signing service",
  });

  await writeFile("contract-signed.pdf", bytes);
  console.log("Document signed with KMS");
}

signWithKms().catch(console.error);

Performance Considerations

Each sign() call makes a network request to Google Cloud KMS, typically adding 50-200ms latency. For bulk signing operations, consider:

  • Batch processing: Sign documents in parallel where possible
  • Regional keys: Use keys in a region close to your application
  • Connection reuse: Reuse the same GoogleKmsSigner instance for multiple documents

Error Handling

GoogleKmsSigner throws KmsSignerError for KMS-specific issues:

import { GoogleKmsSigner, KmsSignerError } from "@libpdf/core";

try {
  const signer = await GoogleKmsSigner.create({
    keyVersionName: "projects/.../cryptoKeyVersions/1",
    certificate: certificateDer,
  });
} catch (error) {
  if (error instanceof KmsSignerError) {
    console.error("KMS error:", error.message);
    // error.cause contains the original error if available
  }
}

Common errors:

ErrorCauseSolution
Key not foundInvalid resource name or no accessVerify the key path and IAM permissions
Permission deniedMissing IAM permissionsGrant roles/cloudkms.signerVerifier
Key is not enabledKey version disabled or destroyedEnable the key version in GCP Console
Certificate mismatchCertificate wasn't issued for this keyRegenerate certificate from KMS public key
Unsupported algorithmsecp256k1 or other unsupported curveUse P-256, P-384, P-521, or RSA

KMS Key Setup

Create a Signing Key

# Create key ring (if needed)
gcloud kms keyrings create document-signing \
  --location us-east1

# Create RSA signing key (HSM-backed)
gcloud kms keys create contract-key \
  --keyring document-signing \
  --location us-east1 \
  --purpose asymmetric-signing \
  --default-algorithm rsa-sign-pkcs1-2048-sha256 \
  --protection-level hsm

# Or create ECDSA key
gcloud kms keys create contract-key-ec \
  --keyring document-signing \
  --location us-east1 \
  --purpose asymmetric-signing \
  --default-algorithm ec-sign-p256-sha256 \
  --protection-level hsm

Get Public Key for Certificate Request

gcloud kms keys versions get-public-key 1 \
  --key contract-key \
  --keyring document-signing \
  --location us-east1 \
  --output-file public-key.pem

Use this public key to generate a Certificate Signing Request (CSR) and submit it to your Certificate Authority.


See Also

On this page