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