I am trying to make a valid signature in a pdf document by using a certificate from CA (Entrust) generated with a private key from Google KMS (private key never goes out from the KMS). The certificate chain is made as: [entrustCert, intermediate, rootCert]
Following the part of the code I am using to make this happen:
String DEST = "/tmp/test_file.pdf";
OutputStream outputFile = new FileOutputStream(DEST);
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate[] chain = new X509Certificate[3];
chain[0] = (X509Certificate) certificateFactory.generateCertificate(entrustCert);
chain[1] = (X509Certificate) certificateFactory.generateCertificate(intermediateCert);
chain[2] = (X509Certificate) certificateFactory.generateCertificate(rootCert);
int estimatedSize = 8192;
PdfReader reader = new PdfReader(contract);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfStamper stamper = PdfStamper.createSignature(reader, outputStream, '\0');
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setReason(“reason”);
appearance.setLocation("Amsterdam");
appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, "sig");
appearance.setCertificate(chain[0]);
PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
dic.setReason(appearance.getReason());
dic.setLocation(appearance.getLocation());
dic.setContact(appearance.getContact());
dic.setDate(new PdfDate(appearance.getSignDate()));
appearance.setCryptoDictionary(dic);
HashMap<PdfName, Integer> exc = new HashMap<>();
exc.put(PdfName.CONTENTS, (estimatedSize * 2 + 2));
appearance.preClose(exc);
String hashAlgorithm = DigestAlgorithms.SHA256;
BouncyCastleDigest bcd = new BouncyCastleDigest();
PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, bcd, false);
InputStream data = appearance.getRangeStream();
byte[] hash = DigestAlgorithms.digest(data, MessageDigest.getInstance("SHA-256"));
byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, null, null, MakeSignature.CryptoStandard.CMS);
// Creating signature with Google Cloud KMS
KeyManagementServiceClient client = KeyManagementServiceClient.create();
AsymmetricSignRequest request = AsymmetricSignRequest.newBuilder()
.setName("path/of/the/key/in/kms")
.setDigest(Digest.newBuilder().setSha256(ByteString.copyFrom(hash)))
.build();
AsymmetricSignResponse r = client.asymmetricSign(request);
byte[] extSignature = r.getSignature().toByteArray();
// Checking if signature is valid
verifySignatureRSA("path/of/the/key/in/kms", hash, extSignature);
sgn.setExternalDigest(extSignature, null, "RSA");
TSAClient tsaClient = new TSAClientBouncyCastle("http://timestamp.entrust.net/...");
estimatedSize += 4192;
byte[] encodedSig = sgn.getEncodedPKCS7(sh, tsaClient, null, null, MakeSignature.CryptoStandard.CMS);
byte[] paddedSig = new byte[estimatedSize];
System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length);
PdfDictionary dic2 = new PdfDictionary();
dic2.put(PdfName.CONTENTS, (new PdfString(paddedSig)).setHexWriting(true));
appearance.close(dic2);
outputStream.writeTo(outputFile);
This is the function from Google Cloud - Creating and validating digital signatures for the signature verification:
public static boolean verifySignatureRSA(String keyName, byte[] message, byte[] signature)
throws IOException, GeneralSecurityException {
try (KeyManagementServiceClient client = KeyManagementServiceClient.create()) {
com.google.cloud.kms.v1.PublicKey pub = client.getPublicKey(keyName);
String pemKey = pub.getPem();
pemKey = pemKey.replaceFirst("-----BEGIN PUBLIC KEY-----", "");
pemKey = pemKey.replaceFirst("-----END PUBLIC KEY-----", "");
pemKey = pemKey.replaceAll("\\s", "");
byte[] derKey = BaseEncoding.base64().decode(pemKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(derKey);
PublicKey rsaKey = KeyFactory.getInstance("RSA").generatePublic(keySpec);
Signature rsaVerify = Signature.getInstance("SHA256withRSA");
rsaVerify.initVerify(rsaKey);
rsaVerify.update(message);
return rsaVerify.verify(signature);
}
}
Currently I am running in the following issues:
- Every signature is invalid: The document has been altered or corrupted since the signature was applied.
- The signature verification from Google is always false.
Broken message digest value
Analysis of file-signed-failed.pdf
In an ASN.1 dump of the contained signature container one issue immediately strikes the eye: The
messageDigestattribute contains a copy of the signed attributes as they should be, i.e. with a propermessageDigestattribute:And indeed, in your code the reason becomes clear:
These two calls must have the same parameters (except the added
tsaClientat second position) because the authenticated attributes (aka signed attributes) retrieved in the first call are the actually signed bytes, so the final signature container must be created with the same inputs to be valid.(If your variable names had been clearer, that problem might have been spotted earlier.)
Thus, to fix the signed attributes, replace
by
RSA padding
Having worked on fixing the issue above, a new issue turned up, "Internal cryptographic library error. Error Code: 0x2726"
Analysis of test_file.pdf
Decrypting the signature bytes using the public RSA key of the signer certificate resulted in
This clearly does not look like a PKCS1v1.5 padded hash value. Thus, either the alleged signer certificate is wrong and we see essentially garbage or the signature does not use PKCS1v1.5 padding at all but instead PSS padding. The trailing
BCis an indicator for the latter but garbage may also end inBC.Meanwhile, though, the OP has confirmed:
This indeed explain the issue with the signature: iText 5.x does not support RSASSA-PSS. When creating a RSA signature it automatically assumes PKCS1v1.5 padding; in particular in the CMS signature container it generates it denotes that the signing algorithm is RSASSA-PKCS1-v1_5. Thus, any validator will fail validating the signature.
The obvious options are to
PdfPKCS7class