java—使用aws kms返回的数字签名对PDF文档进行签名

9wbgstp7  于 2021-07-12  发布在  Java
关注(0)|答案(1)|浏览(502)

我正在尝试使用通过使用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 a返回 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)
    {
    }
}
wnavrhmk

wnavrhmk1#

在本回答的上下文中,假设您已将凭据存储在 default 您的 ~/.aws/credentials 文件和您所在的地区 default 您的 ~/.aws/config 文件。否则你就得适应环境 KmsClient 在下面的代码中示例化或初始化。

为aws kms密钥对生成证书

首先,aws-kms使用普通的非对称密钥对签名,它不提供x.509证书作为公钥。但是,可互操作的pdf签名需要公钥的x.509证书来建立对签名的信任。因此,互操作aws kms pdf签名的第一步是为aws kms签名密钥对的公钥生成x.509证书。
出于测试目的,您可以使用此助手方法创建自签名证书,该方法基于此堆栈溢出答案中的代码:

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的实现 IExternalSignature 或者 IExternalSignatureContainer 接口。这里我们使用前者:

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

(AWSKM签名)
在构造函数中,我们选择一个签名算法,该算法可用于所讨论的密钥。这实际上是在这里非常随意地完成的,而不是简单地采用您可能想要强制使用特定哈希算法的第一个算法。 getHashAlgorithm 以及 getEncryptionAlgorithm 返回签名算法和 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);
}

(测试)简单测试 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();
        }
    }
}

(AWSKMS签名容器)
在构造函数中,我们还选择了一个签名算法,该算法可用于所讨论的密钥。不过,这里我们允许一个函数参数,允许调用者在可用的签名算法中进行选择。这对于rsassa pss的使用尤其必要。

重新审视将其付诸行动

假设您有一个aws kms签名rsa\U 2048密钥对,该密钥对具有别名signingexamples-rsa\U 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);
}

(测试)简单测试 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 helper方法)

大规模签名考虑事项

如果您计划使用aws kms进行批量签名,请注意aws kms为其某些操作建立的请求配额:
配额名称默认值(每秒)加密操作(rsa)请求速率500(共享)rsa cmkscryptographic operations(ecc)请求速率300(共享)椭圆曲线(ecc)cmksgetpublickey请求速率5
(摘自《aws密钥管理服务开发人员指南》/“配额”/“请求配额”/“每个aws kms api操作的请求配额”,查阅日期:2020-12-15)
rsa和ecc加密操作的请求速率可能不是问题。或者更重要的是,如果他们是一个问题,aws kms最有可能是不适合您的需要签署产品;相反,您应该寻找实际的hsm,无论是物理的还是作为服务的,例如aws cloudhsm。
另一方面,getpublickey请求速率很可能是个问题:两者都是 AwsKmsSignature 以及 AwsKmsSignatureContainer 在各自的构造函数中调用该方法。因此,基于它们的简单批量签名代码将限制为每秒5个签名。
根据您的用例,有不同的策略来解决这个问题。
如果只有极少数的签名代码示例同时运行,并且它们只使用极少数不同的密钥,那么只需重新使用 AwsKmsSignature 以及 AwsKmsSignatureContainer 对象,在启动或按需创建它们,然后缓存它们。
否则,您应该重构getpublickey方法的使用 AwsKmsSignature 以及 AwsKmsSignatureContainer 施工人员。它仅用于确定在使用相关密钥签名时要使用的aws kms签名算法标识符。显然,您可以将该标识符与密钥标识符一起存储,这样就不需要getpublickey调用。

相关问题