如何使用PHP从PDF中检索数字签名信息?

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

我有一个应用程序需要从 PDF 文件“附加”的数字签名中检索一些数据(签名者姓名)。

我只找到了 Java 和 C# 中使用 iText 类 AcroFields 方法 GetSignatureNames 的示例

编辑:我尝试过使用dump_data_fields和generate_fpdf使用pdftk,结果是(不幸的是):

/Fields [
<<
/V /dftk.com.lowagie.text.pdf.PdfDictionary@3048918
/T (Signature1)
>>]

FieldType: Signature
FieldName: Signature1
FieldFlags: 0
FieldJustification: Left

提前致谢!

php pdf digital-signature pkcs#7
4个回答
19
投票

嗯,仅使用 PHP 来实现这一点很复杂(我想说甚至是不可能的,但谁知道呢)。

首先请阅读有关Adobe PDF数字签名的文章

其次,读完这篇文章你就会知道,根据 /ByteRange[a b c d] 指标,签名存储在 b 和 c 字节之间

第三,我们可以从文档中提取b和c,然后提取签名本身(指南说它将是十六进制解码的PKCS7#对象)。

<?php

 $content = file_get_contents('test.pdf');

 $regexp = '#ByteRange\[\s*(\d+) (\d+) (\d+)#'; // subexpressions are used to extract b and c

 $result = [];
 preg_match_all($regexp, $content, $result);

 // $result[2][0] and $result[3][0] are b and c
 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('test.pdf', 'rb')) {
         $signature = stream_get_contents($stream, $end - $start - 2, $start + 1); // because we need to exclude < and > from start and end

         fclose($stream);
     }

     file_put_contents('signature.pkcs7', hex2bin($signature));
}

第四,第三步之后,我们在文件signature.pkcs7中得到了PKCS#7对象。不幸的是,我不知道使用 PHP 从签名中提取信息的方法。所以你必须能够运行 shell 命令才能使用 openssl

openssl pkcs7 -in signature.pkcs7 -inform DER -print_certs > info.txt

在文件 info.txt 中运行此命令后,您将获得证书链。最后一项就是您需要的。您可以查看文件的结构并解析所需的数据。

另请参阅这个问题这个问题这个主题

编辑于2017-10-09 我故意建议你看看正是这个问题 您可以根据自己的需要调整代码。

use ASN1\Type\Constructed\Sequence;
use ASN1\Element;
use X509\Certificate\Certificate;       

$seq = Sequence::fromDER($binaryData);
$signed_data = $seq->getTagged(0)->asExplicit()->asSequence();
// ExtendedCertificatesAndCertificates: https://tools.ietf.org/html/rfc2315#section-6.6
$ecac = $signed_data->getTagged(0)->asImplicit(Element::TYPE_SET)->asSet();
// ExtendedCertificateOrCertificate: https://tools.ietf.org/html/rfc2315#section-6.5
$ecoc = $ecac->at($ecac->count() - 1);
$cert = Certificate::fromASN1($ecoc->asSequence());
$commonNameValue = $cert->tbsCertificate()->subject()->toString();
echo $commonNameValue;

我已经帮你调整好了,剩下的请自行调整。


3
投票

这是我在 PHP7 中的工作代码:

<?php


require_once('vendor/autoload.php');

use Sop\ASN1\Type\Constructed\Sequence;
use Sop\ASN1\Element;
use Sop\X509\Certificate\Certificate;  



$currentFile = "./upload/test2.pdf";


$content = file_get_contents($currentFile);


$regexp = '/ByteRange\ \[\s*(\d+) (\d+) (\d+)/'; // subexpressions are used to extract b and c

$result = [];
preg_match_all($regexp, $content, $result);

// $result[2][0] and $result[3][0] are b and c
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($currentFile, 'rb')) {
        $signature = stream_get_contents($stream, $end - $start - 2, $start + 1); // because we need to exclude < and > from start and end

        fclose($stream);
    }

    
    $binaryData = hex2bin($signature);
    
    $seq = Sequence::fromDER($binaryData);
    $signed_data = $seq->getTagged(0)->asExplicit()->asSequence();
    // ExtendedCertificatesAndCertificates: https://tools.ietf.org/html/rfc2315#section-6.6
    $ecac = $signed_data->getTagged(0)->asImplicit(Element::TYPE_SET)->asSet();
    // ExtendedCertificateOrCertificate: https://tools.ietf.org/html/rfc2315#section-6.5
    $ecoc = $ecac->at($ecac->count() - 1);
    $cert = Certificate::fromASN1($ecoc->asSequence());
    $commonNameValue = $cert->tbsCertificate()->subject()->toString();
    echo $commonNameValue;

    
}

作曲:

{
    "require": {
        "sop/asn1": "^4.1",
        "sop/x509": "^0.7.1"
    }
}

1
投票

类似于 @Denis Alimov 提出的解决方案,但仅使用 PHP 函数(而不是 openssl 命令)并且没有 Composer 依赖项:

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

对于某些

test1.pdf
,仅由一个人签名(我们称她为
ALICE
),此脚本应返回:

array(1) {
  [0]=>
  string(5) "ALICE"
}

对于某些

test2.pdf
,由两个人签名(我们称他们为
BOB
CAROL
),此脚本应返回:

array(2) {
  [0]=>
  string(3) "BOB"
  [1]=>
  string(5) "CAROL"
}

欲了解更多信息,请看我的这个问题:


-2
投票

我用过iText,发现它非常可靠,我强烈推荐它。 您始终可以从 PHP 调用 Java 代码作为“微服务”。

© www.soinside.com 2019 - 2024. All rights reserved.