From f3d7ea8adceb7ffcb72c9cbdda1647076609ba7a Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Thu, 7 Sep 2017 08:39:22 +0930 Subject: [PATCH 3/4] write document then update byte offsets and sig on disk --- poppler/Form.cc | 437 ++++++++++++++++++++++++++---------------------- poppler/Form.h | 12 +- poppler/XRef.cc | 10 +- poppler/XRef.h | 5 +- qt5/src/poppler-form.cc | 5 +- qt5/src/poppler-form.h | 2 +- utils/pdfsig.cc | 6 +- 7 files changed, 267 insertions(+), 210 deletions(-) diff --git a/poppler/Form.cc b/poppler/Form.cc index f16144c9..2f73cd47 100644 --- a/poppler/Form.cc +++ b/poppler/Form.cc @@ -29,12 +29,15 @@ #include #include +#include #include #include #include #include "goo/gmem.h" +#include "goo/gfile.h" #include "goo/GooString.h" #include "Error.h" +#include "ErrorCodes.h" #include "Object.h" #include "Array.h" #include "Dict.h" @@ -51,6 +54,7 @@ #include "Annot.h" #include "Link.h" #include "Lexer.h" +#include "Parser.h" //return a newly allocated char* containing an UTF16BE string of size length char* pdfDocEncodingToUTF16 (GooString* orig, int* length) @@ -457,58 +461,247 @@ SignatureInfo *FormWidgetSignature::validateSignature(const char *nssDir, bool d return static_cast(field)->validateSignature(nssDir, doVerifyCert, forceRevalidation, validationTime); } -GBool FormWidgetSignature::signDocument(const char *nssDir, const char* certNickname, const char* digestName, - const char* password, const char* reason) +GBool FormWidgetSignature::signDocument(const char *nssDir, const char *saveFilename, const char* certNickname, + const char* digestName, const char* password, const char* reason) { - GBool ok = gFalse; - if (certNickname) + if (!certNickname) { + fprintf(stderr, "signDocument: Empty nickname\n"); + return gFalse; + } + + // calculate a signature over tmp_buffer with the certificate to get its size + unsigned char tmp_buffer[4]; + memcpy(tmp_buffer, "PDF", 4); + SignatureHandler sigHandler(nssDir, certNickname, SignatureHandler::getHashOidTag(digestName)); + sigHandler.updateHash(tmp_buffer, 4); + GooString* tmpSignature = sigHandler.signDetached(password); + if (!tmpSignature) + return gFalse; + + FormFieldSignature* signatureField = static_cast(field); + GooString gReason(reason ? reason : ""); + char *signerName = sigHandler.getSignerName(); + GooString gName(signerName); + free(signerName); + Object vObj(new Dict(xref)); + if (!createSignature(vObj, gName, gReason, tmpSignature)) { - unsigned char tmp_buffer[4]; - memcpy(tmp_buffer, "PDF", 4); - SignatureHandler sigHandler(nssDir, certNickname, SignatureHandler::getHashOidTag(digestName)); - sigHandler.updateHash(tmp_buffer, 4); - // calculate a signature over tmp_buffer with the certificate to get its size - GooString* tmpSignature = sigHandler.signDetached(password); - if (tmpSignature) - { - FormFieldSignature* signatureField = static_cast(field); - GooString gReason(reason ? reason : ""); - char* signerName = sigHandler.getSignerName(); - GooString gName(signerName); - free(signerName); - Object vObj(new Dict(xref)); - ok = createSignature(vObj, gName, gReason, tmpSignature); - if (!ok) - { - delete tmpSignature; - return ok; - } - // calculate hash with preliminary values for ranges - sigHandler.restartHash(); - Goffset file_size = 0; - Goffset sig_start = 0; - Goffset sig_end = 0; - prepareSignature(&sigHandler, file_size, sig_start, sig_end); - ok = updateSignature(vObj, tmpSignature, file_size, sig_start, sig_end); - delete tmpSignature; - if (!ok) - { - return ok; - } - // recalculate hash with the correct ranges - sigHandler.restartHash(); - prepareSignature(&sigHandler, file_size, sig_start, sig_end); - GooString* signature = sigHandler.signDetached(password); - if (signature) - { - ok = updateSignature(vObj, signature, file_size, sig_start, sig_end); - if (ok) - signatureField->setSignature(signature); - delete signature; - } + delete tmpSignature; + return gFalse; + } + + // Incremental save to avoid breaking any existing signatures + GooString *fname = new GooString(saveFilename); + if (doc->saveAs(fname, writeForceIncremental) != errNone) { + fprintf(stderr, "signDocument: error saving to file \"%s\"\n", saveFilename); + return gFalse; + } + + // Get start/end offset of signature object in the saved PDF + Goffset objStart, objEnd; + if (!getObjectStartEnd(fname, ref.num, &objStart, &objEnd)) { + fprintf(stderr, "signDocument: unable to get signature object offsets\n"); + } + + // Update byte range of signature in the saved PDF + Goffset sigStart, sigEnd, fileSize; + FILE *file = openFile(saveFilename, "r+b"); + if (!updateOffsets(file, objStart, objEnd, &sigStart, &sigEnd, &fileSize)) { + fprintf(stderr, "signDocument: unable update byte range\n"); + return gFalse; + } + + // compute hash of byte ranges + sigHandler.restartHash(); + hashFileRange(file, &sigHandler, 0LL, sigStart); + hashFileRange(file, &sigHandler, sigEnd, fileSize); + + // and sign it + GooString* signature = sigHandler.signDetached(password); + if (!signature) + return gFalse; + + // write signature to saved file + if (!updateSignature(file, sigStart, sigEnd, signature)) { + fprintf(stderr, "signDocument: unable update signature\n"); + return gFalse; + } + signatureField->setSignature(signature); + delete signature; + + fclose(file); + + return gTrue; +} + +// Get start and end file position of objNum in the PDF named filename. +GBool FormWidgetSignature::getObjectStartEnd(GooString *filename, int objNum, Goffset *objStart, Goffset *objEnd) +{ + PDFDoc newDoc(filename); + if (!newDoc.isOk()) + return gFalse; + + XRef *newXref = newDoc.getXRef(); + XRefEntry *entry = newXref->getEntry(objNum); + if (entry->type != xrefEntryUncompressed) + return gFalse; + + *objStart = entry->offset; + newXref->fetch(objNum, entry->gen, 0, objEnd); + return gTrue; +} + +// find next offset containing the dummy offset '9999999999' and overwrite with offset +static char *setNextOffset(char *start, Goffset offset) +{ + char buf[50]; + sprintf(buf, "%lld", offset); + strcat(buf, " "); // add some padding + + char *p = strstr(start, "9999999999"); + if (p) { + strncpy(p, buf, 10); // overwrite exact size. + p += 10; + } else { + return nullptr; + } + return p; +} + +// Updates the ByteRange array of the signature object in the file. +// Returns start/end of signature string and file size. +GBool FormWidgetSignature::updateOffsets(FILE *f, Goffset objStart, Goffset objEnd, + Goffset *sigStart, Goffset *sigEnd, Goffset *fileSize) +{ + Gfseek(f, 0, SEEK_END); + *fileSize = Gftell(f); + + if (objEnd > *fileSize) + objEnd = *fileSize; + + // sanity check object offsets + if (objEnd <= objStart || (objEnd - objStart >= INT_MAX)) { + return gFalse; + } + + int bufSize = static_cast(objEnd - objStart); + Gfseek(f, objStart, SEEK_SET); + char *buf = (char*)gmalloc_checkoverflow(bufSize + 1); + if (!buf) + return gFalse; + fread(buf, bufSize, 1, f); + buf[bufSize] = 0; // prevent string functions from searching past the end + + // search for the Contents field which contains the signature + // which always must start with hex digits 308 + *sigStart = -1; + *sigEnd = -1; + for (int i = 0; i < bufSize - 14; i++) { + if (buf[i] == '/' && strncmp(&buf[i], "/Contents <308", 14) == 0) { + *sigStart = objStart + i + 10; + char *p = strchr(&buf[i], '>'); + if (p) + *sigEnd = objStart + (p - buf) + 1; + break; } } - return ok; + if (*sigStart == -1 || *sigEnd == -1) + return gFalse; + + // Search for ByteRange array and update offsets + for (int i = 0; i < bufSize - 10; i++) { + if (buf[i] == '/' && strncmp(&buf[i], "/ByteRange", 10) == 0) { + // update range + char *p = setNextOffset(&buf[i], *sigStart); + if (!p) + return gFalse; + p = setNextOffset(p, *sigEnd); + if (!p) + return gFalse; + p = setNextOffset(p, *fileSize - *sigEnd); + if (!p) + return gFalse; + break; + } + } + + // write buffer back to disk + Gfseek(f, objStart, SEEK_SET); + fwrite(buf, bufSize, 1, f); + free(buf); + return gTrue; +} + +// update hash with the specified range of data from the file +void FormWidgetSignature::hashFileRange(FILE *f, SignatureHandler *handler, Goffset start, Goffset end) +{ + const int BUF_SIZE = 65536; + + unsigned char *buf = new unsigned char[BUF_SIZE]; + + while (start < end) { + Gfseek(f, start, SEEK_SET); + int len = BUF_SIZE; + if (end - start < len) + len = end - start; + fread(buf, len, 1, f); + handler->updateHash(buf, len); + start += len; + } + delete buf; +} + +// Overwrite signature string in the file with new signature +GBool FormWidgetSignature::updateSignature(FILE *f, Goffset sigStart, Goffset sigEnd, const GooString* signature) +{ + if (signature->getLength()*2 + 2 != sigEnd - sigStart) + return gFalse; + + Gfseek(f, sigStart, SEEK_SET); + const char* c = signature->getCString(); + fprintf(f, "<"); + for (int i = 0; i < signature->getLength(); i++) { + unsigned char value = *(c+i)&0x000000ff; + fprintf(f, "%2.2x", value); + } + fprintf(f, "> "); + return gTrue; +} + +GBool FormWidgetSignature::createSignature(Object &vObj, const GooString& name, + const GooString& reason, + const GooString* signature) +{ + vObj.dictAdd(copyString("Type"), Object(objName, "Sig")); + vObj.dictAdd(copyString("Filter"), Object(objName, "Adobe.PPKLite")); + switch (signatureType()) { + case adbe_pkcs7_sha1: + // we don't support signing with SubFilter "adbe.pkcs7.sha1" + error(errUnimplemented, -1, "adbe.pkcs7.sha1 not supported\n"); + return gFalse; + case adbe_pkcs7_detached: + vObj.dictAdd(copyString("SubFilter"), Object(objName, "adbe.pkcs7.detached")); + break; + case ETSI_CAdES_detached: + vObj.dictAdd(copyString("SubFilter"), Object(objName, "ETSI.CAdES.detached")); + break; + } + vObj.dictAdd(copyString("Name"), Object(name.copy())); + GooString *date = timeToDateString(nullptr); + vObj.dictAdd(copyString("M"), Object(date)); + if (reason.getLength() > 0) + vObj.dictAdd(copyString("Reason"), Object(reason.copy())); + vObj.dictAdd(copyString("Contents"), Object(signature->copy())); + Object bObj(new Array(xref)); + // reserve space in byte range for maximum number of bytes + bObj.arrayAdd(Object(static_cast(0LL))); + bObj.arrayAdd(Object(static_cast(9999999999LL))); + bObj.arrayAdd(Object(static_cast(9999999999LL))); + bObj.arrayAdd(Object(static_cast(9999999999LL))); + vObj.dictAdd(copyString("ByteRange"), bObj.copy()); + obj.dictSet("V", vObj.copy()); + xref->setModifiedObject(&obj, ref); + return gTrue; } std::vector FormWidgetSignature::getSignedRangeBounds() @@ -650,154 +843,6 @@ GooString* FormWidgetSignature::getCheckedSignature(Goffset *checkedFileSize) return nullptr; } -GBool FormWidgetSignature::createSignature(Object &vObj, const GooString& name, - const GooString& reason, - const GooString* signature) -{ - Goffset file_size = doc->getBaseStream()->getLength(); - vObj.dictAdd(copyString("Type"), Object(objName, "Sig")); - vObj.dictAdd(copyString("Filter"), Object(objName, "Adobe.PPKLite")); - switch (signatureType()) { - case adbe_pkcs7_sha1: - // we don't support signing with SubFilter "adbe.pkcs7.sha1" - error(errUnimplemented, -1, "adbe.pkcs7.sha1 not supported\n"); - return gFalse; - case adbe_pkcs7_detached: - vObj.dictAdd(copyString("SubFilter"), Object(objName, "adbe.pkcs7.detached")); - break; - case ETSI_CAdES_detached: - vObj.dictAdd(copyString("SubFilter"), Object(objName, "ETSI.CAdES.detached")); - break; - } - vObj.dictAdd(copyString("Name"), Object(name.copy())); - char buf[24]; - time_t now = time(nullptr); - size_t size = strftime(buf, 24, "D:%Y%m%d%H%M%S%z", localtime(&now)); - if (size >= 2 && size < 22) - { - // put timezone info into single quotes - buf[size] = buf[size-1]; - buf[size-1] = buf[size-2]; - buf[size-2] = '\''; - buf[++size] = '\''; - buf[++size] = '\0'; - GooString gTime(buf, size); - vObj.dictAdd(copyString("M"), Object(gTime.copy())); - } - if (reason.getLength() > 0) - vObj.dictAdd(copyString("Reason"), Object(reason.copy())); - vObj.dictAdd(copyString("Contents"), Object(signature->copy())); - Object bObj(new Array(xref)); - bObj.arrayAdd(Object(static_cast(0))); - bObj.arrayAdd(Object(file_size)); - bObj.arrayAdd(Object(file_size+2*signature->getLength()+2)); - bObj.arrayAdd(Object(static_cast(1000))); - vObj.dictAdd(copyString("ByteRange"), bObj.copy()); - obj.dictSet("V", vObj.copy()); - xref->setModifiedObject(&obj, ref); - return gTrue; -} - -GBool FormWidgetSignature::prepareSignature(SignatureHandler *handler, Goffset& file_size, - Goffset& sig_start, Goffset& sig_end) -{ - GBool ok = gFalse; - size_t size; - char* membuf; - FILE* mfp = open_memstream(&membuf, &size); - if (mfp) - { - FileOutStream* outStr = new FileOutStream(mfp, 0); - doc->saveAs(outStr, writeForceIncremental); - fclose(mfp); - delete outStr; - if (membuf) - { - // search for the Contents field which contains the signature - // which always must start with hex digits 308 - for (Goffset i = 0; i < size; ++i) - { - if (strncmp(&membuf[i], "/Contents <308", 14) == 0) - { - char* p = index(&membuf[i+14], '>'); - if (p) - { - sig_end = ++p - membuf; - for (Goffset j = sig_end; j < size; ++j) - { - if (strncmp(&membuf[j], "%%EOF\r\n", 7) == 0) - { - file_size = j + 7; - ok = gTrue; - } - } - if (ok) - { - sig_start = i + 10; - break; - } - } - } - } - Goffset range_offset = sig_end; - Goffset range_size = 0; - if (ok) - { - ok = gFalse; - for (range_offset = sig_end; range_offset < file_size; ++range_offset) - { - if (strncmp(&membuf[range_offset], "/ByteRange [0 ", 14) == 0) - { - char* p = index(&membuf[range_offset+14], ']'); - if (p) - { - range_size = (++p - membuf) - range_offset; - ok = gTrue; - break; - } - } - } - } - if (ok) - { - // the length of the string printed into range_buf is guaranteed to be less than 128 - char* range_buf = new char[128]; - Goffset end_size = file_size - sig_end; - Goffset s = snprintf(range_buf, 128, "/ByteRange [0 %lld %lld %lld ]", - sig_start, sig_end, end_size); - char* buffer = new char[end_size+128]; - memcpy(buffer, &membuf[sig_end], range_offset-sig_end); - memcpy(&buffer[range_offset-sig_end], range_buf, s); - memcpy(&buffer[range_offset-sig_end+s], &membuf[range_offset+range_size], - file_size-range_offset-range_size); - file_size += s - range_size; - handler->updateHash(reinterpret_cast(membuf), sig_start); - handler->updateHash(reinterpret_cast(buffer), file_size-sig_end); - delete[] range_buf; - delete[] buffer; - } - free(membuf); - } - } - return ok; -} - -GBool FormWidgetSignature::updateSignature(Object &vObj, const GooString* signature, - Goffset file_size, Goffset sig_start, Goffset sig_end) -{ - if (!vObj.isDict() || !signature) - return gFalse; - vObj.dictSet("Contents", Object(signature->copy())); - Object bObj(new Array(xref)); - bObj.arrayAdd(Object(static_cast(0))); - bObj.arrayAdd(Object(sig_start)); - bObj.arrayAdd(Object(sig_end)); - bObj.arrayAdd(Object(file_size-sig_end)); - vObj.dictSet("ByteRange", bObj.copy()); - obj.dictSet("V", vObj.copy()); - return gTrue; -} - void FormWidgetSignature::updateWidgetAppearance() { // Unimplemented diff --git a/poppler/Form.h b/poppler/Form.h index ad41e264..4166995e 100644 --- a/poppler/Form.h +++ b/poppler/Form.h @@ -278,7 +278,7 @@ public: // document except for the signature itself; this byte range is specified in the // field "ByteRange" in the dictionary "V" // return success - GBool signDocument(const char *nssDir, + GBool signDocument(const char *nssDir, const char*filename, const char *certNickname, const char *digestName, const char *password, const char *reason = nullptr); @@ -294,10 +294,12 @@ public: private: GBool createSignature(Object &vObj, const GooString &name, const GooString &reason, const GooString *signature); - GBool prepareSignature(SignatureHandler *handler, - Goffset& file_size, Goffset& sig_start, Goffset& sig_end); - GBool updateSignature(Object &vObj, const GooString *signature, - Goffset file_size, Goffset sig_start, Goffset sig_end); + GBool getObjectStartEnd(GooString *filename, int objNum, Goffset *objStart, Goffset *objEnd); + GBool updateOffsets(FILE *f, Goffset objStart, Goffset objEnd, + Goffset *sigStart, Goffset *sigEnd, Goffset *fileSize); + + void hashFileRange(FILE *f, SignatureHandler *handler, Goffset start, Goffset end); + GBool updateSignature(FILE *f, Goffset sigStart, Goffset sigEnd, const GooString* signature); }; //------------------------------------------------------------------------ diff --git a/poppler/XRef.cc b/poppler/XRef.cc index eca2dc70..532a82de 100644 --- a/poppler/XRef.cc +++ b/poppler/XRef.cc @@ -1106,7 +1106,7 @@ Object XRef::getCatalog() { return catalog; } -Object XRef::fetch(int num, int gen, int recursion) { +Object XRef::fetch(int num, int gen, int recursion, Goffset *endPos) { XRefEntry *e; Parser *parser; Object obj1, obj2, obj3; @@ -1154,6 +1154,8 @@ Object XRef::fetch(int num, int gen, int recursion) { if (longNumber <= INT_MAX && longNumber >= INT_MIN && *end_ptr == '\0') { int number = longNumber; error(errSyntaxWarning, -1, "Cmd was not obj but {0:s}, assuming the creator meant obj {1:d}", cmd, number); + if (endPos) + *endPos = parser->getPos(); delete parser; return Object(number); } @@ -1164,6 +1166,8 @@ Object XRef::fetch(int num, int gen, int recursion) { } Object obj = parser->getObj(gFalse, (encrypted && !e->getFlag(XRefEntry::Unencrypted)) ? fileKey : NULL, encAlgorithm, keyLength, num, gen, recursion); + if (endPos) + *endPos = parser->getPos(); delete parser; return obj; } @@ -1203,6 +1207,8 @@ Object XRef::fetch(int num, int gen, int recursion) { objStrs->put(newkey, newitem); } } + if (endPos) + *endPos = -1; return objStr->getObject(e->gen, num); } @@ -1217,6 +1223,8 @@ Object XRef::fetch(int num, int gen, int recursion) { constructXRef(&xrefReconstructed); return fetch(num, gen, ++recursion); } + if (endPos) + *endPos = -1; return Object(objNull); } diff --git a/poppler/XRef.h b/poppler/XRef.h index e59e8cbb..e6460822 100644 --- a/poppler/XRef.h +++ b/poppler/XRef.h @@ -144,7 +144,10 @@ public: Object getCatalog(); // Fetch an indirect reference. - Object fetch(int num, int gen, int recursion = 0); + // If endPos not null, returns file position after parsing the object. This will + // be a few bytes after the end of the object due to the parser reading ahead. + // Returns -1 if object is in compressed stream. + Object fetch(int num, int gen, int recursion = 0, Goffset *endPos = nullptr); // Return the document's Info dictionary (if any). Object getDocInfo(); diff --git a/qt5/src/poppler-form.cc b/qt5/src/poppler-form.cc index a6cd59f7..11ee402c 100644 --- a/qt5/src/poppler-form.cc +++ b/qt5/src/poppler-form.cc @@ -634,7 +634,7 @@ void FormFieldSignature::setSignatureType(SignatureType type) } } -bool FormFieldSignature::sign(const QString& certNickname, const QString& password, + bool FormFieldSignature::sign(const QString& saveFilename, const QString& certNickname, const QString& password, DigestAlgorithm digestAlg, const QString& reason) { FormWidgetSignature* fws = static_cast(m_formData->fm); @@ -642,6 +642,7 @@ bool FormFieldSignature::sign(const QString& certNickname, const QString& passwo const char* rs = nullptr; char* pw = password.isEmpty() ? nullptr : strdup(password.toUtf8().constData()); char* name = strdup(certNickname.toUtf8().constData()); + char* filename = strdup(saveFilename.toUtf8().constData()); switch (digestAlg) { case SHA1: @@ -662,7 +663,7 @@ bool FormFieldSignature::sign(const QString& certNickname, const QString& passwo } if (!reason.isEmpty()) rs = reason.toUtf8().constData(); - GBool ok = fws->signDocument(nullptr, name, digest, pw, rs); + GBool ok = fws->signDocument(nullptr, filename, name, digest, pw, rs); free(name); free(pw); return ok; diff --git a/qt5/src/poppler-form.h b/qt5/src/poppler-form.h index 270c66b9..3c5b156b 100644 --- a/qt5/src/poppler-form.h +++ b/qt5/src/poppler-form.h @@ -555,7 +555,7 @@ namespace Poppler { @param digestAlg digest algorithm to be used in the signature @param reason a reason for the signature written to the signature field */ - bool sign(const QString& certNickname, const QString& password, + bool sign(const QString& saveFilename, const QString& certNickname, const QString& password, DigestAlgorithm digestAlg = SHA256, const QString& reason = QString()); /** diff --git a/utils/pdfsig.cc b/utils/pdfsig.cc index a4d83e63..58dd256e 100644 --- a/utils/pdfsig.cc +++ b/utils/pdfsig.cc @@ -197,10 +197,8 @@ int main(int argc, char *argv[]) fws->setSignatureType(ETSI_CAdES_detached); const char* pw = (strlen(password) == 0) ? nullptr : password; const char* rs = (strlen(reason) == 0) ? nullptr : reason; - if (fws->signDocument(nssDir.getCString(), certNickname, digestName, pw, rs)) { - GooString* out = new GooString(argv[2]); - exitCode = doc->saveAs(out); - delete out; + if (fws->signDocument(nssDir.getCString(), argv[2], certNickname, digestName, pw, rs)) { + exitCode = 0; goto end; } else { exitCode = 3; -- 2.11.0