使用 AWS KMS 返回的数字签名签署 PdfDocument

Posted

技术标签:

【中文标题】使用 AWS KMS 返回的数字签名签署 PdfDocument【英文标题】:Sign a PdfDocument using the digital signature returned by AWS KMS 【发布时间】:2021-02-28 05:27:56 【问题描述】:

我正在尝试使用通过使用 AWS KMS 对我的 PdfDocument 的 SHA256 摘要进行签名而获得的签名,以在 PDF 本身上应用签名。我什至不确定我是否朝着正确的方向前进。

一切运行正常,但生成文件的签名抛出错误:

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

如果这很重要,我可以从 AWS 检索公钥,但私钥保留在他们身边。我在网上看到的大多数文档都预设了您对私钥的访问权限。此外,由于 AWS 处理签名,我不确定如何或从何处获取证书链。我找到的所有文档也需要该证书链。

代码

首先,我按照大多数文档的指示创建一个空签名字段。我认为PdfName.Adbe_pkcs7_detached 可能存在问题,但如果这是错误的,我不知道还有什么可以代替它。

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

然后我尝试签署文件:

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

作为参考,kmsBackedSignatureContainer 如下。 fileSigner.sign 从 AWS KMS 返回 byte[],如其文档中所定义:

此值是由 ANS X9.62–2005 和 RFC 3279 第 2.2.3 节定义的 DER 编码对象。

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)
    
    

【问题讨论】:

IExternalSignatureContainer 实现预计会返回一个 CMS 签名 container,它是一个除了实际加密签名值之外还包含大量元数据的对象。另一方面,您只有一个由 ANS X9.62–2005 和 RFC 3279 第 2.2.3 节定义的 DER 编码对象,即只有一个 ECDSA 加密签名值。当验证器尝试将该值解析为 CMS 容器时,这会导致“ASN.1 解析错误”。 我明白了。非常感谢您的评论。能否请您指出正确的方向,将我的签名包装在 CMS 容器中?我知道我要问的问题与***.com/questions/60242213/… 的问题完全相同;但是,即使在今天阅读了许多其他答案之后,我仍然完全不知道如何包装我的签名。 在签署之前你有没有 X509 证书(最好是完整的链)? 不幸的是,我没有。 AWS KMS 的签名端点返回的唯一两件事是byte[] 签名和签名中使用的算法。他们的文档在这里docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html。我也可以从 AWS 下载公钥,但它不包含任何证书链信息。 KMS 工作流程是您收到 byte[] 签名,将其与文件一起存储,然后使用其验证端点将其提交回 AWS。我开始怀疑 AWS KMS 不能用于 PDF 签名。 “我开始怀疑 AWS KMS 不能用于 PDF 签名。” - 我认为可以。可能您必须检索您的公钥,从中构建证书请求,然后从中创建自签名证书,或者根据该证书请求从通常受信任的 CA 请求证书。然后,您可能能够使用 AWS KMS 使用该证书对 PDF 进行签名。 【参考方案1】:

在此答案的上下文中,假定您已将凭据存储在 ~/.aws/credentials 文件的 default 部分中,并将您的区域存储在 ~/.aws/config 文件的 default 部分中。否则,您将不得不在以下代码中调整 KmsClient 实例化或初始化。

为 AWS KMS 密钥对生成证书

首先,AWS KMS 使用普通的非对称密钥对进行签名,它不为公钥提供 X.509 证书。但是,可互操作的 PDF 签名需要公钥的 X.509 证书才能建立对签名的信任。因此,可互操作的 AWS KMS PDF 签名的第一步是为您的 AWS KMS 签名密钥对的公钥生成 X.509 证书。

出于测试目的,您可以使用此帮助方法创建自签名证书,该方法基于来自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 辅助方法)

上面代码中使用的AwsKmsContentSigner类就是BouncyCastle接口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)

出于生产目的,您通常需要使用由受信任的 CA 签名的证书。与上述类似,您可以为您的 AWS KMS 公钥创建和签署一个证书请求,将其发送到您选择的 CA,然后从他们那里取回要使用的证书。

使用 AWS KMS 密钥对签署 PDF

要使用 iText 签署 PDF,您需要实现 iText IExternalSignatureIExternalSignatureContainer 接口。这里我们使用前者:

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)

在构造函数中,我们选择可用于相关密钥的签名算法。这实际上是在这里非常随意地完成的,而不是简单地采用第一个算法,您可能希望强制使用特定的哈希算法。

getHashAlgorithmgetEncryptionAlgorithm 返回签名算法各自部分的名称,sign 只是创建一个签名。

付诸行动

假设您的 AWS KMS 签名密钥对具有别名 SigningExamples-ECC_NIST_P256,您可以使用上面的代码签署 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 测试testSignSimpleEcdsa)

重新访问使用 AWS KMS 密钥对签署 PDF

上面我们使用IExternalSignature 的实现进行签名。虽然这是最简单的方法,但它也有一些缺点:本例中使用的 PdfPKCS7 类不支持 RSASSA-PSS 使用,并且对于 ECDSA 签名,它使用错误的 OID 作为签名算法 OID。

为了不受这些问题的影响,我们在这里使用IExternalSignatureContainer 的实现,而不是我们自己使用 BouncyCastle 功能构建完整的 CMS 签名容器。

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)

在构造函数中,我们还选择了可用于相关密钥的签名算法。但是,在这里,我们允许一个函数参数允许调用者在可用的签名算法中进行选择。这对于 RSASSA-PSS 的使用尤其必要。

重新审视将其付诸实践

假设您有一个 AWS KMS 签名 RSA_2048 密钥对,该密钥对具有别名 SigningExamples-RSA_2048,您可以像这样使用上面的代码使用 RSASSA-PSS 签署 PDF:

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 测试testSignSimpleRsaSsaPss)

使用此选择器功能

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 辅助方法)

海量签名注意事项

如果您计划使用 AWS KMS 进行批量签名,请注意 AWS KMS 为其某些操作设置的请求配额:

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

(摘自“AWS 密钥管理服务开发人员指南”/“配额”/“请求配额”/"Request quotas for each AWS KMS API operation" 于 2020 年 12 月 15 日查看)

RSAECC 加密操作请求率 可能不是问题。或者更重要的是,如果它们有问题,AWS KMS 很可能不是满足您需求的正确签名产品;相反,您应该寻找实际的 HSM,无论是物理的还是作为服务的,例如AWS CloudHSM.

另一方面,GetPublicKey 请求率 很可能是个问题:AwsKmsSignatureAwsKmsSignatureContainer 在各自的构造函数中都调用了该方法。因此,基于它们的简单的大规模签名代码将被限制为每秒 5 个签名。

根据您的用例,有不同的策略来解决这个问题。

如果您的签名代码只有极少数实例同时运行并且它们只使用极少数不同的密钥,您可以简单地重复使用您的 AwsKmsSignatureAwsKmsSignatureContainer 对象,或者在启动时或按需创建它们,然后缓存它们。

否则,您应该从 AwsKmsSignatureAwsKmsSignatureContainer 构造函数中重构 GetPublicKey 方法的使用。它仅用于确定在使用相关密钥进行签名时要使用的 AWS KMS 签名算法标识符。显然,您可以将该标识符与密钥标识符一起存储,从而无需调用 GetPublicKey。

【讨论】:

【参考方案2】:

My original answer 展示了如何使用 AWS KMS 和 iText 7 for Java 签署 PDF。为了完整起见,我将其移植到.Net。由于我使用真正的 .Net 类来创建自签名证书,并且它们的 Java 和 .Net 版本之间的 AWS KMS 和 BouncyCastle API 存在一些差异,因此代码不仅在方法名称大写方面有所不同...

CertificateUtils

.Net 提供了自己的方法来创建证书请求和自签名证书,CertificateRequest 类。

与另一个答案中的 BouncyCastle/Java 实现类似,此类也将实际的签名创建(用于自签名证书)委托给一个助手,这里是一个 X509SignatureGenerator 实例。显然 .Net 没有该类的现成变体用于 AWS KMS 签名,因此我们必须自己提供一个,即下面代码中的内部类 SignatureGenerator。幸运的是,我们可以将X509SignatureGenerator 的.Net 变体用于除实际签名方法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

AwsKmsSignature 类可以从 Java 移植而来,只需很少的更改。

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

AwsKmsSignatureContainer 类使用 BouncyCastle 构建要嵌入的 CMS 签名容器,就像在另一个答案中的 Java 版本中一样。

不过,BouncyCastle API 存在某些差异。特别是不使用ContentSigner 的实例进行实际签名,而是使用ISignatureFactory 的实例;该接口代表IStreamCalculator 实例的工厂,它们的功能是Java 中ContentSigner 的实际挂件。这些接口的实现如下AwsKmsSignatureFactoryAwsKmsStreamCalculator

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)

付诸行动

在配置凭证和默认区域的前提条件与另一个答案中相同,假设一个人的 AWS KMS 签名密钥对有别名 SigningExamples-ECC_NIST_P256SigningExamples-RSA_2048 分别匹配它们的算法。

ECDSA 签名

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 测试testSignSimpleEcdsa)

RSA(PKCS#1 v1.5 填充)签名

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 测试testSignSimpleRsa)

RSASSA-PSS 签名

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 测试testSignSimpleRsaSsaPss)

【讨论】:

我们是否使用这种方法在 pdf 中获得了“受信任的证书”标签?谢谢 我们是否在pdf中获得了“受信任的证书”标签,使用这种方法? - 我不知道你的意思是什么标签。但是证书是否被某人信任,取决于你的证书是如何生成的。请参阅我的另一个以 java 为中心的答案:出于测试目的,您可以创建一个自签名证书。 ...出于生产目的,您通常需要使用由受信任的 CA 签名的证书。与上述类似,您可以为您的 AWS KMS 公钥创建和签署证书请求,将其发送到您选择的 CA,然后从他们那里取回要使用的证书。

以上是关于使用 AWS KMS 返回的数字签名签署 PdfDocument的主要内容,如果未能解决你的问题,请参考以下文章

AWS 错误消息:使用 AWS KMS 托管密钥指定服务器端加密的请求需要 AWS 签名版本 4

Google Cloud Api Gateway 是不是提供请求签名和验证 sdk,例如使用 Signature 4 签署 AWS API Gateway 请求?

将 AWS KMS ECDSA_SHA_256 签名从 DER 编码的 ANS.1 格式转换为 JWT base64url 编码的 R || NodeJS/Javascript 中的 S 格式

解密aws kms密钥时出现Nodejs异步问题

AWS lambda 和 kms 密钥别名

使用 Cloudformation 创建 KMS 密钥时出现消息“没有 IAM 权限来处理 AWS::KMS::Key 资源上的标签”