I'm trying to use itext7 to sign a pdf by getting the signature from an external entity. I must be missing something because the deferred pdf signing is not valid. Lets start with the code for deferred signing:
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;
}
The method received the path to the original pdf, the path to the temporary pdf that will contain the placeholder for the signature and a list of X509Certificate that is retrieved from the original service. After reserving the space for the signature, the method calculates the file's hash and prepends it with the sha256 prefix (required by the service that will sign the document).
This information is sent to the service which will return the signature. When the signature is retrieved, the following method is called for filling the signature placeholder with the real signature:
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);
}
EDIT: based on #mkl comment, I've fixed the signing part:
And here's the 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) {
}
}
Even though the code runs without errors, opening the pdf in adobe shows the following error:
EDIT: after fixing the signing code, now the error is different: it will show the signature info but it will say that the file has been changed or is corrupted.
At this point, it seems like the temporary pdf is being generated correctly, but I'm probably missing something...any clues on how I might debug this issue?
Thanks
EDIT: in response to the comments to the solution presented by #mkl, I've tried to update the code. I had a couple more minutes to play with this today and I've tried to follow the presented guidelines, but I'm clearly still missing something.
Before showing the new code, I'd just like to point out that the previous updated version (that whas using 2 IExternalSignatureContainer instances) seemed to be working correctly. ie, opening the signed pdf on adobe would only show me the yellow warning saying that there was something wrong with the signature:
Since the doc is being used with a test chain, it seems like the signing worked ok (though I might be completely wrong).
So, in order to fix the incorrect usage of the container, I've rewritten the code for the IExternalSignatureContainer's Sign methods. Here's the code I've got for the one that prepares the document hash that is going to be sent to the server:
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];
}
Since I must call the GetEncodedPKCS7 method with the same parameters that were passed to GetAuthenticatedAttributes, I'm also saving the document hash obtained through the Digest method. DataToSend will be sent to the server so that it can return the signature for that hash.
And here's the code for the other IExternalSignatureContainer that will be called for the deferred signing (PdfSigner.SignDeferred):
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;
}
Unfortunately, I must be missing something (or lots of things):
Did I completely missed your point?
EDIT: Once again, following #mkl's lead, I was able to make it work. Like he said, you need to hash the GetAuthenticatedAttributeBytes value:
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];
}
Thanks again.
There are two apparent issues, you hash the wrong data and you inject a wrong type of signature:
Hashing the wrong data
You calculate the hash to be signed like this:
using var stream = File.OpenRead(pathToPreparedToBeSignedPdf);
var data = DigestAlgorithms.Digest(stream, DigestAlgorithms.SHA256);
This is not correct.
A signed PDF essentially has this structure (read here for more details):
(By the way, the sketch is not 100% correct as the angled bracket delimiters '<' and '>' around the signature value must also not be hashed.)
Your prepared PDF at pathToPreparedToBeSignedPdf has the same structure, merely the "signature value" is not yet an actual signature value but instead a placeholder of 8192 hex-encoded zero bytes (8192 because that's the number you gave in pdfSigner.SignExternalContainer).
As you can see in the sketch, though, the signature value (or in your case, the placeholder) must not be hashed for signing.
The easiest way to retrieve the prepared PDF except the placeholder is inside the IExternalSignatureContainer implementation you use for preparing the PDF as its Sign method as parameter gets a stream containing exactly that. So instead of the ExternalBlankSignatureContainer use something like the ExternalEmptySignatureContainer from this answer:
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;
}
and after preparing retrieve the byte[] from its Data member:
var container = new ExternalEmptySignatureContainer();
pdfSigner.SignExternalContainer(container, 8192);
byte[] hash = container.Data;
In your case you may have to prepend the sha256SigPrefix to get a complete encoded DigestInfo object.
Injecting a wrong type of signature
Furthermore, considering your screen shot
you apparently inject a signature of a wrong type. You set a subfilter PdfName.Adbe_pkcs7_detached which implies that the signature to embed is a CMS signature container with a single SignerInfo signing the signed bytes of the PDF. The error message indicates, though, that the signature container you embed either is broken or is not a CMS signature container to start with.
After your edit: Injecting a signature container with incorrect contents
To fix the previous issue, "Injecting a wrong type of signature", you changed your ExternalInjectingSignatureContainer to build a CMS signature container for your signature bytes like this:
var sgn = new PdfPKCS7(null, _certificates.ToArray(), "SHA256", false);
sgn.SetExternalDigest(_signature, null, "RSA");
return sgn.GetEncodedPKCS7();
This, unfortunately, uses the PdfPKCS7 class incorrectly resulting in a CMS signature container with incorrect data.
(Correction: The resulting CMS container is not incorrect per se, but it is structurally extremely simply. Nowadays many profiles - e.g. European PAdES - require additional, signed attributes, and to satisfy such profiles you have to fix as described in the following text. If you only need Adobe Reader to show valid, though, the code above suffices for you.)
To fix you can either indeed build a CMS signature container yourself, using the iText PdfPKCS7 class or other means (like BouncyCastle or built-in .Net classes), or you can let iText do that for you.
The easiest approach is the latter one. You actually don't need deferred signing here at all, you simply implement IExternalSignature so that it calls your remote service:
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";
}
}
Now you can simply sign doing
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);
In Android Studio, I use this code to get data from server
url = new URL(url);
HttpURLConnection connection = null;
try
{
HttpURLConnection.setFollowRedirects(false);
connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
connection.setInstanceFollowRedirects(false);
InputStream inputStream = connection.getInputStream();
this.header = connection.getHeaderFields();
this.status = connection.getResponseCode();
}
In Swift 5, I'm able to perform similar task by using URLSession.shared.dataTask(), but I couldn't find anything to replace InputStream inputStream = connection.getInputStream().
After I did some research on Swift 5 inputStream and outputStream, I'm getting more confused, can anyone provide some sample on how to replace this?
Use uploadTask(withStreamedRequest in order to work with streams https://developer.apple.com/documentation/foundation/urlsession/1410934-uploadtask
I am trying to start a REST request with RestSharp on a server that obviously has no valid ssl certificate, as I get the error
The underlying connection was closed: Could not establish trust
relationship for the SSL/TLS secure channel.
I found this question but the provided solution, as simple as it may look doesn't work, I still get the error. Here is my code, any ideas what might be wrong?
private void Test_REST()
{
var client = new RestClient("https://online.gema.de");
// both versions not working
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
client.RemoteCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true;
// execute the request
IRestResponse response = client.Execute(GEMA_Authorize());
var content = response.Content; // raw content as string
}
private RestRequest GEMA_Authorize()
{
RestRequest request = new RestRequest("api/v1/authorize", Method.GET);
request.AddParameter("response_type", "token");
request.AddParameter("client_id", "test_client");
request.AddHeader("Accept", "application/json");
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
return request;
}
When I write the Callback like this:
.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) =>
{
return true;
};
and set a breakpoint at return true; the execution doesn't stop there, so it seems that the callback is never called back. Any ideas what might be the issue? I'm rather new to REST, so I might miss something crucial.
Thanks in advance,
Frank
The usage of
client.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
worked for me.
For IText 5, adding digital signature was fairly easy.
The link for its documentation is:
http://developers.itextpdf.com/examples/security/digital-signatures-white-paper/digital-signatures-chapter-2
Can someone share the link to documentation for doing so in ITEXT 7?
I have tried various ways to no avail. Could not find any links online. I can unsign and check signature, but can't add it.
Ports of the Digital Signatures Whitepaper code examples to iText 7 can be found in the iText 7 Java signature samples github repository test sources package com.itextpdf.samples.signatures, e.g. an excerpt from the simple C2_01_SignHelloWorld example:
public void sign(String src, String dest,
Certificate[] chain,
PrivateKey pk, String digestAlgorithm, String provider,
PdfSigner.CryptoStandard subfilter,
String reason, String location)
throws GeneralSecurityException, IOException {
// Creating the reader and the signer
PdfReader reader = new PdfReader(src);
PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), false);
// Creating the appearance
PdfSignatureAppearance appearance = signer.getSignatureAppearance()
.setReason(reason)
.setLocation(location)
.setReuseAppearance(false);
Rectangle rect = new Rectangle(36, 648, 200, 100);
appearance
.setPageRect(rect)
.setPageNumber(1);
signer.setFieldName("sig");
// Creating the signature
IExternalSignature pks = new PrivateKeySignature(pk, digestAlgorithm, provider);
IExternalDigest digest = new BouncyCastleDigest();
signer.signDetached(digest, pks, chain, null, null, null, 0, subfilter);
}
I make a post request of base64 encoded data to the receipt verification address as follows (this is in C#):
var postSerializer = new JavaScriptSerializer();
byte[] toEncodeAsBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(Receipt);
string returnValue = System.Convert.ToBase64String(toEncodeAsBytes);
var temp = new Dictionary<string, string>();
temp.Add("receipt-data", returnValue);
string jsonReceipt = postSerializer.Serialize(temp);
request.Method = "POST";
request.ContentType = "application/json";
byte[] postBytes = System.Text.Encoding.ASCII.GetBytes(jsonReceipt);
request.ContentLength = postBytes.Length;
Stream dataStream = request.GetRequestStream();
// Write the data to the request stream.
dataStream.Write(postBytes, 0, postBytes.Length);
// Close the Stream object.
dataStream.Close();
WebResponse response = request.GetResponse();
// Display the status.
Console.WriteLine(((HttpWebResponse)response).StatusDescription);
// Get the stream containing content returned by the server.
dataStream = response.GetResponseStream();
// Open the stream using a StreamReader for easy access.
StreamReader reader = new StreamReader(dataStream);
// Read the content.
string responseFromServer = reader.ReadToEnd();
I'm pretty sure things are in the right format because I'm not getting any exceptions back
from the apple receipt verification endpoint. The entirety of the response I get back is
{status : -42352}
And I can't find out what this error means anywhere. Does anyone know what this means or if there's an error in my code?
Just solved the same problem. Got the solution from here: Verify receipt for in App purchase
The problem was in post encoding. When I encoded post on my server using
$receipt = json_encode(array("receipt-data" => base64_encode($receiptdata)));
I had the same -42352 status. When I used my own function for encoding on iPhone - everything worked! Magic...