Using iText 7 and AWS KMS to digitally sign a PDF document

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 7 and the AWS KMS APIs to generate a digital signature and add it to a PDF document.

Using iText 7 and AWS KMS to digitally sign a PDF document

Introduction

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

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 7. 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 7 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 helper method which is based on code from this stack overflow answer:

CertUtils 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. This is actually done quite haphazardly here, so instead of simply taking the first algorithm you may want to enforce use of a specific hashing algorithm.

getHashAlgorithm and getEncryptionAlgorithm return the name of the respective part of the signature algorithm and sign simply creates a signature.

For .NET, the AwsKmsSignature class was able to 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 does not support RSASSA-PSS usage, and for ECDSA signatures it uses the wrong OID as the signature algorithm OID.

To not be subject to these issues, 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.

public class AwsKmsSignatureContainer : IExternalSignatureContainer { public AwsKmsSignatureContainer(X509Certificate x509Certificate, string keyId, Func, string> selector) { this.x509Certificate = x509Certificate; this.keyId = keyId; using (var kmsClient = new AmazonKeyManagementServiceClient()) { GetPublicKeyRequest getPublicKeyRequest = new GetPublicKeyRequest() { KeyId = keyId }; GetPublicKeyResponse getPublicKeyResponse = kmsClient.GetPublicKeyAsync(getPublicKeyRequest).Result; List signingAlgorithms = getPublicKeyResponse.SigningAlgorithms; this.signingAlgorithm = selector.Invoke(signingAlgorithms); if (signingAlgorithm == null) throw new ArgumentException("KMS key has no signing algorithms", nameof(keyId)); signatureFactory = new AwsKmsSignatureFactory(keyId, signingAlgorithm); } } public void ModifySigningDictionary(PdfDictionary signDic) { signDic.Put(PdfName.Filter, new PdfName("MKLx_AWS_KMS_SIGNER")); signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached); } public byte[] Sign(Stream data) { CmsProcessable msg = new CmsProcessableInputStream(data); CmsSignedDataGenerator gen = new CmsSignedDataGenerator(); SignerInfoGenerator signerInfoGenerator = new SignerInfoGeneratorBuilder() .WithSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator()) .Build(signatureFactory, x509Certificate); gen.AddSignerInfoGenerator(signerInfoGenerator); X509CollectionStoreParameters collectionStoreParameters = new X509CollectionStoreParameters(new List { x509Certificate }); IX509Store collectionStore = X509StoreFactory.Create("CERTIFICATE/COLLECTION", collectionStoreParameters); gen.AddCertificates(collectionStore); CmsSignedData sigData = gen.Generate(msg, false); return sigData.GetEncoded(); } X509Certificate x509Certificate; String keyId; string signingAlgorithm; ISignatureFactory signatureFactory; } class AwsKmsSignatureFactory : ISignatureFactory { private string keyId; private string signingAlgorithm; private AlgorithmIdentifier signatureAlgorithm; public AwsKmsSignatureFactory(string keyId, string signingAlgorithm) { this.keyId = keyId; this.signingAlgorithm = signingAlgorithm; string signatureAlgorithmName = signingAlgorithmNameBySpec[signingAlgorithm]; if (signatureAlgorithmName == null) throw new ArgumentException("Unknown signature algorithm " + signingAlgorithm, nameof(signingAlgorithm)); // Special treatment because of issue https://github.com/bcgit/bc-csharp/issues/250 switch (signatureAlgorithmName.ToUpperInvariant()) { case "SHA256WITHECDSA": this.signatureAlgorithm = new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha256); break; case "SHA512WITHECDSA": this.signatureAlgorithm = new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha512); break; default: this.signatureAlgorithm = new DefaultSignatureAlgorithmIdentifierFinder().Find(signatureAlgorithmName); break; } } public object AlgorithmDetails => signatureAlgorithm; public IStreamCalculator CreateCalculator() { return new AwsKmsStreamCalculator(keyId, signingAlgorithm); } static Dictionary signingAlgorithmNameBySpec = new Dictionary() { { "ECDSA_SHA_256", "SHA256withECDSA" }, { "ECDSA_SHA_384", "SHA384withECDSA" }, { "ECDSA_SHA_512", "SHA512withECDSA" }, { "RSASSA_PKCS1_V1_5_SHA_256", "SHA256withRSA" }, { "RSASSA_PKCS1_V1_5_SHA_384", "SHA384withRSA" }, { "RSASSA_PKCS1_V1_5_SHA_512", "SHA512withRSA" }, { "RSASSA_PSS_SHA_256", "SHA256withRSAandMGF1"}, { "RSASSA_PSS_SHA_384", "SHA384withRSAandMGF1"}, { "RSASSA_PSS_SHA_512", "SHA512withRSAandMGF1"} }; } class AwsKmsStreamCalculator : IStreamCalculator { private string keyId; private string signingAlgorithm; private MemoryStream stream = new MemoryStream(); public AwsKmsStreamCalculator(string keyId, string signingAlgorithm) { this.keyId = keyId; this.signingAlgorithm = signingAlgorithm; } public Stream Stream => stream; public object GetResult() { try { using (var kmsClient = new AmazonKeyManagementServiceClient()) { SignRequest signRequest = new SignRequest() { SigningAlgorithm = signingAlgorithm, KeyId = keyId, MessageType = MessageType.RAW, Message = new MemoryStream(stream.ToArray()) }; SignResponse signResponse = kmsClient.SignAsync(signRequest).Result; return new SimpleBlockResult(signResponse.Signature.ToArray()); } } finally { stream = new MemoryStream(); } } }

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 testSignSimpleRsaSsaPss

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.

 



문의

문의가 해결되지 않았습니까? 

저희가 도와드리겠습니다. 연락해 주시면 빠르게 답변해 드리겠습니다.

문의하기
최신 정보를 받아보세요

11,000명 이상의 가입자와 함께 새로운 제품, 업데이트, 팁, 기술 솔루션 및 기회에 대한 최신 정보를 받아보시면서 iText PDF 전문가가 되어보세요.

지금 구독하기