Compare commits
23 Commits
2d3fd3317b
...
v1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
3ce08d33f6
|
|||
|
a75f990565
|
|||
|
46fa50749f
|
|||
|
796a9640d3
|
|||
|
2d08100b58
|
|||
|
1224d9be47
|
|||
|
486cafa6b4
|
|||
|
a0696cdb88
|
|||
|
be8480f8af
|
|||
|
f9f3171c02
|
|||
|
0ea296de67
|
|||
|
9d2412398a
|
|||
|
62206ed344
|
|||
|
9ac8360979
|
|||
|
50578fe99f
|
|||
|
eedd9a550c
|
|||
|
43ca0e5035
|
|||
|
ad239714b0
|
|||
|
daa9cc1ed4
|
|||
|
4c7853c951
|
|||
|
c77a9c052a
|
|||
|
54d86cf25b
|
|||
|
160f09ecc3
|
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017, Přemysl Janouch <p@janouch.name>
|
||||
Copyright (c) 2017 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
16
NEWS
16
NEWS
@@ -1,3 +1,19 @@
|
||||
1.1 (2020-09-05)
|
||||
|
||||
* Make it possible to change the signature reservation with an option
|
||||
|
||||
* Return errors rather than mangle documents in some cases,
|
||||
notably with pre-existing PDF forms
|
||||
|
||||
* Avoid downgrading the document's PDF version to 1.6
|
||||
|
||||
* A few fixes for PDF parsing and serialisation
|
||||
|
||||
* Add an instructive man page
|
||||
|
||||
* Add a native Go port of the utility, also usable as a library
|
||||
|
||||
|
||||
1.0 (2018-08-03)
|
||||
|
||||
* Initial release
|
||||
|
||||
28
README.adoc
28
README.adoc
@@ -1,27 +1,20 @@
|
||||
pdf-simple-sign
|
||||
===============
|
||||
:compact-option:
|
||||
|
||||
'pdf-simple-sign' is a simple open source PDF signer intended for documents
|
||||
generated by Cairo. As such, it currently comes with some restrictions:
|
||||
|
||||
* the document may not have any forms or signatures already, as they will be
|
||||
overwitten
|
||||
* the document may not employ cross-reference streams, or must constitute
|
||||
a hybrid-reference file at least
|
||||
* the document may not be newer than PDF 1.6 already, or it will get downgraded
|
||||
to that version
|
||||
* the signature may take at most 4 kilobytes as a compile-time limit,
|
||||
which should be enough space even for one intermediate certificate
|
||||
|
||||
The signature is attached to the first page and has no appearance.
|
||||
'pdf-simple-sign' is a simple PDF signer intended for documents produced by
|
||||
the Cairo library, GNU troff, ImageMagick, or similar.
|
||||
|
||||
I don't aim to extend the functionality any further. The project is fairly
|
||||
self-contained and it should be easy to grasp and change to suit to your needs.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
See the link:pdf-simple-sign.adoc[man page] for information about usage.
|
||||
The rest of this README will concern itself with externalities.
|
||||
|
||||
Building
|
||||
--------
|
||||
Build dependencies: Meson, a C++11 compiler, pkg-config +
|
||||
Build dependencies: Meson, Asciidoctor, a C++11 compiler, pkg-config +
|
||||
Runtime dependencies: libcrypto (OpenSSL 1.1 API)
|
||||
|
||||
$ git clone https://git.janouch.name/p/pdf-simple-sign.git
|
||||
@@ -30,10 +23,9 @@ Runtime dependencies: libcrypto (OpenSSL 1.1 API)
|
||||
$ cd builddir
|
||||
$ ninja
|
||||
|
||||
Usage
|
||||
-----
|
||||
In addition to the C++ version, also included is a native Go port:
|
||||
|
||||
$ ./pdf-simple-sign document.pdf document.signed.pdf KeyAndCerts.p12 password
|
||||
$ go get janouch.name/pdf-simple-sign/cmd/pdf-simple-sign
|
||||
|
||||
Contributing and Support
|
||||
------------------------
|
||||
|
||||
76
cmd/pdf-simple-sign/main.go
Normal file
76
cmd/pdf-simple-sign/main.go
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright (c) 2018 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
//
|
||||
|
||||
// pdf-simple-sign is a simple PDF signer.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"janouch.name/pdf-simple-sign/pdf"
|
||||
)
|
||||
|
||||
// #include <unistd.h>
|
||||
import "C"
|
||||
|
||||
func isatty(fd uintptr) bool { return C.isatty(C.int(fd)) != 0 }
|
||||
|
||||
func die(status int, format string, args ...interface{}) {
|
||||
msg := fmt.Sprintf(format+"\n", args...)
|
||||
if isatty(os.Stderr.Fd()) {
|
||||
msg = "\x1b[0;31m" + msg + "\x1b[m"
|
||||
}
|
||||
os.Stderr.WriteString(msg)
|
||||
os.Exit(status)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
die(1, "Usage: %s [-h] [-r RESERVATION] INPUT-FILENAME OUTPUT-FILENAME "+
|
||||
"PKCS12-PATH PKCS12-PASS", os.Args[0])
|
||||
}
|
||||
|
||||
var reservation = flag.Int(
|
||||
"r", 4096, "signature reservation as a number of bytes")
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if flag.NArg() != 4 {
|
||||
usage()
|
||||
}
|
||||
|
||||
inputPath, outputPath := flag.Arg(0), flag.Arg(1)
|
||||
doc, err := ioutil.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
die(1, "%s", err)
|
||||
}
|
||||
p12, err := ioutil.ReadFile(flag.Arg(2))
|
||||
if err != nil {
|
||||
die(2, "%s", err)
|
||||
}
|
||||
key, certs, err := pdf.PKCS12Parse(p12, flag.Arg(3))
|
||||
if err != nil {
|
||||
die(3, "%s", err)
|
||||
}
|
||||
if doc, err = pdf.Sign(doc, key, certs, *reservation); err != nil {
|
||||
die(4, "error: %s", err)
|
||||
}
|
||||
if err = ioutil.WriteFile(outputPath, doc, 0666); err != nil {
|
||||
die(5, "%s", err)
|
||||
}
|
||||
}
|
||||
8
go.mod
Normal file
8
go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module janouch.name/pdf-simple-sign
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
|
||||
)
|
||||
13
go.sum
Normal file
13
go.sum
Normal file
@@ -0,0 +1,13 @@
|
||||
go.mozilla.org/pkcs7 v0.0.0-20181029144607-24857c352dd8 h1:W3oGFPlHBLgXdsbPVixWFMYsuPhm81/Qww3XAgBbn/0=
|
||||
go.mozilla.org/pkcs7 v0.0.0-20181029144607-24857c352dd8/go.mod h1:5fWP3IVYEMc04wC+lMJAfkmNmKAl2P1swVv8VS+URZ8=
|
||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
|
||||
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
|
||||
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 h1:et7+NAX3lLIk5qUCTA9QelBjGE/NkhzYw/mhnr0s7nI=
|
||||
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
20
meson.build
20
meson.build
@@ -1,5 +1,23 @@
|
||||
project('pdf-simple-sign', 'cpp', default_options : ['cpp_std=c++11'])
|
||||
project('pdf-simple-sign', 'cpp', default_options : ['cpp_std=c++11'],
|
||||
version : '1.1')
|
||||
|
||||
conf = configuration_data()
|
||||
conf.set('PROJECT_NAME', '"' + meson.project_name() + '"')
|
||||
conf.set('PROJECT_VERSION', '"' + meson.project_version() + '"')
|
||||
configure_file(output : 'config.h', configuration : conf)
|
||||
|
||||
cryptodep = dependency('libcrypto')
|
||||
executable('pdf-simple-sign', 'pdf-simple-sign.cpp',
|
||||
install : true,
|
||||
dependencies : cryptodep)
|
||||
|
||||
asciidoctor = find_program('asciidoctor')
|
||||
foreach page : ['pdf-simple-sign']
|
||||
custom_target('manpage for ' + page,
|
||||
input: page + '.adoc', output: page + '.1',
|
||||
command: [asciidoctor, '-b', 'manpage',
|
||||
'-a', 'release-version=' + meson.project_version(),
|
||||
'@INPUT@', '-o', '@OUTPUT@'],
|
||||
install: true,
|
||||
install_dir: join_paths(get_option('mandir'), 'man1'))
|
||||
endforeach
|
||||
|
||||
79
pdf-simple-sign.adoc
Normal file
79
pdf-simple-sign.adoc
Normal file
@@ -0,0 +1,79 @@
|
||||
pdf-simple-sign(1)
|
||||
==================
|
||||
:doctype: manpage
|
||||
:manmanual: pdf-simple-sign Manual
|
||||
:mansource: pdf-simple-sign {release-version}
|
||||
|
||||
Name
|
||||
----
|
||||
pdf-simple-sign - a simple PDF signer
|
||||
|
||||
Synopsis
|
||||
--------
|
||||
*pdf-simple-sign* [_OPTION_]... _INPUT.pdf_ _OUTPUT.pdf_ _KEY-PAIR.p12_ _PASSWORD_
|
||||
|
||||
Description
|
||||
-----------
|
||||
'pdf-simple-sign' is a simple PDF signer intended for documents produced by
|
||||
the Cairo library, GNU troff, ImageMagick, or similar. As such, it currently
|
||||
comes with some restrictions:
|
||||
|
||||
* the document may not have any forms or signatures already, as they would be
|
||||
overwritten,
|
||||
* the document may not employ cross-reference streams, or must constitute
|
||||
a hybrid-reference file at least.
|
||||
|
||||
The key and certificate pair is accepted in the PKCS#12 format. The _PASSWORD_
|
||||
must be supplied on the command line, and may be empty if it is not needed.
|
||||
|
||||
The signature is attached to the first page and has no appearance.
|
||||
|
||||
If signature data don't fit within the default reservation of 4 kibibytes,
|
||||
you might need to adjust it using the *-r* option, or throw out any unnecessary
|
||||
intermediate certificates.
|
||||
|
||||
Options
|
||||
-------
|
||||
*-r* _RESERVATION_, *--reservation*=_RESERVATION_::
|
||||
Set aside _RESERVATION_ amount of bytes for the resulting signature.
|
||||
Feel free to try a few values in a loop. The program itself has no
|
||||
conceptions about the data, so it can't make accurate predictions.
|
||||
|
||||
*-h*, *--help*::
|
||||
Display a help message and exit.
|
||||
|
||||
*-V*, *--version*::
|
||||
Output version information and exit.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Create a self-signed certificate, make a document containing the current date,
|
||||
sign it and verify the attached signature:
|
||||
|
||||
$ openssl req -newkey rsa:2048 -subj "/CN=Test" -nodes
|
||||
-keyout key.pem -x509 -out cert.pem 2>/dev/null
|
||||
$ openssl pkcs12 -inkey key.pem -in cert.pem \
|
||||
-export -passout pass:test -out key-cert.p12
|
||||
$ date | groff -T pdf > test.pdf
|
||||
$ pdf-simple-sign test.pdf test.signed.pdf key-pair.p12 ""
|
||||
$ pdfsig test.signed.pdf
|
||||
Digital Signature Info of: test.signed.pdf
|
||||
Signature #1:
|
||||
- Signer Certificate Common Name: Test
|
||||
- Signer full Distinguished Name: CN=Test
|
||||
- Signing Time: Sep 05 2020 19:41:22
|
||||
- Signing Hash Algorithm: SHA-256
|
||||
- Signature Type: adbe.pkcs7.detached
|
||||
- Signed Ranges: [0 - 6522], [14716 - 15243]
|
||||
- Total document signed
|
||||
- Signature Validation: Signature is Valid.
|
||||
- Certificate Validation: Certificate issuer isn't Trusted.
|
||||
|
||||
Reporting bugs
|
||||
--------------
|
||||
Use https://git.janouch.name/p/pdf-simple-sign to report bugs, request features,
|
||||
or submit pull requests.
|
||||
|
||||
See also
|
||||
--------
|
||||
*openssl*(1), *pdfsig*(1)
|
||||
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// pdf-simple-sign: simple PDF signer
|
||||
//
|
||||
// Copyright (c) 2017, Přemysl Janouch <p@janouch.name>
|
||||
// Copyright (c) 2017 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted.
|
||||
@@ -37,9 +37,12 @@
|
||||
#include <openssl/x509v3.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
using uint = unsigned int;
|
||||
using ushort = unsigned short;
|
||||
|
||||
static std::string concatenate(const std::vector<std::string>& v, const std::string& delim) {
|
||||
std::string res;
|
||||
@@ -136,12 +139,11 @@ struct pdf_lexer {
|
||||
if (eat_newline(ch))
|
||||
continue;
|
||||
std::string octal;
|
||||
if (*p && strchr(oct_alphabet, *p)) octal += *p++;
|
||||
if (*p && strchr(oct_alphabet, *p)) octal += *p++;
|
||||
if (*p && strchr(oct_alphabet, *p)) octal += *p++;
|
||||
if (!octal.empty()) {
|
||||
value += char(std::stoi(octal, nullptr, 8));
|
||||
continue;
|
||||
if (ch && strchr(oct_alphabet, ch)) {
|
||||
octal += ch;
|
||||
if (*p && strchr(oct_alphabet, *p)) octal += *p++;
|
||||
if (*p && strchr(oct_alphabet, *p)) octal += *p++;
|
||||
ch = std::stoi(octal, nullptr, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,6 +164,7 @@ struct pdf_lexer {
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
p++;
|
||||
if (!buf.empty()) value += char(std::stoi(buf + '0', nullptr, 16));
|
||||
return {pdf_object::STRING, value};
|
||||
}
|
||||
@@ -257,7 +260,7 @@ struct pdf_lexer {
|
||||
static std::string pdf_serialize(const pdf_object& o) {
|
||||
switch (o.type) {
|
||||
case pdf_object::NL: return "\n";
|
||||
case pdf_object::NIL: return "nil";
|
||||
case pdf_object::NIL: return "null";
|
||||
case pdf_object::BOOL: return o.number ? "true" : "false";
|
||||
case pdf_object::NUMERIC:
|
||||
{
|
||||
@@ -301,6 +304,7 @@ static std::string pdf_serialize(const pdf_object& o) {
|
||||
{
|
||||
std::string s;
|
||||
for (const auto i : o.dict)
|
||||
// FIXME the key is also supposed to be escaped by pdf_serialize()
|
||||
s += " /" + i.first + " " + pdf_serialize(i.second);
|
||||
return "<<" + s + " >>";
|
||||
}
|
||||
@@ -341,6 +345,9 @@ public:
|
||||
|
||||
/// Build the cross-reference table and prepare a new trailer dictionary
|
||||
std::string initialize();
|
||||
/// Try to extract the claimed PDF version as a positive decimal number, e.g. 17 for PDF 1.7.
|
||||
/// Returns zero on failure.
|
||||
int version(const pdf_object& root) const;
|
||||
/// Retrieve an object by its number and generation -- may return NIL or END with an error
|
||||
pdf_object get(uint n, uint generation) const;
|
||||
/// Allocate a new object number
|
||||
@@ -353,6 +360,12 @@ public:
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
/// If the object is an error, forward its message, otherwise return err.
|
||||
static std::string pdf_error(const pdf_object& o, const char* err) {
|
||||
if (o.type != pdf_object::END || o.string.empty()) return err;
|
||||
return o.string;
|
||||
}
|
||||
|
||||
pdf_object pdf_updater::parse_obj(pdf_lexer& lex, std::vector<pdf_object>& stack) const {
|
||||
if (stack.size() < 2)
|
||||
return {pdf_object::END, "missing object ID pair"};
|
||||
@@ -370,7 +383,7 @@ pdf_object pdf_updater::parse_obj(pdf_lexer& lex, std::vector<pdf_object>& stack
|
||||
while (1) {
|
||||
auto object = parse(lex, obj.array);
|
||||
if (object.type == pdf_object::END)
|
||||
return {pdf_object::END, "object doesn't end"};
|
||||
return {pdf_object::END, pdf_error(object, "object doesn't end")};
|
||||
if (object.type == pdf_object::KEYWORD && object.string == "endobj")
|
||||
break;
|
||||
obj.array.push_back(std::move(object));
|
||||
@@ -408,7 +421,7 @@ pdf_object pdf_updater::parse(pdf_lexer& lex, std::vector<pdf_object>& stack) co
|
||||
while (1) {
|
||||
auto object = parse(lex, array);
|
||||
if (object.type == pdf_object::END)
|
||||
return {pdf_object::END, "array doesn't end"};
|
||||
return {pdf_object::END, pdf_error(object, "array doesn't end")};
|
||||
if (object.type == pdf_object::E_ARRAY)
|
||||
break;
|
||||
array.push_back(std::move(object));
|
||||
@@ -421,7 +434,7 @@ pdf_object pdf_updater::parse(pdf_lexer& lex, std::vector<pdf_object>& stack) co
|
||||
while (1) {
|
||||
auto object = parse(lex, array);
|
||||
if (object.type == pdf_object::END)
|
||||
return {pdf_object::END, "dictionary doesn't end"};
|
||||
return {pdf_object::END, pdf_error(object, "dictionary doesn't end")};
|
||||
if (object.type == pdf_object::E_DICT)
|
||||
break;
|
||||
array.push_back(std::move(object));
|
||||
@@ -459,7 +472,7 @@ std::string pdf_updater::load_xref(pdf_lexer& lex, std::set<uint>& loaded_entrie
|
||||
while (1) {
|
||||
auto object = parse(lex, throwaway_stack);
|
||||
if (object.type == pdf_object::END)
|
||||
return "unexpected EOF while looking for the trailer";
|
||||
return pdf_error(object, "unexpected EOF while looking for the trailer");
|
||||
if (object.type == pdf_object::KEYWORD && object.string == "trailer")
|
||||
break;
|
||||
|
||||
@@ -505,7 +518,7 @@ std::string pdf_updater::load_xref(pdf_lexer& lex, std::set<uint>& loaded_entrie
|
||||
|
||||
std::string pdf_updater::initialize() {
|
||||
// We only need to look for startxref roughly within the last kibibyte of the document
|
||||
static std::regex haystack_re("[\\s\\S]*\\sstartxref\\s+(\\d+)\\s+%%EOF");
|
||||
static std::regex haystack_re(R"([\s\S]*\sstartxref\s+(\d+)\s+%%EOF)");
|
||||
std::string haystack = document.substr(document.length() < 1024 ? 0 : document.length() - 1024);
|
||||
|
||||
std::smatch m;
|
||||
@@ -529,7 +542,7 @@ std::string pdf_updater::initialize() {
|
||||
|
||||
auto trailer = parse(lex, throwaway_stack);
|
||||
if (trailer.type != pdf_object::DICT)
|
||||
return "invalid trailer dictionary";
|
||||
return pdf_error(trailer, "invalid trailer dictionary");
|
||||
if (loaded_xrefs.empty())
|
||||
this->trailer = trailer.dict;
|
||||
loaded_xrefs.insert(xref_offset);
|
||||
@@ -537,6 +550,7 @@ std::string pdf_updater::initialize() {
|
||||
const auto prev_offset = trailer.dict.find("Prev");
|
||||
if (prev_offset == trailer.dict.end())
|
||||
break;
|
||||
// FIXME we don't check for size_t over or underflow
|
||||
if (!prev_offset->second.is_integer())
|
||||
return "invalid Prev offset";
|
||||
xref_offset = prev_offset->second.number;
|
||||
@@ -552,11 +566,30 @@ std::string pdf_updater::initialize() {
|
||||
return "";
|
||||
}
|
||||
|
||||
int pdf_updater::version(const pdf_object& root) const {
|
||||
auto version = root.dict.find("Version");
|
||||
if (version != root.dict.end() && version->second.type == pdf_object::NAME) {
|
||||
const auto& v = version->second.string;
|
||||
if (isdigit(v[0]) && v[1] == '.' && isdigit(v[2]) && !v[3])
|
||||
return (v[0] - '0') * 10 + (v[2] - '0');
|
||||
}
|
||||
|
||||
// We only need to look for the comment roughly within the first kibibyte of the document
|
||||
static std::regex version_re(R"((?:^|[\r\n])%(?:!PS-Adobe-\d\.\d )?PDF-(\d)\.(\d)[\r\n])");
|
||||
std::string haystack = document.substr(0, 1024);
|
||||
|
||||
std::smatch m;
|
||||
if (std::regex_search(haystack, m, version_re, std::regex_constants::match_default))
|
||||
return std::stoul(m.str(1)) * 10 + std::stoul(m.str(2));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pdf_object pdf_updater::get(uint n, uint generation) const {
|
||||
if (n >= xref_size)
|
||||
return {pdf_object::NIL};
|
||||
|
||||
auto& ref = xref[n];
|
||||
const auto& ref = xref[n];
|
||||
if (ref.free || ref.generation != generation || ref.offset >= document.length())
|
||||
return {pdf_object::NIL};
|
||||
|
||||
@@ -798,12 +831,10 @@ error:
|
||||
/// streams from PDF 1.5, or at least constitutes a hybrid-reference file. The results with
|
||||
/// PDF 2.0 (2017) are currently unknown as the standard costs money.
|
||||
///
|
||||
/// Carelessly assumes that the version of the original document is at most PDF 1.6.
|
||||
///
|
||||
/// https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/Acrobat_DigitalSignatures_in_PDF.pdf
|
||||
/// https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf
|
||||
/// https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PPKAppearances.pdf
|
||||
static std::string pdf_sign(std::string& document) {
|
||||
static std::string pdf_sign(std::string& document, ushort reservation) {
|
||||
pdf_updater pdf(document);
|
||||
auto err = pdf.initialize();
|
||||
if (!err.empty())
|
||||
@@ -827,7 +858,7 @@ static std::string pdf_sign(std::string& document) {
|
||||
pdf.document.append((byterange_len = 32 /* fine for a gigabyte */), ' ');
|
||||
pdf.document.append("\n /Contents <");
|
||||
sign_off = pdf.document.size();
|
||||
pdf.document.append((sign_len = 8192 /* certificate, digest, encrypted digest, ... */), '0');
|
||||
pdf.document.append((sign_len = reservation * 2), '0');
|
||||
pdf.document.append("> >>");
|
||||
|
||||
// We actually need to exclude the hexstring quotes from signing
|
||||
@@ -861,15 +892,21 @@ static std::string pdf_sign(std::string& document) {
|
||||
if (page.type != pdf_object::DICT)
|
||||
return "invalid or unsupported page tree";
|
||||
|
||||
// XXX assuming this won't be an indirectly referenced array
|
||||
auto& annots = page.dict["Annots"];
|
||||
if (annots.type != pdf_object::ARRAY)
|
||||
if (annots.type != pdf_object::ARRAY) {
|
||||
// TODO indirectly referenced arrays might not be that hard to support
|
||||
if (annots.type != pdf_object::END)
|
||||
return "unexpected Annots";
|
||||
|
||||
annots = {pdf_object::ARRAY};
|
||||
}
|
||||
annots.array.emplace_back(pdf_object::REFERENCE, sigfield_n, 0);
|
||||
pdf.update(page.n, [&]{ pdf.document += pdf_serialize(page); });
|
||||
|
||||
// 8.6.1 Interactive Form Dictionary
|
||||
// XXX assuming there are no forms already, overwriting everything
|
||||
if (root.dict.count("AcroForm"))
|
||||
return "the document already contains forms, they would be overwritten";
|
||||
|
||||
root.dict["AcroForm"] = {std::map<std::string, pdf_object>{
|
||||
{"Fields", {std::vector<pdf_object>{
|
||||
{pdf_object::REFERENCE, sigfield_n, 0}
|
||||
@@ -878,9 +915,9 @@ static std::string pdf_sign(std::string& document) {
|
||||
}};
|
||||
|
||||
// Upgrade the document version for SHA-256 etc.
|
||||
// XXX assuming that it's not newer than 1.6 already -- while Cairo can't currently use a newer
|
||||
// version that 1.5, it's not a bad idea to use cairo_pdf_surface_restrict_to_version()
|
||||
root.dict["Version"] = {pdf_object::NAME, "1.6"};
|
||||
if (pdf.version(root) < 16)
|
||||
root.dict["Version"] = {pdf_object::NAME, "1.6"};
|
||||
|
||||
pdf.update(root_ref->second.n, [&]{ pdf.document += pdf_serialize(root); });
|
||||
pdf.flush_updates();
|
||||
|
||||
@@ -911,25 +948,39 @@ static void die(int status, const char* format, ...) {
|
||||
int main(int argc, char* argv[]) {
|
||||
auto invocation_name = argv[0];
|
||||
auto usage = [=]{
|
||||
die(1, "Usage: %s [-h] INPUT-FILENAME OUTPUT-FILENAME PKCS12-PATH PKCS12-PASS",
|
||||
die(1, "Usage: %s [-h] [-r RESERVATION] INPUT-FILENAME OUTPUT-FILENAME PKCS12-PATH PKCS12-PASS",
|
||||
invocation_name);
|
||||
};
|
||||
|
||||
static struct option opts[] = {
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{"version", no_argument, 0, 'V'},
|
||||
{"reservation", required_argument, 0, 'r'},
|
||||
{nullptr, 0, 0, 0},
|
||||
};
|
||||
|
||||
// Reserved space in bytes for the certificate, digest, encrypted digest, ...
|
||||
long reservation = 4096;
|
||||
while (1) {
|
||||
int option_index = 0;
|
||||
auto c = getopt_long(argc, const_cast<char* const*>(argv),
|
||||
"h", opts, &option_index);
|
||||
"hVr:", opts, &option_index);
|
||||
if (c == -1)
|
||||
break;
|
||||
|
||||
char* end = nullptr;
|
||||
switch (c) {
|
||||
case 'h': usage(); break;
|
||||
default: usage();
|
||||
case 'r':
|
||||
errno = 0, reservation = strtol(optarg, &end, 10);
|
||||
if (errno || *end || reservation <= 0 || reservation > USHRT_MAX)
|
||||
die(1, "%s: must be a positive number", optarg);
|
||||
break;
|
||||
case 'V':
|
||||
die(0, "%s", PROJECT_NAME " " PROJECT_VERSION);
|
||||
break;
|
||||
case 'h':
|
||||
default:
|
||||
usage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,7 +1007,7 @@ int main(int argc, char* argv[]) {
|
||||
die(1, "%s: %s", input_path, strerror(errno));
|
||||
}
|
||||
|
||||
auto err = pdf_sign(pdf_document);
|
||||
auto err = pdf_sign(pdf_document, ushort(reservation));
|
||||
if (!err.empty()) {
|
||||
die(2, "Error: %s", err.c_str());
|
||||
}
|
||||
|
||||
1247
pdf/pdf.go
Normal file
1247
pdf/pdf.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user