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-and-verify usage.
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(), pad to sigLen, assign to sig.Contents
}InitSignature sets Filter, SubFilter and the Cert array, then calls
Sign to size the Contents entry. 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, and SigningAlgorithm has to agree with the digest algorithm set on
the signed data: RSASSA_PKCS1_V1_5_SHA_256 or RSASSA_PSS_SHA_256 for
crypto.SHA256, the _SHA_384 and _SHA_512 variants for the larger hashes.
Mismatch them and KMS signs the wrong thing, or rejects the request.
Limitations
InitSignature in the example calls NewDigest and passes the resulting buffer
to Sign, which means a full KMS signing request is made just to find out how
long the signature is, and a second one when the document is written. Two billed
operations per signature. Passing nil instead takes the early return above and
reserves sigLen zero bytes without any network call, which is what the
GlobalSign handler does.
Sign pads the finished PKCS7 package out to sigLen and assigns it whole.
copy truncates anything longer than sigLen without complaint, so a
certificate chain larger than expected produces a signature that will not
verify and no error. The value is 8192 in the example.
es.certChain has to be populated before signature.Initialize() runs, since
Sign indexes certChain[0]. The example fills it in getExternalSignatureAndSign
before creating 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. In
production, use a certificate issued for that key by a CA and set the full chain
on the handler. The AWS region is hardcoded to us-west-1 in
AwsKmsExternalSigner.
Run the example
AwsKmsExternalSigner sets up the session and the signer, and
getExternalSignatureAndSign is where the certificate and the handler come
together. Credentials are read by the AWS SDK from the usual environment and
profile chain. main also makes a preliminary pass with
sighandler.NewEmptyAdobePKCS7Detached to read the byte range, which a
handler-based flow does not need.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/signatures
go run pdf_sign_external_aws_kms.go input.pdf output.pdf KEY_IDIf this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.