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:
Analysis of file-signed-failed.pdf
In an ASN.1 dump of the contained signature container one issue immediately strikes the eye: The messageDigest
attribute contains a copy of the signed attributes as they should be, i.e. with a proper messageDigest
attribute:
<30 5C>
4172 92: . . . . . . SEQUENCE {
<06 09>
4174 9: . . . . . . . OBJECT IDENTIFIER messageDigest (1 2 840 113549 1 9 4)
: . . . . . . . . (PKCS #9)
<31 4F>
4185 79: . . . . . . . SET {
<04 4D>
4187 77: . . . . . . . . OCTET STRING, encapsulates {
<31 4B>
4189 75: . . . . . . . . . SET {
<30 18>
4191 24: . . . . . . . . . . SEQUENCE {
<06 09>
4193 9: . . . . . . . . . . . OBJECT IDENTIFIER
: . . . . . . . . . . . . contentType (1 2 840 113549 1 9 3)
: . . . . . . . . . . . . (PKCS #9)
<31 0B>
4204 11: . . . . . . . . . . . SET {
<06 09>
4206 9: . . . . . . . . . . . . OBJECT IDENTIFIER data (1 2 840 113549 1 7 1)
: . . . . . . . . . . . . . (PKCS #7)
: . . . . . . . . . . . . }
: . . . . . . . . . . . }
<30 2F>
4217 47: . . . . . . . . . . SEQUENCE {
<06 09>
4219 9: . . . . . . . . . . . OBJECT IDENTIFIER
: . . . . . . . . . . . . messageDigest (1 2 840 113549 1 9 4)
: . . . . . . . . . . . . (PKCS #9)
<31 22>
4230 34: . . . . . . . . . . . SET {
<04 20>
4232 32: . . . . . . . . . . . . OCTET STRING
: . . . . . . . . . . . . . 40 76 BC 3F 05 25 E4 C3 @v.?.%..
: . . . . . . . . . . . . . 27 AD 78 FA 73 31 4C 1B '.x.s1L.
: . . . . . . . . . . . . . 82 97 3D AA 4E 81 72 D6 ..=.N.r.
: . . . . . . . . . . . . . 23 3C DD 59 D2 82 81 55
: . . . . . . . . . . . . }
: . . . . . . . . . . . }
: . . . . . . . . . . }
: . . . . . . . . . }
: . . . . . . . . }
: . . . . . . . }
And indeed, in your code the reason becomes clear:
byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, null, null, MakeSignature.CryptoStandard.CMS);
[...]
byte[] encodedSig = sgn.getEncodedPKCS7(sh, tsaClient, null, null, MakeSignature.CryptoStandard.CMS);
These two calls must have the same parameters (except the added tsaClient
at 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
byte[] encodedSig = sgn.getEncodedPKCS7(sh, tsaClient, null, null, MakeSignature.CryptoStandard.CMS);
by
byte[] encodedSig = sgn.getEncodedPKCS7(hash, tsaClient, null, null, MakeSignature.CryptoStandard.CMS);
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
2D9B224E0894E73B1D3EDEE43E5C34A152057B008518538F3D6DA9C5AC73B54AEF33EB165ED0815F2E7851C86308AAFEC3FC0CD5CA77D7A745C056CB37783B7B51484D9B6C1F6D7E42C2B1C49127CD7D1C3A371D943A5C6F5DDA47C758493D2D3CA7D165B35A1BE4FA590911E801D7026822A9B9D202AE9A671DF4F36D42AAD712D43506EC3607E5AC7CCE23389BE288DD32C9C45B92CAA7225897EFD9F8ECFE2A40007FD6AC8B625239E6E529B7521E2EB652659A8F8B3F7262D46E8A0207A3004FEF48C87FC8A52B632268FDD0888A00AE6A3B303A138B18F28A66108467BFF743A859ECD193ADB52268B1FC531690B99D35D5E68BF804B59E24FCB180FABC
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 BC
is an indicator for the latter but garbage may also end in BC
.
Meanwhile, though, the OP has confirmed:
the private key generated in Google KMS is 2048 bit RSA key PSS Padding - SHA256 Digest
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
PdfPKCS7
class