itext7:延迟签名未生成有效的签名 pdf
itext7: deferred signing not producing a valid signed pdf
我正在尝试使用 itext7 通过从外部实体获取签名来签署 pdf。我一定是遗漏了什么,因为延迟的 pdf 签名无效。让我们从延迟签名的代码开始:
public byte[] GetDocHashFromPreparedDocToSign(string pathToOriginalPdf, string pathToPreparedToBeSignedPdf, List<X509Certificate> certificates) {
var pdfSigner = new PdfSigner(new PdfReader(pathToOriginalPdf),
new FileStream(pathToPreparedToBeSignedPdf, FileMode.Create),
new StampingProperties());
pdfSigner.SetFieldName(_signatureFieldname);
var appearance = pdfSigner.GetSignatureAppearance();
appearance.SetPageRect(new Rectangle(144, 144, 200, 100))
.SetPageNumber(1)
.SetCertificate(certificates[0]);
var container = new ExternalBlankSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
pdfSigner.SignExternalContainer(container, 8192);
byte[] sha256SigPrefix = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09,
0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
0x05, 0x00, 0x04, 0x20 };
// service needs to receive sha256 prepended
using var stream = File.OpenRead(pathToPreparedToBeSignedPdf);
var data = DigestAlgorithms.Digest(stream, DigestAlgorithms.SHA256);
var totalHash = new byte[sha256SigPrefix.Length + data.Length];
sha256SigPrefix.CopyTo(totalHash, 0);
data.CopyTo(totalHash, sha256SigPrefix.Length);
return totalHash;
}
该方法接收到原始 pdf 的路径、将包含签名占位符的临时 pdf 的路径以及从原始服务检索到的 X509Certificate 列表。在为签名保留 space 后,该方法计算文件的哈希值并在其前面添加 sha256 前缀(将对文档进行签名的服务需要)。
此信息被发送到将 return 签名的服务。当签名被取回时,会调用下面的方法来用真正的签名填充签名占位符:
public void SignPreparedToBeSignedDoc(string pathToPreparedToBeSignedPdf, string pathToSignedFile, byte[] signature) {
var document = new PdfDocument(new PdfReader(pathToPreparedToBeSignedPdf));
using var writer = new FileStream(pathToSignedFile, FileMode.Create);
var container = new ExternalInjectingSignatureContainer(signature);
PdfSigner.SignDeferred(document, _signatureFieldname, writer, container);
}
编辑:根据@mkl 评论,我修复了签名部分:
这是 ExternalInjectingSignatureContainer:
internal class ExternalInjectingSignatureContainer : IExternalSignatureContainer {
private readonly byte[] _signature;
public ExternalInjectingSignatureContainer(byte[] signature) {
_signature = signature;
}
public byte[] Sign(Stream data){
var sgn = new PdfPKCS7(null, _certificates.ToArray(), "SHA256", false);
sgn.SetExternalDigest(_signature, null, "RSA");
return sgn.GetEncodedPKCS7();
}
public void ModifySigningDictionary(PdfDictionary signDic) {
}
}
即使代码运行没有错误,在 adobe 中打开 pdf 也会显示以下错误:
编辑:修复签名代码后,现在错误不同了:它会显示签名信息,但会说文件已被更改或损坏。
此时,似乎正确生成了临时 pdf,但我可能遗漏了一些东西...关于如何调试此问题的任何线索?
谢谢
编辑:针对@mkl 提出的解决方案的评论,我尝试更新代码。我今天还有几分钟的时间来玩这个,我试着遵循提出的指导方针,但我显然仍然遗漏了一些东西。
在展示新代码之前,我想指出之前的更新版本(使用 2 个 IExternalSignatureContainer
实例)似乎工作正常。即,在 adobe 上打开签名的 pdf 只会显示黄色警告,指出签名有问题:
由于该文档正在与测试链一起使用,因此签名似乎工作正常(尽管我可能完全错了)。
因此,为了修复容器的错误使用,我重写了 IExternalSignatureContainer
的 Sign
方法的代码。这是我为准备要发送到服务器的文档散列而准备的代码:
public override byte[] Sign(Stream data) {
// create PCKS7 for getting attributes
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// get document hash
DocumentDigest = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
var docBytesHash = sgn.GetAuthenticatedAttributeBytes(DocumentDigest,
PdfSigner.CryptoStandard.CMS,
null,
null);
//prepend sha256 prefix
var totalHash = new byte[_sha256SigPrefix.Length + docBytesHash.Length];
_sha256SigPrefix.CopyTo(totalHash, 0);
docBytesHash.CopyTo(totalHash, _sha256SigPrefix.Length);
DataToSend = totalHash;
return new byte[0];
}
因为我必须使用传递给 GetAuthenticatedAttributes
的相同参数调用 GetEncodedPKCS7
方法,所以我还保存了通过 Digest
方法获得的文档哈希。 DataToSend
将被发送到服务器,以便它可以 return 该散列的签名。
这是为延迟签名 (PdfSigner.SignDeferred
) 调用的另一个 IExternalSignatureContainer
的代码:
public byte[] Sign(Stream data) {
// create CMS
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// set the signature bytes
sgn.SetExternalDigest(_signature, null, "RSA");
// call GetEncoded with the same parameters as the original GetAuthenticatedAtt...
//_documentHash == DocumentDigest previous sample
var encodedSig = sgn.GetEncodedPKCS7(_documentHash,
PdfSigner.CryptoStandard.CMS,
null,
null,
null);
return encodedSig;
}
不幸的是,我一定遗漏了一些东西(或很多东西):
我完全没听懂你的意思吗?
编辑:再一次,在@mkl 的带领下,我能够让它发挥作用。就像他说的,你需要散列 GetAuthenticatedAttributeBytes
值:
public override byte[] Sign(Stream data) {
// create PCKS7 for getting attributes
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// get document hash
DocumentDigest = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
var docBytes = sgn.GetAuthenticatedAttributeBytes(DocumentDigest,
PdfSigner.CryptoStandard.CMS,
null,
null);
// hash dochBytes
using var hashMemoryStream = new MemoryStream(docBytes, false);
var docBytesHash = DigestAlgorithms.Digest(hashMemoryStream,
DigestAlgorithms.SHA256);
//prepend sha256 prefix
var totalHash = new byte[_sha256SigPrefix.Length + docBytesHash.Length];
_sha256SigPrefix.CopyTo(totalHash, 0);
docBytesHash.CopyTo(totalHash, _sha256SigPrefix.Length);
DataToSend = totalHash;
return new byte[0];
}
再次感谢。
有两个明显的问题,散列错误的数据和注入错误类型的签名:
散列错误数据
你这样计算要签名的哈希:
using var stream = File.OpenRead(pathToPreparedToBeSignedPdf);
var data = DigestAlgorithms.Digest(stream, DigestAlgorithms.SHA256);
这是不正确的。
签名的 PDF 基本上具有以下结构(阅读 here 了解更多详情):
(顺便说一下,草图不是 100% 正确的,因为签名值周围的尖括号分隔符“<”和“>”也不能被散列。)
您在 pathToPreparedToBeSignedPdf
准备的 PDF 具有相同的结构,只是“签名值”还不是实际的 签名 值,而是一个占位符 8192 十六进制-编码的零字节(8192 因为那是你在 pdfSigner.SignExternalContainer
中给出的数字)。
不过,正如您在草图中看到的那样,签名值(或在您的情况下为占位符)不得经过哈希处理以进行签名。
除了占位符之外,检索准备好的 PDF 的最简单方法是在您用于准备 PDF 的 IExternalSignatureContainer
实现中,因为它的 Sign
方法作为参数获取包含该内容的流。因此,不要使用 ExternalBlankSignatureContainer
,而是使用 中的 ExternalEmptySignatureContainer
:
public class ExternalEmptySignatureContainer : IExternalSignatureContainer
{
public void ModifySigningDictionary(PdfDictionary signDic)
{
signDic.Put(PdfName.Filter, PdfName.Adobe_PPKLite);
signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
}
public byte[] Sign(Stream data)
{
// Store the data to sign and return an empty array
Data = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
return new byte[0];
}
public byte[] Data;
}
并在准备好从其 Data
成员中检索 byte[]
之后:
var container = new ExternalEmptySignatureContainer();
pdfSigner.SignExternalContainer(container, 8192);
byte[] hash = container.Data;
在您的情况下,您可能必须在 sha256SigPrefix
前面加上一个完整的编码 DigestInfo
对象。
注入错误类型的签名
此外,考虑到您的屏幕截图
您显然注入了错误类型的签名。您设置了一个子过滤器 PdfName.Adbe_pkcs7_detached
,这意味着要嵌入的签名是一个 CMS 签名容器,其中有一个 SignerInfo 对 PDF 的签名字节进行签名。不过,错误消息表明您嵌入的签名容器已损坏或不是 CMS 签名容器。
编辑后:注入内容不正确的签名容器
为了解决上一个问题“注入错误类型的签名”,您更改了 ExternalInjectingSignatureContainer
以像这样为您的签名字节构建 CMS 签名容器:
var sgn = new PdfPKCS7(null, _certificates.ToArray(), "SHA256", false);
sgn.SetExternalDigest(_signature, null, "RSA");
return sgn.GetEncodedPKCS7();
不幸的是,这错误地使用了 PdfPKCS7
class 导致 CMS 签名容器包含不正确的数据。
(更正:生成的CMS容器本身并不是不正确,而是结构极其简单。现在许多配置文件 - 例如欧洲 PAdES - 需要额外的签名属性,并且为了满足这些配置文件,您必须按照以下文本中的描述进行修复。如果您只需要 Adobe Reader 来显示有效,那么上面的代码就足够了给你。)
要解决这个问题,您可以自己构建一个 CMS 签名容器,使用 iText PdfPKCS7
class 或其他方式(例如 BouncyCastle 或内置 .Net classes) ,或者您可以让 iText 为您完成。
最简单的方法是后一种方法。您实际上根本不需要延迟签名,您只需实现 IExternalSignature
以便它调用您的远程服务:
public class RemoteSignature : IExternalSignature
{
public virtual byte[] Sign(byte[] message) {
IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
byte[] digestInfo = [... prefix messageHash with sha256SigPrefix ...];
//
// Request signature for DigestInfo digestInfo
// and return signature bytes
//
return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(digestInfo);
}
public virtual String GetHashAlgorithm() {
return "SHA-256";
}
public virtual String GetEncryptionAlgorithm() {
return "RSA";
}
}
现在您只需签名就可以了
var pdfSigner = new PdfSigner(new PdfReader(pathToOriginalPdf),
new FileStream(pathToSignedFile, FileMode.Create),
new StampingProperties());
pdfSigner.SetFieldName(_signatureFieldname);
var appearance = pdfSigner.GetSignatureAppearance();
appearance.SetPageRect(new Rectangle(144, 144, 200, 100))
.SetPageNumber(1)
.SetCertificate(certificates[0]);
var signature = new RemoteSignature();
pdfSigner.SignDetached(signature, certificates, null, null, null, 0, PdfSigner.CryptoStandard.CMS);
我正在尝试使用 itext7 通过从外部实体获取签名来签署 pdf。我一定是遗漏了什么,因为延迟的 pdf 签名无效。让我们从延迟签名的代码开始:
public byte[] GetDocHashFromPreparedDocToSign(string pathToOriginalPdf, string pathToPreparedToBeSignedPdf, List<X509Certificate> certificates) {
var pdfSigner = new PdfSigner(new PdfReader(pathToOriginalPdf),
new FileStream(pathToPreparedToBeSignedPdf, FileMode.Create),
new StampingProperties());
pdfSigner.SetFieldName(_signatureFieldname);
var appearance = pdfSigner.GetSignatureAppearance();
appearance.SetPageRect(new Rectangle(144, 144, 200, 100))
.SetPageNumber(1)
.SetCertificate(certificates[0]);
var container = new ExternalBlankSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
pdfSigner.SignExternalContainer(container, 8192);
byte[] sha256SigPrefix = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09,
0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
0x05, 0x00, 0x04, 0x20 };
// service needs to receive sha256 prepended
using var stream = File.OpenRead(pathToPreparedToBeSignedPdf);
var data = DigestAlgorithms.Digest(stream, DigestAlgorithms.SHA256);
var totalHash = new byte[sha256SigPrefix.Length + data.Length];
sha256SigPrefix.CopyTo(totalHash, 0);
data.CopyTo(totalHash, sha256SigPrefix.Length);
return totalHash;
}
该方法接收到原始 pdf 的路径、将包含签名占位符的临时 pdf 的路径以及从原始服务检索到的 X509Certificate 列表。在为签名保留 space 后,该方法计算文件的哈希值并在其前面添加 sha256 前缀(将对文档进行签名的服务需要)。
此信息被发送到将 return 签名的服务。当签名被取回时,会调用下面的方法来用真正的签名填充签名占位符:
public void SignPreparedToBeSignedDoc(string pathToPreparedToBeSignedPdf, string pathToSignedFile, byte[] signature) {
var document = new PdfDocument(new PdfReader(pathToPreparedToBeSignedPdf));
using var writer = new FileStream(pathToSignedFile, FileMode.Create);
var container = new ExternalInjectingSignatureContainer(signature);
PdfSigner.SignDeferred(document, _signatureFieldname, writer, container);
}
编辑:根据@mkl 评论,我修复了签名部分: 这是 ExternalInjectingSignatureContainer:
internal class ExternalInjectingSignatureContainer : IExternalSignatureContainer {
private readonly byte[] _signature;
public ExternalInjectingSignatureContainer(byte[] signature) {
_signature = signature;
}
public byte[] Sign(Stream data){
var sgn = new PdfPKCS7(null, _certificates.ToArray(), "SHA256", false);
sgn.SetExternalDigest(_signature, null, "RSA");
return sgn.GetEncodedPKCS7();
}
public void ModifySigningDictionary(PdfDictionary signDic) {
}
}
即使代码运行没有错误,在 adobe 中打开 pdf 也会显示以下错误:
编辑:修复签名代码后,现在错误不同了:它会显示签名信息,但会说文件已被更改或损坏。
此时,似乎正确生成了临时 pdf,但我可能遗漏了一些东西...关于如何调试此问题的任何线索?
谢谢
编辑:针对@mkl 提出的解决方案的评论,我尝试更新代码。我今天还有几分钟的时间来玩这个,我试着遵循提出的指导方针,但我显然仍然遗漏了一些东西。
在展示新代码之前,我想指出之前的更新版本(使用 2 个 IExternalSignatureContainer
实例)似乎工作正常。即,在 adobe 上打开签名的 pdf 只会显示黄色警告,指出签名有问题:
由于该文档正在与测试链一起使用,因此签名似乎工作正常(尽管我可能完全错了)。
因此,为了修复容器的错误使用,我重写了 IExternalSignatureContainer
的 Sign
方法的代码。这是我为准备要发送到服务器的文档散列而准备的代码:
public override byte[] Sign(Stream data) {
// create PCKS7 for getting attributes
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// get document hash
DocumentDigest = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
var docBytesHash = sgn.GetAuthenticatedAttributeBytes(DocumentDigest,
PdfSigner.CryptoStandard.CMS,
null,
null);
//prepend sha256 prefix
var totalHash = new byte[_sha256SigPrefix.Length + docBytesHash.Length];
_sha256SigPrefix.CopyTo(totalHash, 0);
docBytesHash.CopyTo(totalHash, _sha256SigPrefix.Length);
DataToSend = totalHash;
return new byte[0];
}
因为我必须使用传递给 GetAuthenticatedAttributes
的相同参数调用 GetEncodedPKCS7
方法,所以我还保存了通过 Digest
方法获得的文档哈希。 DataToSend
将被发送到服务器,以便它可以 return 该散列的签名。
这是为延迟签名 (PdfSigner.SignDeferred
) 调用的另一个 IExternalSignatureContainer
的代码:
public byte[] Sign(Stream data) {
// create CMS
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// set the signature bytes
sgn.SetExternalDigest(_signature, null, "RSA");
// call GetEncoded with the same parameters as the original GetAuthenticatedAtt...
//_documentHash == DocumentDigest previous sample
var encodedSig = sgn.GetEncodedPKCS7(_documentHash,
PdfSigner.CryptoStandard.CMS,
null,
null,
null);
return encodedSig;
}
不幸的是,我一定遗漏了一些东西(或很多东西):
我完全没听懂你的意思吗?
编辑:再一次,在@mkl 的带领下,我能够让它发挥作用。就像他说的,你需要散列 GetAuthenticatedAttributeBytes
值:
public override byte[] Sign(Stream data) {
// create PCKS7 for getting attributes
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// get document hash
DocumentDigest = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
var docBytes = sgn.GetAuthenticatedAttributeBytes(DocumentDigest,
PdfSigner.CryptoStandard.CMS,
null,
null);
// hash dochBytes
using var hashMemoryStream = new MemoryStream(docBytes, false);
var docBytesHash = DigestAlgorithms.Digest(hashMemoryStream,
DigestAlgorithms.SHA256);
//prepend sha256 prefix
var totalHash = new byte[_sha256SigPrefix.Length + docBytesHash.Length];
_sha256SigPrefix.CopyTo(totalHash, 0);
docBytesHash.CopyTo(totalHash, _sha256SigPrefix.Length);
DataToSend = totalHash;
return new byte[0];
}
再次感谢。
有两个明显的问题,散列错误的数据和注入错误类型的签名:
散列错误数据
你这样计算要签名的哈希:
using var stream = File.OpenRead(pathToPreparedToBeSignedPdf);
var data = DigestAlgorithms.Digest(stream, DigestAlgorithms.SHA256);
这是不正确的。
签名的 PDF 基本上具有以下结构(阅读 here 了解更多详情):
(顺便说一下,草图不是 100% 正确的,因为签名值周围的尖括号分隔符“<”和“>”也不能被散列。)
您在 pathToPreparedToBeSignedPdf
准备的 PDF 具有相同的结构,只是“签名值”还不是实际的 签名 值,而是一个占位符 8192 十六进制-编码的零字节(8192 因为那是你在 pdfSigner.SignExternalContainer
中给出的数字)。
不过,正如您在草图中看到的那样,签名值(或在您的情况下为占位符)不得经过哈希处理以进行签名。
除了占位符之外,检索准备好的 PDF 的最简单方法是在您用于准备 PDF 的 IExternalSignatureContainer
实现中,因为它的 Sign
方法作为参数获取包含该内容的流。因此,不要使用 ExternalBlankSignatureContainer
,而是使用 ExternalEmptySignatureContainer
:
public class ExternalEmptySignatureContainer : IExternalSignatureContainer
{
public void ModifySigningDictionary(PdfDictionary signDic)
{
signDic.Put(PdfName.Filter, PdfName.Adobe_PPKLite);
signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
}
public byte[] Sign(Stream data)
{
// Store the data to sign and return an empty array
Data = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
return new byte[0];
}
public byte[] Data;
}
并在准备好从其 Data
成员中检索 byte[]
之后:
var container = new ExternalEmptySignatureContainer();
pdfSigner.SignExternalContainer(container, 8192);
byte[] hash = container.Data;
在您的情况下,您可能必须在 sha256SigPrefix
前面加上一个完整的编码 DigestInfo
对象。
注入错误类型的签名
此外,考虑到您的屏幕截图
您显然注入了错误类型的签名。您设置了一个子过滤器 PdfName.Adbe_pkcs7_detached
,这意味着要嵌入的签名是一个 CMS 签名容器,其中有一个 SignerInfo 对 PDF 的签名字节进行签名。不过,错误消息表明您嵌入的签名容器已损坏或不是 CMS 签名容器。
编辑后:注入内容不正确的签名容器
为了解决上一个问题“注入错误类型的签名”,您更改了 ExternalInjectingSignatureContainer
以像这样为您的签名字节构建 CMS 签名容器:
var sgn = new PdfPKCS7(null, _certificates.ToArray(), "SHA256", false);
sgn.SetExternalDigest(_signature, null, "RSA");
return sgn.GetEncodedPKCS7();
不幸的是,这错误地使用了 PdfPKCS7
class 导致 CMS 签名容器包含不正确的数据。
(更正:生成的CMS容器本身并不是不正确,而是结构极其简单。现在许多配置文件 - 例如欧洲 PAdES - 需要额外的签名属性,并且为了满足这些配置文件,您必须按照以下文本中的描述进行修复。如果您只需要 Adobe Reader 来显示有效,那么上面的代码就足够了给你。)
要解决这个问题,您可以自己构建一个 CMS 签名容器,使用 iText PdfPKCS7
class 或其他方式(例如 BouncyCastle 或内置 .Net classes) ,或者您可以让 iText 为您完成。
最简单的方法是后一种方法。您实际上根本不需要延迟签名,您只需实现 IExternalSignature
以便它调用您的远程服务:
public class RemoteSignature : IExternalSignature
{
public virtual byte[] Sign(byte[] message) {
IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
byte[] digestInfo = [... prefix messageHash with sha256SigPrefix ...];
//
// Request signature for DigestInfo digestInfo
// and return signature bytes
//
return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(digestInfo);
}
public virtual String GetHashAlgorithm() {
return "SHA-256";
}
public virtual String GetEncryptionAlgorithm() {
return "RSA";
}
}
现在您只需签名就可以了
var pdfSigner = new PdfSigner(new PdfReader(pathToOriginalPdf),
new FileStream(pathToSignedFile, FileMode.Create),
new StampingProperties());
pdfSigner.SetFieldName(_signatureFieldname);
var appearance = pdfSigner.GetSignatureAppearance();
appearance.SetPageRect(new Rectangle(144, 144, 200, 100))
.SetPageNumber(1)
.SetCertificate(certificates[0]);
var signature = new RemoteSignature();
pdfSigner.SignDetached(signature, certificates, null, null, null, 0, PdfSigner.CryptoStandard.CMS);