Skip to main content
Skip table of contents

Using iText and AWS KMS to digitally sign a PDF document: Part 5

This example was written for the article "Using iText and AWS KMS to digitally sign a PDF document" and shows an implementation of IExternalSignatureContainer instead of IExternalSignature for signing While IExternalSignature is the easiest way, there are some drawbacks as the PdfPKCS7 class does not support RSASSA-PSS usage, and for ECDSA signatures it uses the wrong OID as the signature algorithm OID.

To avoid these issues we can 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
JAVA
package com.itextpdf.signingexamples.aws.kms;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;

import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.signatures.IExternalSignatureContainer;

import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.GetPublicKeyRequest;
import software.amazon.awssdk.services.kms.model.GetPublicKeyResponse;
import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec;

/**
 * @author mkl
 */
public class AwsKmsSignatureContainer implements IExternalSignatureContainer {
    public AwsKmsSignatureContainer(X509Certificate x509Certificate, String keyId) {
        this(x509Certificate, keyId, a -> a != null && a.size() > 0 ? a.get(0) : null);
    }

    public AwsKmsSignatureContainer(X509Certificate x509Certificate, String keyId, Function<List<SigningAlgorithmSpec>, SigningAlgorithmSpec> selector) {
        this.x509Certificate = x509Certificate;
        this.keyId = keyId;

        try (   KmsClient kmsClient = KmsClient.create() ) {
            GetPublicKeyRequest getPublicKeyRequest = GetPublicKeyRequest.builder()
                    .keyId(keyId)
                    .build();
            GetPublicKeyResponse getPublicKeyResponse = kmsClient.getPublicKey(getPublicKeyRequest);
            signingAlgorithmSpec = selector.apply(getPublicKeyResponse.signingAlgorithms());
            if (signingAlgorithmSpec == null)
                throw new IllegalArgumentException("KMS key has no signing algorithms");
            contentSigner = new AwsKmsContentSigner(keyId, signingAlgorithmSpec);
        }
    }

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            CMSTypedData msg = new CMSTypedDataInputStream(data);

            X509CertificateHolder signCert = new X509CertificateHolder(x509Certificate.getEncoded());

            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

            gen.addSignerInfoGenerator(
                    new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build())
                            .build(contentSigner, signCert));

            gen.addCertificates(new JcaCertStore(Collections.singleton(signCert)));

            CMSSignedData sigData = gen.generate(msg, false);
            return sigData.getEncoded();
        } catch (IOException | OperatorCreationException | CMSException e) {
            throw new GeneralSecurityException(e);
        }
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {
        signDic.put(PdfName.Filter, new PdfName("MKLx_AWS_KMS_SIGNER"));
        signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
    }

    final X509Certificate x509Certificate;
    final String keyId;
    final SigningAlgorithmSpec signingAlgorithmSpec;
    final ContentSigner contentSigner;

    class CMSTypedDataInputStream implements CMSTypedData {
        InputStream in;

        public CMSTypedDataInputStream(InputStream is) {
            in = is;
        }

        @Override
        public ASN1ObjectIdentifier getContentType() {
            return PKCSObjectIdentifiers.data;
        }

        @Override
        public Object getContent() {
            return in;
        }

        @Override
        public void write(OutputStream out) throws IOException,
                CMSException {
            byte[] buffer = new byte[8 * 1024];
            int read;
            while ((read = in.read(buffer)) != -1) {
                out.write(buffer, 0, read);
            }
            in.close();
        }
    }
}


AwsKmsSignatureContainer
C#
using Amazon.KeyManagementService;
using Amazon.KeyManagementService.Model;
using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Cms;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.X509.Store;
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Org.BouncyCastle.Utilities.Collections;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;

namespace iText.SigningExamples.AwsKms
{
    public class AwsKmsSignatureContainer : IExternalSignatureContainer
    {
        public AwsKmsSignatureContainer(X509Certificate x509Certificate, string keyId, Func<List<string>, 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<string> 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);
            
            IStore<X509Certificate> store =CollectionUtilities.CreateStore(new List<X509Certificate> { x509Certificate });
            gen.AddCertificates(store);

            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<IBlockResult> CreateCalculator()
        {
            return new AwsKmsStreamCalculator(keyId, signingAlgorithm);
        }

        static Dictionary<string, string> signingAlgorithmNameBySpec = new Dictionary<string, string>()
        {
            { "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<IBlockResult>
    {
        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 IBlockResult 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();
            }
        }
    }
}


Note: The article assumes 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 code examples written for this article.

For the other examples relating to this article, please see the following links:


JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.