Skip to content

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-1
git 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.

View the full source
Last updated on