Compare commits
No commits in common. "3ce08d33f6bad9931b32082f598b0e98599dd120" and "486cafa6b447d7af296411f7c50d4c078f3eac34" have entirely different histories.
3ce08d33f6
...
486cafa6b4
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2017 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
Copyright (c) 2017, Přemysl Eric Janouch <p@janouch.name>
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted.
|
purpose with or without fee is hereby granted.
|
||||||
|
16
NEWS
16
NEWS
@ -1,19 +1,3 @@
|
|||||||
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)
|
1.0 (2018-08-03)
|
||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
26
README.adoc
26
README.adoc
@ -1,20 +1,27 @@
|
|||||||
pdf-simple-sign
|
pdf-simple-sign
|
||||||
===============
|
===============
|
||||||
|
:compact-option:
|
||||||
|
|
||||||
'pdf-simple-sign' is a simple PDF signer intended for documents produced by
|
'pdf-simple-sign' is a simple PDF signer intended for documents produced by
|
||||||
the Cairo library, GNU troff, ImageMagick, or similar.
|
the Cairo library. 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.
|
||||||
|
|
||||||
I don't aim to extend the functionality any further. The project is fairly
|
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.
|
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
|
Building
|
||||||
--------
|
--------
|
||||||
Build dependencies: Meson, Asciidoctor, a C++11 compiler, pkg-config +
|
Build dependencies: Meson, a C++11 compiler, pkg-config +
|
||||||
Runtime dependencies: libcrypto (OpenSSL 1.1 API)
|
Runtime dependencies: libcrypto (OpenSSL 1.1 API)
|
||||||
|
|
||||||
$ git clone https://git.janouch.name/p/pdf-simple-sign.git
|
$ git clone https://git.janouch.name/p/pdf-simple-sign.git
|
||||||
@ -27,6 +34,11 @@ In addition to the C++ version, also included is a native Go port:
|
|||||||
|
|
||||||
$ go get janouch.name/pdf-simple-sign/cmd/pdf-simple-sign
|
$ go get janouch.name/pdf-simple-sign/cmd/pdf-simple-sign
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
$ ./pdf-simple-sign document.pdf document.signed.pdf KeyAndCerts.p12 password
|
||||||
|
|
||||||
Contributing and Support
|
Contributing and Support
|
||||||
------------------------
|
------------------------
|
||||||
Use https://git.janouch.name/p/pdf-simple-sign to report bugs, request features,
|
Use https://git.janouch.name/p/pdf-simple-sign to report bugs, request features,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) 2018 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2018, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@ -20,9 +20,8 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
|
||||||
|
|
||||||
"janouch.name/pdf-simple-sign/pdf"
|
"janouch.name/pdf-simple-sign/pdf"
|
||||||
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// #include <unistd.h>
|
// #include <unistd.h>
|
||||||
@ -40,13 +39,10 @@ func die(status int, format string, args ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
die(1, "Usage: %s [-h] [-r RESERVATION] INPUT-FILENAME OUTPUT-FILENAME "+
|
die(1, "Usage: %s [-h] INPUT-FILENAME OUTPUT-FILENAME "+
|
||||||
"PKCS12-PATH PKCS12-PASS", os.Args[0])
|
"PKCS12-PATH PKCS12-PASS", os.Args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
var reservation = flag.Int(
|
|
||||||
"r", 4096, "signature reservation as a number of bytes")
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Usage = usage
|
flag.Usage = usage
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@ -55,7 +51,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inputPath, outputPath := flag.Arg(0), flag.Arg(1)
|
inputPath, outputPath := flag.Arg(0), flag.Arg(1)
|
||||||
doc, err := ioutil.ReadFile(inputPath)
|
pdfDocument, err := ioutil.ReadFile(inputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
die(1, "%s", err)
|
die(1, "%s", err)
|
||||||
}
|
}
|
||||||
@ -67,10 +63,10 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
die(3, "%s", err)
|
die(3, "%s", err)
|
||||||
}
|
}
|
||||||
if doc, err = pdf.Sign(doc, key, certs, *reservation); err != nil {
|
if pdfDocument, err = pdf.Sign(pdfDocument, key, certs); err != nil {
|
||||||
die(4, "error: %s", err)
|
die(4, "error: %s", err)
|
||||||
}
|
}
|
||||||
if err = ioutil.WriteFile(outputPath, doc, 0666); err != nil {
|
if err = ioutil.WriteFile(outputPath, pdfDocument, 0666); err != nil {
|
||||||
die(5, "%s", err)
|
die(5, "%s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
meson.build
20
meson.build
@ -1,23 +1,5 @@
|
|||||||
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')
|
cryptodep = dependency('libcrypto')
|
||||||
executable('pdf-simple-sign', 'pdf-simple-sign.cpp',
|
executable('pdf-simple-sign', 'pdf-simple-sign.cpp',
|
||||||
install : true,
|
install : true,
|
||||||
dependencies : cryptodep)
|
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
|
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
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
|
// pdf-simple-sign: simple PDF signer
|
||||||
//
|
//
|
||||||
// Copyright (c) 2017 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2017, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@ -37,12 +37,9 @@
|
|||||||
#include <openssl/x509v3.h>
|
#include <openssl/x509v3.h>
|
||||||
#include <openssl/pkcs12.h>
|
#include <openssl/pkcs12.h>
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
using uint = unsigned int;
|
using uint = unsigned int;
|
||||||
using ushort = unsigned short;
|
|
||||||
|
|
||||||
static std::string concatenate(const std::vector<std::string>& v, const std::string& delim) {
|
static std::string concatenate(const std::vector<std::string>& v, const std::string& delim) {
|
||||||
std::string res;
|
std::string res;
|
||||||
@ -345,9 +342,6 @@ public:
|
|||||||
|
|
||||||
/// Build the cross-reference table and prepare a new trailer dictionary
|
/// Build the cross-reference table and prepare a new trailer dictionary
|
||||||
std::string initialize();
|
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
|
/// Retrieve an object by its number and generation -- may return NIL or END with an error
|
||||||
pdf_object get(uint n, uint generation) const;
|
pdf_object get(uint n, uint generation) const;
|
||||||
/// Allocate a new object number
|
/// Allocate a new object number
|
||||||
@ -518,7 +512,7 @@ std::string pdf_updater::load_xref(pdf_lexer& lex, std::set<uint>& loaded_entrie
|
|||||||
|
|
||||||
std::string pdf_updater::initialize() {
|
std::string pdf_updater::initialize() {
|
||||||
// We only need to look for startxref roughly within the last kibibyte of the document
|
// We only need to look for startxref roughly within the last kibibyte of the document
|
||||||
static std::regex haystack_re(R"([\s\S]*\sstartxref\s+(\d+)\s+%%EOF)");
|
static std::regex haystack_re("[\\s\\S]*\\sstartxref\\s+(\\d+)\\s+%%EOF");
|
||||||
std::string haystack = document.substr(document.length() < 1024 ? 0 : document.length() - 1024);
|
std::string haystack = document.substr(document.length() < 1024 ? 0 : document.length() - 1024);
|
||||||
|
|
||||||
std::smatch m;
|
std::smatch m;
|
||||||
@ -566,25 +560,6 @@ std::string pdf_updater::initialize() {
|
|||||||
return "";
|
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 {
|
pdf_object pdf_updater::get(uint n, uint generation) const {
|
||||||
if (n >= xref_size)
|
if (n >= xref_size)
|
||||||
return {pdf_object::NIL};
|
return {pdf_object::NIL};
|
||||||
@ -831,10 +806,12 @@ error:
|
|||||||
/// streams from PDF 1.5, or at least constitutes a hybrid-reference file. The results with
|
/// 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.
|
/// 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/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/pdf_reference_1-7.pdf
|
||||||
/// https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PPKAppearances.pdf
|
/// https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PPKAppearances.pdf
|
||||||
static std::string pdf_sign(std::string& document, ushort reservation) {
|
static std::string pdf_sign(std::string& document) {
|
||||||
pdf_updater pdf(document);
|
pdf_updater pdf(document);
|
||||||
auto err = pdf.initialize();
|
auto err = pdf.initialize();
|
||||||
if (!err.empty())
|
if (!err.empty())
|
||||||
@ -858,7 +835,7 @@ static std::string pdf_sign(std::string& document, ushort reservation) {
|
|||||||
pdf.document.append((byterange_len = 32 /* fine for a gigabyte */), ' ');
|
pdf.document.append((byterange_len = 32 /* fine for a gigabyte */), ' ');
|
||||||
pdf.document.append("\n /Contents <");
|
pdf.document.append("\n /Contents <");
|
||||||
sign_off = pdf.document.size();
|
sign_off = pdf.document.size();
|
||||||
pdf.document.append((sign_len = reservation * 2), '0');
|
pdf.document.append((sign_len = 8192 /* certificate, digest, encrypted digest, ... */), '0');
|
||||||
pdf.document.append("> >>");
|
pdf.document.append("> >>");
|
||||||
|
|
||||||
// We actually need to exclude the hexstring quotes from signing
|
// We actually need to exclude the hexstring quotes from signing
|
||||||
@ -892,21 +869,15 @@ static std::string pdf_sign(std::string& document, ushort reservation) {
|
|||||||
if (page.type != pdf_object::DICT)
|
if (page.type != pdf_object::DICT)
|
||||||
return "invalid or unsupported page tree";
|
return "invalid or unsupported page tree";
|
||||||
|
|
||||||
|
// XXX assuming this won't be an indirectly referenced array
|
||||||
auto& annots = page.dict["Annots"];
|
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 = {pdf_object::ARRAY};
|
||||||
}
|
|
||||||
annots.array.emplace_back(pdf_object::REFERENCE, sigfield_n, 0);
|
annots.array.emplace_back(pdf_object::REFERENCE, sigfield_n, 0);
|
||||||
pdf.update(page.n, [&]{ pdf.document += pdf_serialize(page); });
|
pdf.update(page.n, [&]{ pdf.document += pdf_serialize(page); });
|
||||||
|
|
||||||
// 8.6.1 Interactive Form Dictionary
|
// 8.6.1 Interactive Form Dictionary
|
||||||
if (root.dict.count("AcroForm"))
|
// XXX assuming there are no forms already, overwriting everything
|
||||||
return "the document already contains forms, they would be overwritten";
|
|
||||||
|
|
||||||
root.dict["AcroForm"] = {std::map<std::string, pdf_object>{
|
root.dict["AcroForm"] = {std::map<std::string, pdf_object>{
|
||||||
{"Fields", {std::vector<pdf_object>{
|
{"Fields", {std::vector<pdf_object>{
|
||||||
{pdf_object::REFERENCE, sigfield_n, 0}
|
{pdf_object::REFERENCE, sigfield_n, 0}
|
||||||
@ -915,9 +886,9 @@ static std::string pdf_sign(std::string& document, ushort reservation) {
|
|||||||
}};
|
}};
|
||||||
|
|
||||||
// Upgrade the document version for SHA-256 etc.
|
// Upgrade the document version for SHA-256 etc.
|
||||||
if (pdf.version(root) < 16)
|
// XXX assuming that it's not newer than 1.6 already -- while Cairo can't currently use a newer
|
||||||
root.dict["Version"] = {pdf_object::NAME, "1.6"};
|
// 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"};
|
||||||
pdf.update(root_ref->second.n, [&]{ pdf.document += pdf_serialize(root); });
|
pdf.update(root_ref->second.n, [&]{ pdf.document += pdf_serialize(root); });
|
||||||
pdf.flush_updates();
|
pdf.flush_updates();
|
||||||
|
|
||||||
@ -948,39 +919,25 @@ static void die(int status, const char* format, ...) {
|
|||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
auto invocation_name = argv[0];
|
auto invocation_name = argv[0];
|
||||||
auto usage = [=]{
|
auto usage = [=]{
|
||||||
die(1, "Usage: %s [-h] [-r RESERVATION] INPUT-FILENAME OUTPUT-FILENAME PKCS12-PATH PKCS12-PASS",
|
die(1, "Usage: %s [-h] INPUT-FILENAME OUTPUT-FILENAME PKCS12-PATH PKCS12-PASS",
|
||||||
invocation_name);
|
invocation_name);
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct option opts[] = {
|
static struct option opts[] = {
|
||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{"version", no_argument, 0, 'V'},
|
|
||||||
{"reservation", required_argument, 0, 'r'},
|
|
||||||
{nullptr, 0, 0, 0},
|
{nullptr, 0, 0, 0},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reserved space in bytes for the certificate, digest, encrypted digest, ...
|
|
||||||
long reservation = 4096;
|
|
||||||
while (1) {
|
while (1) {
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
auto c = getopt_long(argc, const_cast<char* const*>(argv),
|
auto c = getopt_long(argc, const_cast<char* const*>(argv),
|
||||||
"hVr:", opts, &option_index);
|
"h", opts, &option_index);
|
||||||
if (c == -1)
|
if (c == -1)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
char* end = nullptr;
|
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'r':
|
case 'h': usage(); break;
|
||||||
errno = 0, reservation = strtol(optarg, &end, 10);
|
default: usage();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1007,7 +964,7 @@ int main(int argc, char* argv[]) {
|
|||||||
die(1, "%s: %s", input_path, strerror(errno));
|
die(1, "%s: %s", input_path, strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto err = pdf_sign(pdf_document, ushort(reservation));
|
auto err = pdf_sign(pdf_document);
|
||||||
if (!err.empty()) {
|
if (!err.empty()) {
|
||||||
die(2, "Error: %s", err.c_str());
|
die(2, "Error: %s", err.c_str());
|
||||||
}
|
}
|
||||||
|
63
pdf/pdf.go
63
pdf/pdf.go
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) 2018 - 2020, Přemysl Eric Janouch <p@janouch.name>
|
// Copyright (c) 2018, Přemysl Eric Janouch <p@janouch.name>
|
||||||
//
|
//
|
||||||
// Permission to use, copy, modify, and/or distribute this software for any
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
// purpose with or without fee is hereby granted.
|
// purpose with or without fee is hereby granted.
|
||||||
@ -32,7 +32,6 @@ import (
|
|||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
|
||||||
"go.mozilla.org/pkcs7"
|
"go.mozilla.org/pkcs7"
|
||||||
"golang.org/x/crypto/pkcs12"
|
"golang.org/x/crypto/pkcs12"
|
||||||
)
|
)
|
||||||
@ -670,7 +669,7 @@ func (u *Updater) loadXref(lex *Lexer, loadedEntries map[uint]struct{}) error {
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
var trailerRE = regexp.MustCompile(`(?s:.*)\sstartxref\s+(\d+)\s+%%EOF`)
|
var haystackRE = regexp.MustCompile(`(?s:.*)\sstartxref\s+(\d+)\s+%%EOF`)
|
||||||
|
|
||||||
// NewUpdater initializes an Updater, building the cross-reference table and
|
// NewUpdater initializes an Updater, building the cross-reference table and
|
||||||
// preparing a new trailer dictionary.
|
// preparing a new trailer dictionary.
|
||||||
@ -685,7 +684,7 @@ func NewUpdater(document []byte) (*Updater, error) {
|
|||||||
haystack = haystack[len(haystack)-1024:]
|
haystack = haystack[len(haystack)-1024:]
|
||||||
}
|
}
|
||||||
|
|
||||||
m := trailerRE.FindSubmatch(haystack)
|
m := haystackRE.FindSubmatch(haystack)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return nil, errors.New("cannot find startxref")
|
return nil, errors.New("cannot find startxref")
|
||||||
}
|
}
|
||||||
@ -739,31 +738,6 @@ func NewUpdater(document []byte) (*Updater, error) {
|
|||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var versionRE = regexp.MustCompile(
|
|
||||||
`(?:^|[\r\n])%(?:!PS-Adobe-\d\.\d )?PDF-(\d)\.(\d)[\r\n]`)
|
|
||||||
|
|
||||||
// Version extracts the claimed PDF version as a positive decimal number,
|
|
||||||
// e.g. 17 for PDF 1.7. Returns zero on failure.
|
|
||||||
func (u *Updater) Version(root *Object) int {
|
|
||||||
if version, ok := root.Dict["Version"]; ok && version.Kind == Name {
|
|
||||||
if v := version.String; len(v) == 3 && v[1] == '.' &&
|
|
||||||
v[0] >= '0' && v[0] <= '9' && v[2] >= '0' && v[2] <= '9' {
|
|
||||||
return int(v[0]-'0')*10 + int(v[2]-'0')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only need to look for the comment roughly within
|
|
||||||
// the first kibibyte of the document.
|
|
||||||
haystack := u.Document
|
|
||||||
if len(haystack) > 1024 {
|
|
||||||
haystack = haystack[:1024]
|
|
||||||
}
|
|
||||||
if m := versionRE.FindSubmatch(haystack); m != nil {
|
|
||||||
return int(m[1][0]-'0')*10 + int(m[2][0]-'0')
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves an object by its number and generation--may return
|
// Get retrieves an object by its number and generation--may return
|
||||||
// Nil or End with an error.
|
// Nil or End with an error.
|
||||||
//
|
//
|
||||||
@ -1115,14 +1089,15 @@ func FillInSignature(document []byte, signOff, signLen int,
|
|||||||
// There must be at least one certificate, matching the private key.
|
// There must be at least one certificate, matching the private key.
|
||||||
// The certificates must form a chain.
|
// The certificates must form a chain.
|
||||||
//
|
//
|
||||||
// A good default for the reservation is around 4096 (the value is in bytes).
|
|
||||||
//
|
|
||||||
// The presumption here is that the document is valid and that it doesn't
|
// The presumption here is that the document is valid and that it doesn't
|
||||||
// employ cross-reference streams from PDF 1.5, or at least constitutes
|
// employ cross-reference streams from PDF 1.5, or at least constitutes
|
||||||
// a hybrid-reference file. The results with PDF 2.0 (2017) are currently
|
// a hybrid-reference file. The results with PDF 2.0 (2017) are currently
|
||||||
// unknown as the standard costs money.
|
// unknown as the standard costs money.
|
||||||
func Sign(document []byte, key crypto.PrivateKey, certs []*x509.Certificate,
|
//
|
||||||
reservation int) ([]byte, error) {
|
// Carelessly assumes that the version of the original document is at most
|
||||||
|
// PDF 1.6.
|
||||||
|
func Sign(document []byte,
|
||||||
|
key crypto.PrivateKey, certs []*x509.Certificate) ([]byte, error) {
|
||||||
pdf, err := NewUpdater(document)
|
pdf, err := NewUpdater(document)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -1154,7 +1129,7 @@ func Sign(document []byte, key crypto.PrivateKey, certs []*x509.Certificate,
|
|||||||
buf.WriteString("\n /Contents <")
|
buf.WriteString("\n /Contents <")
|
||||||
|
|
||||||
signOff = buf.Len()
|
signOff = buf.Len()
|
||||||
signLen = reservation * 2 // cert, digest, encrypted digest, ...
|
signLen = 8192 // cert, digest, encrypted digest, ...
|
||||||
buf.Write(bytes.Repeat([]byte{'0'}, signLen))
|
buf.Write(bytes.Repeat([]byte{'0'}, signLen))
|
||||||
buf.WriteString("> >>")
|
buf.WriteString("> >>")
|
||||||
|
|
||||||
@ -1191,13 +1166,9 @@ func Sign(document []byte, key crypto.PrivateKey, certs []*x509.Certificate,
|
|||||||
return nil, errors.New("invalid or unsupported page tree")
|
return nil, errors.New("invalid or unsupported page tree")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX: Assuming this won't be an indirectly referenced array.
|
||||||
annots := page.Dict["Annots"]
|
annots := page.Dict["Annots"]
|
||||||
if annots.Kind != Array {
|
if annots.Kind != Array {
|
||||||
// TODO(p): Indirectly referenced arrays might not be
|
|
||||||
// that hard to support.
|
|
||||||
if annots.Kind != End {
|
|
||||||
return nil, errors.New("unexpected Annots")
|
|
||||||
}
|
|
||||||
annots = NewArray(nil)
|
annots = NewArray(nil)
|
||||||
}
|
}
|
||||||
annots.Array = append(annots.Array, NewReference(sigfieldN, 0))
|
annots.Array = append(annots.Array, NewReference(sigfieldN, 0))
|
||||||
@ -1208,21 +1179,17 @@ func Sign(document []byte, key crypto.PrivateKey, certs []*x509.Certificate,
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 8.6.1 Interactive Form Dictionary
|
// 8.6.1 Interactive Form Dictionary
|
||||||
if _, ok := root.Dict["AcroForm"]; ok {
|
// XXX: Assuming there are no forms already, overwriting everything.
|
||||||
return nil, errors.New("the document already contains forms, " +
|
|
||||||
"they would be overwritten")
|
|
||||||
}
|
|
||||||
|
|
||||||
root.Dict["AcroForm"] = NewDict(map[string]Object{
|
root.Dict["AcroForm"] = NewDict(map[string]Object{
|
||||||
"Fields": NewArray([]Object{NewReference(sigfieldN, 0)}),
|
"Fields": NewArray([]Object{NewReference(sigfieldN, 0)}),
|
||||||
"SigFlags": NewNumeric(3 /* SignaturesExist | AppendOnly */),
|
"SigFlags": NewNumeric(3 /* SignaturesExist | AppendOnly */),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Upgrade the document version for SHA-256 etc.
|
// Upgrade the document version for SHA-256 etc.
|
||||||
if pdf.Version(&root) < 16 {
|
// XXX: Assuming that it's not newer than 1.6 already--while Cairo can't
|
||||||
root.Dict["Version"] = NewName("1.6")
|
// 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"] = NewName("1.6")
|
||||||
pdf.Update(rootRef.N, func(buf BytesWriter) {
|
pdf.Update(rootRef.N, func(buf BytesWriter) {
|
||||||
buf.WriteString(root.Serialize())
|
buf.WriteString(root.Serialize())
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user