PHP -如何获得数字签名PDF的签名者?



在阅读了这个Stack Overflow问题(以及下面评论中引用的其他页面)之后,我想出了一个PHP代码,给定一个数字签名的PDF文件,通知谁签署了它:

<?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));
?>

这只是PHP的命令行,你不需要运行任何以前的Composer命令来运行这个脚本。

对于仅由一个人签名的test1.pdf(我们称其为ALICE),该脚本返回:

array(4) {
[0]=>
string(23) "CERTIFICATE AUTHORITY 1"
[1]=>
string(23) "CERTIFICATE AUTHORITY 2"
[2]=>
string(5) "ALICE"
[3]=>
string(5) "ALICE"
}

对于某些由两个人签名的test2.pdf(我们称它们为BOBCAROL),该脚本返回:

array(4) {
[0]=>
string(23) "CERTIFICATE AUTHORITY 1"
[1]=>
string(3) "BOB"
[2]=>
string(23) "CERTIFICATE AUTHORITY 2"
[3]=>
string(23) "CERTIFICATE AUTHORITY 3"
}
这个脚本的问题在于,将它的输出与提供的输出进行比较时,pdfsig

对于相同的test1.pdfpdfsig返回:

Digital Signature Info of: test1.pdf
Signature #1:
- Signer Certificate Common Name: ALICE
...

对于相同的test2.pdfpdfsig返回:

Digital Signature Info of: test2.pdf
Signature #1:
- Signer Certificate Common Name: BOB
...
Signature #2:
- Signer Certificate Common Name: CAROL
...

我做错了什么?我的意思是,我需要做些什么来正确识别签署PDF文件的人(或人们)?

我之前的脚本没有考虑以下内容:

  • 一个PDF文件可以有一个或多个签名(pkcs# 7文件),每个签名由一个ByteRange数组表示(我发现这个阅读PDF规范中的数字签名,由@Denis Alimov提出的解决方案只读取第一个ByteRange)
  • pkcs# 7文件可能包含许多证书,包括证书颁发机构证书和人员证书(我们只对人员证书感兴趣)pkcs# 7文件可能包含重复的证书(如果你知道为什么,请告诉我,这正是我在样本PDF中发现的)

下面是我当前的工作脚本,它返回与pdfsig对齐的输出:

<?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
$pdf_contents = file_get_contents($path_to_pdf);
$regexp = '/ByteRange [s*(d+) (d+) (d+)/';
$result = [];
preg_match_all($regexp, $pdf_contents, $result);
$signatures = [];
if (isset($result[0])) {
$signature_count = count($result[0]);
for ($s = 0; $s < $signature_count; $s++) {
$start = $result[2][$s];
$end = $result[3][$s];
$signature = null;
if ($stream = fopen($path_to_pdf, 'rb')) {
$signature = stream_get_contents($stream, $end - $start - 2, $start + 1);
fclose($stream);
$signature = hex2bin($signature);
$signatures[] = $signature;
}
}
}
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 = [];
$pkcs7_der_signatures = extract_pkcs7_signatures($path_to_pdf);
if (!empty($pkcs7_der_signatures)) {
$parsed_certificates = [];
foreach ($pkcs7_der_signatures as $pkcs7_der_signature) {
$pkcs7_pem_signature = der2pem($pkcs7_der_signature);
$pem_certificates = [];
$result = openssl_pkcs7_read($pkcs7_pem_signature, $pem_certificates);
if ($result) {
foreach ($pem_certificates as $pem_certificate) {
$parsed_certificate = openssl_x509_parse($pem_certificate);
$parsed_certificates[] = $parsed_certificate;
}
}
}
// Remove certificate authorities certificates
$people_certificates = [];
foreach ($parsed_certificates as $certificate_a) {
$is_authority = false;
foreach ($parsed_certificates as $certificate_b) {
if ($certificate_a['subject'] == $certificate_b['issuer']) {
// If certificate A is of the issuer of certificate B, then
// certificate A belongs to a certificate authority and,
// therefore, should be ignored
$is_authority = true;
break;
}
}
if (!$is_authority) {
$people_certificates[] = $certificate_a;
}
}
// Remove duplicate certificates
$distinct_certificates = [];
foreach ($people_certificates as $certificate_a) {
$is_duplicated = false;
if (count($distinct_certificates) > 0) {
foreach ($distinct_certificates as $certificate_b) {
if (
($certificate_a['subject'] == $certificate_b['subject']) &&
($certificate_a['serialNumber'] == $certificate_b['serialNumber']) &&
($certificate_a['issuer'] == $certificate_b['issuer'])
) {
// If certificate B has the same subject, serial number
// and issuer as certificate A, then certificate B is a
// duplicate and, therefore, should be ignored
$is_duplicated = true;
break;
}
}
}
if (!$is_duplicated) {
$distinct_certificates[] = $certificate_a;
}
}
foreach ($distinct_certificates as $certificate) {
$signers[] = $certificate['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);
for ($s = 0; $s < count($signatures); $s++) {
$path_to_pkcs7 = pathinfo($path_to_pdf, PATHINFO_FILENAME) . $s . '.pkcs7';
file_put_contents($path_to_pkcs7, $signatures[$s]);
echo shell_exec("openssl pkcs7 -inform DER -in $path_to_pkcs7 -print_certs -text");
}
exit;
*/
var_dump(who_signed($path_to_pdf));
?>

最新更新