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");
}
}
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:
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.
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.