1

Read through the following references:

Hashing code:

BouncyCastle.X509Certificate[] chain = Utils.GetSignerCertChain();
reader = Utils.GetReader();
MemoryStream stream = new MemoryStream();
using (var stamper = PdfStamper.CreateSignature(reader, stream, '\0'))
{
    PdfSignatureAppearance sap = stamper.SignatureAppearance;
    sap.SetVisibleSignature(
        new Rectangle(36, 740, 144, 770),
        reader.NumberOfPages,
        "SignatureField"
    );
    sap.Certificate = chain[0];
    sap.SignDate = DateTime.Now;
    sap.Reason = "testing web context signatures";

    PdfSignature pdfSignature = new PdfSignature(
        PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED
    );
    pdfSignature.Date = new PdfDate(sap.SignDate);
    pdfSignature.Reason = sap.Reason;
    sap.CryptoDictionary = pdfSignature;

    Dictionary<PdfName, int> exclusionSizes = new Dictionary<PdfName, int>();
    exclusionSizes.Add(PdfName.CONTENTS, SIG_BUFFER * 2 + 2);
    sap.PreClose(exclusionSizes);

    Stream sapStream = sap.GetRangeStream();
    byte[] hash = DigestAlgorithms.Digest(
        sapStream,
        DigestAlgorithms.SHA256
    );

// is this needed?
    PdfPKCS7 sgn = new PdfPKCS7(
        null, chain, DigestAlgorithms.SHA256, true
    );
    byte[] preSigned = sgn.getAuthenticatedAttributeBytes(
        hash, sap.SignDate, null, null, CryptoStandard.CMS
    );

    var hashedValue = Convert.ToBase64String(preSigned);
}

Just a simple test - a dummy Pdf document is created on initial page request, hash is calculated, and put in a hidden input field Base64 encoded. (the hashedValue above)

Then use CAPICOM on client-side to POST the form and get user's signed response:

PdfSignatureAppearance sap = (PdfSignatureAppearance)TempData[TEMPDATA_SAP];
PdfPKCS7 sgn = (PdfPKCS7)TempData[TEMPDATA_PKCS7];
stream = (MemoryStream)TempData[TEMPDATA_STREAM];
byte[] hash = (byte[])TempData[TEMPDATA_HASH];

byte[] originalText = (Encoding.Unicode.GetBytes(hashValue));
// Oid algorithm verified on client side
ContentInfo content = new ContentInfo(new Oid("RSA"), originalText);

SignedCms cms = new SignedCms(content, true);
cms.Decode(Convert.FromBase64String(signedValue));
// CheckSignature does not throw exception
cms.CheckSignature(true);
var encodedSignature = cms.Encode();

/* tried this too, but no effect on result
sgn.SetExternalDigest(
    Convert.FromBase64String(signedValue),
    null,
    "RSA"
);
byte[] encodedSignature = sgn.GetEncodedPKCS7(
    hash, sap.SignDate, null, null, null, CryptoStandard.CMS
);
*/
byte[] paddedSignature = new byte[SIG_BUFFER];
Array.Copy(encodedSignature, 0, paddedSignature, 0, encodedSignature.Length);
var pdfDictionary = new PdfDictionary();
pdfDictionary.Put(
    PdfName.CONTENTS,
    new PdfString(paddedSignature).SetHexWriting(true)
);
sap.Close(pdfDictionary);

So right now I'm not sure if I'm messing up hashing part, signature part, or both. In signature code snippet above and in client code (not shown) I'm calling what I think is signature verification code, but that may be wrong too, since this is a first for me. Get the infamous "Document has been altered or corrupted since it was signed" invalid signature message when opening the PDF.

Client-side code (not authored by me) can be found here. Source has a variable naming error, which was corrected. For reference, CAPICOM documentation says signed response is in PKCS#7 format.

EDIT 2015-03-12:

After some nice pointers from @mkl and more research, it seems CAPICOM is practicably unusable in this scenario. Although not documented clearly, (what else is new?) according to here and here, CAPICOM expects a utf16 string (Encoding.Unicode in .NET) as input to create a digital signature. From there it either pads or truncates (depending which source in previous sentence in correct) whatever data it receives if the length is an odd number. I.e. signature creation will ALWAYS FAIL if the Stream returned by PdfSignatureAppearance.GetRangeStream() has a length that is an odd number. Maybe I should create an I'm lucky option: sign if ranged stream length is even, and throw an InvalidOperationException if odd. (sad attempt at humor)

For reference, here's the test project.

EDIT 2015-03-25:

To close the loop on this, here's a link to a VS 2013 ASP.NET MVC project. May not the be best way, but it does provide a fully working solution to the problem. Because of CAPICOM's strange and inflexible signing implementation, as described above, knew a possible solution would potentially require a second pass and a way to inject an extra byte if the return value of PdfSignatureAppearance.GetRangeStream() (again, Stream.Length) is an odd number. I was going to try the long and hard way by padding the PDF content, but luckily a co-worker found it was much easier to pad PdfSignatureAppearance.Reason. Requiring a second pass to do something with iText[Sharp], is not unprecedented - e.g. adding page x of y for a document page header/footer.

Community
  • 1
  • 1
kuujinbo
  • 9,272
  • 3
  • 44
  • 57
  • 1
    *// is this needed?* - That is needed only if the external signing API you use merely returns a signed digest; in that case the `PdfPKCS7` instance builds the CMS/PKCS#7 signature container. You, on the other hand, use an API for which the *signed response is in PKCS#7 format.* Thus, you don't need the `PdfPKCS7` instance. – mkl Mar 10 '15 at 14:27
  • 1
    Please be aware, though, that `hash ` already is a hash value, i.e. the frontend must not hash it. As I don't know the CAPICOM API, I'm not really sure what the frontend code does. If it does hash again, you will have to tweak it. – mkl Mar 10 '15 at 14:37
  • @mkl - Sorry for the late response and thank you for both comments. (in readonly mode at work, don't ask) Silly of me, but never thought about a potential double hashing problem. Will do a little more testing and update my question. – kuujinbo Mar 11 '15 at 00:31
  • 1
    I looked into the msdn documentation. Indeed the frontend code puts the hash you calculated into the CAPICOM `SignedData` property `Content` which is documented to hold the *Data to be signed* and not the hash of the data to be signed. Thus, you indeed hash twice. Unfortunately I cannot see how to use CAPICOM for data you already have hashed. Thus, it looks like you have to transmit the whole ranged stream which is not practical. – mkl Mar 11 '15 at 10:26
  • 1
    I also looked up some old iTextSharp (version 4.x) signing example using CAPICOM. That code only worked because it created signatures of PDF signature type **adbe.pkcs7.sha1** for which a SHA1 hash of the ranged stream indeed is the data embedded in and signed by the PKCS#7 signature. This is no real option anymore because **A** it requires the use of SHA1 which in serious contexts is invalid, and **B** its use has been discouraged sat least since ISO 32000-1 and will be officially deprecated in ISO 32000-2. – mkl Mar 11 '15 at 10:38
  • @mkl - **THANK YOU** again. Saw the same regarding how [CAPICOM creates signatures](https://msdn.microsoft.com/en-us/library/windows/desktop/aa387726%28v=vs.85%29.aspx): "_A digital signature consists of a hash of the content to be signed_", and wasn't sure if I was interpreting correctly, but you've confirmed it. Did a quick test trying to hash the entire ranged stream (base64 encoded and realize that's not practical in most cases), but that didn't work either. Will look around a bit more... – kuujinbo Mar 11 '15 at 14:21
  • @mkl - updated the question. Took longer than it should have, but finally pinpointed the problem thanks to you. A shame too, because for the size of PDFs we're planning to sign (max 1-2MB), the performance of signing the entire ranged stream wasn't too bad. If you have any other ideas, it would be much appreciated. Please summarize your comments above into an answer and I'll accept. – kuujinbo Mar 13 '15 at 03:13
  • *If you have any other ideas* - I saw that the sign.js function `sign_IE` explicitly adds to `AuthenticatedAttributes`. Perhaps you can also add the MessageDigest attribute in the same manner and not set `Content` (or set it to null). Probably one can so trick CAPICOM into signing the digest one wants. But this really is only a wild idea... – mkl Mar 13 '15 at 14:35
  • BTW, I just downloaded your test project and looked at the code: You are creating the `PdfStamper` in a `using` directive. Thus, in the course of `return View(f)` it is partially de-initialized; this happens before the `Index` call. So your filling in the returned signature container may not properly be executed. – mkl Mar 13 '15 at 14:47

1 Answers1

1

Use of PdfPkcs7

The server-side code contains this block after the calculation of the range stream digest and before forwarding data to the web page:

PdfPKCS7 sgn = new PdfPKCS7(
    null, chain, DigestAlgorithms.SHA256, true
);
byte[] preSigned = sgn.getAuthenticatedAttributeBytes(
    hash, sap.SignDate, null, null, CryptoStandard.CMS
);

var hashedValue = Convert.ToBase64String(preSigned);

In the case at hand this is not necessary. It is needed only if the external signing API you use merely returns a signed digest; in that case the PdfPKCS7 instance builds the CMS/PKCS#7 signature container. You, on the other hand, use an API for which you know

CAPICOM documentation says signed response is in PKCS#7 format.

Thus, you don't need and (more to the point) must not use the PdfPKCS7 instance.

What does sign.js sign

The content of the server-side hash variable already is the hash digest value of the data to sign. Thus, the frontend, i.e. the sign.js used there, must not hash it again to get the message digest attribute value to put into the signature.

But sign.js signing methods for IE eventually execute

var signedData = new ActiveXObject("CAPICOM.SignedData");

// Set the data that we want to sign
signedData.Content = src;

SignedData.Content, on the other hand, is documented as

Content Read/write Data to be signed.

(msdn: "SignedData object")

So the hash from the backend is used as data to be signed and not as hash of the data to be signed, you indeed hash twice and so have the wrong hash value there.

Thus, it looks like you have to transmit the whole ranged stream which is not really practical...

"But there used to be signing samples using CAPICOM..."

Indeed some old iTextSharp (version 4.x) signing example used CAPICOM. But that code only worked because it created signatures of PDF signature type adbe.pkcs7.sha1 for which a SHA1 hash of the ranged stream indeed is the data embedded in and signed by the PKCS#7 signature.

This is no real option anymore because

  • it requires the use of SHA1 which in serious contexts is invalid, and
  • its use has been discouraged at least since ISO 32000-1 (2008) and will be officially deprecated in ISO 32000-2 (under development).
Community
  • 1
  • 1
mkl
  • 90,588
  • 15
  • 125
  • 265
  • Thanks again. And sorry, up-voted this morning, but only marked answer just now. Will also try your other two suggestions next week. – kuujinbo Mar 14 '15 at 02:46