2

I'm trying to use the signature obtained by signing the SHA256 digest of my PdfDocument using AWS KMS to apply a signature on the PDF itself. I'm not even sure if I'm going in the correct direction.

Everything runs correctly, but the produced file's signature throws an error:

Error during signature verification. ASN.1 parsing error:  Error encountered while BER decoding:

If it's important, I can retrieve the public key from AWS, but the private key is kept on their side. Most of the documentation I've seen online presupposes your access to the private key. Additionally, I'm not sure how or where to get a certificate chain from since AWS handles the signing. All of the documentation I've found requires that certificate chain as well.

Code

First, I create an empty signature field as most of the documentation instructs you to do. I think there might be an issue with PdfName.Adbe_pkcs7_detached but I don't know what else to put in its place if that is wrong.

public void addEmptySignatureField(File src, File destination, String fieldName) throws IOException, GeneralSecurityException {
    try (
            var reader = new PdfReader(src);
            var output = new FileOutputStream(destination)
    ) {
        var signer = new PdfSigner(reader, output, new StampingProperties());

        signer.getSignatureAppearance()
                .setPageRect(new Rectangle(36, 748, 200, 100))
                .setPageNumber(1)
                .setLocation("whee")
                .setSignatureCreator("Mario")
                .setReason("because")
                .setLayer2FontSize(14f);
        signer.setFieldName(fieldName);

        IExternalSignatureContainer blankSignatureContainer = new ExternalBlankSignatureContainer(PdfName.Adobe_PPKLite,
                PdfName.Adbe_pkcs7_detached);

        // Sign the document using an blankSignatureContainer container.
        // 8192 is the size of the empty signature placeholder.
        signer.signExternalContainer(blankSignatureContainer, 8192);
    }
}

Then I attempt to sign the document:

public void completeSignature(File src, File destination, String fieldName) throws IOException, GeneralSecurityException {
    try (
            var reader = new PdfReader(src);
            var pdfDocument = new PdfDocument(reader);
            var writer = new PdfWriter(destination)
    ) {
        // Signs a PDF where space was already reserved. The field must cover the whole document.
        PdfSigner.signDeferred(pdfDocument, fieldName, writer, kmsBackedSignatureContainer);
    }
}

And for reference, kmsBackedSignatureContainer is below. fileSigner.sign returns from AWS KMS a byte[] as defined in their documentation:

this value is a DER-encoded object as defined by ANS X9.62–2005 and RFC 3279 Section 2.2.3.

public class KmsBackedSignatureContainer implements IExternalSignatureContainer
{
    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            var bytes = DigestAlgorithms.digest(data, new BouncyCastleDigest().getMessageDigest(DigestAlgorithms.SHA256));
            var derEncodedBytes = fileSigner.sign(bytes);

            return derEncodedBytes;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic)
    {
    }
}
mkl
  • 90,588
  • 15
  • 125
  • 265
Grace Atwood
  • 172
  • 10
  • 1
    An `IExternalSignatureContainer` implementation is expected to return a CMS signature *container* which is an object containing a lot of metadata in addition to the actual cryptographic signature value. You on the other hand merely have a *DER-encoded object as defined by ANS X9.62–2005 and RFC 3279 Section 2.2.3*, i.e. merely an ECDSA cryptographic signature value. This results in the "ASN.1 parsing error" when a validator attempts to parse that value as a CMS container. – mkl Nov 17 '20 at 10:04
  • I see. Thank you so much for your comment. Could I beg you to point me in the right direction for wrapping my signature in a CMS container? I know that what I'm asking is literally the same as this question https://stackoverflow.com/questions/60242213/bouncycastle-cmssigneddata-from-external-signature-and-public-key-certificate; however, even after reading a number of other answers throughout today, I'm still completely lost regarding how to wrap my signature. – Grace Atwood Nov 18 '20 at 08:04
  • Do you have you X509 certificate (complete chain would be best) available before signing? – mkl Nov 18 '20 at 10:36
  • Unfortunately, I do not. The only two things returned by AWS KMS's sign endpoint are the `byte[]` signature and the algorithm used in the signing. Their documentation is here https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html. I can also download a public key from AWS, but it doesn't contain any certificate chain information. The KMS workflow is you receive the `byte[]` signature, store it alongside the file, and then submit it back to AWS using their verify endpoint. I'm starting to suspect that AWS KMS can not be used for PDF signing. – Grace Atwood Nov 18 '20 at 17:34
  • *"I'm starting to suspect that AWS KMS can not be used for PDF signing."* - I assume it can. Probably you'll have to retrieve your public key, build a certificate request from it and either create a self-signed certificate from it or request a certificate from a generally trusted CA based on that certificate request. Then you are likely to be able to use AWS KMS for signing PDFs with that certificate. – mkl Nov 18 '20 at 17:48
  • *"**Q: Can I use asymmetric CMKs for digital signing applications that require digital certificates?** Not directly. AWS KMS doesn’t store or associate digital certificates with asymmetric CMKs it creates. You could choose to have a certificate authority such as ACM PCA issue a certificate for the public portion of your asymmetric CMK. This will allow the entities that are consuming your public key to verify that the public key indeed belongs to you.* https://aws.amazon.com/kms/faqs/ – mkl Nov 18 '20 at 17:55
  • Thank you so much for your time so far - you've been incredibly helpful. I think I'm just incredibly out of my depth here. I know how to sign a CSR with a CA's certificate, and I tried to go down this path when I started out on this problem. However, generating a CSR requires the private key of the asymmetric pair doesn't it? I don't have access to the private key, which is managed by AWS. – Grace Atwood Nov 18 '20 at 19:03
  • *"However, generating a CSR requires the private key of the asymmetric pair doesn't it?"* - Only to sign the request. Thus, you can retrieve a signature for that CSR using the AWS KMS Sign method. There actually is a dedicated openssl engine for KMS, see https://github.com/nakedible/openssl-engine-kms - I have not tried it, though, so I cannot guarantee it's working. – mkl Nov 18 '20 at 19:08
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/224747/discussion-between-daniel-atwood-and-mkl). – Grace Atwood Nov 18 '20 at 19:16

2 Answers2

7

In the context of this answer 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.

Generating a Certificate for an AWS KMS Key Pair

First of all, AWS KMS signs using a plain asymmetric key pair, it does not provide a X.509 certificate for the public key. Interoperable PDF signatures require a X.509 certificate for the public key, though, to establish trust in the signature. Thus, the first step to take for interoperable AWS KMS PDF signing is to generate a 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:

public static Certificate generateSelfSignedCertificate(String keyId, String subjectDN) throws IOException, GeneralSecurityException {
    long now = System.currentTimeMillis();
    Date startDate = new Date(now);

    X500Name dnName = new X500Name(subjectDN);
    BigInteger certSerialNumber = new BigInteger(Long.toString(now));

    Calendar calendar = Calendar.getInstance();
    calendar.setTime(startDate);
    calendar.add(Calendar.YEAR, 1);

    Date endDate = calendar.getTime();

    PublicKey publicKey = null;
    SigningAlgorithmSpec signingAlgorithmSpec = null;
    try (   KmsClient kmsClient = KmsClient.create() ) {
        GetPublicKeyResponse response = kmsClient.getPublicKey(GetPublicKeyRequest.builder().keyId(keyId).build());
        SubjectPublicKeyInfo spki = SubjectPublicKeyInfo.getInstance(response.publicKey().asByteArray());
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
        publicKey = converter.getPublicKey(spki);
        List<SigningAlgorithmSpec> signingAlgorithms = response.signingAlgorithms();
        if (signingAlgorithms != null && !signingAlgorithms.isEmpty())
            signingAlgorithmSpec = signingAlgorithms.get(0);
    }
    JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(dnName, certSerialNumber, startDate, endDate, dnName, publicKey);

    ContentSigner contentSigner = new AwsKmsContentSigner(keyId, signingAlgorithmSpec);

    BasicConstraints basicConstraints = new BasicConstraints(true);
    certBuilder.addExtension(new ASN1ObjectIdentifier("2.5.29.19"), true, basicConstraints);

    return new JcaX509CertificateConverter().setProvider("BC").getCertificate(certBuilder.build(contentSigner));
}

(CertificateUtils helper method)

The AwsKmsContentSigner class used in the code above is this implementation of the BouncyCastle interface ContentSigner:

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

(AwsKmsContentSigner)

For production purposes you'll usually want to use a certificate signed by a trusted CA. Similarly to the 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.

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:

public class AwsKmsSignature implements IExternalSignature {
    public AwsKmsSignature(String keyId) {
        this.keyId = keyId;

        try (   KmsClient kmsClient = KmsClient.create() ) {
            GetPublicKeyRequest getPublicKeyRequest = GetPublicKeyRequest.builder()
                    .keyId(keyId)
                    .build();
            GetPublicKeyResponse getPublicKeyResponse = kmsClient.getPublicKey(getPublicKeyRequest);
            signingAlgorithmSpec = getPublicKeyResponse.signingAlgorithms().get(0);
            switch(signingAlgorithmSpec) {
            case ECDSA_SHA_256:
            case ECDSA_SHA_384:
            case ECDSA_SHA_512:
            case RSASSA_PKCS1_V1_5_SHA_256:
            case RSASSA_PKCS1_V1_5_SHA_384:
            case RSASSA_PKCS1_V1_5_SHA_512:
                break;
            case RSASSA_PSS_SHA_256:
            case RSASSA_PSS_SHA_384:
            case RSASSA_PSS_SHA_512:
                throw new IllegalArgumentException(String.format("Signing algorithm %s not supported directly by iText", signingAlgorithmSpec));
            default:
                throw new IllegalArgumentException(String.format("Unknown signing algorithm: %s", signingAlgorithmSpec));
            }
        }
    }

    @Override
    public String getHashAlgorithm() {
        switch(signingAlgorithmSpec) {
        case ECDSA_SHA_256:
        case RSASSA_PKCS1_V1_5_SHA_256:
            return "SHA-256";
        case ECDSA_SHA_384:
        case RSASSA_PKCS1_V1_5_SHA_384:
            return "SHA-384";
        case ECDSA_SHA_512:
        case RSASSA_PKCS1_V1_5_SHA_512:
            return "SHA-512";
        default:
            return null;
        }
    }

    @Override
    public String getEncryptionAlgorithm() {
        switch(signingAlgorithmSpec) {
        case ECDSA_SHA_256:
        case ECDSA_SHA_384:
        case ECDSA_SHA_512:
            return "ECDSA";
        case RSASSA_PKCS1_V1_5_SHA_256:
        case RSASSA_PKCS1_V1_5_SHA_384:
        case RSASSA_PKCS1_V1_5_SHA_512:
            return "RSA";
        default:
            return null;
        }
    }

    @Override
    public byte[] sign(byte[] message) throws GeneralSecurityException {
        try (   KmsClient kmsClient = KmsClient.create() ) {
            SignRequest signRequest = SignRequest.builder()
                    .signingAlgorithm(signingAlgorithmSpec)
                    .keyId(keyId)
                    .messageType(MessageType.RAW)
                    .message(SdkBytes.fromByteArray(message))
                    .build();
            SignResponse signResponse = kmsClient.sign(signRequest);
            return signResponse.signature().asByteArray();
        }
    }

    final String keyId;
    final SigningAlgorithmSpec signingAlgorithmSpec;
}

(AwsKmsSignature)

In the constructor we select a signing algorithm available for the key in question. This actually is done quite haphazardly here, 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.

Putting It Into Action

Assuming your AWS KMS signing key pair has the alias SigningExamples-ECC_NIST_P256 you can use the code above like this to sign a PDF:

BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);

String keyId = "alias/SigningExamples-ECC_NIST_P256";
AwsKmsSignature signature = new AwsKmsSignature(keyId);
Certificate certificate = CertificateUtils.generateSelfSignedCertificate(keyId, "CN=AWS KMS PDF Signing Test,OU=mkl tests,O=mkl");

try (   PdfReader pdfReader = new PdfReader(PDF_TO_SIGN);
        OutputStream result = new FileOutputStream(SIGNED_PDF)) {
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().useAppendMode());

    IExternalDigest externalDigest = new BouncyCastleDigest();
    pdfSigner.signDetached(externalDigest , signature, new Certificate[] {certificate}, null, null, null, 0, CryptoStandard.CMS);
}

(TestSignSimple test testSignSimpleEcdsa)

Signing a PDF Using an AWS KMS Key Pair Revisited

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

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)

In the constructor we also select a signing algorithm available for the key in question. Here, though, we allow a function parameter that permits the caller to choose among the available signature algorithms. This is necessary in particular for RSASSA-PSS use.

Putting It Into Action Revisited

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:

BouncyCastleProvider provider = new BouncyCastleProvider();
Security.addProvider(provider);

String keyId = "alias/SigningExamples-RSA_2048";
X509Certificate certificate = CertificateUtils.generateSelfSignedCertificate(keyId, "CN=AWS KMS PDF Signing Test,OU=mkl tests,O=mkl");
AwsKmsSignatureContainer signatureContainer = new AwsKmsSignatureContainer(certificate, keyId, TestSignSimple::selectRsaSsaPss);

try (   PdfReader pdfReader = new PdfReader(PDF_TO_SIGN);
        OutputStream result = new FileOutputStream(SIGNED_PDF)) {
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().useAppendMode());

    pdfSigner.signExternalContainer(signatureContainer, 8192);
}

(TestSignSimple test testSignSimpleRsaSsaPss)

with this selector function

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;
}

(TestSignSimple helper method)

Mass-Signing Considerations

If you plan to do mass-signing using AWS KMS, please 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 2020-12-15)

The RSA and ECC cryptographic operations request rates likely are not a problem. Or more to the point, if they are a problem, AWS KMS most likely is not the right signing product for your needs; instead you should 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 AwsKmsSignature and AwsKmsSignatureContainer in their respective constructors call that method. Naive mass signing code based on them, therefore, would be limited to 5 signatures per second.

Depending on your use case there are different strategies to tackle this problem.

If only very few instances of your signing code are running concurrently and they are using a very few different keys only, you can simply re-use your AwsKmsSignature and AwsKmsSignatureContainer objects, either creating them at start-up or on-demand and then caching them.

Otherwise, though, you should refactor the use of the GetPublicKey method out of the AwsKmsSignature and AwsKmsSignatureContainer constructors. It is used in there only to determine which AWS KMS signing algorithm identifier to use when signing with the key in question. Obviously you can store that identifier together with the key identifier, making that GetPublicKey call unnecessary.

mkl
  • 90,588
  • 15
  • 125
  • 265
2

My original answer shows how to sign PDFs with AWS KMS and iText 7 for Java. For completeness sake I ported that to .Net. As I used genuine .Net classes for the creation of the self-signed certificate and as there are some differences in the AWS KMS and BouncyCastle APIs between their Java and .Net versions, the code differs not only in method name capitalization...

CertificateUtils

.Net offers its own means for the creation of certificate requests and self-signed certificates, the CertificateRequest class.

Similarly to the BouncyCastle/Java implementation in the other answer, this class also has the actual signature creation (for the self-signed certificate) delegated to a helper, here a 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 code below. Fortunately we can re-use .Net variants of X509SignatureGenerator for all methods except the actual signing method SignData.

public static X509Certificate2 generateSelfSignedCertificate(string keyId, string subjectDN, Func<List<string>, string> selector)
{
    string signingAlgorithm = null;
    using (var kmsClient = new AmazonKeyManagementServiceClient())
    {
        GetPublicKeyRequest getPublicKeyRequest = new GetPublicKeyRequest() { KeyId = keyId };
        GetPublicKeyResponse getPublicKeyResponse = kmsClient.GetPublicKeyAsync(getPublicKeyRequest).Result;
        List<string> signingAlgorithms = getPublicKeyResponse.SigningAlgorithms;
        signingAlgorithm = selector.Invoke(signingAlgorithms);
        byte[] spkiBytes = getPublicKeyResponse.PublicKey.ToArray();

        CertificateRequest certificateRequest = null;
        X509SignatureGenerator simpleGenerator = null;
        string keySpecString = getPublicKeyResponse.CustomerMasterKeySpec.ToString();
        if (keySpecString.StartsWith("ECC"))
        {
            ECDsa ecdsa = ECDsa.Create();
            int bytesRead = 0;
            ecdsa.ImportSubjectPublicKeyInfo(new ReadOnlySpan<byte>(spkiBytes), out bytesRead);
            certificateRequest = new CertificateRequest(subjectDN, ecdsa, getHashAlgorithmName(signingAlgorithm));
            simpleGenerator = X509SignatureGenerator.CreateForECDsa(ecdsa);
        }
        else if (keySpecString.StartsWith("RSA"))
        {
            RSA rsa = RSA.Create();
            int bytesRead = 0;
            rsa.ImportSubjectPublicKeyInfo(new ReadOnlySpan<byte>(spkiBytes), out bytesRead);
            RSASignaturePadding rsaSignaturePadding = getSignaturePadding(signingAlgorithm);
            certificateRequest = new CertificateRequest(subjectDN, rsa, getHashAlgorithmName(signingAlgorithm), rsaSignaturePadding);
            simpleGenerator = X509SignatureGenerator.CreateForRSA(rsa, rsaSignaturePadding);
        }
        else
        {
            throw new ArgumentException("Cannot determine encryption algorithm for " + keySpecString, nameof(keyId));
        }

        X509SignatureGenerator generator = new SignatureGenerator(keyId, signingAlgorithm, simpleGenerator);
        X509Certificate2 certificate = certificateRequest.Create(new X500DistinguishedName(subjectDN), generator, System.DateTimeOffset.Now, System.DateTimeOffset.Now.AddYears(2), new byte[] { 17 });
        return certificate;
    }
}

public static HashAlgorithmName getHashAlgorithmName(string signingAlgorithm)
{
    if (signingAlgorithm.Contains("SHA_256"))
    {
        return HashAlgorithmName.SHA256;
    }
    else if (signingAlgorithm.Contains("SHA_384"))
    {
        return HashAlgorithmName.SHA384;
    }
    else if (signingAlgorithm.Contains("SHA_512"))
    {
        return HashAlgorithmName.SHA512;
    }
    else
    {
        throw new ArgumentException("Cannot determine hash algorithm for " + signingAlgorithm, nameof(signingAlgorithm));
    }
}

public static RSASignaturePadding getSignaturePadding(string signingAlgorithm)
{
    if (signingAlgorithm.StartsWith("RSASSA_PKCS1_V1_5"))
    {
        return RSASignaturePadding.Pkcs1;
    }
    else if (signingAlgorithm.StartsWith("RSASSA_PSS"))
    {
        return RSASignaturePadding.Pss;
    }
    else
    {
        return null;
    }
}

class SignatureGenerator : X509SignatureGenerator
{
    public SignatureGenerator(string keyId, string signingAlgorithm, X509SignatureGenerator simpleGenerator)
    {
        this.keyId = keyId;
        this.signingAlgorithm = signingAlgorithm;
        this.simpleGenerator = simpleGenerator;
    }

    public override byte[] GetSignatureAlgorithmIdentifier(HashAlgorithmName hashAlgorithm)
    {
        HashAlgorithmName hashAlgorithmHere = getHashAlgorithmName(signingAlgorithm);
        if (hashAlgorithm != hashAlgorithmHere)
        {
            throw new ArgumentException("Hash algorithm " + hashAlgorithm + "does not match signing algorithm " + signingAlgorithm, nameof(hashAlgorithm));
        }
        return simpleGenerator.GetSignatureAlgorithmIdentifier(hashAlgorithm);
    }

    public override byte[] SignData(byte[] data, HashAlgorithmName hashAlgorithm)
    {
        HashAlgorithmName hashAlgorithmHere = getHashAlgorithmName(signingAlgorithm);
        if (hashAlgorithm != hashAlgorithmHere)
        {
            throw new ArgumentException("Hash algorithm " + hashAlgorithm + "does not match signing algorithm " + signingAlgorithm, nameof(hashAlgorithm));
        }

        using (var kmsClient = new AmazonKeyManagementServiceClient())
        {
            SignRequest signRequest = new SignRequest()
            {
                SigningAlgorithm = signingAlgorithm,
                KeyId = keyId,
                MessageType = MessageType.RAW,
                Message = new MemoryStream(data)
            };
            SignResponse signResponse = kmsClient.SignAsync(signRequest).Result;
            return signResponse.Signature.ToArray();
        }
    }

    protected override PublicKey BuildPublicKey()
    {
        return simpleGenerator.PublicKey;
    }

    string keyId;
    string signingAlgorithm;
    X509SignatureGenerator simpleGenerator;
}

(CertificateUtils.cs)

AwsKmsSignature

The AwsKmsSignature class could be ported from Java with very little changes required.

public class AwsKmsSignature : IExternalSignature
{
    public AwsKmsSignature(string keyId, Func<List<string>, string> selector)
    {
        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;
            signingAlgorithm = selector.Invoke(signingAlgorithms);
            switch(signingAlgorithm)
            {
                case "ECDSA_SHA_256":
                case "ECDSA_SHA_384":
                case "ECDSA_SHA_512":
                case "RSASSA_PKCS1_V1_5_SHA_256":
                case "RSASSA_PKCS1_V1_5_SHA_384":
                case "RSASSA_PKCS1_V1_5_SHA_512":
                    break;
                case "RSASSA_PSS_SHA_256":
                case "RSASSA_PSS_SHA_384":
                case "RSASSA_PSS_SHA_512":
                    throw new ArgumentException(String.Format("Signing algorithm {0} not supported directly by iText", signingAlgorithm));
                default:
                    throw new ArgumentException(String.Format("Unknown signing algorithm: {0}", signingAlgorithm));
            }
        }
    }

    public string GetEncryptionAlgorithm()
    {
        switch (signingAlgorithm)
        {
            case "ECDSA_SHA_256":
            case "ECDSA_SHA_384":
            case "ECDSA_SHA_512":
                return "ECDSA";
            case "RSASSA_PKCS1_V1_5_SHA_256":
            case "RSASSA_PKCS1_V1_5_SHA_384":
            case "RSASSA_PKCS1_V1_5_SHA_512":
                return "RSA";
            default:
                return null;
        }
    }

    public string GetHashAlgorithm()
    {
        switch (signingAlgorithm)
        {
            case "ECDSA_SHA_256":
            case "RSASSA_PKCS1_V1_5_SHA_256":
                return "SHA-256";
            case "ECDSA_SHA_384":
            case "RSASSA_PKCS1_V1_5_SHA_384":
                return "SHA-384";
            case "ECDSA_SHA_512":
            case "RSASSA_PKCS1_V1_5_SHA_512":
                return "SHA-512";
            default:
                return null;
        }
    }

    public byte[] Sign(byte[] message)
    {
        using (var kmsClient = new AmazonKeyManagementServiceClient())
        {
            SignRequest signRequest = new SignRequest() {
                SigningAlgorithm = signingAlgorithm,
                KeyId=keyId,
                MessageType=MessageType.RAW,
                Message=new MemoryStream(message)
            };
            SignResponse signResponse = kmsClient.SignAsync(signRequest).Result;
            return signResponse.Signature.ToArray();
        }
    }

    string keyId;
    string signingAlgorithm;
}

(AwsKmsSignature.cs)

AwsKmsSignatureContainer

The AwsKmsSignatureContainer class uses BouncyCastle to build the CMS signature container to embed, just like in the Java version in the other answer.

There are certain differences in the BouncyCastle APIs, though. 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 the actual pendant of the ContentSigner in Java. The implementations of these interfaces are AwsKmsSignatureFactory and AwsKmsStreamCalculator below.

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);

        X509CollectionStoreParameters collectionStoreParameters = new X509CollectionStoreParameters(new List<X509Certificate> { 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<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
{
    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.cs)

Putting it into action

With the same preconditions concerning the configuration of credentials and default regions as in the other answer, one can use the classes above to sign PDFs with AWS KMS keys using iText for .Net as follows, assuming one's AWS KMS signing key pairs have the aliases SigningExamples-ECC_NIST_P256 and SigningExamples-RSA_2048 matching their algorithms respectively.

ECDSA Signing

string keyId = "alias/SigningExamples-ECC_NIST_P256";
Func<System.Collections.Generic.List<string>, string> selector = list => list.Find(name => name.StartsWith("ECDSA_SHA_256"));
AwsKmsSignature signature = new AwsKmsSignature(keyId, selector);
System.Security.Cryptography.X509Certificates.X509Certificate2 certificate2 = CertificateUtils.generateSelfSignedCertificate(
    keyId,
    "CN=AWS KMS PDF Signing Test ECDSA,OU=mkl tests,O=mkl",
    selector
);
X509Certificate certificate = new X509Certificate(X509CertificateStructure.GetInstance(certificate2.RawData));

using (PdfReader pdfReader = new PdfReader(PDF_TO_SIGN))
using (FileStream result = File.Create(SIGNED_PDF))
{
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().UseAppendMode());

    pdfSigner.SignDetached(signature, new X509Certificate[] { certificate }, null, null, null, 0, CryptoStandard.CMS);
}

(TestSignSimple.cs test testSignSimpleEcdsa)

RSA (PKCS#1 v1.5 padding) Signing

string keyId = "alias/SigningExamples-RSA_2048";
Func<System.Collections.Generic.List<string>, string> selector = list => list.Find(name => name.StartsWith("RSASSA_PKCS1_V1_5"));
AwsKmsSignature signature = new AwsKmsSignature(keyId, selector);
System.Security.Cryptography.X509Certificates.X509Certificate2 certificate2 = CertificateUtils.generateSelfSignedCertificate(
    keyId,
    "CN=AWS KMS PDF Signing Test RSA,OU=mkl tests,O=mkl",
    selector
);
X509Certificate certificate = new X509Certificate(X509CertificateStructure.GetInstance(certificate2.RawData));

using (PdfReader pdfReader = new PdfReader(PDF_TO_SIGN))
using (FileStream result = File.Create(SIGNED_PDF))
{
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().UseAppendMode());

    pdfSigner.SignDetached(signature, new X509Certificate[] { certificate }, null, null, null, 0, CryptoStandard.CMS);
} 

(TestSignSimple.cs test testSignSimpleRsa)

RSASSA-PSS signing

string keyId = "alias/SigningExamples-RSA_2048";
Func<System.Collections.Generic.List<string>, string> selector = list => list.Find(name => name.StartsWith("RSASSA_PSS"));
System.Security.Cryptography.X509Certificates.X509Certificate2 certificate2 = CertificateUtils.generateSelfSignedCertificate(
    keyId,
    "CN=AWS KMS PDF Signing Test RSAwithMGF1,OU=mkl tests,O=mkl",
    selector
);
X509Certificate certificate = new X509Certificate(X509CertificateStructure.GetInstance(certificate2.RawData));
AwsKmsSignatureContainer signature = new AwsKmsSignatureContainer(certificate, keyId, selector);

using (PdfReader pdfReader = new PdfReader(PDF_TO_SIGN))
using (FileStream result = File.Create(SIGNED_PDF))
{
    PdfSigner pdfSigner = new PdfSigner(pdfReader, result, new StampingProperties().UseAppendMode());

    pdfSigner.SignExternalContainer(signature, 8192);
} 

(TestSignSimple.cs test testSignSimpleRsaSsaPss)

mkl
  • 90,588
  • 15
  • 125
  • 265
  • do we get a "trusted certificate" label in the pdf, using this method? thanks – Giona Granata Jul 26 '21 at 10:13
  • *do we get a "trusted certificate" label in the pdf, using this method?* - I don't know what kind of label you mean. But whether or not the certificate is trusted by someone, depends on how your certificate is generated. See my other, java centric answer: *For testing purposes you can create a self signed certificate. ... For production purposes you'll usually want to use a certificate signed by a trusted CA. Similarly to the 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.* – mkl Jul 26 '21 at 19:13