PHPのお勉強!

PHP TOP

openssl_pkcs7_sign

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

openssl_pkcs7_signS/MIME メッセージに署名する

説明

openssl_pkcs7_sign(
    string $input_filename,
    string $output_filename,
    OpenSSLCertificate|string $certificate,
    #[\SensitiveParameter] OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key,
    ?array $headers,
    int $flags = PKCS7_DETACHED,
    ?string $untrusted_certificates_filename = null
): bool

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 は、 OpenSSLAsymmetricKey または OpenSSLCertificate クラスのインスタンスを受け入れるようになりました。 これより前のバージョンでは、 OpenSSL key または OpenSSL X.509 CSR 型のリソースを受け入れていました。

例1 openssl_pkcs7_sign() の例

<?php
// 受信者が送信者を確認できるように署名したいメッセージ
$data = <<<EOD

You have my authorization to spend $10,000 on dinner expenses.

The CEO
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" => "Eyes only")
)) {
// メッセージは署名されました。送信しましょう!
exec(ini_get("sendmail_path") . " < signed.txt");
}
?>

add a note

User Contributed Notes 12 notes

up
3
Anonymous
11 years ago
A note about the $flags parameter: PKCS7_BINARY has 2 effects:
* converts LF to CR+LF, as described in http://www.php.net/manual/en/openssl.pkcs7.flags.php
* it creates an opaque pkcs7 signature (p7m)

If you want to prevent the LF->CR+LF conversion *and* still have a detached signature (p7s), use PKCS7_BINARY | PKCS7_DETACHED (both flags are set).

If the signed message is already MIME multi-part, using both flags as described above seems to be the right solution to assemble the message properly. Without any flags, apparently only some of the LF characters are converted. In a specific scenario (the local MTA is Postfix and then the message goes through sendmail on another machine), the MIME boundaries get scrambled in sendmail. However, this doesn't seem to happen if the local MTA is sendmail.
up
1
jcmichot at usenet-fr dot net
7 years ago
Due to lack of example the following code may be useful to some.

# Demo code for openssl_pkcs7_sign() and openssl_pkcs7_encrypt() to sign and encrypt for Paypal EWP.
#
# generate and self sign certificat
# % 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";

//Assign Build Notation for PayPal Support
$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);
}

// sign part of html form message
openssl_pkcs7_sign(
$file_msg,
$file_sign,
$MY_PUBLIC_KEY,
array( $MY_PRIVATE_KEY, "" ), /// private key, password
array(),
PKCS7_BINARY
);

// convert PEM to 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" );
if ( $fp ) {
fwrite($fp, $der );
fclose($fp);
}

// you could verify correct DER signature by:
// % openssl smime -verify -CAfile $MY_PUBLIC_KEY -inform DER -in $file_bsign

//encrypt the message, with Paypal public key
openssl_pkcs7_encrypt(
$file_bsign,
$file_enc,
$PAYPAL_PUBLIC_KEY,
array(),
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-----";

// cleanup
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 );

return( $data );
}
up
2
Maciej_Niemir at ilim dot poznan dot pl
21 years ago
This command doesn't work correctly on WIN32 with IIS. Mails aren?t interpreted correctly by IIS SMTP Server (and by Outlook too). The reason is that UNIX and WINDOWS interpret the ?enter to the next line? ascii code in a different way.

Below I present an improved code:

<?php

$data
= <<<EOD

Testing 123

This is a test

Test

EOD;

//save the message to a file
$fp = fopen("msg.txt","w");
fwrite($fp,$data);
fclose($fp);

//sign the message using the sender's keys
openssl_pkcs7_sign("msg.txt", "signed.eml", "file://c:/max/cert.pem",
array(
"file://c:/max/priv.pem","your_password"),
array(
"To" => "recipient <recipients@mail.com>",
"From" => "sender <sender@mail.com>",
"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);

?>

Besides, if you want to use the keys created with Windows, you should export them (from IE) to the form of PKCS#12 file (*.pfx).

Install OpenSSLWin32 from
http://www.shininglightpro.com/search.php?searchname=Win32+OpenSSL

execute: openssl.exe

enter the commands:

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

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

Next export from IE Root CA certificate as Base-64 *.cer and rename the file to *.pem

And that's all!
up
1
ungdi at hotmail dot com
14 years ago
Amongst the many discussions about signing or encrypting email by itself, none really discuss the pain of having an email BOTH signed AND encrypted.

According to RFC 2311, you can encrypt then sign or sign then encrypt. However, it depends on the client in which you are programming for. In my experience, in Outlook 2000, it prefers it Encrypt then Sign. While in Outlook 2003, it is Sign then Encrypt. Generally, you want Sign then Encrypt, as it seems most logical from a snail-mail piece point of view. You first sign a letter than put it in an envelope. Certain clients complain if you do it in an order it does not like, so you may want to experiement with it.

When you perform the first function, do NOT put in any headers in the headers array parameters, you want to put it in the SECOND function you want to perform. If you put the headers in the first function, the second function will hide it from the mail servers. You do not want that. Here I will sign then encrypt.

<?php
// Setup mail headers.
$headers = array("To" => "someone@nowhere.net",
"From" => "noone@somewhere.net",
"Subject" => "A signed and encrypted message.");

// Sign the message first
openssl_pkcs7_sign("msg.txt","signed.txt",
"signing_cert.pem",array("private_key.pem",
"password"),array());

// Get the public key certificate.
$pubkey = file_get_contents("cert.pem");

//encrypt the message, now put in the headers.
openssl_pkcs7_encrypt("signed.txt", "enc.txt",
$pubkey,$headers,0,1);

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

// separate header and body, to use with mail function
// unfortunate but required, else we have two sets of headers
// and the email client doesn't decode the attachment
$parts = explode("\n\n", $data, 2);

// send mail (headers in the Headers parameter will override those
// generated for the To & Subject parameters)
mail($mail, $subject, $parts[1], $parts[0]);
?>

Note that if you use a function that picks up the data from the disk to be used in another function in your program, remember that you may have used the explode("\n\n",$data,2) function which may have removed the spacing between the header and the message content.

When you take the signed message and feed it in to the encryption part, you have to remember that the line spacing must also be fed AS PART OF THE MESSAGE BODY! If you plan to sign then encrypt, do not feed the header output from the signing into the encrypting as part of the headers array parameter! The output of the signing should stay as part of the message body being encrypted. (And the same is true if you are doing the reverse of encrypting then signing.) An example of both the signing and encryption function made in to a routine for reusability, and then called to sign and encrypt a message.

THIS IS WRONG!:
<?php
// [0] of Array contains headers of message. [1] of Array contains signed body of message.
$signedOutputArray = signMessage($inputMessage,$headers);

// [0] of Array contains headers of message and the signing.
// [1] of Array contains encrypted body of message without the signing header.
$signedAndEncryptedArray = encryptMessage($signedOutputArray[1],
$signedOutputArray[0]);

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

THIS IS CORRECT!
<?php
// [0] of Array contains headers of signing.
// [1] of Array contains signed body of message.
$signedOutputArray = signMessage($inputMessage,array());

// [0] of Array contains headers of message.
// [1] of Array contains encrypted contents of both the signed message and its headers of the signing.
$signedAndEncryptedArray =
encryptMessage($signedOutputArray[0] . "\n\n" . $signedOutputArray[1],$headers);

mail($emailAddr,$subject,$signedAndEncryptedArray[1],
$signedAndEncryptedArray[0]);
?>
up
1
yurchenko dot anton at gmail dot com
15 years ago
I also spent hours when trying to find the reason of error:
"error getting private key".

Sometimes this error appeared, sometimes not.

My solution is using the realpath() for every parameter of openssl_pkcs7_sign. In my case the code looks like:

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

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

// ----
// -- fill $clearfile with the mail to be signed ...
// ----

openssl_pkcs7_sign(realpath($clearfile),
realpath('.').'/'.$encfile, // because $encfile does not exist yet we cannot use realpath($encfile);
'file://'.realpath($Certif_path),

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

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

PKCS7_DETACHED));

?>
up
0
spam at isag dot melbourne
5 years ago
If you want to use mail() with headers, then you'll need to alter the signed version before embedding in the body or the headers and boundaries will end up in the message.

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

With the signed headers removed ($matches[0]) and the headers updated with From (not part of mail()) and Content-Type ($matches[2] / $matches[6] respectively).
up
0
ungdi at hotmail dot com
17 years ago
I would like to make a modification from my previous note. Some clients prefer a certain order in which messages should be signed and encrypted (if both is desired). Newer email clients, such as Thunderbird and Outlook 2003 will accept the most secure method of "sign -> encrypt -> sign again".

Why?

The first signing authenticates the message saying that you did indeed write it. Then the email is encrypted so that only the recipient can open and read it. Then the second signing ensure confidentiality by identifying that the person encrypting is the one whom encrypted it, a message intended for the decrypting person. This is the most secure method. This ensures: Non-Repudiation of message (first sign), Confidentiality (encrypt), and Context Integrity [you were intended to be addressed] (second sign).

If you only sign then encrypt, there is no way you can guarantee that (aside from the contents of the letter, headers are placed in plain text outside the message) that the message was intended for you by the original sender. For example:

Bob signs a love letter and encrypts it to Amy saying only "I love you. -- Bob". Amy decrypts it, sees the message (and plays a joke) and forwards the message to John using John's public key, re-encrypting, but not tampering with the message contents keeping the signature valid. This allows Amy to make it look like Bob sent John a love letter and that Bob loves John, as you cannot verify whom sent it during encryption. That is not what you want!

This is also analogous to someone taking a government document, put it in an envelope themselves and write the government address in the return address and send it to you. You know the letter is written by the government, but you don't know for sure whether the government sent it to you directly or was opened and relayed.

While encrypting then signing has a problem, this is affectively signing on the envelope of a snail mail piece. I know you sent it, but is the message really from you? Or are you forwarding it?

Sign - Encrypt - Sign Again method will make the first sign show that you know the writer of the message is the person, encrypt it to keep others from reading it, sign again to indicate the message was not relayed and that the sender intended to sent the mail to address you.

Just make sure the headers of the mail is applied in the last step and not the second or third step.

For more information about the security and integrity risks of this situation, please read this web page: http://world.std.com/~dtd/sign_encrypt/sign_encrypt7.html
up
0
maarten at xolphin dot nl
19 years ago
It is also possible to sign message including attachments. An easy way to do this:

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

The same can be established by using the PEAR package Mail_Mime in combination with openssl_pkcs7_sign.
up
0
del at babel dot com dot au
22 years ago
The "mycert.pem" parameters as shown in the example above are not correct. You either have to pass a string containing the PEM encoded certificate or key, or the location of a file in file://path/to/file.pem notation. See the comments on the OpenSSL functions page (the page above this one).
up
0
meint dot post at bigfoot dot com
23 years ago
If you want to integrate PKCS7 signing/verifying with a browser and it's not a problem that it's only Internet Explorer (or Netscape + ActiveX plugin) you can look at Capicom. It's a free component and available at the MSDN website.
up
-1
php at toyingwithfate dot com
21 years ago
It's probably worth noting that I had a great deal of difficulty getting either Mozilla 1.4 or Outlook Express 6 to verify signatures generated by openssl_pkcs7_sign() until I added a newline (\n) to the beginning of the message I was signing. Not sure why that is, but as soon as I made that change all problems disappeared.
up
-2
dmitri at gmx dot net
18 years ago
Working example:

<?php

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

You have my authorization to spend 10,000 on dinner expenses.
The CEO
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", "Signed message.", $parts[1], $parts[0]);

echo
"Email sent";

?>