我今天请求您帮助使用 C# 中的 iText8 进行 PaDES Baseline-LTA Profile PDF 签名项目。
我使用受信任的第三方 API 来签名哈希,我想遵循此工作流程:
可信第三方建议我使用 DSS 生成 DTBS (https://github.com/esig/dss),但我正在使用 C# 工作,不想使用 Java。
老实说,我觉得我完全偏离了目标......
是否可以在没有 DSS 帮助的情况下使用 BouncyCastle 创建 DTBS?
我应该使用 iText 什么方法来实现这种类型的签名?
预先感谢您的帮助。
编辑:
生成的pdf:https://drive.google.com/file/d/1rnY7tuXPJkzSgLFasQ6R7I8BDQ1RWwM_/view?usp=sharing
我使用 itext 8.0.3
测试代码:
using Flurl;
using Flurl.Http;
using iText.Bouncycastle.X509;
using iText.Commons.Bouncycastle.Cert;
using iText.Kernel.Geom;
using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.X509;
using SignatureCerteurope.Certeurope;
using SignatureCerteurope.ComponentC.Json;
using System.Security.Cryptography.X509Certificates;
string DEST = "SIGNED.PDF";
string SRC = "TEST.PDF";
var sign = new SignatureAPI();
var cert = await sign.GetCertificate();
string base64Certificate = cert.pem
.Replace("-----BEGIN CERTIFICATE-----\n", "")
.Replace("\n-----END CERTIFICATE-----\n", "");
byte[] certificateBytes = Convert.FromBase64String(base64Certificate);
Org.BouncyCastle.X509.X509CertificateParser parser = new Org.BouncyCastle.X509.X509CertificateParser();
Org.BouncyCastle.X509.X509Certificate x509cert = parser.ReadCertificate(certificateBytes);
iText.Commons.Bouncycastle.Cert.IX509Certificate[] certificateWrappers = new IX509Certificate[1];
certificateWrappers[0] = new X509CertificateBC(x509cert);
new C4_07_ClientServerSigning().Sign(SRC, DEST, certificateWrappers, PdfSigner.CryptoStandard.CMS, "test reason", "test location");
public class C4_07_ClientServerSigning
{
public void Sign(string src, string dest, IX509Certificate[] chain, PdfSigner.CryptoStandard subfilter, string reason, string location)
{
PdfReader reader = new PdfReader(src);
PdfSigner signer = new PdfSigner(reader, new FileStream(dest, FileMode.Create), new StampingProperties());
// Create the signature appearance
Rectangle rect = new Rectangle(36, 648, 200, 100);
signer.SetReason(reason)
.SetLocation(location)
.SetPageRect(rect)
.SetPageNumber(1)
.SetFieldName("sig");
IExternalSignature signature = new ServerSignature();
signer.SignDetached(signature, chain, null, null, null, 0, subfilter);
}
public class ServerSignature : IExternalSignature
{
public string GetDigestAlgorithmName()
{
return DigestAlgorithms.SHA256;
}
public string GetSignatureAlgorithmName()
{
return "RSA";
}
public ISignatureMechanismParams GetSignatureMechanismParameters()
{
return null;
}
public byte[] Sign(byte[] message)
{
HashPDF json = new HashPDF
{
orderRequestId = 444928,
hash = new List<HashItem>()
};
HashItem unHash = new HashItem
{
hash = Convert.ToBase64String(message)
};
json.hash.Add(unHash);
var sign = new SignatureAPI();
HashSignatureRequestCollectDTO jsonres = sign.SignHash("444928", json).GetAwaiter().GetResult();
return Convert.FromBase64String(jsonres.Signatures[0].Signature);
}
}
}
public class SignatureAPI
{
private const int _ORDER_REQUEST_ID = 444928;
private const string _API_BASE_URL = "SIGNURL";
private const string _SERIAL_NUMBER_CERTIFICATE = "17E1BC";
public SignatureAPI()
{
X509Store store = new X509Store(StoreName.My);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindBySerialNumber, _SERIAL_NUMBER_CERTIFICATE, false);
X509Certificate2 certificate = col[0];
FlurlHttp.Clients.WithDefaults(builder => builder.ConfigureInnerHandler(h => { h.ClientCertificates.Add(certificate); }));
}
public async Task<HashSignatureRequestCollectDTO> SignHash(string id, HashPDF json)
{
string apiUrl = "SIGNHASH";
var response = await apiUrl.SetQueryParam("orderRequestId", id).PostJsonAsync(json);
var result = await response.GetStringAsync();
HashSignatureRequestCollectDTO jsonRes = result.ConvertJsonStringWithTypeToObject<HashSignatureRequestCollectDTO>();
return jsonRes;
}
public async Task<CertificateJson> GetCertificate()
{
string apiUrl = "GETCERT";
return await apiUrl.SetQueryParam("orderRequestId", _ORDER_REQUEST_ID).GetJsonAsync<CertificateJson>();
}
}
public class SignatureResponse
{
public int SignatureRequestId { get; set; }
public string Status { get; set; }
public string Hash { get; set; }
public string Signature { get; set; }
public string ErrorMessage { get; set; }
}
public class HashSignatureRequestCollectDTO
{
public int OrderRequestId { get; set; }
public object ExternalOrderRequestId { get; set; }
public List<SignatureResponse> Signatures { get; set; }
}
public class HashItem
{
public string hash { get; set; }
}
public class HashPDF
{
public int orderRequestId { get; set; }
public List<HashItem> hash { get; set; }
}
解密示例 PDF 中签名容器中的签名字节,发现它们不包含包装签名属性哈希的
DigestInfo
结构,而是包含签名属性本身。
因此,当您在代码中假设远程签名服务将散列并包装您发送给它的字节时,该服务实际上不会这样做,但希望您这样做。
这种差异实际上在您的
ServerSignature.Sign
实现中的这一行中很明显:
json.hash.Add(unHash);
在这里,您将一些 un-hashed 添加到集合中,根据其名称,该集合应包含一些 hashed 项目。
要解决此问题,您应该更换
HashItem unHash = new HashItem
{
hash = Convert.ToBase64String(message)
};
json.hash.Add(unHash);
通过类似
byte[] digestValue = <<Calculate SHA-256 hash of message>>;
byte[] digestInfo = <<Wrap digestValue in a DigestInfo object>>;
HashItem digestInfoHashItem = new HashItem
{
hash = Convert.ToBase64String(digestInfo)
};
json.hash.Add(digestInfoHashItem);
要将digestValue 包装在DigestInfo 对象中,RFC 8017 提供了一条捷径:
对于附录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
添加到您的 digestValue
字节即可。