我正在使用 ESP(电子签名提供商)实施一个两阶段的数字签名流程来对文档进行数字签名。过程如下:
第一阶段:
第二阶段:
从 ESP 提供的另一个端点接收签名字节。 我面临以下挑战:
由于签名分两个阶段进行,因此我需要保持 PdfSignatureAppearance 对象处于活动状态,直到签名完成。
尝试从 ESP 响应插入证书时,在 adobe 中签入时签名变得无效
Invalid signature: Document is modified."
此外,我想以适合分布式系统的方式编写此服务。
生成哈希的代码:
public String pdfSigner(File file, HttpServletRequest request, HttpSession session) {
String hashDocument = null;
PdfReader reader;
try {
String sourcefile = file.getAbsolutePath();
System.out.println("Path--->" + sourcefile);
destFile = sourcefile.replace(file.getName(), "Signed_Pdf.pdf");
request.getSession().setAttribute("fileName", "Signed_Pdf.pdf");
reader = new PdfReader(sourcefile);
Rectangle cropBox = reader.getCropBox(1);
Rectangle rectangle = null;
String user = null;
rectangle = new Rectangle(cropBox.getLeft(), cropBox.getBottom(), cropBox.getLeft(100), cropBox.getBottom(90));
fout = new FileOutputStream(destFile);
PdfStamper stamper = PdfStamper.createSignature(reader, fout, '\0', null, true);
appearance = stamper.getSignatureAppearance();
appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
appearance.setAcro6Layers(false);
Font font = new Font();
font.setSize(6);
font.setFamily("Helvetica");
font.setStyle("italic");
appearance.setLayer2Font(font);
Calendar currentDat = Calendar.getInstance();
System.out.println("remove 5 min");
currentDat.add(currentDat.MINUTE, 5); //Adding Delta Time of 5 Minutes....
appearance.setSignDate(currentDat);
if (user == null || user == "null" || user.equals(null) || user.equals("null")) {
appearance.setLayer2Text("Signed");
} else {
appearance.setLayer2Text("Signed by " + user);
}
appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
appearance.setImage(null);
// appearance.setSignDate(currentDat);
appearance.setVisibleSignature(rectangle,
reader.getNumberOfPages(), null);
int contentEstimated = 8192;
HashMap<PdfName, Integer> exc = new HashMap();
exc.put(PdfName.CONTENTS, contentEstimated * 2 + 2);
PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE,
PdfName.ADBE_PKCS7_DETACHED);
dic.setReason(appearance.getReason());
dic.setLocation(appearance.getLocation());
dic.setDate(new PdfDate(appearance.getSignDate()));
appearance.setCryptoDictionary(dic);
// request.getSession().setAttribute("pdfHash",appearance);
appearance.preClose(exc);
// fout.close();
request.getSession().setAttribute("appearance", appearance);
// System.gc();
// getting bytes of file
InputStream is = appearance.getRangeStream();
hashDocument = DigestUtils.sha256Hex(is);
//session=request.getSession();
//session.setAttribute("appearance1",appearance);
System.out.println("hex: " + is.toString());
} catch (Exception e) {
System.out.println("Error in signing doc.");
}
return hashDocument;
}
嵌入签名代码:
public String signPdfwithDS(String response, HttpServletRequest request, HttpSession session) {
int contentEstimated = 8192;
try {
String errorCode = response.substring(response.indexOf("errCode"), response.indexOf("errMsg"));
errorCode = errorCode.trim();
if (errorCode.contains("NA")) {
String pkcsResponse = xmlSigning.parseXml(response.trim());
byte[] sigbytes = Base64.decodeBase64(pkcsResponse);
byte[] paddedSig = new byte[contentEstimated];
System.arraycopy(sigbytes, 0, paddedSig, 0, sigbytes.length);
PdfDictionary dic2 = new PdfDictionary();
dic2.put(PdfName.CONTENTS,
new PdfString(paddedSig).setHexWriting(true));
//fout.close();
appearance.close(dic2);
} else {
destFile = "Error";
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Hash after signing :" + calculateSha256Hash(destFile));
return destFile;
}
插入证书代码:
String certString = response.substring(response.indexOf(User_X509_START_TAG), response.indexOf(User_X509_END_TAG))
.replaceAll(User_X509_START_TAG, "").replaceAll(User_X509_END_TAG, "");
byte[] certBytes = Base64.decodeBase64(certString);
ByteArrayInputStream stream = new ByteArrayInputStream(certBytes);
CertificateFactory factory = CertificateFactory.getInstance(X_509);
Certificate cert = factory.generateCertificate(stream);
List<Certificate> certificates = List.of(cert);
appearance.setCrypto(null, certificates.toArray(new Certificate[0]), null, null);
使用证书生成的Pdf,这里我没有在第一个函数中创建占位符,而是在嵌入签名时在第二个函数中创建了它。
(对引用的评论做出反应)
请找到添加的 pdf 链接
现在链接可以使用了。
之前我在生成哈希后创建签名占位符(在signPdfwithDS函数中),因为我想支持分布式系统并且不想在服务器上存储文件,但签名无效
这是行不通的:如果你想要任何像样的 PDF 签名验证器来积极验证你的签名,你需要对准备好的 PDF(带有包含占位符的签名字典)进行哈希处理,而不是其他东西,因为验证器期望签名的字节范围完全包含所有内容除了占位符本身。
然后转到上面的代码,现在签名有效,但用户详细信息不存在(没有证书pdf)也不支持分布式系统
是的,原则上这就是您必须如何实施签名的方式。有一些选项可以使其在分布式系统中更好地工作,请参见下文。
但是,如果您希望签名外观包含用户详细信息,则必须在签名前的准备阶段了解这些用户详细信息。如果从签名中检索它们是您唯一的选择,您可能必须创建两个签名:第一个用于虚拟数据,用于从签名中提取用户详细信息,第二个用于使用这些用户详细信息准备的 PDF。
有两种替代方法可以替代保持 PdfSignatureAppearance 对象处于活动状态。而不是在会话中保持该对象处于活动状态:
您可以重复PDF准备步骤;即,在步骤 1 中准备好用于签名的 PDF 并计算其哈希值后,您就可以扔掉准备好的 PDF。在第 2 步中,您再次从原始 PDF 开始,准备它,并将签名注入其中。
这里的挑战是确保两个准备好的 PDF 在字节方面是相同的。这要求您在执行此操作时使用相同的参数,不仅是您已控制的相同显式参数,还包括相同的隐式参数,例如系统时间(存储在 PDF 的元数据中,因此是签名数据的一部分) ) 和修订 ID。如果是加密的 PDF,您甚至可能必须控制随机数生成器种子值,具体取决于所使用的加密算法。
执行此操作有点复杂,需要您设置内部 iText 值(通过内省/反射或修补 iText。尽管如此,一些签名服务会这样做,例如 EU eSig DSS 示例实现。
您可以使用一些虚拟值对第一步中准备好的 PDF 进行“签名”,并将此虚拟签名的 PDF 与哈希值返回给调用者。在第二步中,调用者需要向您发送虚拟签名的 PDF 和哈希签名,然后您只需用提供的签名替换虚拟签名的 PDF 中的虚拟值即可。
您已标记您的问题itext5,所以我假设您使用当前的 iText 5 版本,即 5.5.x 版本,即使您的代码仅使用 5.3.x 之前的功能。
iText 5.5.x 包含一个显式方法来替换 PDF 最外层签名的签名值:
MakeSignature.signDeferred
:
/**
* Signs a PDF where space was already reserved.
* @param reader the original PDF
* @param fieldName the field to sign. It must be the last field
* @param outs the output PDF
* @param externalSignatureContainer the signature container doing the actual signing. Only the
* method ExternalSignatureContainer.sign is used
* @throws DocumentException
* @throws IOException
* @throws GeneralSecurityException
*/
public static void signDeferred(PdfReader reader, String fieldName, OutputStream outs, ExternalSignatureContainer externalSignatureContainer) throws DocumentException, IOException, GeneralSecurityException {
reader
必须包含虚拟签名的PDF,fieldName
需要是您创建的签名字段的名称,externalSignatureContainer
可以是简单的ExternalSignatureContainer
实现的实例,其sign
方法仅返回提供的签名,其 modifySigningDictionary
方法不执行任何操作。