使用iText(signdeferred)创建PDF数字签名,验证签名时出现invalid signature问题
Using iText (signdeferred) to create PDF digital signature, invalid signature problem appears when verifying signature
我是一名中国软件开发人员,我现在正在实现这样一个功能,使用Android客户端对PDF进行数字签名,我的实现是这样的
- 在服务器上创建一个空白签名
- 将带有空白签名的 PDF 散列发送给 Android 客户端,Android 客户端对散列进行签名
- 使用makesignature。 signdeferred() 合并服务器中的签名内容
现在遇到这样一个问题,签名后的PDF无法通过PDF验证reader。显示PDF文件被篡改,
需要注意的是,我使用的是sm3和sm2算法。 Adobe reader 无法验证。我们有自己的 reader
https://drive.google.com/file/d/127nVvJ0qtSdG53jM0_GUP-WORYrQ5TBo/view?usp=sharing
现在我加上PDF文件地址,谁能帮我分析一下问题
/**
* @return name.ldd.electSign.webservice.pdf.presign.PresignResponseDetail
* @Author 焦康
* @Description 预签章
* @Date 2021/4/25 14:35
* @Param [pdfReader, tempFile, temp, chain, positions, fieldName, calendar, hashName]
**/
public PreSignResponseDetail preSign(PreSignDAO preSignDAO) throws IOException, DocumentException, GeneralSecurityException {
log.debug("执行预签章");
FileOutputStream fileOutputStream = new FileOutputStream(preSignDAO.getTempPath());
// 设置印章信息
preSignDAO.getPdfReader().setAppendable(true);
PdfStamper pdfStamper = PdfStamper.createSignature(preSignDAO.getPdfReader(), fileOutputStream, '[=10=]', null, true);
pdfStamper.getWriter().setCompressionLevel(PdfStream.BEST_COMPRESSION);
PdfSignatureAppearance pdfSignatureAppearance = pdfStamper.getSignatureAppearance();
pdfSignatureAppearance.setReason("");
pdfSignatureAppearance.setLocation("");
pdfSignatureAppearance.setContact("");
pdfSignatureAppearance.setLayer2Text("");
pdfSignatureAppearance.setLayer4Text("");
pdfSignatureAppearance.setSignatureCreator("");
pdfSignatureAppearance.setOpacity(preSignDAO.getSealOpacity());
pdfSignatureAppearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
pdfSignatureAppearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
pdfSignatureAppearance.setCertificate(preSignDAO.getCertificateChain()[0]);
pdfSignatureAppearance.setVisibleSignature(new Rectangle(200, 200, 300, 300), preSignDAO.getPage(), preSignDAO.getFieldName());
// 设置签名图片
//读取图章图片,这个image是itext包的image
Image image = Image.getInstance(preSignDAO.getSealImagePath());
pdfSignatureAppearance.setImage(image);
// 开始预签章
ExternalSignatureContainer externalBlankSignatureContainer = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.signExternalContainer(pdfSignatureAppearance, externalBlankSignatureContainer, 8192);
// 计算hash
InputStream inputStream = pdfSignatureAppearance.getRangeStream();
BouncyCastleDigest bouncyCastleDigest = new BouncyCastleDigest();
byte[] hash = DigestAlgorithms.digest(inputStream, bouncyCastleDigest.getMessageDigest(preSignDAO.getHashAlgorithm()));
PdfPKCS7 pkcs7 = new PdfPKCS7(null, preSignDAO.getCertificateChain(), preSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
byte[] sh = pkcs7.getAuthenticatedAttributeBytes(hash, Calendar.getInstance(), null, null, MakeSignature.CryptoStandard.CMS);
String stringSh = new sun.misc.BASE64Encoder().encode(sh);
return new PreSignResponseDetail(preSignDAO.getSourcePath(), preSignDAO.getTempPath(), hash, stringSh.replace("\r\n",""), preSignDAO.getFieldName());
}
/**
* @Author 焦康
* @Description 数据签名
* @Date 2021/4/30 14:57
* @Param [Pin, CN, signData, listener]
* @return void
**/
public void ZAYK_SignData(final String Pin,final String CN,final String signData,HttpsCallbackListener listener){
httpsCallbackListener = listener;
new Thread(new Runnable() {
@Override
public void run() {
String result = null;
try {
byte[] bytes = Base64.decode(signData,Base64.DEFAULT);
result = service.SDZM_SignData(bytes,CN,Pin);
JSONObject jsonObject = JSONObject.parseObject(result);
result = jsonObject.getString("signValue");
Log.e("SDZM_SignData",result);
} catch (Exception e) {
e.printStackTrace();
}
sendMessage(result);
}
}).start();
}
/**
* @return
* @Author 焦康
* @Description 延迟签章
* @Date 2021/4/25 17:01
* @Param
**/
public DeferredSignResponseDetail DeferredSign(DeferredSignDAO deferredSignDAO) {
log.debug("执行延迟签章");
BouncyCastleDigest bouncyCastleDigest = new BouncyCastleDigest();
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
PdfReader pdfReader = null;
FileOutputStream os = null;
try {
PdfPKCS7 sgn = new PdfPKCS7(null, deferredSignDAO.getCertificateChain(), deferredSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
sgn.setExternalDigest(deferredSignDAO.getSignData(), null, deferredSignDAO.getDigestEncryptionAlgorithm());
byte[] sig = sgn.getEncodedPKCS7(deferredSignDAO.getHashData(), calendar, null, null, null, MakeSignature.CryptoStandard.CMS);
pdfReader = new PdfReader(deferredSignDAO.getTempPath());
os = new FileOutputStream("D:\ruoyi\uploadPath\pdf\Dest\输出");
ExternalSignatureContainer external = new MyExternalSignatureContainer(sig);
MakeSignature.signDeferred(pdfReader, deferredSignDAO.getFieldName(), os, external);
} catch (Exception e) {
log.error("延迟签章出错:", e);
} finally {
if (pdfReader != null) {
pdfReader.close();
}
try {
if (os != null) {
os.close();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
return new DeferredSignResponseDetail(deferredSignDAO.getTargetPath() + ".pdf");
}
public class MyExternalSignatureContainer implements ExternalSignatureContainer {
byte[] sig = null;
public MyExternalSignatureContainer(byte[] sig) {
this.sig = sig;
}
@Override
public byte[] sign(InputStream paramInputStream) throws GeneralSecurityException {
return this.sig;
}
@Override
public void modifySigningDictionary(PdfDictionary paramPdfDictionary) {
}
}
在计算待签名属性的哈希值时,使用当前时间作为签名时间属性的值:
PdfPKCS7 pkcs7 = new PdfPKCS7(null, preSignDAO.getCertificateChain(), preSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
byte[] sh = pkcs7.getAuthenticatedAttributeBytes(hash, Calendar.getInstance(), null, null, MakeSignature.CryptoStandard.CMS);
String stringSh = new sun.misc.BASE64Encoder().encode(sh);
稍后,当您收到该散列的裸签名时,您将使用当时的当前时间重新构建签名属性:
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
...
PdfPKCS7 sgn = new PdfPKCS7(null, deferredSignDAO.getCertificateChain(), deferredSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
sgn.setExternalDigest(deferredSignDAO.getSignData(), null, deferredSignDAO.getDigestEncryptionAlgorithm());
byte[] sig = sgn.getEncodedPKCS7(deferredSignDAO.getHashData(), calendar, null, null, null, MakeSignature.CryptoStandard.CMS);
因此,最终 CMS 容器的签名属性中的签名时间与用于散列的签名时间不同,因此签名签署了错误的散列。
要解决此问题在两个地方使用相同的签名时间值!
顺便说一句,如果你使用的是当前的 iText 5 版本,你就不会遇到这个问题:因为签名时间属性是可选的(只要在签名字典中设置签名时间)甚至禁止 PAdES 签名,当前的 iText 5 版本不再设置该属性并且 getAuthenticatedAttributeBytes
和 getEncodedPKCS7
不再有 Calendar
参数...
由于我还没有处理中文算法,我无法判断它们是否正确地应用在你的代码或你的示例文件中。
但有一个警告:PdfPKCS7
已在考虑 DSA 和 RSA 的情况下实施;稍后添加了 X9.62 ECDSA 支持,并且有一个错误(iText 7 中仍然存在),Adobe Acrobat 会忽略但不一定会忽略其他验证器;因此,在将 PdfPKCS7
用于任何其他算法之前,您必须验证 class 是否正确处理了其他算法,如果没有,则改进 class。
我是一名中国软件开发人员,我现在正在实现这样一个功能,使用Android客户端对PDF进行数字签名,我的实现是这样的
- 在服务器上创建一个空白签名
- 将带有空白签名的 PDF 散列发送给 Android 客户端,Android 客户端对散列进行签名
- 使用makesignature。 signdeferred() 合并服务器中的签名内容 现在遇到这样一个问题,签名后的PDF无法通过PDF验证reader。显示PDF文件被篡改, 需要注意的是,我使用的是sm3和sm2算法。 Adobe reader 无法验证。我们有自己的 reader
https://drive.google.com/file/d/127nVvJ0qtSdG53jM0_GUP-WORYrQ5TBo/view?usp=sharing 现在我加上PDF文件地址,谁能帮我分析一下问题
/**
* @return name.ldd.electSign.webservice.pdf.presign.PresignResponseDetail
* @Author 焦康
* @Description 预签章
* @Date 2021/4/25 14:35
* @Param [pdfReader, tempFile, temp, chain, positions, fieldName, calendar, hashName]
**/
public PreSignResponseDetail preSign(PreSignDAO preSignDAO) throws IOException, DocumentException, GeneralSecurityException {
log.debug("执行预签章");
FileOutputStream fileOutputStream = new FileOutputStream(preSignDAO.getTempPath());
// 设置印章信息
preSignDAO.getPdfReader().setAppendable(true);
PdfStamper pdfStamper = PdfStamper.createSignature(preSignDAO.getPdfReader(), fileOutputStream, '[=10=]', null, true);
pdfStamper.getWriter().setCompressionLevel(PdfStream.BEST_COMPRESSION);
PdfSignatureAppearance pdfSignatureAppearance = pdfStamper.getSignatureAppearance();
pdfSignatureAppearance.setReason("");
pdfSignatureAppearance.setLocation("");
pdfSignatureAppearance.setContact("");
pdfSignatureAppearance.setLayer2Text("");
pdfSignatureAppearance.setLayer4Text("");
pdfSignatureAppearance.setSignatureCreator("");
pdfSignatureAppearance.setOpacity(preSignDAO.getSealOpacity());
pdfSignatureAppearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
pdfSignatureAppearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
pdfSignatureAppearance.setCertificate(preSignDAO.getCertificateChain()[0]);
pdfSignatureAppearance.setVisibleSignature(new Rectangle(200, 200, 300, 300), preSignDAO.getPage(), preSignDAO.getFieldName());
// 设置签名图片
//读取图章图片,这个image是itext包的image
Image image = Image.getInstance(preSignDAO.getSealImagePath());
pdfSignatureAppearance.setImage(image);
// 开始预签章
ExternalSignatureContainer externalBlankSignatureContainer = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.signExternalContainer(pdfSignatureAppearance, externalBlankSignatureContainer, 8192);
// 计算hash
InputStream inputStream = pdfSignatureAppearance.getRangeStream();
BouncyCastleDigest bouncyCastleDigest = new BouncyCastleDigest();
byte[] hash = DigestAlgorithms.digest(inputStream, bouncyCastleDigest.getMessageDigest(preSignDAO.getHashAlgorithm()));
PdfPKCS7 pkcs7 = new PdfPKCS7(null, preSignDAO.getCertificateChain(), preSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
byte[] sh = pkcs7.getAuthenticatedAttributeBytes(hash, Calendar.getInstance(), null, null, MakeSignature.CryptoStandard.CMS);
String stringSh = new sun.misc.BASE64Encoder().encode(sh);
return new PreSignResponseDetail(preSignDAO.getSourcePath(), preSignDAO.getTempPath(), hash, stringSh.replace("\r\n",""), preSignDAO.getFieldName());
}
/**
* @Author 焦康
* @Description 数据签名
* @Date 2021/4/30 14:57
* @Param [Pin, CN, signData, listener]
* @return void
**/
public void ZAYK_SignData(final String Pin,final String CN,final String signData,HttpsCallbackListener listener){
httpsCallbackListener = listener;
new Thread(new Runnable() {
@Override
public void run() {
String result = null;
try {
byte[] bytes = Base64.decode(signData,Base64.DEFAULT);
result = service.SDZM_SignData(bytes,CN,Pin);
JSONObject jsonObject = JSONObject.parseObject(result);
result = jsonObject.getString("signValue");
Log.e("SDZM_SignData",result);
} catch (Exception e) {
e.printStackTrace();
}
sendMessage(result);
}
}).start();
}
/**
* @return
* @Author 焦康
* @Description 延迟签章
* @Date 2021/4/25 17:01
* @Param
**/
public DeferredSignResponseDetail DeferredSign(DeferredSignDAO deferredSignDAO) {
log.debug("执行延迟签章");
BouncyCastleDigest bouncyCastleDigest = new BouncyCastleDigest();
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
PdfReader pdfReader = null;
FileOutputStream os = null;
try {
PdfPKCS7 sgn = new PdfPKCS7(null, deferredSignDAO.getCertificateChain(), deferredSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
sgn.setExternalDigest(deferredSignDAO.getSignData(), null, deferredSignDAO.getDigestEncryptionAlgorithm());
byte[] sig = sgn.getEncodedPKCS7(deferredSignDAO.getHashData(), calendar, null, null, null, MakeSignature.CryptoStandard.CMS);
pdfReader = new PdfReader(deferredSignDAO.getTempPath());
os = new FileOutputStream("D:\ruoyi\uploadPath\pdf\Dest\输出");
ExternalSignatureContainer external = new MyExternalSignatureContainer(sig);
MakeSignature.signDeferred(pdfReader, deferredSignDAO.getFieldName(), os, external);
} catch (Exception e) {
log.error("延迟签章出错:", e);
} finally {
if (pdfReader != null) {
pdfReader.close();
}
try {
if (os != null) {
os.close();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
return new DeferredSignResponseDetail(deferredSignDAO.getTargetPath() + ".pdf");
}
public class MyExternalSignatureContainer implements ExternalSignatureContainer {
byte[] sig = null;
public MyExternalSignatureContainer(byte[] sig) {
this.sig = sig;
}
@Override
public byte[] sign(InputStream paramInputStream) throws GeneralSecurityException {
return this.sig;
}
@Override
public void modifySigningDictionary(PdfDictionary paramPdfDictionary) {
}
}
在计算待签名属性的哈希值时,使用当前时间作为签名时间属性的值:
PdfPKCS7 pkcs7 = new PdfPKCS7(null, preSignDAO.getCertificateChain(), preSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
byte[] sh = pkcs7.getAuthenticatedAttributeBytes(hash, Calendar.getInstance(), null, null, MakeSignature.CryptoStandard.CMS);
String stringSh = new sun.misc.BASE64Encoder().encode(sh);
稍后,当您收到该散列的裸签名时,您将使用当时的当前时间重新构建签名属性:
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
...
PdfPKCS7 sgn = new PdfPKCS7(null, deferredSignDAO.getCertificateChain(), deferredSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
sgn.setExternalDigest(deferredSignDAO.getSignData(), null, deferredSignDAO.getDigestEncryptionAlgorithm());
byte[] sig = sgn.getEncodedPKCS7(deferredSignDAO.getHashData(), calendar, null, null, null, MakeSignature.CryptoStandard.CMS);
因此,最终 CMS 容器的签名属性中的签名时间与用于散列的签名时间不同,因此签名签署了错误的散列。
要解决此问题在两个地方使用相同的签名时间值!
顺便说一句,如果你使用的是当前的 iText 5 版本,你就不会遇到这个问题:因为签名时间属性是可选的(只要在签名字典中设置签名时间)甚至禁止 PAdES 签名,当前的 iText 5 版本不再设置该属性并且 getAuthenticatedAttributeBytes
和 getEncodedPKCS7
不再有 Calendar
参数...
由于我还没有处理中文算法,我无法判断它们是否正确地应用在你的代码或你的示例文件中。
但有一个警告:PdfPKCS7
已在考虑 DSA 和 RSA 的情况下实施;稍后添加了 X9.62 ECDSA 支持,并且有一个错误(iText 7 中仍然存在),Adobe Acrobat 会忽略但不一定会忽略其他验证器;因此,在将 PdfPKCS7
用于任何其他算法之前,您必须验证 class 是否正确处理了其他算法,如果没有,则改进 class。