After reading this Stack Overflow question (and other pages, referenced below, in the comments) I came up with a PHP code that, given a digitally signed PDF file, informs who signed it:
<?php
function der2pem($der_data) {
// https://www.php.net/manual/en/ref.openssl.php
$pem = chunk_split(base64_encode($der_data), 64, "\n");
$pem = "-----BEGIN CERTIFICATE-----\n".$pem."-----END CERTIFICATE-----\n";
return $pem;
}
function extract_pkcs7_signatures($path_to_pdf) {
// https://stackoverflow.com/q/46430367
$content = file_get_contents($path_to_pdf);
$regexp = '/ByteRange\ \[\s*(\d+) (\d+) (\d+)/';
$result = [];
preg_match_all($regexp, $content, $result);
$signatures = null;
if (isset($result[2]) && isset($result[3]) && isset($result[2][0]) && isset($result[3][0])) {
$start = $result[2][0];
$end = $result[3][0];
if ($stream = fopen($path_to_pdf, 'rb')) {
$signatures = stream_get_contents($stream, $end - $start - 2, $start + 1);
fclose($stream);
$signatures = hex2bin($signatures);
}
}
return $signatures;
}
function who_signed($path_to_pdf) {
// https://www.php.net/manual/en/openssl.certparams.php
// https://www.php.net/manual/en/function.openssl-pkcs7-read.php
// https://www.php.net/manual/en/function.openssl-x509-parse.php
$signers = [];
$signatures = extract_pkcs7_signatures($path_to_pdf);
if (!empty($signatures)) {
$pem = der2pem($signatures);
$certificates = array();
$result = openssl_pkcs7_read($pem, $certificates);
if ($result) {
foreach ($certificates as $certificate) {
$certificate_data = openssl_x509_parse($certificate);
$signers[] = $certificate_data['subject']['CN'];
}
}
}
return $signers;
}
$path_to_pdf = 'test.pdf';
// In case you want to test the extract_pkcs7_signatures() function:
/*
$signatures = extract_pkcs7_signatures($path_to_pdf);
$path_to_pkcs7 = pathinfo($path_to_pdf, PATHINFO_FILENAME) . '.pkcs7';
file_put_contents($path_to_pkcs7, $signatures);
echo shell_exec("openssl pkcs7 -inform DER -in $path_to_pkcs7 -print_certs -text");
exit;
*/
var_dump(who_signed($path_to_pdf));
?>
This is just command line PHP, you don't need to run any previous Composer commands to be able to run this script.
For some test1.pdf
, signed by just one person (let's call her ALICE
), this script returns:
array(4) {
[0]=>
string(23) "CERTIFICATE AUTHORITY 1"
[1]=>
string(23) "CERTIFICATE AUTHORITY 2"
[2]=>
string(5) "ALICE"
[3]=>
string(5) "ALICE"
}
For some test2.pdf
, signed by two people (let's call them BOB
and CAROL
), this script returns:
array(4) {
[0]=>
string(23) "CERTIFICATE AUTHORITY 1"
[1]=>
string(3) "BOB"
[2]=>
string(23) "CERTIFICATE AUTHORITY 2"
[3]=>
string(23) "CERTIFICATE AUTHORITY 3"
}
The problem with this script is that, comparing its outputs to the ones provided by pdfsig, they are wrong.
For the same test1.pdf
, pdfsig returns:
Digital Signature Info of: test1.pdf
Signature #1:
- Signer Certificate Common Name: ALICE
...
For the same test2.pdf
, pdfsig returns:
Digital Signature Info of: test2.pdf
Signature #1:
- Signer Certificate Common Name: BOB
...
Signature #2:
- Signer Certificate Common Name: CAROL
...
What am I doing wrong? I mean, what do I need to do to correctly identify the person (or the people) who signed a PDF file?