|
// |
|
// experiment-pdf-sign-prepare |
|
// Copyright (C) 2019-2021 Masamichi Hosoda. All rights reserved. |
|
// License: BSD-2-Clause |
|
// |
|
// https://gist.github.com/trueroad/0b0a2127aff508caf583265fbef4b644 |
|
// |
|
|
|
#include <fstream> |
|
#include <iostream> |
|
#include <sstream> |
|
#include <string> |
|
|
|
#include <qpdf/QPDF.hh> |
|
#include <qpdf/QPDFObjectHandle.hh> |
|
#include <qpdf/QPDFPageDocumentHelper.hh> |
|
#include <qpdf/QPDFWriter.hh> |
|
|
|
#include "cmdlineparse.hh" |
|
|
|
size_t contents_size {8192}; |
|
std::string time_of_signing; |
|
bool bdocmdp; |
|
|
|
struct offsets |
|
{ |
|
qpdf_offset_t contents; |
|
qpdf_offset_t length1; |
|
qpdf_offset_t offset2; |
|
qpdf_offset_t length2; |
|
}; |
|
|
|
class offset_value |
|
{ |
|
public: |
|
offsets offs; |
|
unsigned int length1; |
|
unsigned int offset2; |
|
unsigned int length2; |
|
|
|
offset_value (offsets off, size_t eof) |
|
{ |
|
offs = off; |
|
length1 = off.contents; |
|
offset2 = length1 + contents_size * 2 + 2; |
|
length2 = eof - offset2; |
|
} |
|
}; |
|
|
|
QPDFObjGen build_intermediate (std::string in, std::string out) |
|
{ |
|
QPDF qpdf; |
|
qpdf.processFile (in.c_str ()); |
|
// TODO: check loaded |
|
|
|
std::cout << "input PDF loaded" << std::endl; |
|
|
|
QPDFObjectHandle acroform; |
|
auto root {qpdf.getRoot ()}; |
|
if (root.hasKey ("/AcroForm")) |
|
{ |
|
std::cout << "/AcroForm is existed in root" << std::endl; |
|
acroform = root.getKey ("/AcroForm"); |
|
// TODO: check /AcroForm type |
|
// TODO: check /SigFlags and /Fields |
|
} |
|
else |
|
{ |
|
std::cout << "Adding /AcroForm to root..." << std::endl; |
|
acroform = qpdf.makeIndirectObject (QPDFObjectHandle::parse |
|
("<< /SigFlags 3 /Fields [] >>")); |
|
root.replaceKey ("/AcroForm", acroform); |
|
} |
|
auto acroform_fields {acroform.getKey ("/Fields")}; |
|
|
|
QPDFPageDocumentHelper pdh (qpdf); |
|
auto pages {pdh.getAllPages ()}; |
|
// TODO: check pages |
|
auto &page1 {pages[0]}; |
|
|
|
QPDFObjectHandle annots; |
|
|
|
if (page1.getObjectHandle ().hasKey ("/Annots")) |
|
{ |
|
std::cout << "/Annots is existed in first page" << std::endl; |
|
annots = page1.getObjectHandle ().getKey ("/Annots"); |
|
// TODO: check /Annots type |
|
} |
|
else |
|
{ |
|
std::cout << "Adding /Annots to first page..." << std::endl; |
|
annots = qpdf.makeIndirectObject (QPDFObjectHandle::newArray ()); |
|
page1.getObjectHandle ().replaceKey ("/Annots", annots); |
|
} |
|
|
|
std::cout << "Creating invisible /Annot..." << std::endl; |
|
auto sigannot {qpdf.makeIndirectObject (QPDFObjectHandle::parse ( |
|
"<<\n" |
|
" /Type /Annot\n" |
|
" /Subtype /Widget\n" |
|
" /FT /Sig\n" |
|
" /T (Signature)\n" |
|
" /Rect [ 0 0 0 0 ]\n" |
|
" /F 132\n" |
|
">>" |
|
))}; |
|
sigannot.replaceKey ("/P", page1.getObjectHandle ()); |
|
|
|
std::cout << "Adding /Annot to /Fields array in /AcroForm..." << std::endl; |
|
acroform_fields.appendItem (sigannot); |
|
std::cout << "Adding /Annot to /Annots..." << std::endl; |
|
annots.appendItem (sigannot); |
|
|
|
std::cout << "Creating dummy /Sig..." << std::endl; |
|
|
|
std::string sigdict_pre { |
|
"<<\n" |
|
" /Type /Sig\n" |
|
" /Filter /Adobe.PPKLite\n" |
|
" /SubFilter /adbe.pkcs7.detached\n" |
|
}; |
|
|
|
if (!time_of_signing.empty ()) |
|
sigdict_pre += " /M (D:" + time_of_signing + ")\n"; |
|
sigdict_pre += |
|
" /ByteRange [ 0 1234567890 1234567890 1234567890 ]\n" |
|
" /Contents <"; |
|
|
|
std::string sigdict_post {">\n>>"}; |
|
std::string sigdict_contents (contents_size * 2, '0'); |
|
auto sigdict {qpdf.makeIndirectObject |
|
(QPDFObjectHandle::parse |
|
(sigdict_pre + sigdict_contents + sigdict_post))}; |
|
|
|
if (bdocmdp) |
|
{ |
|
std::cout << "Adding /Reference to /Sig" << std::endl; |
|
auto refs = qpdf.makeIndirectObject (QPDFObjectHandle::newArray ()); |
|
sigdict.replaceKey ("/Reference", refs); |
|
|
|
std::cout << "Creating /SigRef" << std::endl; |
|
auto sigref |
|
{qpdf.makeIndirectObject (QPDFObjectHandle::parse ( |
|
"<<\n" |
|
" /Type /SigRef\n" |
|
" /TransformMethod /DocMDP\n" |
|
" /TransformParams <<\n" |
|
" /Type /TransformParams\n" |
|
" /P 2\n" |
|
" /V /1.2\n" |
|
" >>\n" |
|
">>" |
|
))}; |
|
|
|
std::cout << "Adding /SigRef to /Reference" << std::endl; |
|
refs.appendItem (sigref); |
|
|
|
QPDFObjectHandle perms; |
|
if (root.hasKey ("/Perms")) |
|
{ |
|
std::cout << "/Perms is existed in root" << std::endl; |
|
perms = root.getKey ("/Perms"); |
|
// TODO: check /Perms |
|
} |
|
else |
|
{ |
|
std::cout << "Adding /Perms to root..." << std::endl; |
|
perms = qpdf.makeIndirectObject |
|
(QPDFObjectHandle::parse ("<< /DocMDP << >> >>")); |
|
root.replaceKey ("/Perms", perms); |
|
} |
|
|
|
std::cout << "Adding /SigRef to /Perms" << std::endl; |
|
perms.replaceKey ("/DocMDP", sigref); |
|
} |
|
|
|
std::cout << "Adding /Sig to /Annot" << std::endl; |
|
sigannot.replaceKey ("/V", sigdict); |
|
|
|
auto og {sigdict.getObjGen ()}; |
|
std::cout << "Added /Sig is " << og.getObj() |
|
<< "/" << og.getGen() << std::endl; |
|
|
|
std::cout << "Writing intermediate PDF" << std::endl; |
|
QPDFWriter w (qpdf, out.c_str ()); |
|
w.setLinearization (true); |
|
// qpdf_o_preserve / qpdf_o_generate / qpdf_o_disable |
|
w.setObjectStreamMode (qpdf_o_generate); |
|
w.setNewlineBeforeEndstream (true); |
|
w.setQDFMode (false); |
|
w.setMinimumPDFVersion ("1.6"); // SHA256 requires PDF 1.6 |
|
w.write (); |
|
|
|
auto og_w {w.getRenumberedObjGen (og)}; |
|
std::cout << "Written /Sig is " << og_w.getObj() |
|
<< "/" << og_w.getGen() << std::endl; |
|
|
|
return og_w; |
|
} |
|
|
|
offsets get_offsets (std::string file, QPDFObjGen og) |
|
{ |
|
offsets offs; |
|
|
|
QPDF qpdf; |
|
qpdf.processFile (file.c_str ()); |
|
// TODO: check loaded |
|
|
|
std::cout << "intermediate PDF reloaded" << std::endl; |
|
|
|
auto sig {qpdf.getObjectByObjGen(og)}; |
|
// TODO: check object |
|
|
|
auto contents {sig.getKey ("/Contents")}; |
|
// TODO: check object |
|
offs.contents = contents.getParsedOffset (); |
|
|
|
auto byterange {sig.getKey ("/ByteRange")}; |
|
// TODO: check object |
|
|
|
offs.length1 = byterange.getArrayItem (1).getParsedOffset (); |
|
offs.offset2 = byterange.getArrayItem (2).getParsedOffset (); |
|
offs.length2 = byterange.getArrayItem (3).getParsedOffset (); |
|
|
|
std::cout << "offset contents = " |
|
<< offs.contents |
|
<< " (0x" |
|
<< std::hex << offs.contents << std::dec |
|
<< ")" << std::endl; |
|
std::cout << "offset /ByteRange length1 = " |
|
<< offs.length1 |
|
<< " (0x" |
|
<< std::hex << offs.length1 << std::dec |
|
<< ")" << std::endl; |
|
std::cout << "offset /ByteRange offset2 = " |
|
<< offs.offset2 |
|
<< " (0x" |
|
<< std::hex << offs.offset2 << std::dec |
|
<< ")" << std::endl; |
|
std::cout << "offset /ByteRange length2 = " |
|
<< offs.length2 |
|
<< " (0x" |
|
<< std::hex << offs.length2 << std::dec |
|
<< ")" << std::endl; |
|
|
|
return offs; |
|
} |
|
|
|
offset_value fix_intermediate (std::string file, offsets offs) |
|
{ |
|
std::fstream fs (file, |
|
std::ios::in | std::ios::out | std::ios::binary); |
|
fs.seekg (0, std::fstream::end); |
|
auto offset_eof {static_cast<size_t> (fs.tellg ())}; |
|
fs.clear (); |
|
|
|
std::cout << "offset eof = " |
|
<< offset_eof |
|
<< " (0x" |
|
<< std::hex << offset_eof << std::dec |
|
<< ")" << std::endl; |
|
|
|
offset_value ov (offs, offset_eof); |
|
|
|
std::stringstream ss; |
|
ss << ov.length1 << " " |
|
<< ov.offset2 << " " |
|
<< ov.length2 << " ]"; |
|
|
|
std::string rewrite {ss.str ()}; |
|
rewrite.resize (10 + 1 + 10 + 1 + 10 + 1 + 1, ' '); |
|
|
|
std::cout << "Rewriting /ByteRange [ " |
|
<< 0 |
|
<< " " |
|
<< ov.length1 |
|
<< " " |
|
<< ov.offset2 |
|
<< " " |
|
<< ov.length2 |
|
<< " ]" << std::endl; |
|
|
|
fs.seekg (offs.length1, std::fstream::beg); |
|
fs.write (rewrite.c_str (), rewrite.size ()); |
|
|
|
return ov; |
|
} |
|
|
|
void create_file_to_be_signed (std::string in, std::string out, |
|
offset_value ov) |
|
{ |
|
std::ifstream ifs (in, std::ios_base::in | std::ios_base::binary); |
|
std::ofstream ofs (out, std::ios_base::out | std::ios_base::binary); |
|
|
|
std::vector<char> buff; |
|
buff.resize (ov.length1); |
|
ifs.seekg (0, std::fstream::beg); |
|
ifs.read (&buff[0], ov.length1); |
|
ofs.write (&buff[0], ov.length1); |
|
|
|
buff.resize (ov.length2); |
|
ifs.seekg (ov.offset2, std::fstream::beg); |
|
ifs.read (&buff[0], ov.length2); |
|
ofs.write (&buff[0], ov.length2); |
|
} |
|
|
|
int main (int argc, char *argv[]) |
|
{ |
|
cmdlineparse::parser cmd; |
|
|
|
cmd.set_version_string ( |
|
"experiment-pdf-sign-prepare\n" |
|
"Copyright (C) Masamichi Hosoda 2019-2021\n" |
|
"License: BSD-2-Clause\n" |
|
); |
|
|
|
cmd.add_default (); |
|
|
|
std::string filename_input; |
|
cmd.add_string (0, "input", &filename_input, "", |
|
" (in) Input file", |
|
"FILENAME.pdf", "Input/output filenames"); |
|
std::string filename_intermediate; |
|
cmd.add_string (0, "intermediate", &filename_intermediate, "", |
|
" (out) Intermediate file", |
|
"FILENAME.pdf", "Input/output filenames"); |
|
std::string filename_to_be_signed; |
|
cmd.add_string (0, "to-be-signed", &filename_to_be_signed, "", |
|
" (out) File to be signed", |
|
"FILENAME.bin", "Input/output filenames"); |
|
std::string filename_offset; |
|
cmd.add_string (0, "offsetfile", &filename_offset, "", |
|
" (out) /Contents value offset file", |
|
"FILENAME.txt", "Input/output filenames"); |
|
|
|
cmd.add_string (0, "time", &time_of_signing, "", |
|
" (in) Time of signing for signature dictionary", |
|
"YYYYMMDDHHmmSSOHH'mm'", "Sign"); |
|
std::string contents_size_str; |
|
cmd.add_string (0, "contents-size", &contents_size_str, "8192", |
|
" (in) /Contents size", |
|
"BYTES", "Sign"); |
|
cmd.add_flag (0, "docmdp", &bdocmdp, |
|
" Use DocMDP signature", "Sign"); |
|
|
|
if (!cmd.parse (argc, argv)) |
|
return 1; |
|
|
|
if (filename_input == "" || |
|
filename_intermediate == "" || |
|
filename_to_be_signed == "" || |
|
filename_offset == "") |
|
{ |
|
std::cout << cmd.build_help (); |
|
return 1; |
|
} |
|
|
|
contents_size = static_cast<size_t> (std::stoi (contents_size_str)); |
|
|
|
std::cout << cmd.get_version_string () << std::endl; |
|
|
|
std::cout |
|
<< "(in) Input file : " << filename_input << std::endl |
|
<< "(out) Intermediate file: " << filename_intermediate << std::endl |
|
<< "(out) File to be sigend: " << filename_to_be_signed << std::endl |
|
<< "(out) Offset file : " << filename_offset << std::endl |
|
<< "(in) /Contents size : " << contents_size << std::endl; |
|
if (!time_of_signing.empty ()) |
|
std::cout << "(in) Time of signing : " << time_of_signing << std::endl; |
|
if (bdocmdp) |
|
std::cout << "Use DocMDP signature" << std::endl; |
|
std::cout << std::endl; |
|
|
|
auto og_sig {build_intermediate (filename_input, filename_intermediate)}; |
|
auto offs {get_offsets (filename_intermediate, og_sig)}; |
|
auto ov {fix_intermediate (filename_intermediate, offs)}; |
|
create_file_to_be_signed (filename_intermediate, filename_to_be_signed, ov); |
|
|
|
std::ofstream ofs (filename_offset); |
|
ofs << offs.contents << std::endl; |
|
ofs.close (); |
|
|
|
std::cout << "complete" << std::endl; |
|
|
|
return 0; |
|
} |
This looks really interesting. I will study it more when I get a chance and will make sure you can run this code with qpdf 9.1 before releasing it. Thanks for sharing.