PHP Conference Japan 2024

openssl_pkcs7_sign

(PHP 4 >= 4.0.6, PHP 5, PHP 7, PHP 8)

openssl_pkcs7_sign签署 S/MIME 消息

描述

openssl_pkcs7_sign(
    字符串 $input_filename,
    字符串 $output_filename,
    OpenSSLCertificate|字符串 $certificate,
    #[\SensitiveParameter] OpenSSLAsymmetricKey|OpenSSLCertificate|数组|字符串 $private_key,
    ?数组 $headers,
    整数 $flags = PKCS7_DETACHED,
    ?字符串 $untrusted_certificates_filename = null
): 布尔值

openssl_pkcs7_sign() 获取名为 input_filename 的文件的内容,并使用由 certificateprivate_key 参数指定的证书及其匹配的私钥对其进行签名。

参数

input_filename

您打算进行数字签名的输入文件。

output_filename

将数字签名写入的文件。

certificate

用于对 input_filename 进行数字签名的 X.509 证书。有关有效值的列表,请参阅 密钥/证书参数

private_key

private_key 是与 certificate 对应的私钥。有关有效值的列表,请参阅 公钥/私钥参数

headers

headers 是一个标头数组,将在数据签名后附加到数据之前(有关此参数的格式的更多信息,请参阅 openssl_pkcs7_encrypt())。

flags

flags 可用于更改输出 - 请参阅 PKCS7 常量

untrusted_certificates_filename

untrusted_certificates_filename 指定包含一组要包含在签名中的额外证书的文件的名称,例如,这些证书可用于帮助接收方验证您使用的证书。

返回值

成功时返回 true,失败时返回 false

变更日志

版本 描述
8.0.0 certificate 现在接受 OpenSSLCertificate 实例;以前,接受类型为 OpenSSL X.509 CSR资源
8.0.0 private_key 现在接受 OpenSSLAsymmetricKeyOpenSSLCertificate 实例;以前,接受类型为 OpenSSL keyOpenSSL X.509 CSR资源

示例

示例 #1 openssl_pkcs7_sign() 示例

<?php
// 您要签名的消息,以便接收者可以确定发送者确实是您
// 发送的
$data = <<<EOD

您有权使用 10,000 美元用于晚餐费用。

首席执行官
EOD;
// 将消息保存到文件
$fp = fopen("msg.txt", "w");
fwrite($fp, $data);
fclose($fp);
// 对其进行加密
if (openssl_pkcs7_sign("msg.txt", "signed.txt", "file://mycert.pem",
array(
"file://mycert.pem", "mypassphrase"),
array(
"To" => "[email protected]", // 使用键值语法
"From: HQ <[email protected]>", // 使用索引语法
"Subject" => "Eyes only")
)) {
// 消息已签名 - 发送它!
exec(ini_get("sendmail_path") . " < signed.txt");
}
?>

添加注释

用户贡献的注释 12 条注释

匿名用户
11 年前
关于 $flags 参数的说明:PKCS7_BINARY 有 2 个作用
* 将 LF 转换为 CR+LF,如 https://php.net/manual/en/openssl.pkcs7.flags.php 中所述
* 它创建了一个不透明的 pkcs7 签名 (p7m)

如果您想阻止 LF->CR+LF 转换 *并且* 仍然具有分离的签名 (p7s),请使用 PKCS7_BINARY | PKCS7_DETACHED(两个标志都已设置)。

如果已签名的消息已经是 MIME 多部分,则如上所述使用这两个标志似乎是正确地组装消息的解决方案。在没有任何标志的情况下,显然只转换了一些 LF 字符。在特定场景(本地 MTA 是 Postfix,然后消息通过另一台机器上的 sendmail 传输)中,MIME 边界在 sendmail 中会被混淆。但是,如果本地 MTA 是 sendmail,则这种情况似乎不会发生。
jcmichot at usenet-fr dot net
7 年前
由于缺乏示例,以下代码可能对某些人有用。

# openssl_pkcs7_sign() 和 openssl_pkcs7_encrypt() 的演示代码,用于为 PayPal EWP 签名和加密。
#
# 生成并自签名证书
# % openssl genrsa -out my-private-key.pem 2048
# % openssl req -new -key my-private-key.pem -x509 -days 3650 -out my-public-key.pem
#

function demo_paypal_encrypt( $webform_hash )
{
$MY_PUBLIC_KEY = "file:///usr/local/etc/paypal/my-public-key.pem";
$MY_PRIVATE_KEY = "file:///usr/local/etc/paypal/my-private-key.pem";
$PAYPAL_PUBLIC_KEY = "file:///usr/local/etc/paypal/paypal_cert_pem.txt";

// 为 PayPal 支持分配构建说明
$webform_hash['bn']= 'MyWebRef.PHP_EWP2';

$data = "";
foreach ($webform_hash as $key => $value)
如果 ($value != "")
$data .= "$key=$value\n";

$file_msg = sprintf( "/tmp/pp-msg-%d.txt", getmypid() );
$file_sign = sprintf( "/tmp/pp-sign-%d.mpem", getmypid() );
$file_bsign = sprintf( "/tmp/pp-sign-%d.der", getmypid() );
$file_enc = sprintf( "/tmp/pp-enc-%d.txt", getmypid() );

如果 ( file_exists( $file_msg ) ) unlink( $file_msg );
如果 ( file_exists( $file_sign ) ) unlink( $file_sign );
如果 ( file_exists( $file_bsign ) ) unlink( $file_bsign );
如果 ( file_exists( $file_enc ) ) unlink( $file_enc );

$fp = fopen( $file_msg, "w" );
如果 ( $fp ) {
fwrite($fp, $data );
fclose($fp);
}

// HTML表单消息的签名部分
openssl_pkcs7_sign(
$file_msg,
$file_sign,
$MY_PUBLIC_KEY,
数组( $MY_PRIVATE_KEY, "" ), /// 私钥,密码
数组(),
PKCS7_BINARY
);

// 将PEM转换为DER
$pem_data = file_get_contents( $file_sign );
$begin = "Content-Transfer-Encoding: base64";
$pem_data = trim( substr($pem_data, strpos($pem_data, $begin)+strlen($begin)) );
$der = base64_decode( $pem_data );

$fp = fopen( $file_bsign, "w" );
如果 ( $fp ) {
fwrite($fp, $der );
fclose($fp);
}

// 你可以通过以下命令验证DER签名的正确性
// % openssl smime -verify -CAfile $MY_PUBLIC_KEY -inform DER -in $file_bsign

// 使用PayPal公钥加密消息
openssl_pkcs7_encrypt(
$file_bsign,
$file_enc,
$PAYPAL_PUBLIC_KEY,
数组(),
PKCS7_BINARY,
OPENSSL_CIPHER_3DES );

$data = file_get_contents( $file_enc );
$data = substr($data, strpos($data, $begin)+strlen($begin));
$data = "-----BEGIN PKCS7-----\n". trim( $data ) . "\n-----END PKCS7-----";

// 清理
如果 ( file_exists( $file_msg ) ) unlink( $file_msg );
如果 ( file_exists( $file_sign ) ) unlink( $file_sign );
如果 ( file_exists( $file_bsign ) ) unlink( $file_bsign );
如果 ( file_exists( $file_enc ) ) unlink( $file_enc );

返回( $data );
}
Maciej_Niemir at ilim dot poznan dot pl
21年前
此命令在使用IIS的WIN32系统上无法正常工作。IIS SMTP服务器(以及Outlook)无法正确解释邮件。原因是UNIX和WINDOWS以不同的方式解释“换行”ASCII码。

下面我提供了一个改进的代码

<?php

$data
= <<<EOD

测试 123

这是一个测试

测试

EOD;

//将消息保存到文件中
$fp = fopen("msg.txt","w");
fwrite($fp,$data);
fclose($fp);

//使用发送者的密钥对消息进行签名
openssl_pkcs7_sign("msg.txt", "signed.eml", "file://c:/max/cert.pem",
array(
"file://c:/max/priv.pem","your_password"),
array(
"To" => "recipient <[email protected]>",
"From" => "sender <[email protected]>",
"Subject" => "Order Notification - Test"),PKCS7_DETACHED,"c:\max\extra_cert.pem");

$file_arry = file("signed.eml");
$file = join ("", $file_arry);
$message = preg_replace("/\r\n|\r|\n/", "\r\n", $file);

$fp = fopen("c:\Inetpub\mailroot\Pickup\signed.eml", "wb");
flock($fp, 2);
fputs($fp, $message);
flock($fp, 3);
fclose($fp);

?>

此外,如果你想使用用Windows创建的密钥,你应该将它们(从IE)导出为PKCS#12文件(*.pfx)。

从以下地址安装OpenSSLWin32
http://www.shininglightpro.com/search.php?searchname=Win32+OpenSSL

执行:openssl.exe

输入以下命令

pkcs12 -in <pfx-file> -nokeys -out <pem-certs-file>

pkcs12 -in <pfx-file> -nocerts -nodes -out <pem-key-file>

接下来,从IE根CA证书导出为Base-64 *.cer并将其重命名为*.pem

就是这样!
ungdi at hotmail dot com
14年前
在关于单独签名或加密电子邮件的众多讨论中,没有一个真正讨论过同时签名和加密电子邮件的难题。

根据RFC 2311,你可以先加密后签名或先签名后加密。但是,这取决于你正在为其编程的客户端。根据我的经验,在Outlook 2000中,它更倾向于先加密后签名。而在Outlook 2003中,它是先签名后加密。通常,你希望先签名后加密,因为它从信件的角度来看似乎更合乎逻辑。你首先签署一封信,然后将其放入信封中。某些客户端如果以它们不喜欢的顺序执行操作,就会抱怨,因此你可能需要尝试一下。

当你执行第一个函数时,不要在headers数组参数中放入任何头信息,你希望将其放入你想要执行的第二个函数中。如果你在第一个函数中放入头信息,第二个函数会将其隐藏在邮件服务器之外。你不想那样。这里我将先签名后加密。

<?php
// 设置邮件头信息。
$headers = array("To" => "[email protected]",
"From" => "[email protected]",
"Subject" => "A signed and encrypted message.");

// 首先对消息进行签名
openssl_pkcs7_sign("msg.txt","signed.txt",
"signing_cert.pem",array("private_key.pem",
"password"),array());

// 获取公钥证书。
$pubkey = file_get_contents("cert.pem");

//加密消息,现在放入头信息。
openssl_pkcs7_encrypt("signed.txt", "enc.txt",
$pubkey,$headers,0,1);

$data = file_get_contents("enc.txt");

// 分离头信息和正文,以便与mail函数一起使用
// 不幸的是,这是必需的,否则我们将有两个头信息集
// 并且电子邮件客户端不会解码附件
$parts = explode("\n\n", $data, 2);

// 发送邮件(Headers参数中的头信息将覆盖为To和Subject参数生成的那些
// 头信息)
mail($mail, $subject, $parts[1], $parts[0]);
?>

请注意,如果你使用一个从磁盘获取数据并在程序中的另一个函数中使用该数据的函数,请记住你可能使用了explode("\n\n",$data,2)函数,该函数可能删除了头信息和消息内容之间的空格。

当你获取已签名的消息并将其馈送到加密部分时,你必须记住,行间距也必须作为消息正文的一部分馈送!如果你计划先签名后加密,不要将签名生成的header输出作为headers数组参数的一部分馈送到加密函数中!签名的输出应保持为要加密的消息正文的一部分。(如果你正在执行先加密后签名的反向操作,也是如此。)签名和加密函数的示例,将其制作成一个可重用的例程,然后调用它来签名和加密消息。

这是错误的!
<?php
// 数组的[0]包含消息头信息。数组的[1]包含消息的已签名正文。
$signedOutputArray = signMessage($inputMessage,$headers);

// 数组的[0]包含消息头信息和签名。
// 数组的[1]包含未包含签名头信息的加密消息正文。
$signedAndEncryptedArray = encryptMessage($signedOutputArray[1],
$signedOutputArray[0]);

mail($emailAddr,$subject,$signedAndEncryptedArray[1],
$signedAndEncryptedArray[0]);
?>

这是正确的!
<?php
// 数组的[0]包含签名头信息。
// 数组的[1]包含消息的已签名正文。
$signedOutputArray = signMessage($inputMessage,array());

// 数组的[0]包含消息头信息。
// 数组的[1]包含已签名消息及其签名头信息的加密内容。
$signedAndEncryptedArray =
encryptMessage($signedOutputArray[0] . "\n\n" . $signedOutputArray[1],$headers);

mail($emailAddr,$subject,$signedAndEncryptedArray[1],
$signedAndEncryptedArray[0]);
?>
yurchenko dot anton at gmail dot com
15年前
在尝试查找错误原因时,我也花费了数小时
"获取私钥错误"。

有时出现此错误,有时不出现。

我的解决方案是为openssl_pkcs7_sign的每个参数使用realpath()。在我的情况下,代码如下

<?php
$Certif_path
= 'certificate/mycertificate.pem';

$clearfile = "certificate/random_name";
$encfile = $clearfile . ".enc";
$clearfile = $clearfile . ".txt";

// ----
// -- 使用 $clearfile 填充要签名的邮件内容 ...
// ----

openssl_pkcs7_sign(realpath($clearfile),
realpath('.').'/'.$encfile, // 由于 $encfile 还不存在,所以不能使用 realpath($encfile);
'file://'.realpath($Certif_path),

array(
'file://'.realpath($Certif_path), PUBLIC_KEY),

array(
"To" => TO_EMAIL,
"From" => FROM_EMAIL,
"Subject" => ""),

PKCS7_DETACHED));

?>
spam at isag dot melbourne
5年前
如果要使用带头部的 mail() 函数,则需要在将签名版本嵌入正文之前对其进行修改,否则头部和边界将最终出现在邮件中。

<?php
openssl_pkcs7_sign
($basedir . 'email.txt', $basedir . 'signed.txt', 'file://' . $basedir . 'cert.pem', array('file://' . $basedir . 'key.pem', $keypass), array('To' => $smime_to, 'From' => $smime_from, 'Subject' => $smime_subject));
if (
preg_match('/To: [^\r\n]+(\r|\n)+(From: [^\r\n]+(\r|\n)+)Subject: [^\r\n]+(\r|\n)+MIME-Version: [^\r\n]+(\r|\n)+(Content-Type: [^\r\n]+)(\r|\n)+/', file_get_contents($basedir . 'signed.txt'), $matches))
{
$result = mail($smime_to, $smime_subject, str_replace($matches[0], '', file_get_contents($basedir . 'signed.txt')), $mailheaders . $matches[2] . $matches[6]);
}
?>

删除了签名的头部($matches[0]),并使用 From(非 mail() 函数的一部分)和 Content-Type(分别为 $matches[2] / $matches[6])更新了头部。
ungdi at hotmail dot com
17年前
我想对我的上一条注释进行修改。一些客户端更喜欢以某种特定的顺序对消息进行签名和加密(如果两者都需要)。较新的电子邮件客户端,例如 Thunderbird 和 Outlook 2003,将接受最安全的方法“签名 -> 加密 -> 再次签名”。

为什么?

第一次签名验证消息,表明您确实编写了它。然后邮件被加密,以便只有收件人可以打开和阅读它。然后第二次签名通过识别加密的人是加密它的人来确保机密性,即发送给解密人的邮件。这是最安全的方法。这确保了:邮件的不可否认性(第一次签名)、机密性(加密)和上下文完整性[您是预期收件人](第二次签名)。

如果您只签名然后加密,则无法保证(除了邮件内容外,头部以纯文本形式放置在邮件外部)该邮件是原始发送者发送给您的。例如

鲍勃签署了一封情书并将其加密发送给艾米,只说“我爱你。——鲍勃”。艾米解密了它,看到了消息(并开了一个玩笑),并使用约翰的公钥将消息转发给约翰,重新加密,但没有篡改消息内容,保持签名有效。这使得艾米可以使其看起来像鲍勃发送了一封情书给约翰,并且鲍勃爱约翰,因为您无法验证在加密过程中谁发送了它。这不是您想要的!

这也类似于有人拿着一份政府文件,自己将其放入信封中,并在回邮地址中写上政府的地址,然后将其发送给您。您知道这封信是政府写的,但您不能确定政府是否直接将其发送给您,或者是否被打开并转发了。

虽然加密然后签名存在问题,但这实际上是在普通邮件信件的信封上签名。我知道您发送了它,但邮件真的是您发送的吗?或者您正在转发它?

签名 - 加密 - 再次签名方法将使第一次签名表明您知道邮件的作者是此人,将其加密以防止其他人阅读它,再次签名以表明邮件未被转发,并且发送者打算将邮件发送给您。

只需确保邮件的头部在最后一步应用,而不是第二步或第三步。

有关此情况的安全性和完整性风险的更多信息,请阅读此网页:http://world.std.com/~dtd/sign_encrypt/sign_encrypt7.html
maarten at xolphin dot nl
19年前
还可以对包含附件的消息进行签名。一种简单的方法是

<?php
$boundary
= md5(uniqid(time()));
$boddy = "MIME-Version: 1.0\n";
$boddy .= "Content-Type: multipart/mixed; boundary=\"" . $boundary. "\"\n";
$boddy .= "Content-Transfer-Encoding: quoted-printable\n\n";
$boddy .= "This is a multi-part message in MIME format.\n\n";
$boddy .= "--$boundary\n";
$boddy .= "Content-Type: text/plain; charset=\"iso-8859-1\"\n";
$boddy .= "Content-Transfer-Encoding: quoted-printable\n\n";
$boddy .= $EmailText . "\n\n";
// 将附件添加到邮件中
do {
$boddy .= "--$boundary\n";
$boddy .= "Content-Type: application/pdf; name=\"FileName\"\n";
$boddy .= "Content-Transfer-Encoding: base64\n";
$boddy .= "Content-Disposition: attachment;\n\n";
$boddy .= chunk_split(base64_encode($file)) . "\n\n";
} while ( {
要附加的文件} );
$boddy .= "--$boundary--\n";

// 将邮件保存到文件
$msg = 'msg.txt';
$signed = 'signed.txt';
$fp = fopen($msg, "w");
fwrite($fp, $boddy);
fclose($fp);

// 对其进行签名
if (openssl_pkcs7_sign($msg, $signed, 'file://cert.pem',
array(
'file://key.pem', 'test'),
array(
"To" => "[email protected]", // 使用键语法
"From: HQ <[email protected]>", // 使用索引语法
"Subject" => "Eyes only"), PKCS7_DETACHED, 'intermediate_cert.pem' )) {
exec(ini_get('sendmail_path') . ' < ' . $signed);
}
?>

也可以通过结合使用 PEAR 包 Mail_Mime 和 openssl_pkcs7_sign 来实现。
del at babel dot com dot au
22年前
上面示例中显示的“mycert.pem”参数不正确。您必须传递包含 PEM 编码证书或密钥的字符串,或者以 file://path/to/file.pem 表示法的文件位置。请参阅 OpenSSL 函数页面(此页面上方的页面)上的注释。
meint dot post at bigfoot dot com
23年前

如果您想将PKCS7签名/验证与浏览器集成,并且仅限于Internet Explorer(或Netscape + ActiveX插件)不是问题,您可以考虑使用Capicom。它是一个免费组件,可以在MSDN网站上找到。
php at toyingwithfate dot com
21年前
可能值得注意的是,在将要签名的消息开头添加一个换行符(\n)之前,我很难让Mozilla 1.4或Outlook Express 6验证由openssl_pkcs7_sign()生成的签名。不确定为什么会出现这种情况,但一旦我进行了更改,所有问题都消失了。
dmitri at gmx dot net
18年前
工作示例

<?php

$data
= <<< EOF
Content-Type: text/plain;
charset="us-ascii"
Content-Transfer-Encoding: 7bit

您有权使用10,000用于晚餐费用。
CEO
EOF;

$fp = fopen("msg.txt", "w");
fwrite($fp, $data);
fclose($fp);

$headers = array("From" => "[email protected]");

openssl_pkcs7_sign("msg.txt", "signed.txt", "file://email.pem", array("file://email.pem", "123456"), $headers);

$data = file_get_contents("signed.txt");

$parts = explode("\n\n", $data, 2);

mail("[email protected]", "Signed message.", $parts[1], $parts[0]);

echo
"邮件已发送";

?>
To Top