2024 年 PHP Conference Japan

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" => "joes@example.com", // 鍵值語法
"From: HQ <ceo@example.com>", // 索引語法
"Subject" => "僅限閱覽")
)) {
// 訊息已簽署 - 發送它!
exec(ini_get("sendmail_path") . " < signed.txt");
}
?>

新增註解

使用者貢獻的註解 12 則註解

匿名
11 年前
關於 $flags 參數的說明:PKCS7_BINARY 有兩個作用
* 將 LF 轉換為 CR+LF,如 https://php.dev.org.tw/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)
if ($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() );

if ( file_exists( $file_msg ) ) unlink( $file_msg );
if ( file_exists( $file_sign ) ) unlink( $file_sign );
if ( file_exists( $file_bsign ) ) unlink( $file_bsign );
if ( file_exists( $file_enc ) ) unlink( $file_enc );

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

// 簽署 HTML 表單訊息的部分內容
openssl_pkcs7_sign(
$file_msg,
$file_sign,
$MY_PUBLIC_KEY,
array( $MY_PRIVATE_KEY, "" ), /// 私鑰、密碼
array(),
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 );
if ( $fp ) {
$fp = fopen( $file_bsign, "w" );
fclose($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,
array(),
$file_enc,
$PAYPAL_PUBLIC_KEY,

PKCS7_BINARY,
OPENSSL_CIPHER_3DES );
$data = file_get_contents( $file_enc );

$data = substr($data, strpos($data, $begin)+strlen($begin));
if ( file_exists( $file_msg ) ) unlink( $file_msg );
if ( file_exists( $file_sign ) ) unlink( $file_sign );
if ( file_exists( $file_bsign ) ) unlink( $file_bsign );
if ( file_exists( $file_enc ) ) unlink( $file_enc );

$data = "-----BEGIN PKCS7-----\n". trim( $data ) . "\n-----END PKCS7-----";
}
// 清理
return( $data );
Maciej_Niemir at ilim dot poznan dot pl

21 年前

<?php

$data
= <<<EOD

Testing 123

This is a test

Test

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","您的密碼"),
array(
"To" => "收件者 <recipients@mail.com>",
"From" => "寄件者 <sender@mail.com>",
"Subject" => "訂單通知 - 測試"),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 檔案> -nokeys -out <pem 憑證檔案>

pkcs12 -in <pfx 檔案> -nocerts -nodes -out <pem 金鑰檔案>

接下來,從 IE 匯出根 CA 憑證為 Base-64 *.cer 格式,並將檔案重新命名為 *.pem

這樣就完成了!
ungdi at hotmail dot com
14 年前
在許多關於郵件簽章或加密的討論中,沒有人真正討論過同時進行郵件簽章和加密的痛苦。

根據 RFC 2311,您可以先加密後簽章,或者先簽章後加密。然而,這取決於您編程的用戶端。根據我的經驗,Outlook 2000 偏好先加密後簽章。而在 Outlook 2003 中,則是先簽章後加密。一般來說,您會希望先簽章後加密,因為從傳統郵件的角度來看,這似乎是最合乎邏輯的。您會先簽署一封信,然後再將其放入信封中。如果您以某些用戶端不喜歡的順序執行,它們可能會出現問題,因此您可能需要進行一些實驗。

當您執行第一個函式時,請勿在 headers 陣列參數中放入任何標頭,您需要將其放入要執行的第二個函式中。如果您將標頭放在第一個函式中,第二個函式會將其從郵件伺服器中隱藏。您不希望這樣。這裡我會先簽名,然後加密。

<?php
// 設定郵件標頭。
$headers = array("To" => "someone@nowhere.net",
"From" => "noone@somewhere.net",
"Subject" => "已簽名和加密的訊息。");

// 先簽署訊息
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) 函式,該函式可能已移除標頭和訊息內容之間的間距。

當您取得已簽名的訊息並將其饋送到加密部分時,您必須記住,行距也必須作為訊息主體的一部分饋送!如果您打算先簽名然後加密,請勿將簽名輸出的標頭作為 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,會接受最安全的「簽章 -> 加密 -> 再簽章」方法。

為什麼?

第一次簽章驗證了訊息,表明確實是由您撰寫的。然後,郵件會被加密,以便只有收件人才能開啟和閱讀。第二次簽章則透過識別加密者就是加密郵件的人來確保機密性,這是一個發送給解密者的訊息。這是最安全的方法。這確保了:訊息的不可否認性(第一次簽章)、機密性(加密)和上下文完整性 [訊息預期是發送給您的](第二次簽章)。

如果您只簽章然後加密,就無法保證(除了信件內容外,標頭是以純文字的形式放在郵件外部)該郵件是由原始寄件者發送給您的。例如:

Bob 簽署了一封情書並將其加密給 Amy,內容只有「我愛妳。-- Bob」。Amy 解密後,看到了訊息(並開了個玩笑),使用 John 的公鑰將訊息轉發給 John,重新加密,但沒有竄改訊息內容,保持簽章有效。這讓 Amy 看起來像是 Bob 寄了情書給 John,而且 Bob 愛 John,因為您無法驗證加密過程中是誰寄出的。這不是您想要的!

這也類似於有人拿了一份政府文件,自己把它放進信封裡,並在寄件人地址寫上政府地址,然後寄給您。您知道這封信是由政府撰寫的,但您不確定政府是否直接寄給您,還是被拆開後轉寄的。

雖然先加密後簽章會有問題,但這實際上是在信封上簽名。我知道您寄出了它,但訊息真的是您發出的嗎?或者您只是轉寄?

「簽章 - 加密 - 再簽章」方法會讓第一個簽章表明您知道訊息的撰寫者是誰,加密是為了防止其他人閱讀,再次簽章則表示訊息沒有被轉寄,並且寄件人打算將郵件發送給您。

只要確保郵件的標頭是在最後一步套用,而不是在第二步或第三步。

有關此情況下安全性和完整性風險的更多資訊,請閱讀此網頁: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";
// Add the attachment to the message
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 ( {
files left to be attached} );
$boddy .= "--$boundary--\n";

// Save message to a file
$msg = 'msg.txt';
$signed = 'signed.txt';
$fp = fopen($msg, "w");
fwrite($fp, $boddy);
fclose($fp);

// Sign it
if (openssl_pkcs7_sign($msg, $signed, 'file://cert.pem',
array(
'file://key.pem', 'test'),
array(
"To" => "joes@example.com", // keyed syntax
"From: HQ <ceo@example.com>", // indexed syntax
"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
return( $data );
值得一提的是,我在使用 openssl_pkcs7_sign() 產生的簽章時,Mozilla 1.4 或 Outlook Express 6 都難以驗證,直到我在要簽章的訊息開頭加上一個換行符號 (\n) 才解決。我不確定原因,但加上換行符號後,所有問題都消失了。
dmitri at gmx dot net
18 年前
範例程式碼

<?php

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

您已授權我支出 10,000 元的餐費。
執行長
EOF;

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

$headers = array("From" => "me@email.com");

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("you@email.com", "已簽署的訊息", $parts[1], $parts[0]);

echo
"郵件已送出";

?>
To Top