Digitally sign a PDF using AWS KMS and iText

Fri - 01/29/2021

Securing and automating digital document workflows is increasingly important in the modern business world. A crucial part of creating secure digital signatures is generating public and private keys for signing, and cloud providers such as Amazon, Google, and Microsoft now offer highly-secure cryptographic key management services. Since iText is used by many businesses and signing services to integrate secure digital signatures into PDFs, this step-by-step article shows developers how to use iText and the AWS KMS APIs to generate a digital signature and add it to a PDF document.

Share this article

Using iText and AWS KMS to digitally sign a PDF document

Introduction

Note: This article was originally written for iText Core version 7.1.x. However, the text and code examples have been revised to account for the API changes and improvements to digital signing we introduced in iText Core version 8.

Here at iText we’ve long been involved with PDF digital signatures. We first published our digital signatures eBook back in 2013, which provided a comprehensive overview of PDF features, industry standards and technology options relating to secure digital signatures, together with in-depth best practices, real-life examples, and code samples for PDF development.

Since then, we’ve continued to promote the technology for secure PDF documents, as it provides integrity, authenticity, non-repudiation, and assurance of when a document was signed. We’ve also kept pace with advances in the field, supporting the PAdES framework and PDF 2.0, and updating our Java and C# (.NET) code examples to apply to the latest versions of iText.

An essential component in creating a secure digital signature is the generation of an asymmetric key pair, consisting of both a public and a private key. There are a number of ways to generate such a key pair, but one of the most secure is the use of a hardware security module (or HSM). This is a physical computing device and is usually very expensive.

Here Comes a New Challenger

However, Amazon Web Services now offers the generation of asymmetric keys as part of its Key Management Service (KMS) which makes it easy to create and manage cryptographic keys and control their use across a range of AWS services and in your applications. Similar to the symmetric key features that were previously available, asymmetric keys can be generated as customer master keys (CMKs) where the private portion never leaves the service, or as a data key where the private portion is returned to your calling application encrypted under a CMK.

Since it’s a scalable service with no upfront charges, AWS KMS can be an attractive option for digitally signing PDFs. It’s not all plain sailing though. Since AWS KMS doesn’t store or associate digital certificates with asymmetric CMKs it creates, it’s not directly possible to use the asymmetric CMK for signing PDFs, as you would first have to generate a certificate for the public key of your AWS KMS signing key pair.

This topic came up in a recent Stack Overflow question, and the comprehensive answer provided by Michael Klink led to this article which we hope many of you will benefit from. We’ll walk through the whole process of accessing the AWS KMS API to generate a digital signature, and then applying that signature to a PDF with iText. In addition, we’ll also point out some things you’ll need to consider if you plan to do mass-signing operations with AWS KMS.

Of course, Amazon is not the only big player in cloud services, and so it should not be surprising that Google and Microsoft also provide similar functionality. Google has its Cloud Key Management and Microsoft Azure offers their Key Vault, both of which lower the cost of entry to using HSMs for cryptographic key management. While we won’t be covering them in this article, the process of signing a PDF using these services should be largely the same.

 Once again, we’ve worked with independent PDF expert and top StackOverflow contributor Michael Klink (@mkl) who kindly provided C# versions of his Java code examples from his original answer for our .NET users. Each code snippet included in this article has a link leading to the full Java/C# example on our Knowledge Base.

Different strokes for different folks

A quick note before we continue. While the Java and C# iText Core APIs are largely the same, there are a number of differences in the code examples here, in part due to certain differences in the Java and .NET AWS KMS APIs. For example, the latter API uses the async pattern for .NET.

In addition, genuine .NET classes were used for the creation of the self-signed certificate and there are some differences between the BouncyCastle Java and .NET APIs. So, the differences in the code are not just in its method name capitalization...

Don’t worry though, as we’ll be pointing out these differences where they occur throughout the article

Signing a PdfDocument using the digital signature returned by AWS KMS

In the context of this article it is assumed that you have stored your credentials in the default section of your ~/.aws/credentials file and your region in the default section of your ~/.aws/config file. Otherwise, you'll have to adapt the KmsClient instantiation or initialization in the following code examples.

Generating a Certificate for an AWS KMS Key Pair

As we noted above, AWS KMS signs using a plain asymmetric key pair and it does not provide a X.509 certificate for the public key. However, interoperable PDF signatures require a X.509 certificate for the public key, to establish trust in the signature. Thus, the first step to take for interoperable AWS KMS PDF signing is to generate an X.509 certificate for the public key of your AWS KMS signing key pair.

For testing purposes, you can create a self-signed certificate using this Java helper method which is based on code from this stack overflow answer:

CertificateUtils helper method

.NET offers its own means for the creation of certificate requests and self-signed certificates, the CertificateRequest class.

As with the BouncyCastle implementation in the Java example, this class also has the actual signature creation (for the self-signed certificate) delegated to a helper, which here is an X509SignatureGenerator instance. Obviously, .NET does not have a ready-to-use variant of that class for AWS KMS signing, so we have to provide one ourselves, the inner class SignatureGenerator in the .NET example. Fortunately, we can re-use .NET variants of X509SignatureGenerator for all methods except the actual SignData signing method.

Returning to our Java example, the AwsKmsContentSigner class used in the code above is this implementation of the BouncyCastle interface ContentSigner:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class AwsKmsContentSigner implements ContentSigner {
    final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    final String keyId;
    final SigningAlgorithmSpec signingAlgorithmSpec;
    final AlgorithmIdentifier signatureAlgorithm;
 
    public AwsKmsContentSigner(String keyId, SigningAlgorithmSpec signingAlgorithmSpec) {
        this.keyId = keyId;
        this.signingAlgorithmSpec = signingAlgorithmSpec;
        String signatureAlgorithmName = signingAlgorithmNameBySpec.get(signingAlgorithmSpec);
        if (signatureAlgorithmName == null)
            throw new IllegalArgumentException("Unknown signature algorithm " + signingAlgorithmSpec);
        this.signatureAlgorithm = new DefaultSignatureAlgorithmIdentifierFinder().find(signatureAlgorithmName);
    }
 
    @Override
    public byte[] getSignature() {
        try (   KmsClient kmsClient = KmsClient.create() ) {
            SignRequest signRequest = SignRequest.builder()
                    .signingAlgorithm(signingAlgorithmSpec)
                    .keyId(keyId)
                    .messageType(MessageType.RAW)
                    .message(SdkBytes.fromByteArray(outputStream.toByteArray()))
                    .build();
            SignResponse signResponse = kmsClient.sign(signRequest);
            SdkBytes signatureSdkBytes = signResponse.signature();
            return signatureSdkBytes.asByteArray();
        } finally {
            outputStream.reset();
        }
    }
 
    @Override
    public OutputStream getOutputStream() {
        return outputStream;
    }
 
    @Override
    public AlgorithmIdentifier getAlgorithmIdentifier() {
        return signatureAlgorithm;
    }
 
    final static Map<SigningAlgorithmSpec, String> signingAlgorithmNameBySpec;
 
    static {
        signingAlgorithmNameBySpec = new HashMap<>();
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.ECDSA_SHA_256, "SHA256withECDSA");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.ECDSA_SHA_384, "SHA384withECDSA");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.ECDSA_SHA_512, "SHA512withECDSA");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_256, "SHA256withRSA");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_384, "SHA384withRSA");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_512, "SHA512withRSA");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.RSASSA_PSS_SHA_256, "SHA256withRSAandMGF1");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.RSASSA_PSS_SHA_384, "SHA384withRSAandMGF1");
        signingAlgorithmNameBySpec.put(SigningAlgorithmSpec.RSASSA_PSS_SHA_512, "SHA512withRSAandMGF1");
    }
}

 

AwsContentSigner

For production purposes however, you'll usually want to use a certificate signed by a trusted Certificate Authority (CA). Similar to the example above you can create and sign a certificate request for your AWS KMS public key, send it to your CA of choice, and get back the certificate to use from them.

There’s no .NET equivalent of AwsKmsContentSigner.java since our .NET version of CertificateUtils doesn’t use BouncyCastle for certificate generation but instead uses the .NET X509SignatureGenerator, thus no BouncyCastle ContentSigner implementation is required.

Signing a PDF using an AWS KMS Key Pair

To sign a PDF with iText you need an implementation of the iText IExternalSignature or IExternalSignatureContainer interface. Here we use the former:

AwsKmsSignature

In the constructor we select a signing algorithm available for the key in question. The single argument constructor simply takes the first algorithm from the available ones, the double argument constructor allows you to select a specific one.

getDigestAlgorithmName returns the name of the respective part of the signature algorithm, getSignatureMechanismParameters returns additional parameters where required, and sign simply creates a signature.

For .NET, the AwsKmsSignature class could be ported from Java with very little changes required.

Putting It Into Action

Assuming your AWS KMS signing key pair has the alias SigningExamples-ECC_NIST_P256 and is indeed an ECC_NIST_P256 key pair you can use the code above to sign a PDF like this:

TestSignSimple test testSignSimpleEcdsa

Signing a PDF Using an AWS KMS Key Pair: Redux

Above we used an implementation of IExternalSignature for signing. While that is the easiest way, it has some drawbacks: The class PdfPKCS7 used in this case is not very flexible, e.g. it doesn't allow to to add custom attributes to the signature.

To not be subject to these limitations, we here use an implementation of IExternalSignatureContainer instead in which we build the complete CMS signature container ourselves using only BouncyCastle functionality.

For .NET, while the AwsKmsSignatureContainer class uses BouncyCastle to build the CMS signature container to embed just like in the Java version, there are certain differences in the .NET BouncyCastle API. In particular one does not use an instance of ContentSigner for the actual signing but an instance of ISignatureFactory; that interface represents a factory of IStreamCalculator instances which in their function are equivalent to the ContentSigner in Java. The implementations of these interfaces are AwsKmsSignatureFactory and AwsKmsStreamCalculator in the .NET example.

AwsKmsSignatureContainer

Putting It Into Action: Redux

Assuming you have an AWS KMS signing RSA_2048 key pair which has the alias SigningExamples-RSA_2048 you can use the code above like this to sign a PDF using RSASSA-PSS:

TestSignSimple test testSignSimpleRsaSsaPssExternal

with this selector function for Java:

1
2
3
4
5
6
static SigningAlgorithmSpec selectRsaSsaPss (List<SigningAlgorithmSpec> specs) {
    if (specs != null)
        return specs.stream().filter(spec -> spec.toString().startsWith("RSASSA_PSS")).findFirst().orElse(null);
    else
        return null;
}

 

For .NET, since the C# AWS KMS API works with String representations of the algorithm the corresponding expression on the C# side is:

1
Func<System.Collections.Generic.List<string>, string> selector = list => list.Find(name => name.StartsWith("RSASSA_PSS"));

 

Final Thoughts and Mass-Signing Considerations

If you plan to do mass-signing using AWS KMS, you should be aware of the request quotas established by AWS KMS for some of its operations:

Quota Name

Default value (per second)

Cryptographic operations (RSA) request rate

500 (shared) for RSA CMKs

Cryptographic operations (ECC) request rate

300 (shared) for elliptic curve (ECC) CMKs

GetPublicKey request rate

5


(excerpt from "AWS Key Management Service Developer Guide" / "Quotas" / "Request Quotas" / "Request quotas for each AWS KMS API operation" viewed 1/28/2021)

The RSA and ECC cryptographic operations request rates are probably not a problem. Or more to the point, if they are a problem, AWS KMS is most likely not the right signing product for your needs. You should instead look for actual HSMs, be they physical or as-a-service, e.g., AWS CloudHSM.

The GetPublicKey request rate on the other hand may well be a problem: Both the AwsKmsSignature and AwsKmsSignatureContainer constructors respectively call that method. Naive mass-signing code based on them, would therefore be limited to 5 signatures per second.

Depending on your use case there are different strategies to tackle this problem.

If very few instances of your signing code are running concurrently and they are using only a very few different keys, you can simply re-use your AwsKmsSignature and AwsKmsSignatureContainer objects, either creating them at start-up or on-demand, and then caching them.

Otherwise, you should refactor the use of the GetPublicKey method outside of the AwsKmsSignature and AwsKmsSignatureContainer constructors. It is used inside there only to determine which AWS KMS signing algorithm identifier to use when signing with the key in question. Obviously, you can instead store that identifier together with the key identifier, making that GetPublicKey call unnecessary.

Conclusion

We hope you have found this article and its code examples useful if you’ve run into issues when using the AWS KMS or equivalent services. Once again, we’d like to thank Michael Klink for taking the time to port his Java examples from the initial Stack Overflow question to .NET, and indeed for his many contributions to the iText community.

 



Contact

Still have questions? 

We're happy to answer your questions. Reach out to us and we'll get back to you shortly.

Contact us
Stay updated

Join 11,000+ subscribers and become an iText PDF expert by staying up to date with our new products, updates, tips, technical solutions and happenings.

Subscribe Now