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/123The 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.