1
votes

We are programmatically creating PDF using our in house lib (C++) by adding all the required objects so that PDF readers can render them properly. Currently we are enhancing the lib to support digital signatures in PDF. Our users will use USB token or Windows certificates to sign the PDF. On studying raw PDF file with digital signature, we were able to make sense of all the objects except for the contents of Sig type object.

18 0 obj
<<
/Type /Sig
/Filter /Adobe.PPKLite              --> signature handler for authenticating the fields contents
/SubFilter /adbe.pkcs7.sha1         --> submethod of the handler
/Contents <....>                    --> signature token
/ByteRange [ 0 101241 105931 7981                                                                                                                                                                                       
]                                   --> byte range for digest calc
/M (D:20210505094125+05'30')        --> timestamp
/Reason ()
/Location ()
/ContactInfo ()
>>
endobj

We have referred https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSigDC/Acrobat_DigitalSignatures_in_PDF.pdf to understand what all constitutes the signature token. We need direction on how to programmatically create the signature token for PDF using windows APIs. Currently we are not looking at 3rd party lib solutions. Thanks in advance.

Update

We tried the following:

  1. Updated our in-house PDF lib to support incremental updates so that digital signing related objects can be added. We added something like this apart from the obj# 18 mentioned above:
16 0 obj                       --> new Acroform obj
<<
/Fields [ 17 0 R ]
/SigFlags 3
>>
endobj
2 0 obj                        --> Updating root to add AcroForm
<<
/Type /Catalog
/Pages 3 0 R
/AcroForm 16 0 R
>>
endobj
17 0 obj                       --> new obj for signature field
<<
/T (SignatureField1)
/Type /Annot
/Subtype /Widget
/FT /Sig
/F 4
/Rect [ 270 159 503 201 ]  --> field position. this will have image of sign
/P 5 0 R
/V 18 0 R
/AP <<
/N 19 0 R
>>
>>
endobj
5 0 obj                    --> updating existing page obj with Annots
<<
/Type /Page
/Parent 3 0 R
/MediaBox [ 0 0 595 841 ]
/Resources 4 0 R
/Contents 6 0 R
/Annots [ 17 0 R ]
>>
endobj
18 0 obj
<<
/Type /Sig
/Filter /Adobe.PPKLite
/SubFilter /adbe.pkcs7.sha1  --> we tried with adbe.pkcs7.detached as well
/Contents <>           --> updated contents using windows APIs
/ByteRange [ 0 100381 102645 7322                                                                                                                                                                                       
]                      --> updated ByteRange with right offsets and lengths
/M (D:20210610125837+05'30') --> sign verified time
/Reason ()
/Location ()
/ContactInfo ()
>>
endobj
19 0 obj                  --> new obj
<<
/Length 7
/BBox [ 0 0 233 42 ]
/Type /XObject
/Subtype /Form
/Resources <<
/XObject <<
/FRM 20 0 R
>>
>>
>>
stream
/FRM Do
endstream
endobj
20 0 obj                 --> new obj for image manipulation
<<
/Length 29
/Type /XObject
/Subtype /Form
/Resources <<
/XObject <<
/Im1 21 0 R
>>
>>
/BBox [ 0 0 233 42 ]
>>
stream
q 233 0 0 42 0 0 cm /Im1 Do Q
endstream
endobj
21 0 obj           --> image obj which contains sign info. Generated by us
<<
/Length 6166
/Type /XObject
/Subtype /Image
/Width 372
/Height 82
/ColorSpace /DeviceRGB
/BitsPerComponent 8
/Filter /DCTDecode
>>
stream
---------------------------------> image stream
endstream
endobj
xref               --> updated xref
0 1
0000000000 65535 f 
2 1
0000099954 00000 n 
5 1
0000100172 00000 n 
16 6
0000099901 00000 n 
0000100020 00000 n 
0000100297 00000 n 
0000102944 00000 n 
0000103096 00000 n 
0000103271 00000 n 
trailer             --> updated trailer
<<
/Root 2 0 R
/Info 1 0 R
/Size 22
/ID [ <982AAACB948CE1AD9FDD976D177BF316> <982AAACB948CE1AD9FDD976D177BF316> ] 
--> ID generated via windows API
/Prev 99491
>>
startxref
109605
%%EOF

  1. For contents data, we used the below API:
bool SignMessageBySubjectName (BytePtr pMessage, ULong pMessageSize, StrPtr pSubjectName, CRYPT_DATA_BLOB * pSignBlob)
{
        HCERTSTORE               store_handle   = NULL;
        PCCERT_CONTEXT           cert_context   = NULL;
        BYTE *                   signed_blob    = NULL;
        ULong                    signed_blob_size;
        ULong                    message_size;
        CRYPT_SIGN_MESSAGE_PARA  signature_params;
        BYTE *                   message;

    pSignBlob->cbData = 0;
    pSignBlob->pbData = NULL;

    message = (BYTE *) pMessage;

    message_size = (pMessageSize + 1) * sizeof(Char); //Size in bytes

        const BYTE * message_array[] = {message};
        DWORD        message_array_size[1];

    message_array_size[0] = message_size;

    store_handle = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, NULL,
                                 CERT_SYSTEM_STORE_CURRENT_USER, L"MY");

    cert_context = CertFindCertificateInStore( store_handle, PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, 0,
                                               CERT_FIND_SUBJECT_STR, pSubjectName, NULL);

    signature_params.cbSize = sizeof(CRYPT_SIGN_MESSAGE_PARA);
    signature_params.dwMsgEncodingType = PKCS_7_ASN_ENCODING | X509_ASN_ENCODING;
    signature_params.pSigningCert = cert_context;
    signature_params.HashAlgorithm.pszObjId = szOID_RSA_SHA1RSA;
    signature_params.HashAlgorithm.Parameters.cbData = NULL;
    signature_params.cMsgCert = 1;
    signature_params.rgpMsgCert = &cert_context;
    signature_params.cAuthAttr = 0;
    signature_params.dwInnerContentType = 0;
    signature_params.cMsgCrl = 0;
    signature_params.cUnauthAttr = 0;
    signature_params.dwFlags = 0;
    signature_params.pvHashAuxInfo = NULL;
    signature_params.rgAuthAttr = NULL;

    //Get size of signed message
CryptSignMessage(&signature_params, TRUE, 1, message_array, message_array_size,NULL, &signed_blob_size);

signed_blob = (BYTE *) Malloc(signed_blob_size);


CryptSignMessage(&signature_params, TRUE, 1, message_array, message_array_size,  signed_blob, &signed_blob_size);

    pSignBlob->cbData = signed_blob_size;
    pSignBlob->pbData = signed_blob;

    CertFreeCertificateContext(cert_context);
    CertCloseStore(store_handle, CERT_CLOSE_STORE_FORCE_FLAG);

    return true;
}

While using CryptSignMessage() with detached parameter as TRUE, we get a around 850 length sign token which we convert to hex and add in the contents part. It'll approximately be around 1700 chars.

  1. In case of the image used in the Field newly added, we generated our own image and added it as a PDF obj.

  2. For the ID in trailer part, we generated the same using API from Bcrypt.lib (BCryptGenRandom()), converted its output to hex and updated the ID part.

  3. Listing out the steps we did:

  • We generated 2 buffers. Both buffers are identical with respect to all the PDF objects required, the ID generated from BCryptGenRandom() and ByteRange array updated with actual values. buffer1 has contents data as 0s for a definite length acting as a placeholder. buffer2 has empty contents data (/Contents <>)

  • buffer2 will be passed onto CryptSignMessage() to generate the sign token. This will be converted to hex.

  • The hex sign token will be added to contents part of buffer1 replacing the 0s based on its length.

  • buffer1 will be written to a PDF file.

When we did all these, and opened the PDF in readers, we got errors like

  • Signature is invalid
  • Document has been corrupted or altered since the signature was applied.

Error from a PDF Reader:

Error from a PDF Reader

Detailed Error:

Detailed Error

But with these errors too, the reader was able to identify the user, certificate, hash algorithm and signature algorithm used.

Certificate details

We think we need to somehow add timestamp data as part of the sign token so as to avoid this error. Or something else we would have missed.

PFA sample PDF here:https://drive.google.com/file/d/1Udog4AmGoq2ls3Tu3Wq5s2xU9LxaI3fH/view?usp=sharing

Kindly help us solve this issue. Thanks in advance.

2
(A) consider reading the pdf specification (ISO 32000-2; or ISO 32000-1 plus the ETSI PAdES specs if you have no budget for specs) (B) the Contents value format depends on the SubFilter value you use; in most cases it's a CMS signature container, so you should check the CMS specs (C) the SubFilter you chose, adbe.pkcs7.sha1, is a very bad choice as SHA1 is obsolete for pdf signatures.mkl
Thanks for your response @mkl We'll check the mentioned specs. Also we are sticking with adbe.pkcs7.sha1 as we don't want to increment the PDF version we are currently using i.e. 1.3Sumesh Mathews
"Also we are sticking with adbe.pkcs7.sha1 as we don't want to increment the PDF version" - Then at least use adbe.pkcs7.detached which has been around for a similarly long time but does not require the use of SHA1. If you insist on using SHA1 as document hash algorithm, though, be prepared that no one who knows a bit of this topic will accept your signatures.mkl
got your point @mkl. Will assess the usage of adbe.pkcs7.detached as well. Thanks.Sumesh Mathews
Why don't you check PoDoFo sourcesVictor Gubin

2 Answers

1
votes

We used a different set of APIs to make this work. Pasting the code here:

bool SignatureHandler::SignMessageTest (BytePtr pMessage, ULong pMessageSize, StrPtr pSubjectName, CRYPT_DATA_BLOB * pSignBlob, LPSTR pOid, DWORD pFlag, DWORD pType)
{
        HCERTSTORE              store_handle  = NULL;
        PCCERT_CONTEXT          cert_context  = NULL;
        BYTE *                  signed_blob   = NULL;
        ULong                   signed_blob_size = 0;
        CRYPT_SIGN_MESSAGE_PARA signature_params;
        BYTE *                  message;
        BOOL                    rc;

        pSignBlob->cbData = 0;
        pSignBlob->pbData = NULL;

    store_handle = CertOpenStore (CERT_STORE_PROV_SYSTEM, 0, NULL, CERT_SYSTEM_STORE_CURRENT_USER, L"MY");

    cert_context = CertFindCertificateInStore (store_handle, (PKCS_7_ASN_ENCODING | X509_ASN_ENCODING), 0, CERT_FIND_SUBJECT_STR, pSubjectName, NULL);

        HCRYPTPROV_OR_NCRYPT_KEY_HANDLE a         = 0;
        DWORD                           ks        = 0;
        BOOL                            bfr       = false;

        HCRYPTPROV_OR_NCRYPT_KEY_HANDLE PrivateKeys;
        CERT_BLOB                       CertsIncluded;
        CMSG_SIGNER_ENCODE_INFO         Signers;

        HCRYPTMSG                       hMsg;

    rc = CryptAcquireCertificatePrivateKey (cert_context, 0, 0, &a, &ks, &bfr);

        CMSG_SIGNER_ENCODE_INFO SignerEncodeInfo = {0};

    SignerEncodeInfo.cbSize = sizeof (CMSG_SIGNER_ENCODE_INFO);

    if (a)
        SignerEncodeInfo.hCryptProv = a;

    if (bfr)
        PrivateKeys = a;

    CERT_BLOB SignerCertBlob;
    
    SignerCertBlob.cbData = cert_context->cbCertEncoded;
    SignerCertBlob.pbData = cert_context->pbCertEncoded;
    CertsIncluded         = SignerCertBlob;

                    SignerEncodeInfo.cbSize    = sizeof (CMSG_SIGNER_ENCODE_INFO);
                    SignerEncodeInfo.pCertInfo = cert_context->pCertInfo;
                    SignerEncodeInfo.dwKeySpec = ks;
                    SignerEncodeInfo.HashAlgorithm.pszObjId          = pOid;
                    SignerEncodeInfo.HashAlgorithm.Parameters.cbData = NULL;
                    SignerEncodeInfo.pvHashAuxInfo = NULL;
        
                    Signers = SignerEncodeInfo;
        
                    CMSG_SIGNED_ENCODE_INFO SignedMsgEncodeInfo = {0};
        
                    SignedMsgEncodeInfo.cbSize        = sizeof (CMSG_SIGNED_ENCODE_INFO);
                    SignedMsgEncodeInfo.cSigners      = 1;
                    SignedMsgEncodeInfo.rgSigners     = &Signers;
                    SignedMsgEncodeInfo.cCertEncoded  = 1;
                    SignedMsgEncodeInfo.rgCertEncoded = &CertsIncluded;
                    SignedMsgEncodeInfo.rgCrlEncoded  = NULL;
        
                    signed_blob_size = 0;
        
                    signed_blob_size = CryptMsgCalculateEncodedLength ((PKCS_7_ASN_ENCODING | X509_ASN_ENCODING), pFlag, pType, &SignedMsgEncodeInfo, 0, pMessageSize);
        
                    if (signed_blob_size) {
        
                        signed_blob_size *= 2;
        
                        hMsg = CryptMsgOpenToEncode (CERTIFICATE_ENCODING_TYPE,
                                                     pFlag,                   
                                                     pType,                   
                                                     &SignedMsgEncodeInfo, 
                                                     0,                    
                                                     NULL);
        
                        if (hMsg) {
        
                            signed_blob = (BYTE *)malloc (signed_blob_size);
        
                                BOOL CU = CryptMsgUpdate (hMsg, (BYTE *)pMessage, (DWORD)pMessageSize, true);
        
                            if (CU) {
        
                                if (CryptMsgGetParam (
                                        hMsg,               // Handle to the message
                                        CMSG_CONTENT_PARAM, // Parameter type
                                        0,                  // Index
                                        signed_blob,  // Pointer to the BLOB
                                        &signed_blob_size))    // Size of the BLOB
                                {
        
                                    signed_blob = (BYTE *)realloc (signed_blob, signed_blob_size);
        
                                    if (hMsg) {
        
                                        CryptMsgClose (hMsg);
                                        hMsg = 0;
                                    }
                                }
                            }
                            if (hMsg)
                                CryptMsgClose (hMsg);
                            hMsg = 0;
                        }
                    }
        
    CryptReleaseContext (a, 0);

    pSignBlob->cbData = signed_blob_size;
    pSignBlob->pbData = signed_blob;

    CertFreeCertificateContext (cert_context);
    CertCloseStore (store_handle, CERT_CLOSE_STORE_FORCE_FLAG);

    return true;
}

The oid, flag and type we used are szOID_RSA_SHA1RSA, CMSG_DETACHED_FLAG and CMSG_SIGNED respectively. On converting pSignBlob->pbData to hex and adding it to /Contents, the PDF and signature became valid when opened in PDF readers.

0
votes

Ok, the signature container is embedded correctly.

But there are issues with the signature container itself:

  • Both in the SignedData.digestAlgorithms collection and in the SignerInfo.digestAlgorithm value you have used the OID of SHA1withRSA, but that is a full signature algorithm, not the mere digest algorithm SHA1 expected there.

  • Then the SHA1 hash of the signed bytes is BB78A402F7A537A34D6892B83881266501A691A8 but the hash you signed is 90E28B8A0D8E48691DAFE2BA10A4761FFFDCCD3D. This might be because you hash buffer2 and

    buffer2 has empty contents data (/Contents <>)

    The hex string delimiters '<' and '>' also belong to the contents value and, therefore, must also be removed in buffer2.

Furthermore, your signature is very weak:

  • It uses SHA1 as hash algorithm. SHA1 meanwhile has been recognized as too weak a hash algorithm for document signatures.
  • It doesn't use signed attributes, neither the ESS signing certificate nor the algorithm identifier protection attribute. Many validation policies require such special attributes.