Compare commits

...

54 Commits
v1.0 ... master

Author SHA1 Message Date
f4b08fb951
lpg: improve Picture sizing, clean up
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
Restraining a Picture in one dimension with a Frame should make it
report the right overall dimensions (keeping its aspect ratio).

Also applying the F.9 C++ Core Guideline.
2025-01-12 10:11:33 +01:00
e8752e53ac
Add a Lua PDF generator
All checks were successful
Alpine 3.20 Success
Arch Linux AUR Success
Publishing my old invoice layouter in a reusable scripting-based form,
rather than an annoyingly fixed binary.

Because Lua compiled for C++ might be hard to find, we provide a wrap.
Curiously, only GitHub releases seem to contain onelua.c,
which is a very handy file.

We could have also subprojected libqr, which is in the public domain,
however the other main dependencies are also LGPL like libqrencode is.
And it is likely to be installed.

The user manual also serves as a test.
2025-01-11 15:25:13 +01:00
147b880524
Fix test.sh for Alpine's current lowriter
All checks were successful
Alpine 3.19 Success
Arch Linux AUR Success
Alpine 3.20 Success
2024-04-09 22:50:39 +02:00
a02966d1d1
README.adoc: actually make the extfs name match
All checks were successful
Arch Linux AUR Success
2024-02-04 06:35:37 +01:00
ba5fdf20df
README.adoc: fix and improve Go instructions 2024-02-04 06:04:16 +01:00
a8dc72349b
extfs-pdf: add a file extension for FlateDecode
It is recognised by shared-mime-info.
2024-02-04 06:03:58 +01:00
32e9acfa77
Go: enable multiple updates in a sequence
This is not something anyone should do, but let's do things correctly.
2024-02-04 05:17:26 +01:00
ff7de4b141
Go: cleanup 2024-02-04 05:16:28 +01:00
0b837b3a0e
Go: add PDF 1.5 cross-reference stream support
This is not particularly complete, but it works (again) for Cairo.
2024-02-04 04:27:10 +01:00
55a17a69b7
README.adoc: update package information 2023-07-01 22:03:18 +02:00
3781aa8e85
Don't fail tests when gropdf isn't installed 2023-06-28 23:27:30 +02:00
69b939c707
Fix tests, document new limitation 2023-06-28 23:12:42 +02:00
87681d15ba
Go: bump modules 2023-06-28 22:35:49 +02:00
f01d25596e
Fix the man page
> Any reference to the subject of the current manual page
> should be written with the name in bold.
2022-09-25 18:28:19 +02:00
67596a8153
extfs-pdf: improve the listing format 2021-12-09 20:33:40 +01:00
8a00d7064b
Update documentation 2021-12-09 15:28:01 +01:00
b358467791
Add an external VFS for Midnight Commander 2021-12-09 15:24:25 +01:00
d0f80aa6ae
Go: enable listing all indirect objects 2021-12-09 14:07:15 +01:00
97ffe3d46e
Go: implement stream parsing/serialization 2021-12-09 14:07:14 +01:00
1a3c7a8282
Go: add Updater.Dereference() 2021-12-08 21:33:26 +01:00
d8171b9ac4
Go: improve error handling 2021-12-08 20:49:06 +01:00
bcb24af926
Minor revision 2021-12-08 20:39:02 +01:00
c0927c05dd
Add .gitignore 2021-11-06 12:28:25 +01:00
5e87223b5d
Add clang-format configuration, clean up 2021-11-06 12:27:39 +01:00
58a4ba1d05
meson.build: use set_quoted() 2021-11-06 11:42:57 +01:00
350cf89e51
Bump Go modules to 1.17 2021-08-19 05:36:46 +02:00
d4ff9a6e89
README.adoc: add a PkgGoDev badge 2020-09-11 00:15:58 +02:00
a5176b5bbb
Bump version, update NEWS 2020-09-06 05:16:40 +02:00
af6a937033
Go: avoid non-deterministic output
The code has even turned out simpler.
2020-09-06 05:16:40 +02:00
8913f8ba9c
Add a test script to verify basic function 2020-09-06 05:16:39 +02:00
524eea9b2f
Manual: fix the example
Things managed to work once but for rather arbitrary reasons.
2020-09-05 21:32:05 +02:00
3ce08d33f6
Bump version, update NEWS 2020-09-05 20:10:48 +02:00
a75f990565
Add an instructive man page 2020-09-05 20:10:47 +02:00
46fa50749f
Add a --version option
And fix that --reservation was missing from the optstring.
2020-09-05 20:08:41 +02:00
796a9640d3
Make it possible to change the signature reservation 2020-09-04 18:33:12 +02:00
2d08100b58
Avoid downgrading the document's PDF version 2020-09-04 18:30:09 +02:00
1224d9be47
Return errors rather than mangle documents 2020-09-04 16:05:14 +02:00
486cafa6b4
Go: update dependencies 2020-08-12 06:15:41 +02:00
a0696cdb88
Name change 2020-08-12 06:14:03 +02:00
be8480f8af
Consistency 2018-12-14 02:52:05 +01:00
f9f3171c02
Use Go modules 2018-12-01 22:43:11 +01:00
0ea296de67
Go: less API stupidity coming from the C++ heritage 2018-10-04 14:46:12 +02:00
9d2412398a
Go: additional small fixes 2018-10-04 14:14:04 +02:00
62206ed344
Go: documentation cleanup 2018-10-04 13:18:37 +02:00
9ac8360979
Go: use multiple return values
The compiler has made it more obvious where we eat error messages.
2018-10-04 13:09:29 +02:00
50578fe99f
Go: add Object constructors 2018-10-04 12:51:23 +02:00
eedd9a550c
Go: cleanups 2018-10-04 12:11:43 +02:00
43ca0e5035
Add a Go port
It should be roughly at feature parity.
2018-10-04 01:03:45 +02:00
ad239714b0
Add comments about some potential issues
- lack of number range verification
 - lack of sanitization when serializing dicts
2018-10-03 22:47:47 +02:00
daa9cc1ed4
Mark a variable const 2018-10-03 22:47:47 +02:00
4c7853c951
Try to return the innermost error message
Improving debugging experience.
2018-10-03 22:47:46 +02:00
c77a9c052a
Fix parsing of hex strings 2018-10-03 22:17:05 +02:00
54d86cf25b
Fix serialization of null values 2018-10-02 23:17:46 +02:00
160f09ecc3
Fix octal escapes 2018-10-02 23:17:15 +02:00
23 changed files with 3820 additions and 101 deletions

8
.clang-format Normal file
View File

@ -0,0 +1,8 @@
BasedOnStyle: Chromium
ColumnLimit: 100
IndentCaseLabels: false
AccessModifierOffset: -2
ContinuationIndentWidth: 2
SpaceAfterTemplateKeyword: false
SpaceAfterCStyleCast: true
SpacesBeforeTrailingComments: 2

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/builddir
/pdf-simple-sign.cflags
/pdf-simple-sign.config
/pdf-simple-sign.creator
/pdf-simple-sign.creator.user
/pdf-simple-sign.cxxflags
/pdf-simple-sign.files
/pdf-simple-sign.includes

View File

@ -1,4 +1,4 @@
Copyright (c) 2017, Přemysl Janouch <p@janouch.name>
Copyright (c) 2017 - 2025, 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.

25
NEWS
View File

@ -1,3 +1,28 @@
1.1.1 (2020-09-06)
* Fix a dysfunctional example in the manual
* Go: write the xref table in a deterministic order
* Add a trivial test suite, based on pdfsig from poppler-utils
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

View File

@ -1,27 +1,30 @@
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 (≤ 1.17.4 or using PDF 1.4), 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.
Packages
--------
Regular releases are sporadic. git master should be stable enough.
You can get a package with the latest development version using Arch Linux's
https://aur.archlinux.org/packages/pdf-simple-sign-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Documentation
-------------
See the link:pdf-simple-sign.adoc[man page] for information about usage.
The rest of this README will concern itself with externalities.
image:https://pkg.go.dev/badge/janouch.name/pdf-simple-sign@master/pdf["PkgGoDev", link="https://pkg.go.dev/janouch.name/pdf-simple-sign@master/pdf"]
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 +33,39 @@ Runtime dependencies: libcrypto (OpenSSL 1.1 API)
$ cd builddir
$ ninja
Usage
-----
Go
~~
In addition to the C++ version, also included is a native Go port,
which has enhanced PDF 1.5 support:
$ ./pdf-simple-sign document.pdf document.signed.pdf KeyAndCerts.p12 password
----
$ go install janouch.name/pdf-simple-sign/cmd/pdf-simple-sign@master
----
and a crude external VFS for Midnight Commander, that may be used to extract
all streams from a given PDF file:
----
$ GOBIN=$HOME/.local/share/mc/extfs.d \
go install janouch.name/pdf-simple-sign/cmd/extfs-pdf@master
----
To enable the VFS, edit your _~/.config/mc/mc.ext.ini_ to contain:
----
[pdf]
Type=^PDF
Open=%cd %p/extfs-pdf://
----
Lua PDF generator
~~~~~~~~~~~~~~~~~
Build dependencies: Meson, a C++17 compiler, pkg-config +
Runtime dependencies: C++ Lua >= 5.3 (custom Meson wrap fallback),
cairo >= 1.15.4, pangocairo, libqrencode
This is a parasitic subproject located in the _lpg_ subdirectory.
It will generate its own documentation.
Contributing and Support
------------------------

141
cmd/extfs-pdf/main.go Normal file
View File

@ -0,0 +1,141 @@
//
// Copyright (c) 2021 - 2024, 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.
//
// extfs-pdf is an external VFS plugin for Midnight Commander.
// More serious image extractors should rewrite this to use pdfimages(1).
package main
import (
"flag"
"fmt"
"os"
"time"
"janouch.name/pdf-simple-sign/pdf"
)
func die(status int, format string, args ...interface{}) {
os.Stderr.WriteString(fmt.Sprintf(format+"\n", args...))
os.Exit(status)
}
func usage() {
die(1, "Usage: %s [-h] COMMAND DOCUMENT [ARG...]", os.Args[0])
}
func streamSuffix(o *pdf.Object) string {
if filter, _ := o.Dict["Filter"]; filter.Kind == pdf.Name {
switch filter.String {
case "JBIG2Decode":
// This is the file extension used by pdfimages(1).
// This is not a complete JBIG2 standalone file.
return "jb2e"
case "JPXDecode":
return "jp2"
case "DCTDecode":
return "jpg"
case "FlateDecode":
return "zz"
default:
return filter.String
}
}
return "stream"
}
func list(mtime time.Time, updater *pdf.Updater) {
stamp := mtime.Local().Format("01-02-2006 15:04:05")
for _, o := range updater.ListIndirect() {
object, err := updater.Get(o.N, o.Generation)
size := 0
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
} else {
// Accidental transformation, retrieving original data is more work.
size = len(object.Serialize())
}
fmt.Printf("-r--r--r-- 1 0 0 %d %s n%dg%d\n",
size, stamp, o.N, o.Generation)
if object.Kind == pdf.Stream {
fmt.Printf("-r--r--r-- 1 0 0 %d %s n%dg%d.%s\n", len(object.Stream),
stamp, o.N, o.Generation, streamSuffix(&object))
}
}
}
func copyout(updater *pdf.Updater, storedFilename, extractTo string) {
var (
n, generation uint
suffix string
)
m, err := fmt.Sscanf(storedFilename, "n%dg%d%s", &n, &generation, &suffix)
if m < 2 {
die(3, "%s: %s", storedFilename, err)
}
object, err := updater.Get(n, generation)
if err != nil {
die(3, "%s: %s", storedFilename, err)
}
content := []byte(object.Serialize())
if suffix != "" {
content = object.Stream
}
if err = os.WriteFile(extractTo, content, 0666); err != nil {
die(3, "%s", err)
}
}
func main() {
flag.Usage = usage
flag.Parse()
if flag.NArg() < 2 {
usage()
}
command, documentPath := flag.Arg(0), flag.Arg(1)
doc, err := os.ReadFile(documentPath)
if err != nil {
die(1, "%s", err)
}
mtime := time.UnixMilli(0)
if info, err := os.Stat(documentPath); err == nil {
mtime = info.ModTime()
}
updater, err := pdf.NewUpdater(doc)
if err != nil {
die(2, "%s", err)
}
switch command {
default:
die(1, "unsupported command: %s", command)
case "list":
if flag.NArg() != 2 {
usage()
} else {
list(mtime, updater)
}
case "copyout":
if flag.NArg() != 4 {
usage()
} else {
copyout(updater, flag.Arg(2), flag.Arg(3))
}
}
}

View 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
View File

@ -0,0 +1,8 @@
module janouch.name/pdf-simple-sign
go 1.17
require (
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
golang.org/x/crypto v0.10.0
)

13
go.sum Normal file
View File

@ -0,0 +1,13 @@
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=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
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/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
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=

9
lpg/.clang-format Normal file
View File

@ -0,0 +1,9 @@
BasedOnStyle: LLVM
ColumnLimit: 80
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation
SpaceAfterCStyleCast: true
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
SpacesBeforeTrailingComments: 2

1160
lpg/lpg.cpp Normal file

File diff suppressed because it is too large Load Diff

240
lpg/lpg.lua Normal file
View File

@ -0,0 +1,240 @@
#!/usr/bin/env lpg
local project_url = "https://git.janouch.name/p/pdf-simple-sign"
function h1 (title)
return lpg.VBox {fontsize=18., fontweight=600,
title, lpg.HLine {2}, lpg.Filler {-1, 6}}
end
function h2 (title)
return lpg.VBox {fontsize=16., fontweight=600,
lpg.Filler {-1, 8}, title, lpg.HLine {1}, lpg.Filler {-1, 6}}
end
function h3 (title)
return lpg.VBox {fontsize=14., fontweight=600,
lpg.Filler {-1, 8}, title, lpg.HLine {.25}, lpg.Filler {-1, 6}}
end
function p (...)
return lpg.VBox {..., lpg.Filler {-1, 6}}
end
function code (...)
return lpg.VBox {
lpg.Filler {-1, 4},
lpg.HBox {
lpg.Filler {12},
lpg.VBox {"<tt>" .. table.concat {...} .. "</tt>"},
lpg.Filler {},
},
lpg.Filler {-1, 6},
}
end
function define (name, ...)
return lpg.VBox {
lpg.Filler {-1, 2},
lpg.Text {fontweight=600, name}, lpg.Filler {-1, 2},
lpg.HBox {lpg.Filler {12}, lpg.VBox {...}, lpg.Filler {}},
lpg.Filler {-1, 2},
}
end
function pad (widget)
return lpg.VBox {
lpg.Filler {-1, 2},
lpg.HBox {lpg.Filler {4}, widget, lpg.Filler {}, lpg.Filler {4}},
lpg.Filler {-1, 2},
}
end
local page1 = lpg.VBox {fontfamily="sans serif", fontsize=12.,
h1("lpg User Manual"),
p("<b>lpg</b> is a Lua-based PDF document generator, exposing a trivial " ..
"layouting engine on top of the Cairo graphics library, " ..
"with manual paging."),
p("The author has primarily been using this system to typeset invoices."),
h2("Synopsis"),
p("<b>lpg</b> <i>program.lua</i> [<i>args...</i>]"),
h2("API"),
p("The Lua program receives <b>lpg</b>'s and its own path joined " ..
"as <tt>arg[0]</tt>. Any remaining sequential elements " ..
"of this table represent the passed <i>args</i>."),
h3("Utilities"),
define("lpg.cm (centimeters)",
p("Returns how many document points are needed " ..
"for the given physical length.")),
define("lpg.ntoa {number [, precision=…]\n" ..
"\t[, thousands_sep=…] [, decimal_point=…] [, grouping=…]}",
p("Formats a number using the C++ localization " ..
"and I/O libraries. " ..
"For example, the following call results in “3 141,59”:"),
code("ntoa {3141.592, precision=2,\n" ..
" thousands_sep=\" \", decimal_point=\",\", " ..
"grouping=\"\\003\"}")),
define("lpg.escape (values...)",
p("Interprets all values as strings, " ..
"and escapes them to be used as literal text—" ..
"all text within <b>lpg</b> is parsed as Pango markup, " ..
"which is a subset of XML.")),
h3("PDF documents"),
define("lpg.Document (filename, width, height [, margin])",
p("Returns a new <i>Document</i> object, whose pages are all " ..
"the same size in 72 DPI points, as specified by <b>width</b> " ..
"and <b>height</b>. The <b>margin</b> is used by <b>show</b> " ..
"on all sides of pages."),
p("The file is finalized when the object is garbage collected.")),
define("<i>Document</i>.title, author, subject, keywords, " ..
"creator, create_date, mod_date",
p("Write-only PDF <i>Info</i> dictionary metadata strings.")),
define("<i>Document</i>:show ([widget...])",
p("Starts a new document page, and renders <i>Widget</i> trees over " ..
"the whole print area.")),
lpg.Filler {},
}
local page2 = lpg.VBox {fontfamily="sans serif", fontsize=12.,
h3("Widgets"),
p("The layouting system makes heavy use of composition, " ..
"and thus stays simple."),
p("For convenience, anywhere a <i>Widget</i> is expected but another " ..
"kind of value is received, <b>lpg.Text</b> widget will be invoked " ..
"on that value."),
p("Once a <i>Widget</i> is included in another <i>Widget</i>, " ..
"the original Lua object can no longer be used, " ..
"as its reference has been consumed."),
p("<i>Widgets</i> can be indexed by strings to get or set " ..
"their <i>attributes</i>. All <i>Widget</i> constructor tables " ..
"also accept attributes, for convenience. Attributes can be " ..
"either strings or numbers, mostly only act " ..
"on specific <i>Widget</i> kinds, and are hereditary. " ..
"Prefix their names with an underscore to set them privately."),
p("<i>Widget</i> sizes can be set negative, which signals to their " ..
"container that they should take any remaining space, " ..
"after all their siblings requests have been satisfied. " ..
"When multiple widgets make this request, that space is distributed " ..
"in proportion to these negative values."),
define("lpg.Filler {[width] [, height]}",
p("Returns a new blank widget with the given dimensions, " ..
"which default to -1, -1.")),
define("lpg.HLine {[thickness]}",
p("Returns a new widget that draws a simple horizontal line " ..
"of the given <b>thickness</b>.")),
define("lpg.VLine {[thickness]}",
p("Returns a new widget that draws a simple vertical line " ..
"of the given <b>thickness</b>.")),
define("lpg.Text {[value...]}",
p("Returns a new text widget that renders the concatenation of all " ..
"passed values filtered through Luas <b>tostring</b> " ..
"function. Non-strings will additionally be escaped."),
define("<i>Text</i>.fontfamily, fontsize, fontweight, lineheight",
p("Various font properties, similar to their CSS counterparts."))),
define("lpg.Frame {widget}",
p("Returns a special container widget that can override " ..
"a few interesting properties."),
define("<i>Frame</i>.color",
p("Text and line colour, for example <tt>0xff0000</tt> for red.")),
define("<i>Frame</i>.w_override",
p("Forcefully changes the child <i>Widget</i>s " ..
"requested width, such as to negative values.")),
define("<i>Frame</i>.h_override",
p("Forcefully changes the child <i>Widget</i>s " ..
"requested height, such as to negative values."))),
lpg.Filler {},
}
local page3 = lpg.VBox {fontfamily="sans serif", fontsize=12.,
define("lpg.Link {target, widget}",
p("Returns a new hyperlink widget pointing to the <b>target</b>, " ..
"which is a URL. The hyperlink applies " ..
"to the entire area of the child widget. " ..
"It has no special appearance.")),
define("lpg.HBox {[widget...]}",
p("Returns a new container widget that places children " ..
"horizontally, from left to right."),
p("If any space remains after satisfying the children widgets " ..
"requisitions, it is distributed equally amongst all of them. " ..
"Also see the note about negative sizes.")),
define("lpg.VBox {[widget...]}",
p("Returns a new container widget that places children " ..
"vertically, from top to bottom.")),
define("lpg.Picture {filename}",
p("Returns a new picture widget, showing the given <b>filename</b>, " ..
"which currently must be in the PNG format. " ..
"Pictures are rescaled to fit, but keep their aspect ratio.")),
define("lpg.QR {contents, module}",
p("Returns a new QR code widget, encoding the <b>contents</b> " ..
"string using the given <b>module</b> size. " ..
"The QR code version is chosen automatically.")),
h2("Examples"),
p("See the source code of this user manual " ..
"for the general structure of scripts."),
h3("Size distribution and composition"),
lpg.VBox {
lpg.HLine {},
lpg.HBox {
lpg.VLine {}, lpg.Frame {_w_override=lpg.cm(3), pad "3cm"},
lpg.VLine {}, lpg.Frame {pad "Measured"},
lpg.VLine {}, lpg.Frame {_w_override=-1, pad "-1"},
lpg.VLine {}, lpg.Frame {_w_override=-2, pad "-2"},
lpg.VLine {},
},
lpg.HLine {},
},
lpg.Filler {-1, 6},
code([[
<small><b>function</b> pad (widget)
<b>local function</b> f (...) <b>return</b> lpg.Filler {...} <b>end</b>
<b>return</b> lpg.VBox {f(-1, 2), lpg.HBox {f(4), w, f(), f(4)}, f(-1, 2)}
<b>end</b>
lpg.VBox {lpg.HLine {}, lpg.HBox {
lpg.VLine {}, lpg.Frame {_w_override=lpg.cm(3), pad "3cm"},
lpg.VLine {}, lpg.Frame {pad "Measured"},
lpg.VLine {}, lpg.Frame {_w_override=-1, pad "-1"},
lpg.VLine {}, lpg.Frame {_w_override=-2, pad "-2"},
lpg.VLine {},
}, lpg.HLine {}}</small>]]),
h3("Clickable QR code link"),
lpg.HBox {
lpg.VBox {
p("Go here to report bugs, request features, " ..
"or submit pull requests:"),
code(([[
url = "%s"
lpg.Link {url, lpg.QR {url, 2.5}}]]):format(project_url)),
},
lpg.Filler {},
lpg.Link {project_url, lpg.QR {project_url, 2.5}},
},
lpg.Filler {},
}
if #arg < 1 then
io.stderr:write("Usage: " .. arg[0] .. " OUTPUT-PDF..." .. "\n")
os.exit(false)
end
local width, height, margin = lpg.cm(21), lpg.cm(29.7), lpg.cm(2.0)
for i = 1, #arg do
local pdf = lpg.Document(arg[i], width, height, margin)
pdf.title = "lpg User Manual"
pdf.subject = "lpg User Manual"
pdf.author = "Přemysl Eric Janouch"
pdf.creator = ("lpg (%s)"):format(project_url)
pdf:show(page1)
pdf:show(page2)
pdf:show(page3)
end

24
lpg/meson.build Normal file
View File

@ -0,0 +1,24 @@
project('lpg', 'cpp', default_options : ['cpp_std=c++17'],
version : '1.1.1')
conf = configuration_data()
conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('PROJECT_VERSION', meson.project_version())
configure_file(output : 'config.h', configuration : conf)
luapp = dependency('lua++', allow_fallback : true)
cairo = dependency('cairo')
pangocairo = dependency('pangocairo')
libqrencode = dependency('libqrencode')
lpg_exe = executable('lpg', 'lpg.cpp',
install : true,
dependencies : [luapp, cairo, pangocairo, libqrencode])
# XXX: https://github.com/mesonbuild/meson/issues/825
docdir = get_option('datadir') / 'doc' / meson.project_name()
lpg_pdf = custom_target('lpg.pdf',
output : 'lpg.pdf',
input : 'lpg.lua',
command : [lpg_exe, '@INPUT@', '@OUTPUT@'],
install_dir : docdir,
build_by_default : true)

View File

@ -0,0 +1,10 @@
[wrap-file]
directory = lua-5.4.7
source_url = https://github.com/lua/lua/archive/refs/tags/v5.4.7.tar.gz
source_filename = lua-5.4.7.tar.gz
source_hash = 5c39111b3fc4c1c9e56671008955a1730f54a15b95e1f1bd0752b868b929d8e3
patch_directory = lua-5.4.7
[provide]
lua++-5.4 = lua_dep
lua++ = lua_dep

View File

@ -0,0 +1,20 @@
Copyright (c) 2025 Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2021 The Meson development team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,50 @@
project(
'lua-5.4',
'cpp',
license : 'MIT',
meson_version : '>=0.49.2',
version : '5.4.7',
default_options : ['c_std=c99', 'warning_level=2'],
)
cxx = meson.get_compiler('cpp')
# Skip bogus warning.
add_project_arguments(cxx.get_supported_arguments(
'-Wno-string-plus-int', '-Wno-stringop-overflow'), language : 'cpp')
# Platform-specific defines.
is_posix = host_machine.system() in ['cygwin', 'darwin', 'dragonfly', 'freebsd',
'gnu', 'haiku', 'linux', 'netbsd', 'openbsd', 'sunos']
if is_posix
add_project_arguments('-DLUA_USE_POSIX', language : 'cpp')
endif
# Library dependencies.
lua_lib_deps = [cxx.find_library('m', required : false)]
if meson.version().version_compare('>= 0.62')
dl_dep = dependency('dl', required : get_option('loadlib'))
else
dl_dep = cxx.find_library('dl', required : get_option('loadlib'))
endif
if dl_dep.found()
lua_lib_deps += dl_dep
add_project_arguments('-DLUA_USE_DLOPEN', language : 'cpp')
endif
# Targets.
add_project_arguments('-DMAKE_LIB', language : 'cpp')
lua_lib = static_library(
'lua',
'onelua.cpp',
dependencies : lua_lib_deps,
implicit_include_directories : false,
)
inc = include_directories('.')
lua_dep = declare_dependency(
link_with : lua_lib,
include_directories : inc,
)

View File

@ -0,0 +1,4 @@
option(
'loadlib', type : 'feature',
description : 'Allow Lua to "require" C extension modules'
)

View File

@ -0,0 +1 @@
#include "onelua.c"

View File

@ -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.1')
conf = configuration_data()
conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('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

80
pdf-simple-sign.adoc Normal file
View File

@ -0,0 +1,80 @@
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 -addext keyUsage=digitalSignature \
-out cert.pem 2>/dev/null
$ openssl pkcs12 -inkey key.pem -in cert.pem \
-export -passout pass: -out key-pair.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)

View File

@ -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.
@ -16,30 +16,33 @@
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include <cstdio>
#include <cmath>
#include <cstdio>
#undef NDEBUG
#include <cassert>
#include <vector>
#include <map>
#include <regex>
#include <memory>
#include <regex>
#include <set>
#include <vector>
#if defined __GLIBCXX__ && __GLIBCXX__ < 20140422
#error Need libstdc++ >= 4.9 for <regex>
#endif
#include <unistd.h>
#include <getopt.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
#include <openssl/pkcs12.h>
#include <openssl/x509v3.h>
#include <unistd.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;
@ -52,7 +55,7 @@ static std::string concatenate(const std::vector<std::string>& v, const std::str
template<typename... Args>
std::string ssprintf(const std::string& format, Args... args) {
size_t size = std::snprintf(nullptr, 0, format.c_str(), args... ) + 1;
size_t size = std::snprintf(nullptr, 0, format.c_str(), args...) + 1;
std::unique_ptr<char[]> buf(new char[size]);
std::snprintf(buf.get(), size, format.c_str(), args...);
return std::string(buf.get(), buf.get() + size - 1);
@ -61,7 +64,7 @@ std::string ssprintf(const std::string& format, Args... args) {
// -------------------------------------------------------------------------------------------------
/// PDF token/object thingy. Objects may be composed either from one or a sequence of tokens.
/// The PDF Reference doesn't actually speak of tokens.
/// The PDF Reference doesn't actually speak of tokens, though ISO 32000-1:2008 does.
struct pdf_object {
enum type {
END, NL, COMMENT, NIL, BOOL, NUMERIC, KEYWORD, NAME, STRING,
@ -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,16 +260,14 @@ 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:
{
case pdf_object::NUMERIC: {
if (o.is_integer()) return std::to_string((long long) o.number);
return std::to_string(o.number);
}
case pdf_object::KEYWORD: return o.string;
case pdf_object::NAME:
{
case pdf_object::NAME: {
std::string escaped = "/";
for (char c : o.string) {
if (c == '#' || strchr(pdf_lexer::delimiters, c) || strchr(pdf_lexer::whitespace, c))
@ -276,8 +277,7 @@ static std::string pdf_serialize(const pdf_object& o) {
}
return escaped;
}
case pdf_object::STRING:
{
case pdf_object::STRING: {
std::string escaped;
for (char c : o.string) {
if (c == '\\' || c == '(' || c == ')')
@ -290,17 +290,16 @@ static std::string pdf_serialize(const pdf_object& o) {
case pdf_object::E_ARRAY: return "]";
case pdf_object::B_DICT: return "<<";
case pdf_object::E_DICT: return ">>";
case pdf_object::ARRAY:
{
case pdf_object::ARRAY: {
std::vector<std::string> v;
for (const auto& i : o.array)
v.push_back(pdf_serialize(i));
return "[ " + concatenate(v, " ") + " ]";
}
case pdf_object::DICT:
{
case pdf_object::DICT: {
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 +340,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,14 +355,20 @@ 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"};
auto g = stack.back(); stack.pop_back();
auto n = stack.back(); stack.pop_back();
if (!g.is_integer() || g.number < 0 || g.number > UINT_MAX
|| !n.is_integer() || n.number < 0 || n.number > UINT_MAX)
if (!g.is_integer() || g.number < 0 || g.number > UINT_MAX ||
!n.is_integer() || n.number < 0 || n.number > UINT_MAX)
return {pdf_object::END, "invalid object ID pair"};
pdf_object obj{pdf_object::OBJECT};
@ -370,7 +378,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));
@ -384,8 +392,8 @@ pdf_object pdf_updater::parse_R(std::vector<pdf_object>& stack) const {
auto g = stack.back(); stack.pop_back();
auto n = stack.back(); stack.pop_back();
if (!g.is_integer() || g.number < 0 || g.number > UINT_MAX
|| !n.is_integer() || n.number < 0 || n.number > UINT_MAX)
if (!g.is_integer() || g.number < 0 || g.number > UINT_MAX ||
!n.is_integer() || n.number < 0 || n.number > UINT_MAX)
return {pdf_object::END, "invalid reference ID pair"};
pdf_object ref{pdf_object::REFERENCE};
@ -402,26 +410,24 @@ pdf_object pdf_updater::parse(pdf_lexer& lex, std::vector<pdf_object>& stack) co
case pdf_object::COMMENT:
// These are not important to parsing, not even for this procedure's needs
return parse(lex, stack);
case pdf_object::B_ARRAY:
{
case pdf_object::B_ARRAY: {
std::vector<pdf_object> array;
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));
}
return array;
}
case pdf_object::B_DICT:
{
case pdf_object::B_DICT: {
std::vector<pdf_object> array;
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,13 +465,13 @@ 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;
auto second = parse(lex, throwaway_stack);
if (!object.is_integer() || object.number < 0 || object.number > UINT_MAX
|| !second.is_integer() || second.number < 0 || second.number > UINT_MAX)
if (!object.is_integer() || object.number < 0 || object.number > UINT_MAX ||
!second.is_integer() || second.number < 0 || second.number > UINT_MAX)
return "invalid xref section header";
const size_t start = object.number;
@ -474,9 +480,9 @@ std::string pdf_updater::load_xref(pdf_lexer& lex, std::set<uint>& loaded_entrie
auto off = parse(lex, throwaway_stack);
auto gen = parse(lex, throwaway_stack);
auto key = parse(lex, throwaway_stack);
if (!off.is_integer() || off.number < 0 || off.number > document.length()
|| !gen.is_integer() || gen.number < 0 || gen.number > 65535
|| key.type != pdf_object::KEYWORD)
if (!off.is_integer() || off.number < 0 || off.number > document.length() ||
!gen.is_integer() || gen.number < 0 || gen.number > 65535 ||
key.type != pdf_object::KEYWORD)
return "invalid xref entry";
bool free = true;
@ -505,7 +511,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 +535,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,7 +543,8 @@ std::string pdf_updater::initialize() {
const auto prev_offset = trailer.dict.find("Prev");
if (prev_offset == trailer.dict.end())
break;
if (!prev_offset->second.is_integer())
// FIXME do not read offsets and sizes as floating point numbers
if (!prev_offset->second.is_integer() || prev_offset->second.number < 0)
return "invalid Prev offset";
xref_offset = prev_offset->second.number;
}
@ -552,11 +559,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};
@ -624,8 +650,8 @@ void pdf_updater::flush_updates() {
}
trailer["Size"] = {pdf_object::NUMERIC, double(xref_size)};
document += "trailer\n" + pdf_serialize(trailer)
+ ssprintf("\nstartxref\n%zu\n%%%%EOF\n", startxref);
document +=
"trailer\n" + pdf_serialize(trailer) + ssprintf("\nstartxref\n%zu\n%%%%EOF\n", startxref);
}
// -------------------------------------------------------------------------------------------------
@ -667,9 +693,9 @@ static pdf_object pdf_get_first_page(pdf_updater& pdf, uint node_n, uint node_ge
// XXX technically speaking, this may be an indirect reference. The correct way to solve this
// seems to be having "pdf_updater" include a wrapper around "obj.dict.find"
auto kids = obj.dict.find("Kids");
if (kids == obj.dict.end() || kids->second.type != pdf_object::ARRAY
|| kids->second.array.empty()
|| kids->second.array.at(0).type != pdf_object::REFERENCE)
if (kids == obj.dict.end() || kids->second.type != pdf_object::ARRAY ||
kids->second.array.empty() ||
kids->second.array.at(0).type != pdf_object::REFERENCE)
return {pdf_object::NIL};
// XXX nothing prevents us from recursing in an evil circular graph
@ -707,8 +733,8 @@ static std::string pdf_fill_in_signature(std::string& document, size_t sign_off,
// OpenSSL error reasons will usually be of more value than any distinction I can come up with
std::string err = "OpenSSL failure";
if (!(p12 = d2i_PKCS12_fp(pkcs12_fp, nullptr))
|| !PKCS12_parse(p12, pkcs12_pass.c_str(), &private_key, &certificate, &chain)) {
if (!(p12 = d2i_PKCS12_fp(pkcs12_fp, nullptr)) ||
!PKCS12_parse(p12, pkcs12_pass.c_str(), &private_key, &certificate, &chain)) {
err = pkcs12_path + ": parse failure";
goto error;
}
@ -733,8 +759,8 @@ static std::string pdf_fill_in_signature(std::string& document, size_t sign_off,
#endif
// The default digest is SHA1, which is mildly insecure now -- hence using PKCS7_sign_add_signer
if (!(p7 = PKCS7_sign(nullptr, nullptr, nullptr, nullptr, sign_flags))
|| !PKCS7_sign_add_signer(p7, certificate, private_key, EVP_sha256(), sign_flags))
if (!(p7 = PKCS7_sign(nullptr, nullptr, nullptr, nullptr, sign_flags)) ||
!PKCS7_sign_add_signer(p7, certificate, private_key, EVP_sha256(), sign_flags))
goto error;
// For RFC 3161, this is roughly how a timestamp token would be attached (see Appendix A):
// PKCS7_add_attribute(signer_info, NID_id_smime_aa_timeStampToken, V_ASN1_SEQUENCE, value)
@ -744,10 +770,10 @@ static std::string pdf_fill_in_signature(std::string& document, size_t sign_off,
// Adaptation of the innards of the undocumented PKCS7_final() -- I didn't feel like making
// a copy of the whole document. Hopefully this writes directly into a digest BIO.
if (!(p7bio = PKCS7_dataInit(p7, nullptr))
|| (ssize_t) sign_off != BIO_write(p7bio, document.data(), sign_off)
|| (ssize_t) tail_len != BIO_write(p7bio, document.data() + tail_off, tail_len)
|| BIO_flush(p7bio) != 1 || !PKCS7_dataFinal(p7, p7bio))
if (!(p7bio = PKCS7_dataInit(p7, nullptr)) ||
(ssize_t) sign_off != BIO_write(p7bio, document.data(), sign_off) ||
(ssize_t) tail_len != BIO_write(p7bio, document.data() + tail_off, tail_len) ||
BIO_flush(p7bio) != 1 || !PKCS7_dataFinal(p7, p7bio))
goto error;
#if 0
@ -798,12 +824,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())
@ -819,7 +843,7 @@ static std::string pdf_sign(std::string& document) {
// 8.7 Digital Signatures - /signature dictionary/
auto sigdict_n = pdf.allocate();
size_t byterange_off = 0, byterange_len = 0, sign_off = 0, sign_len = 0;
pdf.update(sigdict_n, [&]{
pdf.update(sigdict_n, [&] {
// The timestamp is important for Adobe Acrobat Reader DC. The ideal would be to use RFC 3161.
pdf.document.append("<< /Type/Sig /Filter/Adobe.PPKLite /SubFilter/adbe.pkcs7.detached\n"
" /M" + pdf_serialize(pdf_date(time(nullptr))) + " /ByteRange ");
@ -827,7 +851,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
@ -852,7 +876,7 @@ static std::string pdf_sign(std::string& document) {
}}});
auto sigfield_n = pdf.allocate();
pdf.update(sigfield_n, [&]{ pdf.document += pdf_serialize(sigfield); });
pdf.update(sigfield_n, [&] { pdf.document += pdf_serialize(sigfield); });
auto pages_ref = root.dict.find("Pages");
if (pages_ref == root.dict.end() || pages_ref->second.type != pdf_object::REFERENCE)
@ -861,15 +885,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); });
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,10 +908,10 @@ 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"};
pdf.update(root_ref->second.n, [&]{ pdf.document += pdf_serialize(root); });
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();
// Now that we know the length of everything, store byte ranges of what we're about to sign,
@ -910,26 +940,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",
invocation_name);
auto usage = [=] {
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);
auto c = getopt_long(argc, const_cast<char* const*>(argv), "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 +999,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());
}

1663
pdf/pdf.go Normal file

File diff suppressed because it is too large Load Diff

86
test.sh Executable file
View File

@ -0,0 +1,86 @@
#!/bin/sh -e
# Test basic functionality of both versions
# Usage: ./test.sh builddir/pdf-simple-sign cmd/pdf-simple-sign/pdf-simple-sign
log() { echo "`tput sitm`-- $1`tput sgr0`"; }
die() { echo "`tput bold`-- $1`tput sgr0`"; exit 1; }
# Get rid of old test files
rm -rf tmp
mkdir tmp
# Create documents in various tools
log "Creating source documents"
inkscape --pipe --export-filename=tmp/cairo.pdf --export-pdf-version=1.4 \
<<'EOF' 2>/dev/null || :
<svg xmlns="http://www.w3.org/2000/svg"><text x="5" y="10">Hello</text></svg>
EOF
date > tmp/lowriter.txt
if command -v gropdf >/dev/null
then groff -T pdf < tmp/lowriter.txt > tmp/groff.pdf
fi
lowriter --convert-to pdf tmp/lowriter.txt --outdir tmp >/dev/null || :
convert rose: tmp/imagemagick.pdf || :
# Create a root CA certificate pair
log "Creating certificates"
openssl req -newkey rsa:2048 -subj "/CN=Test CA" -nodes \
-keyout tmp/ca.key.pem -x509 -out tmp/ca.cert.pem 2>/dev/null
# Create a private NSS database and insert our test CA there
rm -rf tmp/nssdir
mkdir tmp/nssdir
certutil -N --empty-password -d sql:tmp/nssdir
certutil -d sql:tmp/nssdir -A -n root -t ,C, -a -i tmp/ca.cert.pem
# Create a leaf certificate pair
cat > tmp/cert.cfg <<'EOF'
[smime]
basicConstraints = CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = emailProtection
nsCertType = email
EOF
openssl req -newkey rsa:2048 -subj "/CN=Test Leaf" -nodes \
-keyout tmp/key.pem -out tmp/cert.csr 2>/dev/null
openssl x509 -req -in tmp/cert.csr -out tmp/cert.pem \
-CA tmp/ca.cert.pem -CAkey tmp/ca.key.pem -set_serial 1 \
-extensions smime -extfile tmp/cert.cfg 2>/dev/null
openssl verify -CAfile tmp/ca.cert.pem tmp/cert.pem >/dev/null
# The second line accomodates the Go signer,
# which doesn't support SHA-256 within pkcs12 handling
openssl pkcs12 -inkey tmp/key.pem -in tmp/cert.pem \
-certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES -macalg sha1 \
-export -passout pass: -out tmp/key-pair.p12
for tool in "$@"; do
rm -f tmp/*.signed.pdf
for source in tmp/*.pdf; do
log "Testing $tool with $source"
result=${source%.pdf}.signed.pdf
$tool "$source" "$result" tmp/key-pair.p12 ""
pdfsig -nssdir sql:tmp/nssdir "$result" | grep Validation
# Only some of our generators use PDF versions higher than 1.5
log "Testing $tool for version detection"
grep -q "/Version /1[.]6" "$result" \
|| grep -q "^%PDF-1[.][67]" "$result" \
|| die "Version detection seems to misbehave (no upgrade)"
done
log "Testing $tool for expected failures"
$tool "$result" "$source.fail.pdf" tmp/key-pair.p12 "" \
&& die "Double signing shouldn't succeed"
$tool -r 1 "$source" "$source.fail.pdf" tmp/key-pair.p12 "" \
&& die "Too low reservations shouldn't succeed"
sed '1s/%PDF-1../%PDF-1.7/' "$source" > "$source.alt"
$tool "$source.alt" "$result.alt" tmp/key-pair.p12 ""
grep -q "/Version /1.6" "$result.alt" \
&& die "Version detection seems to misbehave (downgraded)"
done
log "OK"