Skip to content
Sign with Google Cloud KMS

Sign with Google Cloud KMS

Signing with Google Cloud KMS follows the same shape as any other key that never leaves its host: a custom model.SignatureHandler assembles the PKCS7 package locally and hands the digest to a crypto.Signer that calls AsymmetricSign. The key needs purpose ASYMMETRIC_SIGN and algorithm RSA_SIGN_PKCS1_2048_SHA256, RSA_SIGN_PKCS1_3072_SHA256 or RSA_SIGN_PKCS1_4096_SHA256, and you address a specific key version, not the key:

projects/my-project/locations/us-east1/keyRings/my-key-ring/cryptoKeys/my-key/cryptoKeyVersions/123

The service account needs the cloudkms.signerVerifier role on the key. Setup notes are in the external signing README.

Wiring up the client

client, err := kms.NewKeyManagementClient(context.Background(),
    gcOption.WithCredentialsFile(gcCredentialsPath))
if err != nil {
    return err
}
defer client.Close()

handler, err := GcKmsExternalSigner(client, keyName)
if err != nil {
    return err
}

The client stays open until the document is written, because KMS signs during appender.Write. GcKmsExternalSigner builds the signer with NewCryptoSigner(client, keyName), which calls GetPublicKey once. It rejects any key version whose algorithm is not one of the three RSA PKCS#1 v1.5 SHA-256 algorithms above, checks the CRC32C of the returned PEM and keeps the parsed key for Public(). Sign() sends the digest:

req := &kmspb.AsymmetricSignRequest{
    Name: cs.keyName,
    Digest: &kmspb.Digest{
        Digest: &kmspb.Digest_Sha256{Sha256: digest},
    },
    DigestCrc32C: wrapperspb.Int64(int64(crc32c(digest))),
}

The Digest_Sha256 variant fixes the hash at SHA-256, which has to match both signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256) in the handler and the key’s own algorithm. The algorithm check in NewCryptoSigner keeps the three in step. An EC or RSA-PSS key fails there with an error naming the key’s algorithm, before any signing request is made.

Like the AWS KMS handler, the signer must also implement EncryptionAlgorithmOID() asn1.ObjectIdentifier, which pkcs7 picks up through its EncryptionAlgorithmReporter interface. Without it AddSigner rejects the signer as an unknown private key type, because its fallback is a type switch over the standard library key types.

Limitations

Both the request and the response carry CRC32C checksums and the example checks them. A corrupt GetPublicKey or AsymmetricSign response returns an error.

InitSignature calls Sign(sig, nil), which reserves sigLen zero bytes for Contents without a network round trip. The document signature is one AsymmetricSign call, made during appender.Write.

The signature is padded to sigLen, 8192 in the example, and Sign returns an error if the PKCS7 package is longer. The KMS client should be closed when you are done with it; the example defers client.Close() in main.

The certificate in the example is self-signed with the KMS key, which validates nowhere and costs one more AsymmetricSign call when the handler is created. Replace it with a certificate issued for that key and set the full chain on the handler.

Run the example

main creates the KMS client, GcKmsExternalSigner builds the signer and the certificate and returns the handler, and generateSignedFile signs and writes the document in a single pass. The example takes four arguments, including the path to a service account JSON key file.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/external
go run pdf_sign_external_google_cloud_kms.go <INPUT_PDF_PATH> <OUTPUT_PDF_PATH> <GOOGLE_CLOUD_CREDENTIALS_PATH> <KEY_NAME>

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source
Last updated on