Skip to content

Commit

Permalink
Support LTV and Timestamp (#70)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Carlos de Alfonso Laguna <[email protected]>
  • Loading branch information
3 people authored Jul 13, 2024
1 parent 5690e90 commit 37ff265
Show file tree
Hide file tree
Showing 11 changed files with 2,009 additions and 56 deletions.
2 changes: 1 addition & 1 deletion pdfsign.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@
}
}
}
}
}
1 change: 0 additions & 1 deletion pdfsigni.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,3 @@
}
}
}
?>
73 changes: 73 additions & 0 deletions pdfsignlts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env php
<?php
/*
This file is part of SAPP
Simple and Agnostic PDF Parser (SAPP) - Parse PDF documents in PHP (and update them)
Copyright (C) 2020 - Carlos de Alfonso ([email protected])
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

use ddn\sapp\PDFDoc;

require_once('vendor/autoload.php');

if ($argc < 3)
fwrite(STDERR, sprintf("usage: %s <filename> <certfile> <tsaUrl>\n
tsaUrl - optional TSA server url to timestamp pdf document.
", $argv[0]));
else {
if (!file_exists($argv[1]))
fwrite(STDERR, "failed to open file " . $argv[1]);
else {
// Silently prompt for the password
fwrite(STDERR, "Password: ");
system('stty -echo');
$password = trim(fgets(STDIN));
system('stty echo');
fwrite(STDERR, "\n");

$tsa = $argv[3] ?? null;
if (empty($tsa)) {
// Silently prompt for the timestamp autority
fwrite(STDERR, "TSA(\"http://timestamp.digicert.com\") type \"no\" to bypass tsa: ");
system('stty -echo');
$tsa = trim(fgets(STDIN)) ?: "http://timestamp.digicert.com";
system('stty echo');
fwrite(STDERR, "\n");
}

$file_content = file_get_contents($argv[1]);
$obj = PDFDoc::from_string($file_content);

if ($obj === false)
fwrite(STDERR, "failed to parse file " . $argv[1]);
else {
if (!$obj->set_signature_certificate($argv[2], $password))
fwrite(STDERR, "the certificate is not valid");
else {
if ($tsa != 'no') {
$obj->set_tsa($tsa);
}
$obj->set_ltv();
$docsigned = $obj->to_pdf_file_s();
if ($docsigned === false)
fwrite(STDERR, "could not sign the document");
else
echo $docsigned;
}
}
}
}
66 changes: 66 additions & 0 deletions pdfsigntsa.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env php
<?php
/*
This file is part of SAPP
Simple and Agnostic PDF Parser (SAPP) - Parse PDF documents in PHP (and update them)
Copyright (C) 2020 - Carlos de Alfonso ([email protected])
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

use ddn\sapp\PDFDoc;

require_once('vendor/autoload.php');

if ($argc < 3)
fwrite(STDERR, sprintf("usage: %s <filename> <certfile> <tsaUrl>\n
tsaUrl - optional TSA server url to timestamp pdf document.
", $argv[0]));
else {
if (!file_exists($argv[1]))
fwrite(STDERR, "failed to open file " . $argv[1]);
else {
// Silently prompt for the password
fwrite(STDERR, "Password: ");
system('stty -echo');
$password = trim(fgets(STDIN));
system('stty echo');
fwrite(STDERR, "\n");

$tsa = $argv[3] ?? null;
if (empty($tsa)) {
// Silently prompt for the timestamp autority
fwrite(STDERR, "TSA(\"http://timestamp.digicert.com\"): ");
system('stty -echo');
$tsa = trim(fgets(STDIN)) ?: "http://timestamp.digicert.com";
system('stty echo');
fwrite(STDERR, "\n");
}

$file_content = file_get_contents($argv[1]);
$obj = PDFDoc::from_string($file_content);

if ($obj === false)
fwrite(STDERR, "failed to parse file " . $argv[1]);
else {
if (!$obj->set_signature_certificate($argv[2], $password))
fwrite(STDERR, "the certificate is not valid");
else {
$obj->set_tsa($tsa);
$docsigned = $obj->to_pdf_file_s();
if ($docsigned === false)
fwrite(STDERR, "could not sign the document");
else
echo $docsigned;
}
}
}
}
1 change: 0 additions & 1 deletion pdfsignx.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,3 @@
}
}
}
?>
81 changes: 70 additions & 11 deletions src/PDFDoc.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
use ddn\sapp\pdfvalue\PDFValueSimple;
use ddn\sapp\pdfvalue\PDFValueHexString;
use ddn\sapp\pdfvalue\PDFValueString;
use ddn\sapp\helpers\CMS;
use ddn\sapp\helpers\x509;
use ddn\sapp\helpers\asn1;
use ddn\sapp\helpers\Buffer;
use ddn\sapp\helpers\UUID;
use ddn\sapp\helpers\DependencyTreeObject;
Expand Down Expand Up @@ -67,6 +70,8 @@ class PDFDoc extends Buffer {
protected $_buffer = "";
protected $_backup_state = [];
protected $_certificate = null;
protected $_signature_ltv_data = null;
protected $_signature_tsa = null;
protected $_appearance = null;
protected $_xref_table_version;
protected $_revisions;
Expand Down Expand Up @@ -286,7 +291,8 @@ public function clear_signature_certificate() {

/**
* Function that stores the certificate to use, when signing the document
* @param certfile a file that contains a user certificate in pkcs12 format, or an array [ 'cert' => <cert.pem>, 'pkey' => <key.pem> ]
* @param certfile a file that contains a user certificate in pkcs12 format,
* or an array [ 'cert' => <cert.pem>, 'pkey' => <key.pem>, 'extracerts' => <extracerts.pem|null> ]
* that would be the output of openssl_pkcs12_read
* @param password the password to read the private key
* @return valid true if the certificate can be used to sign the document, false otherwise
Expand All @@ -302,6 +308,12 @@ public function set_signature_certificate($certfile, $certpass = null) {
return p_error("invalid private key");
if (! openssl_x509_check_private_key($certificate["cert"], $certificate["pkey"]))
return p_error("private key doesn't corresponds to certificate");

if (is_string($certificate['extracerts'] ?? null)) {
$certificate['extracerts'] = array_filter(explode("-----END CERTIFICATE-----\n", $certificate['extracerts']));
foreach ($certificate['extracerts'] as &$extracerts)
$extracerts = $extracerts . "-----END CERTIFICATE-----\n";
}
} else {
$certfilecontent = file_get_contents($certfile);
if ($certfilecontent === false)
Expand All @@ -316,6 +328,32 @@ public function set_signature_certificate($certfile, $certpass = null) {
return true;
}

/**
* Function that stores the ltv configuration to use, when signing the document
* @param $ocspURI OCSP Url to validate cert file
* @param $crlURIorFILE Crl filename/url to validate cert
* @param $issuerURIorFILE issuer filename/url
*/
public function set_ltv($ocspURI=null, $crlURIorFILE=null, $issuerURIorFILE=null) {
$this->_signature_ltv_data['ocspURI'] = $ocspURI;
$this->_signature_ltv_data['crlURIorFILE'] = $crlURIorFILE;
$this->_signature_ltv_data['issuerURIorFILE'] = $issuerURIorFILE;
}

/**
* Function that stores the tsa configuration to use, when signing the document
* @param $tsaurl Link to tsa service
* @param $tsauser the user for tsa service
* @param $tsapass the password for tsa service
*/
public function set_tsa($tsa, $tsauser = null, $tsapass = null) {
$this->_signature_tsa['host'] = $tsa;
if ($tsauser && $tsapass) {
$this->_signature_tsa['user'] = $tsauser;
$this->_signature_tsa['password'] = $tsapass;
}
}

/**
* Function to set the metadata properties for the certificate options
* @param $name
Expand Down Expand Up @@ -415,10 +453,31 @@ protected function _generate_signature_in_document() {
// Prepare the signature object (we need references to it)
$signature = null;
if ($this->_certificate !== null) {
// Perform signature test to get signature size to define __SIGNATURE_MAX_LENGTH
p_debug(" ########## PERFORM SIGNATURE LENGTH CHECK ##########\n");
$CMS = new helpers\CMS;
$CMS->signature_data['signcert'] = $this->_certificate['cert'];
$CMS->signature_data['extracerts'] = $this->_certificate['extracerts']??null;
$CMS->signature_data['hashAlgorithm'] = 'sha256';
$CMS->signature_data['privkey'] = $this->_certificate['pkey'];
$CMS->signature_data['tsa'] = $this->_signature_tsa;
$CMS->signature_data['ltv'] = $this->_signature_ltv_data;
$res = $CMS->pkcs7_sign('0');
$len = strlen($res);
p_debug(" Signature Length is \"$len\" Bytes");
p_debug(" ########## FINISHED SIGNATURE LENGTH CHECK #########\n\n");
define('__SIGNATURE_MAX_LENGTH', $len);

$signature = $this->create_object([], "ddn\sapp\PDFSignatureObject", false);
//$signature = new PDFSignatureObject([]);
$signature->set_metadata($this->_metadata_name, $this->_metadata_reason, $this->_metadata_location, $this->_metadata_contact_info);
$signature->set_certificate($this->_certificate);
if($this->_signature_tsa !== null) {
$signature->set_signature_tsa($this->_signature_tsa);
}
if($this->_signature_ltv_data !== null) {
$signature->set_signature_ltv($this->_signature_ltv_data);
}

// Update the value to the annotation object
$annotation_object["V"] = new PDFValueReference($signature->get_oid());
Expand Down Expand Up @@ -811,17 +870,17 @@ public function to_pdf_file_b($rebuild = false) : Buffer {
$_signature->set_sizes($_doc_to_xref->size(), $_doc_from_xref->size());
$_signature["Contents"] = new PDFValueSimple("");
$_signable_document = new Buffer($_doc_to_xref->get_raw() . $_signature->to_pdf_entry() . $_doc_from_xref->get_raw());

// We need to write the content to a temporary folder to use the pkcs7 signature mechanism
$temp_filename = tempnam(__TMP_FOLDER, 'pdfsign');
$temp_file = fopen($temp_filename, 'wb');
fwrite($temp_file, $_signable_document->get_raw());
fclose($temp_file);

// Calculate the signature and remove the temporary file
$certificate = $_signature->get_certificate();
$signature_contents = PDFUtilFnc::calculate_pkcs7_signature($temp_filename, $certificate['cert'], $certificate['pkey'], __TMP_FOLDER);
unlink($temp_filename);
$extracerts = (array_key_exists('extracerts', $certificate)) ? $certificate['extracerts'] : null;
$cms = new CMS;
$cms->signature_data['hashAlgorithm'] = 'sha256';
$cms->signature_data['privkey'] = $certificate['pkey'];
$cms->signature_data['extracerts'] = $extracerts;
$cms->signature_data['signcert'] = $certificate['cert'];
$cms->signature_data['ltv'] = $_signature->get_ltv();
$cms->signature_data['tsa'] = $_signature->get_tsa();
$signature_contents = $cms->pkcs7_sign($_signable_document->get_raw());
$signature_contents = str_pad($signature_contents, __SIGNATURE_MAX_LENGTH, '0');

// Then restore the contents field
$_signature["Contents"] = new PDFValueHexString($signature_contents);
Expand Down
17 changes: 16 additions & 1 deletion src/PDFSignatureObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
// The maximum signature length, needed to create a placeholder to calculate the range of bytes
// that will cover the signature.
if (!defined('__SIGNATURE_MAX_LENGTH'))
define('__SIGNATURE_MAX_LENGTH', 11742);
//define('__SIGNATURE_MAX_LENGTH', 11742);
define('__SIGNATURE_MAX_LENGTH', 27742);

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

// A placeholder for the certificate to use to sign the document
protected $_certificate = null;
protected $_signature_ltv_data = null;
protected $_signature_tsa = null;
/**
* Sets the certificate to use to sign
* @param cert the pem-formatted certificate and private to use to sign as
Expand All @@ -57,13 +60,25 @@ class PDFSignatureObject extends PDFObject {
public function set_certificate($certificate) {
$this->_certificate = $certificate;
}
public function set_signature_ltv($signature_ltv_data) {
$this->_signature_ltv_data = $signature_ltv_data;
}
public function set_signature_tsa($signature_tsa) {
$this->_signature_tsa = $signature_tsa;
}
/**
* Obtains the certificate set with function set_certificate
* @return cert the certificate
*/
public function get_certificate() {
return $this->_certificate;
}
public function get_tsa() {
return $this->_signature_tsa;
}
public function get_ltv() {
return $this->_signature_ltv_data;
}
/**
* Constructs the object and sets the default values needed to sign
* @param oid the oid for the object
Expand Down
41 changes: 0 additions & 41 deletions src/PDFUtilFnc.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
use ddn\sapp\PDFObjectParser;
use ddn\sapp\helpers\StreamReader;
use ddn\sapp\helpers\Buffer;

use function ddn\sapp\helpers\p_debug;
use function ddn\sapp\helpers\p_debug_var;
use function ddn\sapp\helpers\p_error;
Expand Down Expand Up @@ -446,46 +445,6 @@ public static function acquire_structure(&$_buffer, $depth = null) {
];
}

/**
* Signs a file using the certificate and key and obtains the signature content padded to the max signature length
* @param filename the name of the file to sign
* @param certificate the public key to sign
* @param key the private key to sign
* @param tmpfolder the folder in which to store a temporary file needed
* @return signature the signature, in hexadecimal string, padded to the maximum length (i.e. for PDF) or false in case of error
*/
public static function calculate_pkcs7_signature($filenametosign, $certificate, $key, $tmpfolder = "/tmp") {
$filesize_original = filesize($filenametosign);
if ($filesize_original === false)
return p_error("could not open file $filenametosign");

$temp_filename = tempnam($tmpfolder, "pdfsign");

if ($temp_filename === false)
return p_error("could not create a temporary filename");

if (openssl_pkcs7_sign($filenametosign, $temp_filename, $certificate, $key, array(), PKCS7_BINARY | PKCS7_DETACHED) !== true) {
unlink($temp_filename);
return p_error("failed to sign file $filenametosign");
}

$signature = file_get_contents($temp_filename);
// extract signature
$signature = substr($signature, $filesize_original);
$signature = substr($signature, (strpos($signature, "%%EOF\n\n------") + 13));

$tmparr = explode("\n\n", $signature);
$signature = $tmparr[1];
// decode signature
$signature = base64_decode(trim($signature));

// convert signature to hex
$signature = current(unpack('H*', $signature));
$signature = str_pad($signature, __SIGNATURE_MAX_LENGTH, '0');

return $signature;
}

/**
* Function that finds a the object at the specific position in the buffer
* @param buffer the buffer from which to read the document
Expand Down
Loading

0 comments on commit 37ff265

Please sign in to comment.