Skip to content

Commit 37ff265

Browse files
hidaswerikn69dealfonso
authored
Support LTV and Timestamp (#70)
* first update to support timestamp * Add last parameter for chain certs Add last parameter for chain certs * Add last parameter for support chain certs * expand length to embedded tsa * update * add ltv and timestamp * Support Timestamp and LTV And improve signature process . no need to write/read/parse tempfile in signing process. * Update PDFDoc.php * Update PDFDoc.php * Update pdfsign.php * Update CMS.php * removed ex test and ex log * Update PDFDoc.php * Update PDFUtilFnc.php * Update CMS.php remove logging function, use sapp default logging function * aligned classes structure migrate asn1 class functions to dynamic functions * remove example * Update pdfsign.php -change some message text. -set default $ocspUrl & $crl to prevent php notice message append in pdf result * update args & text * Update asn1.php * Update x509.php prevent php notice msg when ocsp server send not common resp status * Update CMS.php prevent same extracerts certificate (duplicate cert) to embeded * Fix example scripts * Small fixes * calculate __SIGNATURE_MAX_LENGTH calculated __SIGNATURE_MAX_LENGTH exactly. no longer waste signature space with zero padding. * merged pdfsignltv.php & pdfsigntsa.php * Update CMS.php - support path validation (unlimited) - removed ocsp, crl and issuer parameter (because it is difficult to implement if the certificate has a long path) * merged * Support <extracerts.pem> * Update pdfsignlts.php * Create pdfsigntsa.php * Update asn1.php * minor bugs * minor bugs --------- Co-authored-by: erikn69 <erikn_69@hotmail.com> Co-authored-by: Carlos de Alfonso Laguna <caralla@upv.es>
1 parent 5690e90 commit 37ff265

11 files changed

+2009
-56
lines changed

pdfsign.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@
5353
}
5454
}
5555
}
56-
}
56+
}

pdfsigni.php

-1
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,3 @@
8888
}
8989
}
9090
}
91-
?>

pdfsignlts.php

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env php
2+
<?php
3+
/*
4+
This file is part of SAPP
5+
6+
Simple and Agnostic PDF Parser (SAPP) - Parse PDF documents in PHP (and update them)
7+
Copyright (C) 2020 - Carlos de Alfonso (caralla76@gmail.com)
8+
9+
This program is free software: you can redistribute it and/or modify
10+
it under the terms of the GNU Lesser General Public License as published by
11+
the Free Software Foundation, either version 3 of the License, or
12+
(at your option) any later version.
13+
14+
This program is distributed in the hope that it will be useful,
15+
but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
GNU General Public License for more details.
18+
19+
You should have received a copy of the GNU Lesser General Public License
20+
along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
use ddn\sapp\PDFDoc;
24+
25+
require_once('vendor/autoload.php');
26+
27+
if ($argc < 3)
28+
fwrite(STDERR, sprintf("usage: %s <filename> <certfile> <tsaUrl>\n
29+
tsaUrl - optional TSA server url to timestamp pdf document.
30+
", $argv[0]));
31+
else {
32+
if (!file_exists($argv[1]))
33+
fwrite(STDERR, "failed to open file " . $argv[1]);
34+
else {
35+
// Silently prompt for the password
36+
fwrite(STDERR, "Password: ");
37+
system('stty -echo');
38+
$password = trim(fgets(STDIN));
39+
system('stty echo');
40+
fwrite(STDERR, "\n");
41+
42+
$tsa = $argv[3] ?? null;
43+
if (empty($tsa)) {
44+
// Silently prompt for the timestamp autority
45+
fwrite(STDERR, "TSA(\"http://timestamp.digicert.com\") type \"no\" to bypass tsa: ");
46+
system('stty -echo');
47+
$tsa = trim(fgets(STDIN)) ?: "http://timestamp.digicert.com";
48+
system('stty echo');
49+
fwrite(STDERR, "\n");
50+
}
51+
52+
$file_content = file_get_contents($argv[1]);
53+
$obj = PDFDoc::from_string($file_content);
54+
55+
if ($obj === false)
56+
fwrite(STDERR, "failed to parse file " . $argv[1]);
57+
else {
58+
if (!$obj->set_signature_certificate($argv[2], $password))
59+
fwrite(STDERR, "the certificate is not valid");
60+
else {
61+
if ($tsa != 'no') {
62+
$obj->set_tsa($tsa);
63+
}
64+
$obj->set_ltv();
65+
$docsigned = $obj->to_pdf_file_s();
66+
if ($docsigned === false)
67+
fwrite(STDERR, "could not sign the document");
68+
else
69+
echo $docsigned;
70+
}
71+
}
72+
}
73+
}

pdfsigntsa.php

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env php
2+
<?php
3+
/*
4+
This file is part of SAPP
5+
Simple and Agnostic PDF Parser (SAPP) - Parse PDF documents in PHP (and update them)
6+
Copyright (C) 2020 - Carlos de Alfonso (caralla76@gmail.com)
7+
This program is free software: you can redistribute it and/or modify
8+
it under the terms of the GNU Lesser General Public License as published by
9+
the Free Software Foundation, either version 3 of the License, or
10+
(at your option) any later version.
11+
This program is distributed in the hope that it will be useful,
12+
but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
GNU General Public License for more details.
15+
You should have received a copy of the GNU Lesser General Public License
16+
along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
use ddn\sapp\PDFDoc;
20+
21+
require_once('vendor/autoload.php');
22+
23+
if ($argc < 3)
24+
fwrite(STDERR, sprintf("usage: %s <filename> <certfile> <tsaUrl>\n
25+
tsaUrl - optional TSA server url to timestamp pdf document.
26+
", $argv[0]));
27+
else {
28+
if (!file_exists($argv[1]))
29+
fwrite(STDERR, "failed to open file " . $argv[1]);
30+
else {
31+
// Silently prompt for the password
32+
fwrite(STDERR, "Password: ");
33+
system('stty -echo');
34+
$password = trim(fgets(STDIN));
35+
system('stty echo');
36+
fwrite(STDERR, "\n");
37+
38+
$tsa = $argv[3] ?? null;
39+
if (empty($tsa)) {
40+
// Silently prompt for the timestamp autority
41+
fwrite(STDERR, "TSA(\"http://timestamp.digicert.com\"): ");
42+
system('stty -echo');
43+
$tsa = trim(fgets(STDIN)) ?: "http://timestamp.digicert.com";
44+
system('stty echo');
45+
fwrite(STDERR, "\n");
46+
}
47+
48+
$file_content = file_get_contents($argv[1]);
49+
$obj = PDFDoc::from_string($file_content);
50+
51+
if ($obj === false)
52+
fwrite(STDERR, "failed to parse file " . $argv[1]);
53+
else {
54+
if (!$obj->set_signature_certificate($argv[2], $password))
55+
fwrite(STDERR, "the certificate is not valid");
56+
else {
57+
$obj->set_tsa($tsa);
58+
$docsigned = $obj->to_pdf_file_s();
59+
if ($docsigned === false)
60+
fwrite(STDERR, "could not sign the document");
61+
else
62+
echo $docsigned;
63+
}
64+
}
65+
}
66+
}

pdfsignx.php

-1
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,3 @@
5656
}
5757
}
5858
}
59-
?>

src/PDFDoc.php

+70-11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
use ddn\sapp\pdfvalue\PDFValueSimple;
3131
use ddn\sapp\pdfvalue\PDFValueHexString;
3232
use ddn\sapp\pdfvalue\PDFValueString;
33+
use ddn\sapp\helpers\CMS;
34+
use ddn\sapp\helpers\x509;
35+
use ddn\sapp\helpers\asn1;
3336
use ddn\sapp\helpers\Buffer;
3437
use ddn\sapp\helpers\UUID;
3538
use ddn\sapp\helpers\DependencyTreeObject;
@@ -67,6 +70,8 @@ class PDFDoc extends Buffer {
6770
protected $_buffer = "";
6871
protected $_backup_state = [];
6972
protected $_certificate = null;
73+
protected $_signature_ltv_data = null;
74+
protected $_signature_tsa = null;
7075
protected $_appearance = null;
7176
protected $_xref_table_version;
7277
protected $_revisions;
@@ -286,7 +291,8 @@ public function clear_signature_certificate() {
286291

287292
/**
288293
* Function that stores the certificate to use, when signing the document
289-
* @param certfile a file that contains a user certificate in pkcs12 format, or an array [ 'cert' => <cert.pem>, 'pkey' => <key.pem> ]
294+
* @param certfile a file that contains a user certificate in pkcs12 format,
295+
* or an array [ 'cert' => <cert.pem>, 'pkey' => <key.pem>, 'extracerts' => <extracerts.pem|null> ]
290296
* that would be the output of openssl_pkcs12_read
291297
* @param password the password to read the private key
292298
* @return valid true if the certificate can be used to sign the document, false otherwise
@@ -302,6 +308,12 @@ public function set_signature_certificate($certfile, $certpass = null) {
302308
return p_error("invalid private key");
303309
if (! openssl_x509_check_private_key($certificate["cert"], $certificate["pkey"]))
304310
return p_error("private key doesn't corresponds to certificate");
311+
312+
if (is_string($certificate['extracerts'] ?? null)) {
313+
$certificate['extracerts'] = array_filter(explode("-----END CERTIFICATE-----\n", $certificate['extracerts']));
314+
foreach ($certificate['extracerts'] as &$extracerts)
315+
$extracerts = $extracerts . "-----END CERTIFICATE-----\n";
316+
}
305317
} else {
306318
$certfilecontent = file_get_contents($certfile);
307319
if ($certfilecontent === false)
@@ -316,6 +328,32 @@ public function set_signature_certificate($certfile, $certpass = null) {
316328
return true;
317329
}
318330

331+
/**
332+
* Function that stores the ltv configuration to use, when signing the document
333+
* @param $ocspURI OCSP Url to validate cert file
334+
* @param $crlURIorFILE Crl filename/url to validate cert
335+
* @param $issuerURIorFILE issuer filename/url
336+
*/
337+
public function set_ltv($ocspURI=null, $crlURIorFILE=null, $issuerURIorFILE=null) {
338+
$this->_signature_ltv_data['ocspURI'] = $ocspURI;
339+
$this->_signature_ltv_data['crlURIorFILE'] = $crlURIorFILE;
340+
$this->_signature_ltv_data['issuerURIorFILE'] = $issuerURIorFILE;
341+
}
342+
343+
/**
344+
* Function that stores the tsa configuration to use, when signing the document
345+
* @param $tsaurl Link to tsa service
346+
* @param $tsauser the user for tsa service
347+
* @param $tsapass the password for tsa service
348+
*/
349+
public function set_tsa($tsa, $tsauser = null, $tsapass = null) {
350+
$this->_signature_tsa['host'] = $tsa;
351+
if ($tsauser && $tsapass) {
352+
$this->_signature_tsa['user'] = $tsauser;
353+
$this->_signature_tsa['password'] = $tsapass;
354+
}
355+
}
356+
319357
/**
320358
* Function to set the metadata properties for the certificate options
321359
* @param $name
@@ -415,10 +453,31 @@ protected function _generate_signature_in_document() {
415453
// Prepare the signature object (we need references to it)
416454
$signature = null;
417455
if ($this->_certificate !== null) {
456+
// Perform signature test to get signature size to define __SIGNATURE_MAX_LENGTH
457+
p_debug(" ########## PERFORM SIGNATURE LENGTH CHECK ##########\n");
458+
$CMS = new helpers\CMS;
459+
$CMS->signature_data['signcert'] = $this->_certificate['cert'];
460+
$CMS->signature_data['extracerts'] = $this->_certificate['extracerts']??null;
461+
$CMS->signature_data['hashAlgorithm'] = 'sha256';
462+
$CMS->signature_data['privkey'] = $this->_certificate['pkey'];
463+
$CMS->signature_data['tsa'] = $this->_signature_tsa;
464+
$CMS->signature_data['ltv'] = $this->_signature_ltv_data;
465+
$res = $CMS->pkcs7_sign('0');
466+
$len = strlen($res);
467+
p_debug(" Signature Length is \"$len\" Bytes");
468+
p_debug(" ########## FINISHED SIGNATURE LENGTH CHECK #########\n\n");
469+
define('__SIGNATURE_MAX_LENGTH', $len);
470+
418471
$signature = $this->create_object([], "ddn\sapp\PDFSignatureObject", false);
419472
//$signature = new PDFSignatureObject([]);
420473
$signature->set_metadata($this->_metadata_name, $this->_metadata_reason, $this->_metadata_location, $this->_metadata_contact_info);
421474
$signature->set_certificate($this->_certificate);
475+
if($this->_signature_tsa !== null) {
476+
$signature->set_signature_tsa($this->_signature_tsa);
477+
}
478+
if($this->_signature_ltv_data !== null) {
479+
$signature->set_signature_ltv($this->_signature_ltv_data);
480+
}
422481

423482
// Update the value to the annotation object
424483
$annotation_object["V"] = new PDFValueReference($signature->get_oid());
@@ -811,17 +870,17 @@ public function to_pdf_file_b($rebuild = false) : Buffer {
811870
$_signature->set_sizes($_doc_to_xref->size(), $_doc_from_xref->size());
812871
$_signature["Contents"] = new PDFValueSimple("");
813872
$_signable_document = new Buffer($_doc_to_xref->get_raw() . $_signature->to_pdf_entry() . $_doc_from_xref->get_raw());
814-
815-
// We need to write the content to a temporary folder to use the pkcs7 signature mechanism
816-
$temp_filename = tempnam(__TMP_FOLDER, 'pdfsign');
817-
$temp_file = fopen($temp_filename, 'wb');
818-
fwrite($temp_file, $_signable_document->get_raw());
819-
fclose($temp_file);
820-
821-
// Calculate the signature and remove the temporary file
822873
$certificate = $_signature->get_certificate();
823-
$signature_contents = PDFUtilFnc::calculate_pkcs7_signature($temp_filename, $certificate['cert'], $certificate['pkey'], __TMP_FOLDER);
824-
unlink($temp_filename);
874+
$extracerts = (array_key_exists('extracerts', $certificate)) ? $certificate['extracerts'] : null;
875+
$cms = new CMS;
876+
$cms->signature_data['hashAlgorithm'] = 'sha256';
877+
$cms->signature_data['privkey'] = $certificate['pkey'];
878+
$cms->signature_data['extracerts'] = $extracerts;
879+
$cms->signature_data['signcert'] = $certificate['cert'];
880+
$cms->signature_data['ltv'] = $_signature->get_ltv();
881+
$cms->signature_data['tsa'] = $_signature->get_tsa();
882+
$signature_contents = $cms->pkcs7_sign($_signable_document->get_raw());
883+
$signature_contents = str_pad($signature_contents, __SIGNATURE_MAX_LENGTH, '0');
825884

826885
// Then restore the contents field
827886
$_signature["Contents"] = new PDFValueHexString($signature_contents);

src/PDFSignatureObject.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
// The maximum signature length, needed to create a placeholder to calculate the range of bytes
3636
// that will cover the signature.
3737
if (!defined('__SIGNATURE_MAX_LENGTH'))
38-
define('__SIGNATURE_MAX_LENGTH', 11742);
38+
//define('__SIGNATURE_MAX_LENGTH', 11742);
39+
define('__SIGNATURE_MAX_LENGTH', 27742);
3940

4041
// The maximum expected length of the byte range, used to create a placeholder while the size
4142
// is not known. 68 digits enable 20 digits for the size of the document
@@ -49,6 +50,8 @@ class PDFSignatureObject extends PDFObject {
4950

5051
// A placeholder for the certificate to use to sign the document
5152
protected $_certificate = null;
53+
protected $_signature_ltv_data = null;
54+
protected $_signature_tsa = null;
5255
/**
5356
* Sets the certificate to use to sign
5457
* @param cert the pem-formatted certificate and private to use to sign as
@@ -57,13 +60,25 @@ class PDFSignatureObject extends PDFObject {
5760
public function set_certificate($certificate) {
5861
$this->_certificate = $certificate;
5962
}
63+
public function set_signature_ltv($signature_ltv_data) {
64+
$this->_signature_ltv_data = $signature_ltv_data;
65+
}
66+
public function set_signature_tsa($signature_tsa) {
67+
$this->_signature_tsa = $signature_tsa;
68+
}
6069
/**
6170
* Obtains the certificate set with function set_certificate
6271
* @return cert the certificate
6372
*/
6473
public function get_certificate() {
6574
return $this->_certificate;
6675
}
76+
public function get_tsa() {
77+
return $this->_signature_tsa;
78+
}
79+
public function get_ltv() {
80+
return $this->_signature_ltv_data;
81+
}
6782
/**
6883
* Constructs the object and sets the default values needed to sign
6984
* @param oid the oid for the object

src/PDFUtilFnc.php

-41
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use ddn\sapp\PDFObjectParser;
2525
use ddn\sapp\helpers\StreamReader;
2626
use ddn\sapp\helpers\Buffer;
27-
2827
use function ddn\sapp\helpers\p_debug;
2928
use function ddn\sapp\helpers\p_debug_var;
3029
use function ddn\sapp\helpers\p_error;
@@ -446,46 +445,6 @@ public static function acquire_structure(&$_buffer, $depth = null) {
446445
];
447446
}
448447

449-
/**
450-
* Signs a file using the certificate and key and obtains the signature content padded to the max signature length
451-
* @param filename the name of the file to sign
452-
* @param certificate the public key to sign
453-
* @param key the private key to sign
454-
* @param tmpfolder the folder in which to store a temporary file needed
455-
* @return signature the signature, in hexadecimal string, padded to the maximum length (i.e. for PDF) or false in case of error
456-
*/
457-
public static function calculate_pkcs7_signature($filenametosign, $certificate, $key, $tmpfolder = "/tmp") {
458-
$filesize_original = filesize($filenametosign);
459-
if ($filesize_original === false)
460-
return p_error("could not open file $filenametosign");
461-
462-
$temp_filename = tempnam($tmpfolder, "pdfsign");
463-
464-
if ($temp_filename === false)
465-
return p_error("could not create a temporary filename");
466-
467-
if (openssl_pkcs7_sign($filenametosign, $temp_filename, $certificate, $key, array(), PKCS7_BINARY | PKCS7_DETACHED) !== true) {
468-
unlink($temp_filename);
469-
return p_error("failed to sign file $filenametosign");
470-
}
471-
472-
$signature = file_get_contents($temp_filename);
473-
// extract signature
474-
$signature = substr($signature, $filesize_original);
475-
$signature = substr($signature, (strpos($signature, "%%EOF\n\n------") + 13));
476-
477-
$tmparr = explode("\n\n", $signature);
478-
$signature = $tmparr[1];
479-
// decode signature
480-
$signature = base64_decode(trim($signature));
481-
482-
// convert signature to hex
483-
$signature = current(unpack('H*', $signature));
484-
$signature = str_pad($signature, __SIGNATURE_MAX_LENGTH, '0');
485-
486-
return $signature;
487-
}
488-
489448
/**
490449
* Function that finds a the object at the specific position in the buffer
491450
* @param buffer the buffer from which to read the document

0 commit comments

Comments
 (0)