Click here to Skip to main content
14,983,071 members
Articles / Hosted Services / AWS
Article
Posted 1 Mar 2021

Stats

3.2K views
3 bookmarked

Using iText 7 PDF library and AWS KMS to Digitally Sign a PDF Document

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
1 Mar 2021CPOL9 min read
How to use iText 7 and AWS KMS APIs to generate a digital signature and add it to 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 article shows developers the steps to use iText 7 and the AWS KMS APIs to generate a digital signature and add it to a PDF document.

Introduction

iText has long been involved with PDF digital signatures. The digital signatures eBook was first published 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, iText continued to promote the technology for secure PDF documents, as it provides integrity, authenticity, non-repudiation, and assurance of when a document was signed. The PDF library 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:

Java
  1  public static Certificate generateSelfSignedCertificate(String keyId, String subjectDN) 
  2         throws IOException, GeneralSecurityException {
  3      long now = System.currentTimeMillis();
  4      Date startDate = new Date(now);
  5  
  6      X500Name dnName = new X500Name(subjectDN);
  7      BigInteger certSerialNumber = new BigInteger(Long.toString(now));
  8  
  9      Calendar calendar = Calendar.getInstance();
 10      calendar.setTime(startDate);
 11      calendar.add(Calendar.YEAR, 1);
 12  
 13      Date endDate = calendar.getTime();
 14  
 15      PublicKey publicKey = null;
 16      SigningAlgorithmSpec signingAlgorithmSpec = null;
 17      try (   KmsClient kmsClient = KmsClient.create() ) {
 18          GetPublicKeyResponse response = 
 19             kmsClient.getPublicKey(GetPublicKeyRequest.builder().keyId(keyId).build());
 20          SubjectPublicKeyInfo spki = 
 21             SubjectPublicKeyInfo.getInstance(response.publicKey().asByteArray());
 22          JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
 23          publicKey = converter.getPublicKey(spki);
 24          ListSigningalgorithmSpec signingAlgorithms = response.signingAlgorithms();
 25          if (signingAlgorithms != null && !signingAlgorithms.isEmpty())
 26              signingAlgorithmSpec = signingAlgorithms.get(0);
 27      }
 28      JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder
 29               (dnName, certSerialNumber, startDate, endDate, dnName, publicKey);
 30  
 31      ContentSigner contentSigner = new AwsKmsContentSigner(keyId, signingAlgorithmSpec);
 32  
 33      BasicConstraints basicConstraints = new BasicConstraints(true);
 34      certBuilder.addExtension
 35                 (new ASN1ObjectIdentifier("2.5.29.19"), true, basicConstraints);
 36  
 37      return new JcaX509CertificateConverter().setProvider("BC").getCertificate
 38                                               (certBuilder.build(contentSigner));
 39  }
C#
  1  public static X509Certificate2 generateSelfSignedCertificate
  2         (string keyId, string subjectDN, Func<list<string>, string> selector)
  3  {
  4      string signingAlgorithm = null;
  5      using (var kmsClient = new AmazonKeyManagementServiceClient())
  6      {
  7          GetPublicKeyRequest getPublicKeyRequest = 
  8                              new GetPublicKeyRequest() { KeyId = keyId };
  9          GetPublicKeyResponse getPublicKeyResponse = 
 10                   kmsClient.GetPublicKeyAsync(getPublicKeyRequest).Result;
 11          List<string> signingAlgorithms = getPublicKeyResponse.SigningAlgorithms;
 12          signingAlgorithm = selector.Invoke(signingAlgorithms);
 13          byte[] spkiBytes = getPublicKeyResponse.PublicKey.ToArray();
 14  
 15          CertificateRequest certificateRequest = null;
 16          X509SignatureGenerator simpleGenerator = null;
 17          string keySpecString = getPublicKeyResponse.CustomerMasterKeySpec.ToString();
 18          if (keySpecString.StartsWith("ECC"))
 19          {
 20              ECDsa ecdsa = ECDsa.Create();
 21              int bytesRead = 0;
 22              ecdsa.ImportSubjectPublicKeyInfo
 23                          (new ReadOnlySpan<byte>(spkiBytes), out bytesRead);
 24              certificateRequest = new CertificateRequest
 25                         (subjectDN, ecdsa, getHashAlgorithmName(signingAlgorithm));
 26              simpleGenerator = X509SignatureGenerator.CreateForECDsa(ecdsa);
 27          }
 28          else if (keySpecString.StartsWith("RSA"))
 29          {
 30              RSA rsa = RSA.Create();
 31              int bytesRead = 0;
 32              rsa.ImportSubjectPublicKeyInfo
 33                  (new ReadOnlySpan<byte>(spkiBytes), out bytesRead);
 34              RSASignaturePadding rsaSignaturePadding = 
 35                                  getSignaturePadding(signingAlgorithm);
 36              certificateRequest = new CertificateRequest
 37              (subjectDN, rsa, getHashAlgorithmName(signingAlgorithm), 
 38                                                    rsaSignaturePadding);
 39              simpleGenerator = X509SignatureGenerator.CreateForRSA
 40                                (rsa, rsaSignaturePadding);
 41          }
 42          else
 43          {
 44              throw new ArgumentException("Cannot determine encryption algorithm for " + 
 45                                           keySpecString, nameof(keyId));
 46          }
 47  
 48          X509SignatureGenerator generator = new SignatureGenerator
 49                                 (keyId, signingAlgorithm, simpleGenerator);
 50          X509Certificate2 certificate = certificateRequest.Create
 51                           (new X500DistinguishedName(subjectDN), generator, 
 52                            System.DateTimeOffset.Now, 
 53                            System.DateTimeOffset.Now.AddYears(2), 
 54                            new byte[] { 17 });
 55          return certificate;
 56      }
 57  }
 58  
 59  public static HashAlgorithmName getHashAlgorithmName(string signingAlgorithm)
 60  {
 61      if (signingAlgorithm.Contains("SHA_256"))
 62      {
 63          return HashAlgorithmName.SHA256;
 64      }
 65      else if (signingAlgorithm.Contains("SHA_384"))
 66      {
 67          return HashAlgorithmName.SHA384;
 68      }
 69      else if (signingAlgorithm.Contains("SHA_512"))
 70      {
 71          return HashAlgorithmName.SHA512;
 72      }
 73      else
 74      {
 75          throw new ArgumentException("Cannot determine hash algorithm for " + 
 76                                       signingAlgorithm, nameof(signingAlgorithm));
 77      }
 78  }
 79  
 80  public static RSASignaturePadding getSignaturePadding(string signingAlgorithm)
 81  {
 82      if (signingAlgorithm.StartsWith("RSASSA_PKCS1_V1_5"))
 83      {
 84          return RSASignaturePadding.Pkcs1;
 85      }
 86      else if (signingAlgorithm.StartsWith("RSASSA_PSS"))
 87      {
 88          return RSASignaturePadding.Pss;
 89      }
 90      else
 91      {
 92          return null;
 93      }
 94  }
 95  
 96  class SignatureGenerator : X509SignatureGenerator
 97  {
 98      public SignatureGenerator(string keyId, string signingAlgorithm, 
 99                                X509SignatureGenerator simpleGenerator)
100      {
101          this.keyId = keyId;
102          this.signingAlgorithm = signingAlgorithm;
103          this.simpleGenerator = simpleGenerator;
104      }
105  
106      public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm)
107      {
108          HashAlgorithmName hashAlgorithmHere = getHashAlgorithmName(signingAlgorithm);
109          if (hashAlgorithm != hashAlgorithmHere)
110          {
111              throw new ArgumentException("Hash algorithm " + hashAlgorithm + 
112              "does not match signing algorithm " + signingAlgorithm, nameof(hashAlgorithm));
113          }
114          return simpleGenerator.GetSignatureAlgorithmIdentifier(hashAlgorithm);
115      }
116  
117      public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm)
118      {
119          HashAlgorithmName hashAlgorithmHere = getHashAlgorithmName(signingAlgorithm);
120          if (hashAlgorithm != hashAlgorithmHere)
121          {
122              throw new ArgumentException("Hash algorithm " + hashAlgorithm + 
123              "does not match signing algorithm " + signingAlgorithm, nameof(hashAlgorithm));
124          }
125  
126          using (var kmsClient = new AmazonKeyManagementServiceClient())
127          {
128              SignRequest signRequest = new SignRequest()
129              {
130                  SigningAlgorithm = signingAlgorithm,
131                  KeyId = keyId,
132                  MessageType = MessageType.RAW,
133                  Message = new MemoryStream(data)
134              };
135              SignResponse signResponse = kmsClient.SignAsync(signRequest).Result;
136              return signResponse.Signature.ToArray();
137          }
138      }
139  
140      protected override PublicKey BuildPublicKey()
141      {
142          return simpleGenerator.PublicKey;
143      }
144  
145      string keyId;
146      string signingAlgorithm;
147      X509SignatureGenerator simpleGenerator;
148  }

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:

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

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:

Java
  1      public class AwsKmsSignature implements IExternalSignature {
  2      public AwsKmsSignature(String keyId) {
  3          this.keyId = keyId;
  4  
  5          try (   KmsClient kmsClient = KmsClient.create() ) {
  6              GetPublicKeyRequest getPublicKeyRequest = GetPublicKeyRequest.builder()
  7                      .keyId(keyId)
  8                      .build();
  9              GetPublicKeyResponse getPublicKeyResponse = 
 10                                   kmsClient.getPublicKey(getPublicKeyRequest);
 11              signingAlgorithmSpec = getPublicKeyResponse.signingAlgorithms().get(0);
 12              switch(signingAlgorithmSpec) {
 13              case ECDSA_SHA_256:
 14              case ECDSA_SHA_384:
 15              case ECDSA_SHA_512:
 16              case RSASSA_PKCS1_V1_5_SHA_256:
 17              case RSASSA_PKCS1_V1_5_SHA_384:
 18              case RSASSA_PKCS1_V1_5_SHA_512:
 19                  break;
 20              case RSASSA_PSS_SHA_256:
 21              case RSASSA_PSS_SHA_384:
 22              case RSASSA_PSS_SHA_512:
 23                  throw new IllegalArgumentException(String.format
 24                  ("Signing algorithm %s not supported directly by iText", 
 25                  signingAlgorithmSpec));
 26              default:
 27                  throw new IllegalArgumentException
 28                  (String.format("Unknown signing algorithm: %s", signingAlgorithmSpec));
 29              }
 30          }
 31      }
 32  
 33      @Override
 34      public String getHashAlgorithm() {
 35          switch(signingAlgorithmSpec) {
 36          case ECDSA_SHA_256:
 37          case RSASSA_PKCS1_V1_5_SHA_256:
 38              return "SHA-256";
 39          case ECDSA_SHA_384:
 40          case RSASSA_PKCS1_V1_5_SHA_384:
 41              return "SHA-384";
 42          case ECDSA_SHA_512:
 43          case RSASSA_PKCS1_V1_5_SHA_512:
 44              return "SHA-512";
 45          default:
 46              return null;
 47          }
 48      }
 49  
 50      @Override
 51      public String getEncryptionAlgorithm() {
 52          switch(signingAlgorithmSpec) {
 53          case ECDSA_SHA_256:
 54          case ECDSA_SHA_384:
 55          case ECDSA_SHA_512:
 56              return "ECDSA";
 57          case RSASSA_PKCS1_V1_5_SHA_256:
 58          case RSASSA_PKCS1_V1_5_SHA_384:
 59          case RSASSA_PKCS1_V1_5_SHA_512:
 60              return "RSA";
 61          default:
 62              return null;
 63          }
 64      }
 65  
 66      @Override
 67      public byte[] sign(byte[] message) throws GeneralSecurityException {
 68          try (   KmsClient kmsClient = KmsClient.create() ) {
 69              SignRequest signRequest = SignRequest.builder()
 70                      .signingAlgorithm(signingAlgorithmSpec)
 71                      .keyId(keyId)
 72                      .messageType(MessageType.RAW)
 73                      .message(SdkBytes.fromByteArray(message))
 74                      .build();
 75              SignResponse signResponse = kmsClient.sign(signRequest);
 76              return signResponse.signature().asByteArray();
 77          }
 78      }
 79  
 80      final String keyId;
 81      final SigningAlgorithmSpec signingAlgorithmSpec;
 82  }
C#
  1      public class AwsKmsSignature : IExternalSignature
  2  {
  3      public AwsKmsSignature(string keyId, Func<list<string>, string> selector)
  4      {
  5          this.keyId = keyId;
  6          using (var kmsClient = new AmazonKeyManagementServiceClient())
  7          {
  8              GetPublicKeyRequest getPublicKeyRequest = 
  9                                  new GetPublicKeyRequest() { KeyId = keyId };
 10              GetPublicKeyResponse getPublicKeyResponse = 
 11                          kmsClient.GetPublicKeyAsync(getPublicKeyRequest).Result;
 12              List<string> signingAlgorithms = getPublicKeyResponse.SigningAlgorithms;
 13              signingAlgorithm = selector.Invoke(signingAlgorithms);
 14              switch(signingAlgorithm)
 15              {
 16                  case "ECDSA_SHA_256":
 17                  case "ECDSA_SHA_384":
 18                  case "ECDSA_SHA_512":
 19                  case "RSASSA_PKCS1_V1_5_SHA_256":
 20                  case "RSASSA_PKCS1_V1_5_SHA_384":
 21                  case "RSASSA_PKCS1_V1_5_SHA_512":
 22                      break;
 23                  case "RSASSA_PSS_SHA_256":
 24                  case "RSASSA_PSS_SHA_384":
 25                  case "RSASSA_PSS_SHA_512":
 26                      throw new ArgumentException(String.Format("Signing algorithm {0} 
 27                      not supported directly by iText", signingAlgorithm));
 28                  default:
 29                      throw new ArgumentException(String.Format("Unknown signing algorithm: 
 30                                                 {0}", signingAlgorithm));
 31              }
 32          }
 33      }
 34  
 35      public string GetEncryptionAlgorithm()
 36      {
 37          switch (signingAlgorithm)
 38          {
 39              case "ECDSA_SHA_256":
 40              case "ECDSA_SHA_384":
 41              case "ECDSA_SHA_512":
 42                  return "ECDSA";
 43              case "RSASSA_PKCS1_V1_5_SHA_256":
 44              case "RSASSA_PKCS1_V1_5_SHA_384":
 45              case "RSASSA_PKCS1_V1_5_SHA_512":
 46                  return "RSA";
 47              default:
 48                  return null;
 49          }
 50      }
 51  
 52      public string GetHashAlgorithm()
 53      {
 54          switch (signingAlgorithm)
 55          {
 56              case "ECDSA_SHA_256":
 57              case "RSASSA_PKCS1_V1_5_SHA_256":
 58                  return "SHA-256";
 59              case "ECDSA_SHA_384":
 60              case "RSASSA_PKCS1_V1_5_SHA_384":
 61                  return "SHA-384";
 62              case "ECDSA_SHA_512":
 63              case "RSASSA_PKCS1_V1_5_SHA_512":
 64                  return "SHA-512";
 65              default:
 66                  return null;
 67          }
 68      }
 69  
 70      public byte[] Sign(byte[] message)
 71      {
 72          using (var kmsClient = new AmazonKeyManagementServiceClient())
 73          {
 74              SignRequest signRequest = new SignRequest() {
 75                  SigningAlgorithm = signingAlgorithm,
 76                  KeyId=keyId,
 77                  MessageType=MessageType.RAW,
 78                  Message=new MemoryStream(message)
 79              };
 80              SignResponse signResponse = kmsClient.SignAsync(signRequest).Result;
 81              return signResponse.Signature.ToArray();
 82          }
 83      }
 84  
 85      string keyId;
 86      string signingAlgorithm;
 87  }

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 few 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:

Java
  1      BouncyCastleProvider provider = new BouncyCastleProvider();
  2  Security.addProvider(provider);
  3  
  4  String keyId = "alias/SigningExamples-ECC_NIST_P256";
  5  AwsKmsSignature signature = new AwsKmsSignature(keyId);
  6  Certificate certificate = CertificateUtils.generateSelfSignedCertificate
  7              (keyId, "CN=AWS KMS PDF Signing Test,OU=mkl tests,O=mkl");
  8  
  9  try (   PdfReader pdfReader = new PdfReader(PDF_TO_SIGN);
 10          OutputStream result = new FileOutputStream(SIGNED_PDF)) {
 11      PdfSigner pdfSigner = new PdfSigner
 12                            (pdfReader, result, new StampingProperties().useAppendMode());
 13  
 14      IExternalDigest externalDigest = new BouncyCastleDigest();
 15      pdfSigner.signDetached(externalDigest , signature, 
 16         new Certificate[] {certificate}, null, null, null, 0, CryptoStandard.CMS);
 17  }
C#
  1      string keyId = "alias/SigningExamples-ECC_NIST_P256";
  2  Func<system.collections.generic.list<string>, string> selector = 
  3              list => list.Find(name => name.StartsWith("ECDSA_SHA_256"));
  4  AwsKmsSignature signature = new AwsKmsSignature(keyId, selector);
  5  System.Security.Cryptography.X509Certificates.X509Certificate2 certificate2 = 
  6                  CertificateUtils.generateSelfSignedCertificate(
  7      keyId,
  8      "CN=AWS KMS PDF Signing Test ECDSA,OU=mkl tests,O=mkl",
  9      selector
 10  );
 11  X509Certificate certificate = new X509Certificate
 12                  (X509CertificateStructure.GetInstance(certificate2.RawData));
 13  
 14  using (PdfReader pdfReader = new PdfReader(PDF_TO_SIGN))
 15  using (FileStream result = File.Create(SIGNED_PDF))
 16  {
 17      PdfSigner pdfSigner = new PdfSigner(pdfReader, result, 
 18                            new StampingProperties().UseAppendMode());
 19  
 20      pdfSigner.SignDetached(signature, new X509Certificate[] { certificate }, 
 21                             null, null, null, 0, CryptoStandard.CMS);
 22  }
 23  </system.collections.generic.list<string>

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, here we 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.

Java
  1      public class AwsKmsSignatureContainer implements IExternalSignatureContainer {
  2      public AwsKmsSignatureContainer(X509Certificate x509Certificate, String keyId) {
  3          this(x509Certificate, keyId, a -> a != null && a.size() > 0 ? a.get(0) : null);
  4      }
  5  
  6      public AwsKmsSignatureContainer(X509Certificate x509Certificate, 
  7      String keyId, Function<list<signingalgorithmspec>, SigningAlgorithmSpec> selector) {
  8          this.x509Certificate = x509Certificate;
  9          this.keyId = keyId;
 10  
 11          try (   KmsClient kmsClient = KmsClient.create() ) {
 12              GetPublicKeyRequest getPublicKeyRequest = GetPublicKeyRequest.builder()
 13                      .keyId(keyId)
 14                      .build();
 15              GetPublicKeyResponse getPublicKeyResponse = 
 16                                   kmsClient.getPublicKey(getPublicKeyRequest);
 17              signingAlgorithmSpec = 
 18                     selector.apply(getPublicKeyResponse.signingAlgorithms());
 19              if (signingAlgorithmSpec == null)
 20                  throw new IllegalArgumentException("KMS key has no signing algorithms");
 21              contentSigner = new AwsKmsContentSigner(keyId, signingAlgorithmSpec);
 22          }
 23      }
 24  
 25      @Override
 26      public byte[] sign(InputStream data) throws GeneralSecurityException {
 27          try {
 28              CMSTypedData msg = new CMSTypedDataInputStream(data);
 29  
 30              X509CertificateHolder signCert = new X509CertificateHolder
 31                                               (x509Certificate.getEncoded());
 32  
 33              CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
 34  
 35              gen.addSignerInfoGenerator(
 36                      new JcaSignerInfoGeneratorBuilder
 37                      (new JcaDigestCalculatorProviderBuilder().setProvider("BC").build())
 38                              .build(contentSigner, signCert));
 39  
 40              gen.addCertificates(new JcaCertStore(Collections.singleton(signCert)));
 41  
 42              CMSSignedData sigData = gen.generate(msg, false);
 43              return sigData.getEncoded();
 44          } catch (IOException | OperatorCreationException | CMSException e) {
 45              throw new GeneralSecurityException(e);
 46          }
 47      }
 48  
 49      @Override
 50      public void modifySigningDictionary(PdfDictionary signDic) {
 51          signDic.put(PdfName.Filter, new PdfName("MKLx_AWS_KMS_SIGNER"));
 52          signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
 53      }
 54  
 55      final X509Certificate x509Certificate;
 56      final String keyId;
 57      final SigningAlgorithmSpec signingAlgorithmSpec;
 58      final ContentSigner contentSigner;
 59  
 60      class CMSTypedDataInputStream implements CMSTypedData {
 61          InputStream in;
 62  
 63          public CMSTypedDataInputStream(InputStream is) {
 64              in = is;
 65          }
 66  
 67          @Override
 68          public ASN1ObjectIdentifier getContentType() {
 69              return PKCSObjectIdentifiers.data;
 70          }
 71  
 72          @Override
 73          public Object getContent() {
 74              return in;
 75          }
 76  
 77          @Override
 78          public void write(OutputStream out) throws IOException,
 79                  CMSException {
 80              byte[] buffer = new byte[8 * 1024];
 81              int read;
 82              while ((read = in.read(buffer)) != -1) {
 83                  out.write(buffer, 0, read);
 84              }
 85              in.close();
 86          }
 87      }
 88  }
C#
  1      public class AwsKmsSignatureContainer : IExternalSignatureContainer
  2  {
  3      public AwsKmsSignatureContainer(X509Certificate x509Certificate, 
  4                                      string keyId, Func, string> selector)
  5      {
  6          this.x509Certificate = x509Certificate;
  7          this.keyId = keyId;
  8  
  9          using (var kmsClient = new AmazonKeyManagementServiceClient())
 10          {
 11              GetPublicKeyRequest getPublicKeyRequest = 
 12                                  new GetPublicKeyRequest() { KeyId = keyId };
 13              GetPublicKeyResponse getPublicKeyResponse = 
 14                       kmsClient.GetPublicKeyAsync(getPublicKeyRequest).Result;
 15              List signingAlgorithms = getPublicKeyResponse.SigningAlgorithms;
 16              this.signingAlgorithm = selector.Invoke(signingAlgorithms);
 17              if (signingAlgorithm == null)
 18                  throw new ArgumentException
 19                        ("KMS key has no signing algorithms", nameof(keyId));
 20              signatureFactory = new AwsKmsSignatureFactory(keyId, signingAlgorithm);
 21          }
 22      }
 23  
 24      public void ModifySigningDictionary(PdfDictionary signDic)
 25      {
 26          signDic.Put(PdfName.Filter, new PdfName("MKLx_AWS_KMS_SIGNER"));
 27          signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
 28      }
 29  
 30      public byte[] Sign(Stream data)
 31      {
 32          CmsProcessable msg = new CmsProcessableInputStream(data);
 33  
 34          CmsSignedDataGenerator gen = new CmsSignedDataGenerator();
 35  
 36          SignerInfoGenerator signerInfoGenerator = new SignerInfoGeneratorBuilder()
 37              .WithSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator())
 38              .Build(signatureFactory, x509Certificate);
 39          gen.AddSignerInfoGenerator(signerInfoGenerator);
 40  
 41          X509CollectionStoreParameters collectionStoreParameters = 
 42                        new X509CollectionStoreParameters(new List { x509Certificate });
 43          IX509Store collectionStore = 
 44               X509StoreFactory.Create
 45                   ("CERTIFICATE/COLLECTION", collectionStoreParameters);
 46          gen.AddCertificates(collectionStore);
 47  
 48          CmsSignedData sigData = gen.Generate(msg, false);
 49          return sigData.GetEncoded();
 50      }
 51  
 52      X509Certificate x509Certificate;
 53      String keyId;
 54      string signingAlgorithm;
 55      ISignatureFactory signatureFactory;
 56  }
 57  
 58  class AwsKmsSignatureFactory : ISignatureFactory
 59  {
 60      private string keyId;
 61      private string signingAlgorithm;
 62      private AlgorithmIdentifier signatureAlgorithm;
 63  
 64      public AwsKmsSignatureFactory(string keyId, string signingAlgorithm)
 65      {
 66          this.keyId = keyId;
 67          this.signingAlgorithm = signingAlgorithm;
 68          string signatureAlgorithmName = signingAlgorithmNameBySpec[signingAlgorithm];
 69          if (signatureAlgorithmName == null)
 70              throw new ArgumentException("Unknown signature algorithm " + 
 71                    signingAlgorithm, nameof(signingAlgorithm));
 72  
 73          // Special treatment because of issue https://github.com/bcgit/bc-csharp/issues/250
 74          switch (signatureAlgorithmName.ToUpperInvariant())
 75          {
 76              case "SHA256WITHECDSA":
 77                  this.signatureAlgorithm = 
 78                       new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha256);
 79                  break;
 80              case "SHA512WITHECDSA":
 81                  this.signatureAlgorithm = 
 82                       new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha512);
 83                  break;
 84              default:
 85                  this.signatureAlgorithm = 
 86                  new DefaultSignatureAlgorithmIdentifierFinder().
 87                                      Find(signatureAlgorithmName);
 88                  break;
 89          }
 90      }
 91  
 92      public object AlgorithmDetails => signatureAlgorithm;
 93  
 94      public IStreamCalculator CreateCalculator()
 95      {
 96          return new AwsKmsStreamCalculator(keyId, signingAlgorithm);
 97      }
 98  
 99      static Dictionary signingAlgorithmNameBySpec = new Dictionary()
100      {
101          { "ECDSA_SHA_256", "SHA256withECDSA" },
102          { "ECDSA_SHA_384", "SHA384withECDSA" },
103          { "ECDSA_SHA_512", "SHA512withECDSA" },
104          { "RSASSA_PKCS1_V1_5_SHA_256", "SHA256withRSA" },
105          { "RSASSA_PKCS1_V1_5_SHA_384", "SHA384withRSA" },
106          { "RSASSA_PKCS1_V1_5_SHA_512", "SHA512withRSA" },
107          { "RSASSA_PSS_SHA_256", "SHA256withRSAandMGF1"},
108          { "RSASSA_PSS_SHA_384", "SHA384withRSAandMGF1"},
109          { "RSASSA_PSS_SHA_512", "SHA512withRSAandMGF1"}
110      };
111  }
112  
113  class AwsKmsStreamCalculator : IStreamCalculator
114  {
115      private string keyId;
116      private string signingAlgorithm;
117      private MemoryStream stream = new MemoryStream();
118  
119      public AwsKmsStreamCalculator(string keyId, string signingAlgorithm)
120      {
121          this.keyId = keyId;
122          this.signingAlgorithm = signingAlgorithm;
123      }
124  
125      public Stream Stream => stream;
126  
127      public object GetResult()
128      {
129          try
130          {
131              using (var kmsClient = new AmazonKeyManagementServiceClient())
132              {
133                  SignRequest signRequest = new SignRequest()
134                  {
135                      SigningAlgorithm = signingAlgorithm,
136                      KeyId = keyId,
137                      MessageType = MessageType.RAW,
138                      Message = new MemoryStream(stream.ToArray())
139                  };
140                  SignResponse signResponse = kmsClient.SignAsync(signRequest).Result;
141                  return new SimpleBlockResult(signResponse.Signature.ToArray());
142              }
143          }
144          finally
145          {
146              stream = new MemoryStream();
147          }
148      }
149  }

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:

Java
  1      BouncyCastleProvider provider = new BouncyCastleProvider();
  2  Security.addProvider(provider);
  3  
  4  String keyId = "alias/SigningExamples-RSA_2048";
  5  X509Certificate certificate = CertificateUtils.generateSelfSignedCertificate
  6                                (keyId, "CN=AWS KMS PDF Signing Test,OU=mkl tests,O=mkl");
  7  AwsKmsSignatureContainer signatureContainer = new AwsKmsSignatureContainer
  8                           (certificate, keyId, TestSignSimple::selectRsaSsaPss);
  9  
 10  try (   PdfReader pdfReader = new PdfReader(PDF_TO_SIGN);
 11          OutputStream result = new FileOutputStream(SIGNED_PDF)) {
 12      PdfSigner pdfSigner = new PdfSigner(pdfReader, result, 
 13                            new StampingProperties().useAppendMode());
 14  
 15      pdfSigner.signExternalContainer(signatureContainer, 8192);
 16  }
C#
  1      string keyId = "alias/SigningExamples-RSA_2048";
  2  Func<system.collections.generic.list<string>, string> selector = 
  3              list => list.Find(name => name.StartsWith("RSASSA_PSS"));
  4  System.Security.Cryptography.X509Certificates.X509Certificate2 certificate2 = 
  5                  CertificateUtils.generateSelfSignedCertificate(
  6      keyId,
  7      "CN=AWS KMS PDF Signing Test RSAwithMGF1,OU=mkl tests,O=mkl",
  8      selector
  9  );
 10  X509Certificate certificate = new X509Certificate
 11                  (X509CertificateStructure.GetInstance(certificate2.RawData));
 12  AwsKmsSignatureContainer signature = 
 13                 new AwsKmsSignatureContainer(certificate, keyId, selector);
 14  
 15  using (PdfReader pdfReader = new PdfReader(PDF_TO_SIGN))
 16  using (FileStream result = File.Create(SIGNED_PDF))
 17  {
 18      PdfSigner pdfSigner = new PdfSigner(pdfReader, result, 
 19                            new StampingProperties().UseAppendMode());
 20  
 21      pdfSigner.SignExternalContainer(signature, 8192);
 22  }
 23  </system.collections.generic.list<string>

testSignSimple test testSignSimpleRsaSsaPss

With this selector function for Java:

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

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

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

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.

History

  • 1st March, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

CptCavemn
Product Manager
Belgium Belgium
Ever-expanding my knowledge of C# and JAVA.
Specialized in document workflow automation.

Comments and Discussions

 
-- There are no messages in this forum --