Avoid downgrading the document's PDF version

This commit is contained in:
Přemysl Eric Janouch 2020-09-04 17:16:42 +02:00
parent 1224d9be47
commit 2d08100b58
Signed by: p
GPG Key ID: A0420B94F92B9493
3 changed files with 57 additions and 17 deletions

View File

@ -9,8 +9,6 @@ the Cairo library. As such, it currently comes with some restrictions:
overwritten overwritten
* the document may not employ cross-reference streams, or must constitute * the document may not employ cross-reference streams, or must constitute
a hybrid-reference file at least 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, * the signature may take at most 4 kilobytes as a compile-time limit,
which should be enough space even for one intermediate certificate which should be enough space even for one intermediate certificate

View File

@ -342,6 +342,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 +515,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 +563,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,8 +828,6 @@ 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
@ -892,9 +912,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 than 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();

View File

@ -670,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.
@ -685,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")
} }
@ -739,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.
// //
@ -1094,9 +1119,6 @@ func FillInSignature(document []byte, signOff, signLen int,
// 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.
//
// Carelessly assumes that the version of the original document is at most
// PDF 1.6.
func Sign(document []byte, func Sign(document []byte,
key crypto.PrivateKey, certs []*x509.Certificate) ([]byte, error) { key crypto.PrivateKey, certs []*x509.Certificate) ([]byte, error) {
pdf, err := NewUpdater(document) pdf, err := NewUpdater(document)
@ -1195,10 +1217,10 @@ func Sign(document []byte,
}) })
// 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
// cairo_pdf_surface_restrict_to_version().
root.Dict["Version"] = NewName("1.6") 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())
}) })