Compare commits
	
		
			6 Commits
		
	
	
		
			486cafa6b4
			...
			3ce08d33f6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3ce08d33f6 | |||
| a75f990565 | |||
| 46fa50749f | |||
| 796a9640d3 | |||
| 2d08100b58 | |||
| 1224d9be47 | 
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | |||||||
| Copyright (c) 2017, Přemysl Eric 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 | 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,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) | 1.0 (2018-08-03) | ||||||
| 
 | 
 | ||||||
|  * Initial release |  * Initial release | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.adoc
									
									
									
									
									
								
							| @ -1,27 +1,20 @@ | |||||||
| 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.  As such, it currently comes with some restrictions: | the Cairo library, GNU troff, ImageMagick, or similar. | ||||||
| 
 |  | ||||||
|  * 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, a C++11 compiler, pkg-config + | Build dependencies: Meson, Asciidoctor, 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 | ||||||
| @ -34,11 +27,6 @@ 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, Přemysl Eric Janouch <p@janouch.name> | // Copyright (c) 2018 - 2020, 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,8 +20,9 @@ import ( | |||||||
| 	"flag" | 	"flag" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"janouch.name/pdf-simple-sign/pdf" |  | ||||||
| 	"os" | 	"os" | ||||||
|  | 
 | ||||||
|  | 	"janouch.name/pdf-simple-sign/pdf" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // #include <unistd.h> | // #include <unistd.h> | ||||||
| @ -39,10 +40,13 @@ func die(status int, format string, args ...interface{}) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func usage() { | func usage() { | ||||||
| 	die(1, "Usage: %s [-h] INPUT-FILENAME OUTPUT-FILENAME "+ | 	die(1, "Usage: %s [-h] [-r RESERVATION] 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() | ||||||
| @ -51,7 +55,7 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	inputPath, outputPath := flag.Arg(0), flag.Arg(1) | 	inputPath, outputPath := flag.Arg(0), flag.Arg(1) | ||||||
| 	pdfDocument, err := ioutil.ReadFile(inputPath) | 	doc, err := ioutil.ReadFile(inputPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		die(1, "%s", err) | 		die(1, "%s", err) | ||||||
| 	} | 	} | ||||||
| @ -63,10 +67,10 @@ func main() { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		die(3, "%s", err) | 		die(3, "%s", err) | ||||||
| 	} | 	} | ||||||
| 	if pdfDocument, err = pdf.Sign(pdfDocument, key, certs); err != nil { | 	if doc, err = pdf.Sign(doc, key, certs, *reservation); err != nil { | ||||||
| 		die(4, "error: %s", err) | 		die(4, "error: %s", err) | ||||||
| 	} | 	} | ||||||
| 	if err = ioutil.WriteFile(outputPath, pdfDocument, 0666); err != nil { | 	if err = ioutil.WriteFile(outputPath, doc, 0666); err != nil { | ||||||
| 		die(5, "%s", err) | 		die(5, "%s", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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') | 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 | ||||||
|  | |||||||
							
								
								
									
										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
 | // pdf-simple-sign: simple PDF signer
 | ||||||
| //
 | //
 | ||||||
| // Copyright (c) 2017, Přemysl Eric 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
 | // 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,9 +37,12 @@ | |||||||
| #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; | ||||||
| @ -342,6 +345,9 @@ 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
 | ||||||
| @ -512,7 +518,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("[\\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::string haystack = document.substr(document.length() < 1024 ? 0 : document.length() - 1024); | ||||||
| 
 | 
 | ||||||
|   std::smatch m; |   std::smatch m; | ||||||
| @ -560,6 +566,25 @@ 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}; | ||||||
| @ -806,12 +831,10 @@ 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) { | static std::string pdf_sign(std::string& document, ushort reservation) { | ||||||
|   pdf_updater pdf(document); |   pdf_updater pdf(document); | ||||||
|   auto err = pdf.initialize(); |   auto err = pdf.initialize(); | ||||||
|   if (!err.empty()) |   if (!err.empty()) | ||||||
| @ -835,7 +858,7 @@ static std::string pdf_sign(std::string& document) { | |||||||
|     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 = 8192 /* certificate, digest, encrypted digest, ... */), '0'); |     pdf.document.append((sign_len = reservation * 2), '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
 | ||||||
| @ -869,15 +892,21 @@ static std::string pdf_sign(std::string& document) { | |||||||
|   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
 | ||||||
|   // 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>{ |   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} | ||||||
| @ -886,9 +915,9 @@ static std::string pdf_sign(std::string& document) { | |||||||
|   }}; |   }}; | ||||||
| 
 | 
 | ||||||
|   // Upgrade the document version for SHA-256 etc.
 |   // 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
 |   if (pdf.version(root) < 16) | ||||||
|   //   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"}; | ||||||
|   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(); | ||||||
| 
 | 
 | ||||||
| @ -919,25 +948,39 @@ 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] 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); |             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), | ||||||
|                          "h", opts, &option_index); |                          "hVr:", opts, &option_index); | ||||||
|     if (c == -1) |     if (c == -1) | ||||||
|       break; |       break; | ||||||
| 
 | 
 | ||||||
|  |     char* end = nullptr; | ||||||
|     switch (c) { |     switch (c) { | ||||||
|     case 'h': usage(); break; |     case 'r': | ||||||
|     default: usage(); |       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(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -964,7 +1007,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); |   auto err = pdf_sign(pdf_document, ushort(reservation)); | ||||||
|   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, Přemysl Eric Janouch <p@janouch.name> | // Copyright (c) 2018 - 2020, 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,6 +32,7 @@ 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" | ||||||
| ) | ) | ||||||
| @ -669,7 +670,7 @@ func (u *Updater) loadXref(lex *Lexer, loadedEntries map[uint]struct{}) error { | |||||||
| 
 | 
 | ||||||
| // ----------------------------------------------------------------------------- | // ----------------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| var haystackRE = regexp.MustCompile(`(?s:.*)\sstartxref\s+(\d+)\s+%%EOF`) | var trailerRE = 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. | ||||||
| @ -684,7 +685,7 @@ func NewUpdater(document []byte) (*Updater, error) { | |||||||
| 		haystack = haystack[len(haystack)-1024:] | 		haystack = haystack[len(haystack)-1024:] | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	m := haystackRE.FindSubmatch(haystack) | 	m := trailerRE.FindSubmatch(haystack) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return nil, errors.New("cannot find startxref") | 		return nil, errors.New("cannot find startxref") | ||||||
| 	} | 	} | ||||||
| @ -738,6 +739,31 @@ 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. | ||||||
| // | // | ||||||
| @ -1089,15 +1115,14 @@ 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, | ||||||
| // Carelessly assumes that the version of the original document is at most | 	reservation int) ([]byte, error) { | ||||||
| // 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 | ||||||
| @ -1129,7 +1154,7 @@ func Sign(document []byte, | |||||||
| 		buf.WriteString("\n   /Contents <") | 		buf.WriteString("\n   /Contents <") | ||||||
| 
 | 
 | ||||||
| 		signOff = buf.Len() | 		signOff = buf.Len() | ||||||
| 		signLen = 8192 // cert, digest, encrypted digest, ... | 		signLen = reservation * 2 // cert, digest, encrypted digest, ... | ||||||
| 		buf.Write(bytes.Repeat([]byte{'0'}, signLen)) | 		buf.Write(bytes.Repeat([]byte{'0'}, signLen)) | ||||||
| 		buf.WriteString("> >>") | 		buf.WriteString("> >>") | ||||||
| 
 | 
 | ||||||
| @ -1166,9 +1191,13 @@ func Sign(document []byte, | |||||||
| 		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)) | ||||||
| @ -1179,17 +1208,21 @@ func Sign(document []byte, | |||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	// 8.6.1 Interactive Form Dictionary | 	// 8.6.1 Interactive Form Dictionary | ||||||
| 	// XXX: Assuming there are no forms already, overwriting everything. | 	if _, ok := root.Dict["AcroForm"]; ok { | ||||||
|  | 		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. | ||||||
| 	// XXX: Assuming that it's not newer than 1.6 already--while Cairo can't | 	if pdf.Version(&root) < 16 { | ||||||
| 	// currently use a newer version that 1.5, it's not a bad idea to use | 		root.Dict["Version"] = NewName("1.6") | ||||||
| 	// 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