首先,尽管我已经关注StackOverflow很长时间了,但这是我第一次发布内容,因此,如果我根据规则做错了什么,请随时指出正确的地方方向。
我正在使用iText5开发PDF数字签名应用程序,在准备要签名的PDF之后,它依赖于外部服务来提供签名的哈希。
如iText documentation中所述,在第一阶段中,我准备了PDF(在最终实现中,所有PDF都可能是多符号的,因此我使用附加模式),如下所示:
public static byte[] GetBytesToSign(string unsignedPdf, string tempPdf, string signatureFieldName, List<Org.BouncyCastle.X509.X509Certificate> certificateChain) {
// we create a reader and a stamper
using (PdfReader reader = new PdfReader(unsignedPdf)) {
using (FileStream baos = File.OpenWrite(tempPdf)) {
List<Org.BouncyCastle.X509.X509Certificate> chain = certificateChain;
PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
sap = pdfStamper.SignatureAppearance;
sap.Certificate = certificateChain[0];
sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
//sap.SetVisibleSignature(signatureFieldName);
sap.SignDate = DateTime.Now;
PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
dic.Date = new PdfDate(sap.SignDate);
dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
sap.CryptoDictionary = dic;
sap.Certificate = certificateChain[0];
sap.Acro6Layers = true;
sap.Reason = "test";
sap.Location = "test";
IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);
return hash;
}
}
}
完成此步骤后,我将哈希发送到外部服务,该服务将返回签名的哈希。
检查我发送给服务的哈希值,这似乎是正确的,因为它涵盖了除新签名内容之外的所有PDF。
然后我使用以下方法结束签名过程:
private byte[] Sign(PdfPKCS7 signatureContainer, List<X509Certificate2> chain2, List<Org.BouncyCastle.X509.X509Certificate> chain, byte[] hash, byte[] signedBytes, string tmpPdf, string signedPdf, string signatureFieldName) {
System.Security.Cryptography.RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = chain2[0].PublicKey.Key as System.Security.Cryptography.RSACryptoServiceProvider;
bool verify = publicCertifiedRSACryptoServiceProvider.VerifyHash(hash, "SHA256", signedBytes); //verify if the computed hash is same as signed hash using the cert public key
Console.WriteLine("PKey signed computed hash is equal to signed hash: " + verify);
AsnEncodedData asnEncodedData = new AsnEncodedData(signedBytes);
Console.WriteLine(asnEncodedData.Format(true));
//ITEXT5
try {
//Console.WriteLine("Signed bytes: " + Encoding.UTF8.GetString(signedBytes));
using (PdfReader reader = new PdfReader(tmpPdf)) {
using (FileStream outputStream = File.OpenWrite(signedPdf)) {
IExternalSignatureContainer external = new Objects.MyExternalSignatureContainer(signedBytes, chain, signatureContainer);
MakeSignature.SignDeferred(reader, signatureFieldName, outputStream, external);
}
}
return new byte[] { };
}
catch(Exception ex) {
File.Delete(tmpPdf);
Console.WriteLine("Error signing file: " + ex.Message);
return new byte[] { };
}
}
在Sign方法的开头,我验证发送给使用相同证书签名的外部服务的哈希值是否等于外部服务响应,这是正确的。
MyExternalSignatureContainer代码:
public class MyExternalSignatureContainer : IExternalSignatureContainer {
private readonly byte[] signedBytes;
public List<Org.BouncyCastle.X509.X509Certificate> Chain;
private PdfPKCS7 sigField;
public MyExternalSignatureContainer(byte[] signedBytes) {
this.signedBytes = signedBytes;
}
public MyExternalSignatureContainer(byte[] signedBytes, List<Org.BouncyCastle.X509.X509Certificate> chain, PdfPKCS7 pdfPKCS7) {
this.signedBytes = signedBytes;
this.Chain = chain;
this.sigField = pdfPKCS7;
}
public byte[] Sign(Stream data) {
try {
sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
}
catch (IOException ioe) {
throw ioe;
}
}
public void ModifySigningDictionary(PdfDictionary signDic) {
}
}
问题是当我在Acrobat中打开PDF时,它指出自应用签名以来,文档已被修改或损坏。
(如果我在PDF-XChange中打开相同的PDF,则表明该PDF未修改)。
到目前为止我尝试过的没有运气的东西:
不能完全确定外部服务是否使用SHA256,我已经尝试将摘要的摘要更改为预签名的SHA1,从而导致Acrobat Reader中出现“格式错误”。
就像在StackOverlow的另一篇文章中提到的一样(我找不到链接的文章),潜在的问题是临时文件使用不同的流。我已经尝试过使用相同的流,但是也没有运气。
PDF的样本:
已将Base64哈希发送到该服务:
XYfaS/SisA/tk5hcl035RpBjOczrH9E5rgiAMpqgkjI=
已发送Base64签名的哈希作为响应:
CnV3WL7skhMCtZG1r1Qi2oyE9WPO3KP4Ieu/Xm4lec+DAbYbhQxCvjMISsG3sTwYY7Lqi4luD60uceViDH848rS9OkTn8szzAnnX2fSYIwqDpG3qjJAb6NOXEv41hy+XYhSBJWS4ji2mM2ReruwPafxB1aM25L5Jyd0V7WecuNFUevUrvd85Y2KBkyBw9zCA8NDAQPPY0UT4GkXZi3Z35+Sf/s2o8zxCOlBDaIJyMvJ9De79nw4jC5L9NesHpFxx3mX1g1N33GHjUNdETgFMhnd8RDUlGLW6bsAyv78gvwE6aXF6COObap/VtlLvMOME68MzLr6izKte6uA35Zwj9Q==
在mkl's answer之后更新:
根据答案,我仅在一个阶段中更改了代码签名文档的代码,最终使用以下方法:
using (PdfReader reader = new PdfReader(fileLocation)) {
using (FileStream baos = File.OpenWrite(tmpFile)) {
List<Org.BouncyCastle.X509.X509Certificate> chain = Chain;
PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
sap.Certificate = Chain[0];
sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
//sap.SetVisibleSignature(signatureFieldName);
sap.SignDate = DateTime.Now;
PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
dic.Date = new PdfDate(sap.SignDate);
dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
sap.CryptoDictionary = dic;
sap.Certificate = Chain[0];
sap.Acro6Layers = true;
//sap.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS;
sap.Reason = "test";
sap.Location = "test";
IExternalSignature signature = new Objects.RemoteSignature(client, signatureRequest);
MakeSignature.SignDetached(sap, signature, Chain, null, null, null, 8192, CryptoStandard.CMS);
}
}
以及IExternalSignature实现:
public virtual byte[] Sign(byte[] message) {
IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
//
// Request signature for hash value messageHash
// and return signature bytes
//
signatureRequest.Hash = messageHash;
SignatureService.SignatureResponse signatureResponse = client.Signature(signatureRequest);
if (signatureResponse.Status.Code == "00") {
return signatureResponse.DocumentSignature;
}
else {
throw new Exception("Error signing file: " + signatureResponse.Status.Message);
}
}
signatureResponse.DocumentSignature 表示服务返回的已签名字节。
现在在结果PDF中,我遇到了BER解码错误。
分析示例PDF,您似乎将错误的证书声明为签署者证书
尽管我知道当前证书无效,但这是由服务提供的,并且在该服务的先前实现中,我将发送整个PDF进行签名,但已签名的PDF也已与此证书进行了签名。< / p>
问题:知道在两阶段签名中我能够使用此证书对PDF进行签名(除了签名错误后更改或损坏的文档之外),这种方法也不应该使用相同的证书?
当前,正在发生的事情是
检查签名:
同样,如果我在PDF-XChange中打开相同的PDF,则签名有效,并且文档未修改。要求PDF在Acrobat中有效,但读者之间的这种差异使我感到困惑。
即您只需要在字节序列前为哈希添加前缀30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20。
在将此SHA256前缀添加到消息摘要之后,现在可以正确签名生成的PDF。
Adobe Reader会接受固定签名吗?
我对此表示怀疑。签署者证书的密钥用法仅包含用于签署其他证书的值。
当前证书仅用于测试。在生产环境中,我相信外部服务提供的证书将是有效的。
我对此问题还有两个疑问:
对于您的代码,这意味着您必须先将哈希打包到DigestInfo结构中,然后再将其发送到服务。
问:您如何检查签名容器以得出不正确的结论?
问:在我的初始代码中,我有两个阶段的签名。在单符号方法中应用的相同主体仍然有效,即,使用SHA256前缀执行预签名字节,并在将摘要设置为最终的带符号字节之后吗?答案 0 :(得分:3)
您的代码中存在许多问题。
首先,您的代码混合了不同的iText签名API代。有较旧的API世代需要您非常接近PDF内部,而有较新的API(从5.3.x版本开始),该API被实现为较旧API的一层,并且不需要您知道那些内部知识
“ PDF文档的数字签名”白皮书着重于显示较新的API,仅4.3.3节“使用在客户端上创建的签名在服务器上对文档进行签名”使用旧的API,因为用例不允许使用更新的API。
但是,您的用例确实允许使用较新的API,因此您应该尝试仅使用它。
(在某些情况下,可以混合使用这些API,但是您应该真正知道自己在做什么,但仍然可能会犯错……)
但是现在有一些更具体的问题:
MakeSignature.Sign*
方法隐式关闭了基础的PdfStamper
和SignatureAppearance
对象,因此此后不应使用这些对象来产生明智的信息。
但是在GetBytesToSign
中,您可以这么做
MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
因此,sap.GetRangeStream()
可能返回错误消息。 (可能它仍然会返回正确的数据,但您不应指望它。)
GetBytesToSign
返回签名的PDF文档范围的哈希值:
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);
return hash;
但是,稍后,您的代码将使用该返回值,对其进行签名,然后尝试将返回的签名字节嵌入PdfPKCS7
签名容器中。这是错误的,必须为签名容器的签名者信息的经过身份验证的属性而不是文档哈希创建签名字节。
(顺便说一句,在这里您使用的是较旧的签名API而不了解它,因此使用不正确。)
在MyExternalSignatureContainer
中,您在两个调用中使用了已签名的字节:
sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
第一个电话是正确的,它们在这里。不过,在第二个调用中,应该使用签名文档范围的原始哈希。
(在这里您再次使用较旧的签名API而不理解它,然后再次错误地使用它。)
分析示例PDF,您似乎将错误的证书声明为签署者证书。我想是因为
首先,如果我对您的理解正确,那么您需要从其他服务器请求签名,并且该其他服务器会快速做出反应,因此在等待签名时无需释放所有资源。在这种情况下,不需要两步签名过程,您应该一步一步完成。您只需要一个自定义IExternalSignature
实现,类似
class RemoteSignature : IExternalSignature
{
public virtual byte[] Sign(byte[] message) {
IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
//
// Request signature for hash value messageHash
// and return signature bytes
//
return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(messageHash);
}
public virtual String GetHashAlgorithm() {
return "SHA-256";
}
public virtual String GetEncryptionAlgorithm() {
return "RSA";
}
}
并像这样使用它进行签名:
PdfReader reader = new PdfReader(...);
PdfStamper pdfStamper = PdfStamper.CreateSignature(...);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
// set sap properties for signing
IExternalSignature signature = new RemoteSignature();
MakeSignature.SignDetached(sap, signature, chain, null, null, null, 0, CryptoStandard.CMS);
IExternalSignature
实现的更新在您的问题更新中,您添加了一个签名的PDF,其中应用了上述更改。通过分析签名容器中的签名字节,可以清楚地看到您的签名服务被设计为非常笨拙,它使用PKCS1 v1.5填充和RSA加密,但是假定其输入已经打包到DigestInfo
结构中。根据我的经验,这是一个不常见的假设,您应该告诉签名提供者正确记录这一点。
对于您的代码,这意味着您必须在将哈希发送到服务之前将其打包为DigestInfo
结构。
RFC 8017 section 9.2 note 1中介绍了一种简单的方法:
对于附录B.1中提到的九种哈希函数,DER
DigestInfo
值的编码T等于以下值:... SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H. ...
即您只需在哈希前面加上字节序列30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20
。
因此,RemoteSignature
类的一种变体,用于需要调用者将摘要打包到DigestInfo
结构中的服务,如下所示:
class RemoteSignature : IExternalSignature
{
public virtual byte[] Sign(byte[] message) {
IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
byte[] sha256Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
byte[] digestInfo = new byte[sha256Prefix.Length + messageHash.Length];
sha256Prefix.CopyTo(digestInfo, 0);
messageHash.CopyTo(digestInfo, sha256Prefix.Length);
//
// Request signature for DigestInfo value digestInfo
// and return signature bytes
//
return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_DIGEST_INFO(digestInfo);
}
public virtual String GetHashAlgorithm() {
return "SHA-256";
}
public virtual String GetEncryptionAlgorithm() {
return "RSA";
}
}
我对此表示怀疑。签署者证书的密钥用法仅包含用于签署其他证书的值。
如果您查看Adobe Digital Signatures Guide for IT,将会看到有效的密钥用法扩展名是
nonRepudiation
signTransaction
(仅11.0.09)digitalSignature
(11.0.10及更高版本)因此,证书的signCertificate
值可能是个问题。