Skip to content

Instantly share code, notes, and snippets.

@trueroad
Last active June 13, 2023 18:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trueroad/0b0a2127aff508caf583265fbef4b644 to your computer and use it in GitHub Desktop.
Save trueroad/0b0a2127aff508caf583265fbef4b644 to your computer and use it in GitHub Desktop.
Experiment PDF sign tool

Experiment PDF sign tool

https://gist.github.com/trueroad/0b0a2127aff508caf583265fbef4b644

Copyright (C) 2019 Masamichi Hosoda. All rights reserved.

License: BSD-2-Clause


Experiment PDF sign tool is PDF/A-2 compliant PDF digitally signing tool.

Extract PDF signature tool can extract PDF signature as PKCS#7 signed-data and can verify with OpenSSL.

Require

Build

  • Edit QPDF_PREFIX valiable in Makefile
  • Run make command

Usage

Convert client certificate file

If your client certificate file is .pfx or .p12, use the following command to convert it.

$ openssl pkcs12 -in JohnDoe.pfx -out JohnDoe.pem

Password of sample client certificate file JohnDoe.pfx is example.

Sign

$ ./experiment-pdf-sign.sh INPUT.pdf SIGNED.pdf JohnDoe.pem

Some intermediate file are created. The following command removes them.

$ rm intermediate.*.pdf to-be-signed.*.bin signed-data.*.p7s offset.*.txt
//
// experiment-pdf-sign-finalize
// Copyright (C) 2019 Masamichi Hosoda. All rights reserved.
// License: BSD-2-Clause
//
// https://gist.github.com/trueroad/0b0a2127aff508caf583265fbef4b644
//
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
const size_t BUFF_SIZE = 4096;
void filecopy (std::string in, std::string out)
{
std::ifstream ifs (in, std::ios::in | std::ios::binary);
std::ofstream ofs (out, std::ios::out | std::ios::binary);
std::vector<char> buff;
buff.resize (BUFF_SIZE);
while (!ifs.eof ())
{
ifs.read (&buff[0], BUFF_SIZE);
ofs.write (&buff[0], ifs.gcount ());
}
}
void rewrite (std::string pdf, std::string data, size_t offset)
{
std::ifstream ifs (data, std::ios::in | std::ios::binary);
std::fstream fs (pdf, std::ios::in | std::ios::out | std::ios::binary);
fs.seekp (offset);
while (!ifs.eof ())
{
int c = ifs.get ();
if (c == EOF)
break;
std::string str;
{
std::stringstream ss;
ss << std::setfill('0') << std::setw(2) << std::hex << c;
str = ss.str ();
}
fs.write (str.c_str (), str.size ());
}
}
int main (int argc, char *argv[])
{
std::cout
<< "experiment-pdf-sign-finalize" << std::endl
<< "Copyright (C) Masamichi Hosoda 2019" << std::endl
<< "License: BSD-2-Clause" << std::endl << std::endl;
if (argc != 5)
{
std::cerr
<< "usage: " << argv[0]
<< "(in)OFFSET (in)INTERMEDIATE.pdf (in)SIGNED-DATA.p7s "
"(out)SIGNED.pdf" << std::endl;
return 1;
}
auto offset {std::stoul (argv[1])};
std::string filename_intermediate {argv[2]};
std::string filename_signed_data {argv[3]};
std::string filename_signed_pdf {argv[4]};
std::cout << "(in) OFFSET : " << offset << std::endl
<< "(in) INTERMEDIATE: " << filename_intermediate << std::endl
<< "(in) SIGNED-DATA : " << filename_signed_data << std::endl
<< "(out) SIGNED-PDF : " << filename_signed_pdf << std::endl;
filecopy (filename_intermediate, filename_signed_pdf);
rewrite (filename_signed_pdf, filename_signed_data, offset + 1);
return 0;
}
//
// 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;
}
#!/bin/sh
#
# experiment-pdf-sign.sh
# Copyright (C) 2019 Masamichi Hosoda. All rights reserved.
# License: BSD-2-Clause
#
# https://gist.github.com/trueroad/0b0a2127aff508caf583265fbef4b644
#
if [ $# -ne 3 ]; then
echo "usage: ./experiment-pdf-sign.sh INPUT.pdf SIGNED.pdf CERT.pem"
exit 1
fi
echo
echo "*** preparing ***"
echo
./experiment-pdf-sign-prepare --input $1 --intermediate intermediate.$$.pdf \
--to-be-signed to-be-signed.$$.bin \
--offsetfile offset.$$.txt
if [ $? -ne 0 ]; then
echo
echo "prepare failed"
exit 1
fi
echo
echo "*** openssl signing ***"
echo
openssl smime --sign -binary -noattr \
-in to-be-signed.$$.bin -out signed-data.$$.p7s \
-outform DER -signer $3
if [ $? -ne 0 ]; then
echo
echo "openssl failed"
exit 1
fi
echo
echo "*** finalizing ***"
echo
./experiment-pdf-sign-finalize `cat offset.$$.txt` intermediate.$$.pdf \
signed-data.$$.p7s $2
if [ $? -ne 0 ]; then
echo
echo "finalize failed"
exit 1
fi
echo
echo "*** complete ***"
echo
QPDF_PREFIX = /usr
QPDF_PKGCONFIG_PATH = $(QPDF_PREFIX)/lib/pkgconfig
PREPARE_BIN = experiment-pdf-sign-prepare
FINALIZE_BIN = experiment-pdf-sign-finalize
WGET = wget
.PHONY: all clean
BIN = $(PREPARE_BIN) $(FINALIZE_BIN)
all: $(BIN)
PREPARE_OBJS = $(addsuffix .o,$(PREPARE_BIN))
FINALIZE_OBJS = $(addsuffix .o,$(FINALIZE_BIN))
OBJS = $(PREPARE_OBJS) $(FINALIZE_OBJS)
CXXFLAGS_STD = -std=c++11
QPDF_CPPFLAGS = \
$(shell PKG_CONFIG_PATH=$(QPDF_PKGCONFIG_PATH) pkg-config --cflags libqpdf)
QPDF_LDLIBS = \
$(shell PKG_CONFIG_PATH=$(QPDF_PKGCONFIG_PATH) pkg-config --libs libqpdf)
CXXFLAGS += $(CXXFLAGS_STD)
CPPFLAGS += $(QPDF_CPPFLAGS)
LDLIBS += $(QPDF_LDLIBS)
DEPS = $(OBJS:.o=.d)
CPPFLAGS += -MMD -MP -MF $(@:.o=.d) -MT $@
-include $(DEPS)
clean:
$(RM) *~ $(OBJS) $(DEPS)
$(PREPARE_BIN): $(PREPARE_OBJS)
$(LINK.cc) $^ $(LOADLIBES) $(LDLIBS) -o $@
$(FINALIZE_BIN): $(FINALIZE_OBJS)
$(LINK.cc) $^ $(LOADLIBES) $(LDLIBS) -o $@
experiment-pdf-sign-prepare.o: cmdlineparse.hh
cmdlineparse.hh:
$(WGET) https://github.com/trueroad/cmdlineparse/raw/master/cmdlineparse.hh
#QPDF_DLL_PATH = $(QPDF_PREFIX)/bin
#QPDF_DLL = $(notdir $(wildcard $(QPDF_DLL_PATH)/cygqpdf-*.dll))
#all: $(QPDF_DLL)
#
#$(QPDF_DLL): $(QPDF_DLL_PATH)/$(QPDF_DLL)
# cp $^ $@
@jberkenbilt
Copy link

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.

@trueroad
Copy link
Author

@jberkenbilt
qpdf 9.1.rc1 works fine for this tool.
Thank you.

@jberkenbilt
Copy link

Thanks for your contributions and for testing with the release candidate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment