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

问题描述 投票:0回答:1

阅读完这个 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
,由两个人签名(让我们称他们为
BOB
CAROL
),这个脚本返回:

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.pdf
pdfsig返回:

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

对于相同的

test2.pdf
pdfsig返回:

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

我做错了什么?我的意思是,我需要做什么才能正确识别签署 PDF 文件的人?

php pdf openssl digital-signature pkcs#7
1个回答
0
投票

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

  • 一个 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));
?>
© www.soinside.com 2019 - 2024. All rights reserved.