2

AWS KMS を使用して PdfDocument の SHA256 ダイジェストに署名して取得した署名を使用して、PDF 自体に署名を適用しようとしています。正しい方向に進んでいるかどうかさえわかりません。

すべてが正しく実行されますが、生成されたファイルの署名はエラーをスローします:

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

重要な場合は、AWS から公開キーを取得できますが、秘密キーは 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)
    {
    }
}
4

2 に答える 2

6

この回答のコンテキストでは、資格情報をファイルの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 に送信して、使用する証明書を CA から取得できます。

AWS KMS キーペアを使用して PDF に署名する

iText で PDF に署名するには、iTextIExternalSignatureまたは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;
}

( 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 を使用する場合に必要です。

行動に移す

エイリアス SigningExamples-RSA_2048 を持つ AWS KMS 署名 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 によって設定された一部のオペレーションのリクエスト クォータに注意してください。

クォータ名 デフォルト値 (1 秒あたり)
暗号操作 (RSA) 要求率 RSA CMK の場合は 500 (共有)
暗号操作 (ECC) 要求率 楕円曲線 (ECC) CMK の場合は 300 (共有)
GetPublicKey リクエスト率 5

( 2020-12-15閲覧「AWS Key Management Service Developer Guide」/「Quotas」/「Request Quotas」/ 「Request quotas for each AWS KMS API operation」より抜粋)

RSAおよびECC 暗号化操作の要求率は、おそらく問題ではありません。さらに言えば、それらが問題である場合、AWS KMS はおそらくニーズに合った適切な署名製品ではありません。代わりに、 AWS CloudHSMなど、物理的またはサービスとしての実際の HSM を探す必要があります。

一方、 GetPublicKey 要求率AwsKmsSignatureは問題になる可能性があります。との両方AwsKmsSignatureContainerが、それぞれのコンストラクターでそのメソッドを呼び出します。したがって、それらに基づく単純な一括署名コードは、1 秒あたり 5 つの署名に制限されます。

ユースケースに応じて、この問題に取り組むためのさまざまな戦略があります。

署名コードの非常に少数のインスタンスのみが同時に実行され、それらが非常に少数の異なるキーのみを使用している場合は、起動時またはオンデマンドで作成してキャッシュすることで、オブジェクトをAwsKmsSignature再利用できます。AwsKmsSignatureContainer

それ以外AwsKmsSignatureの場合は、コンストラクターとコンストラクターから GetPublicKey メソッドの使用をリファクタリングする必要がありますAwsKmsSignatureContainer。そこでは、問題のキーで署名するときにどの AWS KMS 署名アルゴリズム識別子を使用するかを決定するためにのみ使用されます。明らかに、その識別子をキー識別子と一緒に保存して、その GetPublicKey 呼び出しを不要にすることができます。

于 2020-12-08T17:40:25.513 に答える