我有一个应用程序需要从 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 来实现这一点很复杂(我想说甚至是不可能的,但谁知道呢)。
首先请阅读有关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;
我已经帮你调整好了,剩下的请自行调整。
这是我在 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"
}
}
类似于 @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"
}
欲了解更多信息,请看我的这个问题:
我用过iText,发现它非常可靠,我强烈推荐它。 您始终可以从 PHP 调用 Java 代码作为“微服务”。