使用Google KMS和Entrust证书签名的PDF文档
我正在尝试通过使用CA(Entrust)提供的证书在pdf文档中进行有效签名,该证书是使用Google KMS的私钥生成的(私钥永远不会从KMS发出). 证书链的结构如下:[entrustCert,intermediate,rootCert]
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);
这是来自 Google Cloud的函数-创建和验证数字签名以进行签名验证:
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:
- 每个签名均无效:自应用签名以来,文档已被更改或损坏.
- 来自Google的签名验证始终为假.
损坏的消息摘要值
Broken message digest value
Analysis of file-signed-failed.pdf
在所包含的签名容器的ASN.1转储中,一个问题立即引起关注:messageDigest
属性包含应有签名属性的副本,即具有正确的messageDigest
属性:
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);
这两个调用必须必须具有相同的参数(除了在第二个位置添加的tsaClient
之外),因为在第一次调用中检索到了经过身份验证的属性(又名带符号的属性)是实际签名的字节,因此必须使用相同的输入创建最终的签名容器才能有效.
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);
作者
byte[] encodedSig = sgn.getEncodedPKCS7(hash, tsaClient, null, null, MakeSignature.CryptoStandard.CMS);
RSA填充
已经解决了上述问题,出现了一个新问题,内部密码库错误.错误代码:0x2726"
使用签名者证书的公共RSA密钥解密签名字节会导致
Decrypting the signature bytes using the public RSA key of the signer certificate resulted in
2D9B224E0894E73B1D3EDEE43E5C34A152057B008518538F3D6DA9C5AC73B54AEF33EB165ED0815F2E7851C86308AAFEC3FC0CD5CA77D7A745C056CB37783B7B51484D9B6C1F6D7E42C2B1C49127CD7D1C3A371D943A5C6F5DDA47C758493D2D3CA7D165B35A1BE4FA590911E801D7026822A9B9D202AE9A671DF4F36D42AAD712D43506EC3607E5AC7CCE23389BE288DD32C9C45B92CAA7225897EFD9F8ECFE2A40007FD6AC8B625239E6E529B7521E2EB652659A8F8B3F7262D46E8A0207A3004FEF48C87FC8A52B632268FDD0888A00AE6A3B303A138B18F28A66108467BFF743A859ECD193ADB52268B1FC531690B99D35D5E68BF804B59E24FCB180FABC
这显然看起来不像是PKCS1v1.5填充哈希值.因此,所指称的签名者证书有误,或者我们实质上看到了垃圾,或者签名根本不使用PKCS1v1.5填充,而是使用PSS填充.后面的BC
是后者的指示符,但垃圾也可能以BC
结尾.
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
.
尽管如此,OP已确认:
Meanwhile, though, the OP has confirmed:
在Google KMS中生成的私钥是2048位RSA密钥PSS填充-SHA256摘要
the private key generated in Google KMS is 2048 bit RSA key PSS Padding - SHA256 Digest
这确实说明了签名的问题:iText 5.x确实不支持RSASSA-PSS.创建RSA签名时,它会自动采用PKCS1v1.5填充;特别是在CMS签名容器中,它生成它表示签名算法为RSASSA-PKCS1-v1_5.因此,任何验证器都将无法验证签名.
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.
显而易见的选择是
- 通过拉皮条或替换iText
PdfPKCS7
类来添加RSASSA-PSS支持 - 或切换到RSASSA-PKCS1-v1_5键.
- either add RSASSA-PSS support by pimping or replacing the iText
PdfPKCS7
class - or switch to a RSASSA-PKCS1-v1_5 key.