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.