この回答のコンテキストでは、資格情報をファイルの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 )
コンストラクターで、問題の鍵に使用できる署名アルゴリズムを選択します。これは、実際には、最初のアルゴリズムを単純に使用する代わりに、特定のハッシュ アルゴリズムの使用を強制したい場合に、かなり無計画に行われます。
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);
}
( 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 呼び出しを不要にすることができます。