javapdfgoogle-cloud-platformentrustgoogle-cloud-kms

PDF document signing with Google KMS and Entrust certificate


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:

  1. Every signature is invalid: The document has been altered or corrupted since the signature was applied.
  2. The signature verification from Google is always false.

Solution

  • 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 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);
    

    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

    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