Sign with AWS KMS
AWS KMS never releases the private key, so signing has to happen inside KMS. The
way to reach it from UniPDF is a custom model.SignatureHandler that builds the
PKCS7 package locally and delegates the one operation that needs the key to a
crypto.Signer backed by the KMS Sign API. The key must be an asymmetric key
with SIGN_VERIFY usage and an RSA key spec (RSA_2048, RSA_3072 or
RSA_4096). The example signs with RSASSA_PKCS1_V1_5_SHA_256 and stops with
an error if GetPublicKey returns anything other than an RSA key. Setup notes for
the key, credentials and IAM permissions are in the
external signing README.
Unlike the placeholder-and-patch flow, a handler is called
by appender.Write while the revision is being laid out, so the finished file
comes out of Write with the real signature already in place.
The signature handler
model.SignatureHandler needs five methods. Only two carry real work:
// NewDigest decides what Sign receives. Returning a buffer means the handler
// gets the raw signed byte range and hashes it itself, through pkcs7.
func (es *externalSigner) NewDigest(sig *model.PdfSignature) (model.Hasher, error) {
return bytes.NewBuffer(nil), nil
}
func (es *externalSigner) Sign(sig *model.PdfSignature, digest model.Hasher) error {
if digest == nil {
// Reserve space without contacting KMS.
sig.Contents = core.MakeHexString(string(make([]byte, sigLen)))
return nil
}
signedData, err := pkcs7.NewSignedData(digest.(*bytes.Buffer).Bytes())
if err != nil {
return err
}
signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
if err := signedData.AddSigner(es.certChain[0], es.signer, pkcs7.SignerInfoConfig{}); err != nil {
return err
}
signedData.Detach()
// ... Finish(), fail if longer than sigLen, pad to sigLen, assign to sig.Contents
}InitSignature sets Filter, SubFilter and the Cert array, then calls
Sign(sig, nil). The nil digest takes the early return and reserves sigLen
zero bytes for Contents without contacting KMS, so the document signature costs
one KMS Sign request, made during appender.Write. IsApplicable decides whether the handler
is used when validating an existing signature, and Validate is only reached
during validation.
The crypto.Signer
The signer passed to AddSigner must implement one method beyond
crypto.Signer:
func (cs *CryptoSigner) EncryptionAlgorithmOID() asn1.ObjectIdentifier {
return pkcs7.OIDEncryptionAlgorithmRSASHA256
}pkcs7 looks for this through its EncryptionAlgorithmReporter interface. Its
fallback is a type switch over *rsa.PrivateKey, *ecdsa.PrivateKey and
*dsa.PrivateKey, so a custom signer without the method fails with pkcs7: cannot convert encryption algorithm to oid, unknown private key type. This is
required, not an optimization.
Sign then forwards to KMS. The digest arrives already hashed, so MessageType
is DIGEST. SigningAlgorithm is RSASSA_PKCS1_V1_5_SHA_256, which has to agree
with two other settings: the SHA-256 digest algorithm set on the signed data, and
the RSA PKCS#1 v1.5 with SHA-256 OID that EncryptionAlgorithmOID reports. Change
one without the others and the signature will not verify, or KMS rejects the
request.
Limitations
Sign pads the finished PKCS7 package out to sigLen, 8192 in the example, and
returns an error if the package is longer. A longer certificate chain needs a
larger sigLen.
es.certChain has to be populated before appender.Write calls Sign, since
Sign indexes certChain[0]. The example fills it in AwsKmsExternalSigner
when it creates the handler.
Because NewDigest returns a buffer rather than a hash.Hash, the whole signed
byte range is held in memory while the package is built.
The example also self-signs its certificate using the KMS key, which is fine for
a smoke test and useless for validation: readers have no trust path to it.
Self-signing costs one more KMS Sign request when the handler is created, on top
of the GetPublicKey call. In production, use a certificate issued for that key
by a CA and set the full chain on the handler.
Run the example
AwsKmsExternalSigner sets up the session, the signer and the certificate, and
returns the handler. generateSignedFile signs and writes the document in a
single pass. The AWS SDK reads credentials and region from the standard AWS
configuration: AWS_PROFILE or AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY,
plus AWS_REGION, or ~/.aws/credentials and ~/.aws/config. The example exits
with an error if no region is set.
export AWS_REGION=us-west-1git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures/external
go run pdf_sign_external_aws_kms.go <INPUT_PDF_PATH> <OUTPUT_PDF_PATH> <KEY_ID>If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.