I'm trying to migrate an existing code base to itext 7 which is responsible for signing pdf files (ti's deferred signing). After migrating to the new apis, the document does get signed, but it's giving an error which says that the document has been modified after signing:
I guess that I'm probably miscalculating the size reserved for the signing, but I'm not sure on where I'm messing things up. here's my current code:
TSAClientBouncyCastle tsaClient = new("https://freetsa.org/tsr");
// crl list for revocation
// userCertificationChain is a IEnumerable<x509> with the certificate chain used for signimng
List<ICrlClient> crlClients = new() {new CrlClientOnline(userCertificatesChain.ToArray())};
// added ocsp client
OcspClientBouncyCastle ocspClient = new(null);
PdfSignerHelper sgn = new PdfSignerHelper(userCertificatesChain.ToArray( ));
PdfSigningManager pdfSigner = new(userCertificatesChain,
sgn,
crlClients: crlClients,
ocspClient: ocspClient,
tsaClient: tsaClient);
HashesForSigning hashInformation = pdfSigner.CreateTemporaryPdfForSigning(
new SigningInformation(pdfToBeSigned,
temporaryPdf,
Reason: "Because yes",
Location: "Funchal",
Logo: logo));
// signinginformation is a simple record for passing info between objects
CreateTemporaryPdfForSigning
looks like this:
public HashesForSigning CreateTemporaryPdfForSigning(SigningInformation signingInformation) {
StampingProperties properties = new();
properties.UseAppendMode();
PdfSigner pdfSigner = new(new PdfReader(signingInformation.PathToPdf),
new FileStream(signingInformation.PathToIntermediaryPdf, FileMode.Create),
properties);
pdfSigner.SetFieldName(_signatureFieldname);
SignatureFieldAppearance appearance = new(pdfSigner.GetFieldName());
if(signingInformation.Logo is null) {
appearance.SetContent(BuildVisibleInformation(signingInformation.Reason,
signingInformation.Location));
}
else {
appearance.SetContent(BuildVisibleInformation(signingInformation.Reason,
signingInformation.Location),
signingInformation.Logo);
}
appearance.SetFontSize(6f);
pdfSigner.SetSignatureAppearance(appearance);
pdfSigner.SetPageNumber(signingInformation.PageNumber)
.SetPageRect(new Rectangle(10, 750, 150,50));
IList<byte[]>? crlBytesList = GetCrlByteList();
IList<byte[]>? ocspBytesList = GetOcspBytesList();
int estimatedSize = EstimateContainerSize(crlBytesList, ocspBytesList);
Console.WriteLine($"Estimated size: {estimatedSize}");
PrepareForAmaSigningContainer container = new(_sgn,
crlBytesList,
ocspBytesList);
pdfSigner.SignExternalContainer(container, estimatedSize); // add size for timestamp in signature
return new HashesForSigning(container.HashToBeSignedByAma, container.NakedHash);
}
The EstimateContainerSize
method is responsible for calculating the estimated size of the signature. Btw, here's how it's getting calculated:
private int EstimateContainerSize(IEnumerable<byte[]>? crlBytesList,
, IEnumerable<byte[]>? ocspBytesList) {
int estimatedSize = 8192 + //initial base container size
( _ocspClient != null ? 4192 : 0 ) +
( _tsaClient != null ? 4600 : 0 );
if(crlBytesList != null) {
estimatedSize += crlBytesList.Sum(crlBytes => crlBytes.Length + 10);
}
if(ocspBytesList != null) {
estimatedSize += ocspBytesList.Sum(c => c.Length + 10);
}
return estimatedSize;
}
EDIT: as suggested by @mkl, I've updated the code so that the same PdfPKCS7
instance is used when required. It's encapsulated in a new type which looks like this:
public sealed class PdfSignerHelper {
private const string _signaturePolicyUri = "https://www.autenticacao.gov.pt/documents/20126/0/POL%2316.PolAssQual_signed_signed.pdf";
private readonly PdfPKCS7 _sgn;
public PdfSignerHelper(IEnumerable<IX509Certificate> certificates) {
_sgn = new(null,
certificates.ToArray( ),
DigestAlgorithms.SHA256,
false);
// set signature policy information
MemoryStream policyIdByteMs = new(Encoding.ASCII.GetBytes("2.16.620.2.1.2.2"), false);
byte[]? policyIdByte = DigestAlgorithms.Digest(policyIdByteMs, DigestAlgorithms.SHA256);
SignaturePolicyInfo spi = new("2.16.620.2.1.2.2", policyIdByte, DigestAlgorithms.SHA256, _signaturePolicyUri);
_sgn.SetSignaturePolicy(spi);
}
public PdfPKCS7 Signer => _sgn;
}
This type is injected in several of the other existing types and it's kept on a field named _sgn
(for instance, you'll see it on the CreateTemporaryPdfForSigning' method being passed to the constructor of
PrepareForAmaSigningContainer`
PrepareForAmaSigningContainer
is a simple class which extends the ExternalBlankSignatureContainer
for creating a "blank" signature:
public class PrepareForAmaSigningContainer : ExternalBlankSignatureContainer {
private static readonly byte[] _sha256SigPrefix = {
0x30, 0x31, 0x30, 0x0d, 0x06, 0x09,
0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
0x05, 0x00, 0x04, 0x20
};
private readonly IEnumerable<byte[]>? _crlBytesCollection;
private readonly IEnumerable<byte[]>? _ocspBytes;
private readonly PdfSignerHelper _sgn;
/// <summary>
/// Creates a new instance of the container
/// </summary>
/// <param name="certificates">User certficate chain</param>
/// <param name="crlBytesCollection">Collection of CRL bytes for revocation check</param>
/// <param name="ocspBytes">Collection od OCSP bytes for revocation</param>
public PrepareForAmaSigningContainer(PdfSignerHelper sgn,
IEnumerable<byte[]>? crlBytesCollection,
IEnumerable<byte[]>? ocspBytes) : base(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached) {
_crlBytesCollection = crlBytesCollection;
_ocspBytes = ocspBytes;
_sgn = sgn;
}
/// <summary>
/// Returns the hash that must be send for signing
/// </summary>
public byte[] HashToBeSignedByAma { get; private set; } = new byte[0];
/// <summary>
/// Original naked hash of the document (used for injecting the signature when it's retrieved from AMA)
/// </summary>
public byte[] NakedHash { get; private set; } = new byte[0];
/// <summary>
/// Method that will be called during the signing process
/// </summary>
/// <param name="data">Stream with the doc data that should be used in the hasing process</param>
/// <returns></returns>
public override byte[] Sign(Stream data) {
// get hash for document bytes
NakedHash = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
byte[]? docBytes = _sgn.Signer.GetAuthenticatedAttributeBytes(NakedHash,
PdfSigner.CryptoStandard.CMS,
_ocspBytes?.ToList( ),
_crlBytesCollection?.ToList( ));
// hash it again
using MemoryStream hashMemoryStream = new(docBytes, false);
byte[]? docBytesHash = DigestAlgorithms.Digest(hashMemoryStream, DigestAlgorithms.SHA256);
//prepend sha256 prefix
byte[] totalHash = new byte[_sha256SigPrefix.Length + docBytesHash.Length];
_sha256SigPrefix.CopyTo(totalHash, 0);
docBytesHash.CopyTo(totalHash, _sha256SigPrefix.Length);
HashToBeSignedByAma = totalHash;
Console.WriteLine($"size hash to be signed ama: {totalHash.Length}");
return Array.Empty<byte>();
}
}
The returned HashesForSigning
is then passed to an external service which returns a byte array with the signature that will get appended to the intermediate doc:
byte[] signature = GetExternalSignature();
pdfSigner.SignIntermediatePdf(new SignatureInformation(temporaryPdf,
finalPdf,
signature,
hashInformation.NakedHash));
And here's the helper method that appends the signature to the intermediate pdf to get the signed pdf:
public void SignIntermediatePdf(SignatureInformation signatureInformation) {
PdfDocument document = new(new PdfReader(signatureInformation.PathToIntermediaryPdf));
using FileStream writer = new(signatureInformation.pathToSignedPdf, FileMode.Create);
IList<byte[]>? crlBytesList = GetCrlByteList();
IList<byte[]>? ocspBytesList = GetOcspBytesList();
Console.WriteLine($"pdf signing manager najed hash interm: {signatureInformation.NakedHashFromIntermediaryPdf.Length}");
InjectAmaSignatureContainer container = new(signatureInformation.Signature,
_sgn,
signatureInformation.NakedHashFromIntermediaryPdf,
crlBytesList,
ocspBytesList,
_tsaClient);
PdfSigner.SignDeferred(document, _signatureFieldname, writer, container);
}
Finally, InjectAmaSignatureContainer
is class which implements the IExternalSignatureContainer
and that looks like this:
public class InjectAmaSignatureContainer : IExternalSignatureContainer {
private readonly IEnumerable<IX509Certificate> _certificates;
private readonly IEnumerable<byte[]>? _crlBytesCollection;
private readonly byte[] _documentHash;
private readonly IEnumerable<byte[]>? _ocspBytes;
private readonly byte[] _signature;
private readonly ITSAClient? _tsaClient;
private readonly PdfSignerHelper _sgn;
private const string _signaturePolicyUri = "https://www.autenticacao.gov.pt/documents/20126/0/POL%2316.PolAssQual_signed_signed.pdf";
/// <summary>
/// Creates a new instance of the external container
/// </summary>
/// <param name="signature">Byte array with AMA's generated signature for specified doc</param>
/// <param name="certificates">User's certificate chain</param>
/// <param name="documentHash">Naked document hash used during preparation phase</param>
/// <param name="crlBytesCollection">CRL information that should be embedded</param>
/// <param name="ocspBytes">OCSP information that should be embedded</param>
/// <param name="tsaClient">TSA client that should be used for timestamping the document</param>
public InjectAmaSignatureContainer(byte[] signature,
PdfSignerHelper sgn,
byte[] documentHash,
IEnumerable<byte[]>? crlBytesCollection,
IEnumerable<byte[]>? ocspBytes,
ITSAClient? tsaClient = null) {
_signature = signature;
_sgn = sgn;
_documentHash = documentHash;
_crlBytesCollection = crlBytesCollection;
_ocspBytes = ocspBytes;
_tsaClient = tsaClient;
}
/// <summary>
/// Append signature and optional data to the temporary PDF document
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public byte[] Sign(Stream data) {
// set the signature bytes
_sgn.Signer.SetExternalSignatureValue(_signature,
null,
"RSA");
// call GetEncoded with the same parameters as the original GetAuthenticatedAtt...
byte[]? encodedSig = _sgn.Signer.GetEncodedPKCS7(_documentHash,
PdfSigner.CryptoStandard.CMS,
_tsaClient,
_ocspBytes?.ToList(),
_crlBytesCollection?.ToList());
return encodedSig;
}
public void ModifySigningDictionary(PdfDictionary signDic) {
}
}
EDIT: adding the code for the helper methods GetCrlByteList
and GetOcspBytesList
because it seems like there's seomthing wrong with the way I'm getting the info:
private IList<byte[]>? GetCrlByteList() => _crlClients == null
? null
: _userCertificateChain.Select(x509 => GetCrlClientBytesList(x509))
.SelectMany(crlBytes => crlBytes)
.ToList();
private IList<byte[]>? GetCrlClientBytesList(IX509Certificate certificate) {
List<byte[]>? crls = _crlClients?.Select(crlClient => crlClient.GetEncoded(certificate, null))
.Where(encoded => encoded != null)
.SelectMany(bytes => bytes)
.ToList();
return crls;
}
private IList<byte[]>? GetOcspBytesList() {
if(_userCertificateChain.Count <= 1 || _ocspClient is null) {
return null;
}
IX509Certificate userCert = _userCertificateChain[0];
IX509Certificate root = _userCertificateChain[1];
IX509Certificate intermediate = _userCertificateChain[2];
List<byte[]> list = new();
byte[]? encoded = _ocspClient.GetEncoded(userCert, intermediate, null);
if(encoded != null) {
list.Add(encoded);
}
encoded = _ocspClient.GetEncoded(intermediate, root, null);
if(encoded != null) {
list.Add(encoded);
}
return list;
}
There's definitely something wrong with the code I'm using because when I don't set the ocsp or clr info, the pdf is considered valid...
This ends up generating the signed pdf, but unfortunately, it gives the previous error. Any clues on what I'm missing?
Thanks.
The reason for your signature validation problem is that in the embedded CMS signature in the final PDF the signed hash value is not the hash of the authenticated attributes. This happens because you create different authenticated attributes in the first run (from which you calculate the hash to sign) and in the second run (which you put into the final signature container).
They differ in the signature policy attribute and in the adbe-revocationInfoArchival attribute.
In PrepareForAmaSigningContainer
you set a policy value of the PdfPKCS7 sgn
before retrieving the authenticated attributes:
PdfPKCS7 sgn = new(null,
_certificates.ToArray(),
DigestAlgorithms.SHA256,
false);
// set signature policy information
MemoryStream policyIdByteMs = new(Encoding.ASCII.GetBytes("2.16.620.2.1.2.2"), false);
byte[]? policyIdByte = DigestAlgorithms.Digest(policyIdByteMs, DigestAlgorithms.SHA256);
SignaturePolicyInfo spi = new("2.16.620.2.1.2.2", policyIdByte, DigestAlgorithms.SHA256, _signaturePolicyUri);
sgn.SetSignaturePolicy(spi);
// get hash for document bytes
NakedHash = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
byte[]? docBytes = sgn.GetAuthenticatedAttributeBytes(NakedHash,
PdfSigner.CryptoStandard.CMS,
_ocspBytes?.ToList(),
_crlBytesCollection?.ToList());
Thus, the authenticated attributes there include a policy attribute and the AMA signature is created for attributes including that policy.
But in InjectAmaSignatureContainer
you create a PdfPKCS7 sgn
without setting the policy:
PdfPKCS7 sgn = new(null,
_certificates.ToArray(),
DigestAlgorithms.SHA256,
false);
// set the signature bytes
sgn.SetExternalSignatureValue(_signature,
null,
"RSA");
// call GetEncoded with the same parameters as the original GetAuthenticatedAtt...
byte[]? encodedSig = sgn.GetEncodedPKCS7(_documentHash,
PdfSigner.CryptoStandard.CMS,
_tsaClient,
_ocspBytes?.ToList(),
_crlBytesCollection?.ToList());
Thus, the encoded CMS container you create there and eventually embed into the PDF has an attribute set without a policy entry and a signature value calculated for an attribute set with a policy entry. Consequentially, there is a mismatch.
The best option would be to create the PdfPKCS7
instance only once and to keep and re-use it, so it is guaranteed to be used in an identically initialized form.
If you really need to drop the original PdfPKCS7
and re-create it later, make sure to re-create it with the identical parametrization. Consider putting that creation and re-creation of the PdfPKCS7
into a central method you call from both IExternalSignatureContainer
implementations.
After fixing this issue you still had a hash value mismatch. As it turned out the OCSP responses in the Adobe revocation information attribute differed in the two runs:
Both in CreateTemporaryPdfForSigning
and in SignIntermediatePdf
you call GetCrlByteList()
and GetOcspBytesList()
to retrieve the CRLs and OCSP responses respectively.
When you eventually posted the code of these methods, it became clear, though, that these methods may not return the same result in the first call as in the second call:
They both ask the given CRL client and OCSP client respectively for revocation data for the certificates in the signer certificate chain. But these clients are instances of CrlClientOnline
and OcspClientBouncyCastle
which collect online the revocation information they are asked for. Thus, the clients may return different information in the first and in the second call.
While for CRLs the chance is pretty low that the online CRL changes in-between those two calls, OCSP responses usually are generated anew for each request. And as OCSP responses contain their generation time (producedAt
), this means that each GetOcspBytesList()
call returns a different result.
You, therefore, should call GetCrlByteList()
and GetOcspBytesList()
only once when generating a signature, and forward them as IList
instances to CreateTemporaryPdfForSigning
and in SignIntermediatePdf
, so that these methods can embed the same collections of revocation information in the authenticated attributes.