897 lines
32 KiB
PHP
897 lines
32 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2002-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* @author Mike Cochrane <mike@graftonhall.co.nz>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Crypt
|
|
*/
|
|
|
|
/**
|
|
* Library to interact with the OpenSSL library and implement S/MIME.
|
|
*
|
|
* @author Mike Cochrane <mike@graftonhall.co.nz>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @copyright 2002-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Crypt
|
|
*/
|
|
class Horde_Crypt_Smime extends Horde_Crypt
|
|
{
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $params Configuration parameters:
|
|
* - temp: (string) Location of temporary directory.
|
|
*/
|
|
public function __construct($params = array())
|
|
{
|
|
parent::__construct($params);
|
|
}
|
|
|
|
/**
|
|
* Verify a passphrase for a given private key.
|
|
*
|
|
* @param string $private_key The user's private key.
|
|
* @param string $passphrase The user's passphrase.
|
|
*
|
|
* @return boolean Returns true on valid passphrase, false on invalid
|
|
* passphrase.
|
|
*/
|
|
public function verifyPassphrase($private_key, $passphrase)
|
|
{
|
|
$res = is_null($passphrase)
|
|
? openssl_pkey_get_private($private_key)
|
|
: openssl_pkey_get_private($private_key, $passphrase);
|
|
|
|
return is_resource($res);
|
|
}
|
|
|
|
/**
|
|
* Encrypt text using S/MIME.
|
|
*
|
|
* @param string $text The text to be encrypted.
|
|
* @param array $params The parameters needed for encryption.
|
|
* See the individual _encrypt*() functions for
|
|
* the parameter requirements.
|
|
*
|
|
* @return string The encrypted message.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function encrypt($text, $params = array())
|
|
{
|
|
/* Check for availability of OpenSSL PHP extension. */
|
|
$this->checkForOpenSSL();
|
|
|
|
if (isset($params['type'])) {
|
|
if ($params['type'] === 'message') {
|
|
return $this->_encryptMessage($text, $params);
|
|
} elseif ($params['type'] === 'signature') {
|
|
return $this->_encryptSignature($text, $params);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypt text via S/MIME.
|
|
*
|
|
* @param string $text The text to be smime decrypted.
|
|
* @param array $params The parameters needed for decryption.
|
|
* See the individual _decrypt*() functions for
|
|
* the parameter requirements.
|
|
*
|
|
* @return string The decrypted message.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function decrypt($text, $params = array())
|
|
{
|
|
/* Check for availability of OpenSSL PHP extension. */
|
|
$this->checkForOpenSSL();
|
|
|
|
if (isset($params['type'])) {
|
|
if ($params['type'] === 'message') {
|
|
return $this->_decryptMessage($text, $params);
|
|
} elseif (($params['type'] === 'signature') ||
|
|
($params['type'] === 'detached-signature')) {
|
|
return $this->_decryptSignature($text, $params);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a signature using via S/MIME.
|
|
*
|
|
* @param string $text The multipart/signed data to be verified.
|
|
* @param mixed $certs Either a single or array of root certificates.
|
|
*
|
|
* @return stdClass Object with the following elements:
|
|
* <pre>
|
|
* cert - (string) The certificate of the signer stored in the message (in
|
|
* PEM format).
|
|
* email - (string) The email of the signing person.
|
|
* msg - (string) Status string.
|
|
* verify - (boolean) True if certificate was verified.
|
|
* </pre>
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function verify($text, $certs)
|
|
{
|
|
/* Check for availability of OpenSSL PHP extension. */
|
|
$this->checkForOpenSSL();
|
|
|
|
/* Create temp files for input/output. */
|
|
$input = $this->_createTempFile('horde-smime');
|
|
$output = $this->_createTempFile('horde-smime');
|
|
|
|
/* Write text to file */
|
|
file_put_contents($input, $text);
|
|
unset($text);
|
|
|
|
$root_certs = array();
|
|
if (!is_array($certs)) {
|
|
$certs = array($certs);
|
|
}
|
|
foreach ($certs as $file) {
|
|
if (file_exists($file)) {
|
|
$root_certs[] = $file;
|
|
}
|
|
}
|
|
|
|
$ob = new stdClass;
|
|
|
|
if (!empty($root_certs) &&
|
|
(openssl_pkcs7_verify($input, 0, $output, $root_certs) === true)) {
|
|
/* Message verified */
|
|
$ob->msg = Horde_Crypt_Translation::t("Message verified successfully.");
|
|
$ob->verify = true;
|
|
} else {
|
|
/* Try again without verfying the signer's cert */
|
|
$result = openssl_pkcs7_verify($input, PKCS7_NOVERIFY, $output);
|
|
|
|
if ($result === -1) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Verification failed - an unknown error has occurred."));
|
|
} elseif ($result === false) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Verification failed - this message may have been tampered with."));
|
|
}
|
|
|
|
$ob->msg = Horde_Crypt_Translation::t("Message verified successfully but the signer's certificate could not be verified.");
|
|
$ob->verify = false;
|
|
}
|
|
|
|
$ob->cert = file_get_contents($output);
|
|
$ob->email = $this->getEmailFromKey($ob->cert);
|
|
|
|
return $ob;
|
|
}
|
|
|
|
/**
|
|
* Extract the contents from signed S/MIME data.
|
|
*
|
|
* @param string $data The signed S/MIME data.
|
|
* @param string $sslpath The path to the OpenSSL binary. @deprecated and
|
|
* not used, just for backwards-compatibility.
|
|
*
|
|
* @return string The contents embedded in the signed data.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function extractSignedContents($data, $sslpath = null)
|
|
{
|
|
/* Check for availability of OpenSSL PHP extension. */
|
|
$this->checkForOpenSSL();
|
|
|
|
/* Create temp files for input/output/certs. */
|
|
$input = $this->_createTempFile('horde-smime');
|
|
$output = $this->_createTempFile('horde-smime');
|
|
$certs = $this->_createTempFile('horde-smime');
|
|
|
|
/* Write text to file. */
|
|
file_put_contents($input, $data);
|
|
unset($data);
|
|
|
|
/* Unfortunatelly the openssl_pkcs7_verify method does not behave as
|
|
* explained in openssl extensions documentation, it does not return
|
|
* content if no certs specified. Therefore, we need to use double
|
|
* verification which the first one tries to extract certificats then
|
|
* the second to extract content. */
|
|
if (openssl_pkcs7_verify($input, PKCS7_NOVERIFY, $certs) === true &&
|
|
openssl_pkcs7_verify($input, PKCS7_NOVERIFY, $certs, array(), $certs, $output) === true) {
|
|
$ret = file_get_contents($output);
|
|
if ($ret) {
|
|
return $ret;
|
|
}
|
|
}
|
|
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("OpenSSL error: Could not extract data from signed S/MIME part."));
|
|
}
|
|
|
|
/**
|
|
* Sign a MIME part using S/MIME. This produces S/MIME Version 3.2
|
|
* compatible data (see RFC 5751 [3.4]).
|
|
*
|
|
* @param Horde_Mime_Part $mime_part The object to sign.
|
|
* @param array $params The parameters required for signing.
|
|
*
|
|
* @return Horde_Mime_Part A signed MIME part object.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function signMIMEPart($mime_part, $params)
|
|
{
|
|
/* Sign the part as a message */
|
|
$message = $this->encrypt(
|
|
$mime_part->toString(array(
|
|
'headers' => true,
|
|
'canonical' => true
|
|
)),
|
|
$params
|
|
);
|
|
|
|
/* Break the result into its components */
|
|
$mime_message = Horde_Mime_Part::parseMessage(
|
|
$message,
|
|
array('forcemime' => true)
|
|
);
|
|
|
|
$smime_sign = $mime_message->getPart('2');
|
|
$smime_sign->setDescription(
|
|
Horde_Crypt_Translation::t("S/MIME Signature")
|
|
);
|
|
$smime_sign->setTransferEncoding('base64', array('send' => true));
|
|
|
|
$smime_part = new Horde_Mime_Part();
|
|
$smime_part->setType('multipart/signed');
|
|
$smime_part->setContents(
|
|
"This is a cryptographically signed message in MIME format.\n"
|
|
);
|
|
$smime_part->setContentTypeParameter(
|
|
'protocol',
|
|
'application/pkcs7-signature'
|
|
);
|
|
$smime_part->setContentTypeParameter(
|
|
'micalg', $mime_message->getContentTypeParameter('micalg')
|
|
);
|
|
$smime_part->addPart($mime_part);
|
|
$smime_part->addPart($smime_sign);
|
|
|
|
return $smime_part;
|
|
}
|
|
|
|
/**
|
|
* Encrypt a MIME part using S/MIME. This produces S/MIME Version 3.2
|
|
* compatible data (see RFC 5751 [3.3]).
|
|
*
|
|
* @param Horde_Mime_Part $mime_part The object to encrypt.
|
|
* @param array $params The parameters required for
|
|
* encryption.
|
|
*
|
|
* @return Horde_Mime_Part An encrypted MIME part object.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function encryptMIMEPart($mime_part, $params = array())
|
|
{
|
|
/* Sign the part as a message */
|
|
$message = $this->encrypt(
|
|
$mime_part->toString(array(
|
|
'headers' => true,
|
|
'canonical' => true
|
|
)),
|
|
$params
|
|
);
|
|
|
|
$msg = new Horde_Mime_Part();
|
|
$msg->setCharset($this->_params['email_charset']);
|
|
$msg->setHeaderCharset('UTF-8');
|
|
$msg->setDescription(
|
|
Horde_Crypt_Translation::t("S/MIME Encrypted Message")
|
|
);
|
|
$msg->setDisposition('inline');
|
|
$msg->setType('application/pkcs7-mime');
|
|
$msg->setContentTypeParameter('smime-type', 'enveloped-data');
|
|
$msg->setContents(
|
|
substr($message, strpos($message, "\n\n") + 2),
|
|
array('encoding' => 'base64')
|
|
);
|
|
|
|
return $msg;
|
|
}
|
|
|
|
/**
|
|
* Encrypt a message in S/MIME format using a public key.
|
|
*
|
|
* @param string $text The text to be encrypted.
|
|
* @param array $params The parameters needed for encryption.
|
|
* - type: (string) [REQUIRED] 'message'.
|
|
* - pubkey: (mixed) [REQUIRED] Public key/cert or array of public
|
|
* keys/certs.
|
|
*
|
|
* @return string The encrypted message.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
protected function _encryptMessage($text, $params)
|
|
{
|
|
/* Check for required parameters. */
|
|
if (!isset($params['pubkey'])) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t(
|
|
"A public S/MIME key is required to encrypt a message."
|
|
));
|
|
}
|
|
|
|
/* Create temp files for input/output. */
|
|
$input = $this->_createTempFile('horde-smime');
|
|
$output = $this->_createTempFile('horde-smime');
|
|
|
|
/* Store message in file. */
|
|
file_put_contents($input, $text);
|
|
unset($text);
|
|
|
|
/* Encrypt the document. */
|
|
$ciphers = array(
|
|
// SHOULD- support (RFC 5751 [2.7])
|
|
OPENSSL_CIPHER_3DES
|
|
);
|
|
if (defined('OPENSSL_CIPHER_AES_128_CBC')) {
|
|
// MUST support (RFC 5751 [2.7])
|
|
array_unshift($ciphers, OPENSSL_CIPHER_AES_128_CBC);
|
|
// SHOULD+ support (RFC 5751 [2.7])
|
|
array_unshift($ciphers, OPENSSL_CIPHER_AES_192_CBC);
|
|
array_unshift($ciphers, OPENSSL_CIPHER_AES_256_CBC);
|
|
}
|
|
|
|
foreach ($ciphers as $val) {
|
|
$success = openssl_pkcs7_encrypt(
|
|
$input,
|
|
$output,
|
|
$params['pubkey'],
|
|
array(),
|
|
0,
|
|
$val
|
|
);
|
|
|
|
if ($success && ($result = file_get_contents($output))) {
|
|
return $this->_fixContentType($result, 'message');
|
|
}
|
|
}
|
|
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t(
|
|
"Could not S/MIME encrypt message."
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Sign a message in S/MIME format using a private key.
|
|
*
|
|
* @param string $text The text to be signed.
|
|
* @param array $params The (string) parameters needed for signing:
|
|
* - 'certs': Additional signing certs (Optional)
|
|
* - 'passphrase': Passphrase for key (REQUIRED)
|
|
* - 'privkey': Private key (REQUIRED)
|
|
* - 'pubkey': Public key (REQUIRED)
|
|
* - 'sigtype': Determine the signature type to use. (Optional):
|
|
* - 'cleartext': Make a clear text signature
|
|
* - 'detach': Make a detached signature (DEFAULT)
|
|
* - 'type': 'signature' (REQUIRED)
|
|
*
|
|
* @return string The signed message.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
protected function _encryptSignature($text, $params)
|
|
{
|
|
/* Check for required parameters. */
|
|
if (!isset($params['pubkey']) ||
|
|
!isset($params['privkey']) ||
|
|
!array_key_exists('passphrase', $params)) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public S/MIME key, private S/MIME key, and passphrase are required to sign a message."));
|
|
}
|
|
|
|
/* Create temp files for input/output/certificates. */
|
|
$input = $this->_createTempFile('horde-smime');
|
|
$output = $this->_createTempFile('horde-smime');
|
|
$certs = $this->_createTempFile('horde-smime');
|
|
|
|
/* Store message in temporary file. */
|
|
file_put_contents($input, $text);
|
|
unset($text);
|
|
|
|
/* Store additional certs in temporary file. */
|
|
if (!empty($params['certs'])) {
|
|
file_put_contents($certs, $params['certs']);
|
|
}
|
|
|
|
/* Determine the signature type to use. */
|
|
$flags = (isset($params['sigtype']) && ($params['sigtype'] == 'cleartext'))
|
|
? PKCS7_TEXT
|
|
: PKCS7_DETACHED;
|
|
|
|
$privkey = (is_null($params['passphrase'])) ? $params['privkey'] : array($params['privkey'], $params['passphrase']);
|
|
|
|
if (empty($params['certs'])) {
|
|
$res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags);
|
|
} else {
|
|
$res = openssl_pkcs7_sign($input, $output, $params['pubkey'], $privkey, array(), $flags, $certs);
|
|
}
|
|
|
|
if (!$res) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not S/MIME sign message."));
|
|
}
|
|
|
|
/* Output from openssl_pkcs7_sign may contain both \n and \r\n EOLs.
|
|
* Canonicalize to \r\n. */
|
|
$fp = fopen($output, 'r');
|
|
stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
|
|
stream_filter_append($fp, 'horde_eol');
|
|
$data = stream_get_contents($fp);
|
|
fclose($fp);
|
|
|
|
return $this->_fixContentType($data, 'signature');
|
|
}
|
|
|
|
/**
|
|
* Decrypt an S/MIME encrypted message using a private/public keypair
|
|
* and a passhprase.
|
|
*
|
|
* @param string $text The text to be decrypted.
|
|
* @param array $params The parameters needed for decryption.
|
|
* <pre>
|
|
* Parameters:
|
|
* ===========
|
|
* 'type' => 'message' (REQUIRED)
|
|
* 'pubkey' => public key. (REQUIRED)
|
|
* 'privkey' => private key. (REQUIRED)
|
|
* 'passphrase' => Passphrase for Key. (REQUIRED)
|
|
* </pre>
|
|
*
|
|
* @return string The decrypted message.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
protected function _decryptMessage($text, $params)
|
|
{
|
|
/* Check for required parameters. */
|
|
if (!isset($params['pubkey']) ||
|
|
!isset($params['privkey']) ||
|
|
!array_key_exists('passphrase', $params)) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("A public S/MIME key, private S/MIME key, and passphrase are required to decrypt a message."));
|
|
}
|
|
|
|
/* Create temp files for input/output. */
|
|
$input = $this->_createTempFile('horde-smime');
|
|
$output = $this->_createTempFile('horde-smime');
|
|
|
|
/* Store message in file. */
|
|
file_put_contents($input, $text);
|
|
unset($text);
|
|
|
|
$privkey = is_null($params['passphrase'])
|
|
? $params['privkey']
|
|
: array($params['privkey'], $params['passphrase']);
|
|
if (openssl_pkcs7_decrypt($input, $output, $params['pubkey'], $privkey)) {
|
|
return file_get_contents($output);
|
|
}
|
|
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Could not decrypt S/MIME data."));
|
|
}
|
|
|
|
/**
|
|
* Sign and Encrypt a MIME part using S/MIME.
|
|
*
|
|
* @param Horde_Mime_Part $mime_part The object to sign and encrypt.
|
|
* @param array $sign_params The parameters required for
|
|
* signing. @see _encryptSignature().
|
|
* @param array $encrypt_params The parameters required for
|
|
* encryption.
|
|
* @see _encryptMessage().
|
|
*
|
|
* @return mixed A Horde_Mime_Part object that is signed and encrypted.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function signAndEncryptMIMEPart($mime_part, $sign_params = array(),
|
|
$encrypt_params = array())
|
|
{
|
|
$part = $this->signMIMEPart($mime_part, $sign_params);
|
|
return $this->encryptMIMEPart($part, $encrypt_params);
|
|
}
|
|
|
|
/**
|
|
* Convert a PEM format certificate to readable HTML version.
|
|
*
|
|
* @param string $cert PEM format certificate.
|
|
*
|
|
* @return string HTML detailing the certificate.
|
|
*/
|
|
public function certToHTML($cert)
|
|
{
|
|
$fieldnames = array(
|
|
/* Common Fields */
|
|
'description' => Horde_Crypt_Translation::t("Description"),
|
|
'emailAddress' => Horde_Crypt_Translation::t("Email Address"),
|
|
'commonName' => Horde_Crypt_Translation::t("Common Name"),
|
|
'organizationName' => Horde_Crypt_Translation::t("Organisation"),
|
|
'organizationalUnitName' => Horde_Crypt_Translation::t("Organisational Unit"),
|
|
'countryName' => Horde_Crypt_Translation::t("Country"),
|
|
'stateOrProvinceName' => Horde_Crypt_Translation::t("State or Province"),
|
|
'localityName' => Horde_Crypt_Translation::t("Location"),
|
|
'streetAddress' => Horde_Crypt_Translation::t("Street Address"),
|
|
'telephoneNumber' => Horde_Crypt_Translation::t("Telephone Number"),
|
|
'surname' => Horde_Crypt_Translation::t("Surname"),
|
|
'givenName' => Horde_Crypt_Translation::t("Given Name"),
|
|
|
|
/* X590v3 Extensions */
|
|
'extendedKeyUsage' => Horde_Crypt_Translation::t("Extended Key Usage"),
|
|
'basicConstraints' => Horde_Crypt_Translation::t("Basic Constraints"),
|
|
'subjectAltName' => Horde_Crypt_Translation::t("Subject Alternative Name"),
|
|
'subjectKeyIdentifier' => Horde_Crypt_Translation::t("Subject Key Identifier"),
|
|
'certificatePolicies' => Horde_Crypt_Translation::t("Certificate Policies"),
|
|
'crlDistributionPoints' => Horde_Crypt_Translation::t("CRL Distribution Points"),
|
|
'keyUsage' => Horde_Crypt_Translation::t("Key Usage")
|
|
);
|
|
|
|
$details = $this->parseCert($cert);
|
|
|
|
$text = '<pre class="fixed">';
|
|
|
|
/* Subject (a/k/a Certificate Owner) */
|
|
$text .= '<strong>' . Horde_Crypt_Translation::t("Certificate Owner")
|
|
. ':</strong>';
|
|
|
|
foreach ($details['subject'] as $key => $value) {
|
|
$text .= sprintf(
|
|
"\n %s: %s",
|
|
htmlspecialchars(
|
|
isset($fieldnames[$key]) ? $fieldnames[$key] : $key
|
|
),
|
|
htmlspecialchars($value)
|
|
);
|
|
}
|
|
$text .= "\n";
|
|
|
|
/* Issuer */
|
|
$text .=
|
|
'<strong>' . Horde_Crypt_Translation::t("Issuer") . ':</strong>';
|
|
|
|
foreach ($details['issuer'] as $key => $value) {
|
|
$text .= sprintf(
|
|
"\n %s: %s",
|
|
htmlspecialchars(
|
|
isset($fieldnames[$key]) ? $fieldnames[$key] : $key
|
|
),
|
|
htmlspecialchars($value)
|
|
);
|
|
}
|
|
$text .= "\n";
|
|
|
|
/* Dates */
|
|
$text .=
|
|
'<strong>' . Horde_Crypt_Translation::t("Validity") . ':</strong>'
|
|
. sprintf(
|
|
"\n %s: %s",
|
|
Horde_Crypt_Translation::t("Not Before"),
|
|
strftime(
|
|
'%x %X', $details['validity']['notbefore']->getTimestamp()
|
|
)
|
|
)
|
|
. sprintf(
|
|
"\n %s: %s",
|
|
Horde_Crypt_Translation::t("Not After"),
|
|
strftime(
|
|
'%x %X', $details['validity']['notafter']->getTimestamp()
|
|
)
|
|
);
|
|
|
|
/* X509v3 extensions */
|
|
if (!empty($details['extensions'])) {
|
|
$text .= "\n" . '<strong>'
|
|
. Horde_Crypt_Translation::t("X509v3 extensions")
|
|
. ':</strong>';
|
|
|
|
foreach ($details['extensions'] as $key => $value) {
|
|
$value = $this->_implodeValues($value);
|
|
$text .= sprintf(
|
|
"\n %s:\n%s",
|
|
htmlspecialchars(
|
|
isset($fieldnames[$key]) ? $fieldnames[$key] : $key
|
|
),
|
|
$value
|
|
);
|
|
}
|
|
}
|
|
$text .= "\n";
|
|
|
|
/* Certificate Details */
|
|
$text .= '<strong>' . Horde_Crypt_Translation::t("Certificate Details")
|
|
. ':</strong>'
|
|
. sprintf(
|
|
"\n %s: %d",
|
|
Horde_Crypt_Translation::t("Version"),
|
|
$details['version']
|
|
)
|
|
. sprintf(
|
|
"\n %s: %d",
|
|
Horde_Crypt_Translation::t("Serial Number"),
|
|
$details['serialNumber']
|
|
);
|
|
|
|
return $text . '</pre>';
|
|
}
|
|
|
|
/**
|
|
* Formats a multi-value cert field.
|
|
*
|
|
* @param array|string $values A cert field value.
|
|
* @param integer $indent The indention level.
|
|
*
|
|
* @return string The formatted cert field value(s).
|
|
*/
|
|
protected function _implodeValues($values)
|
|
{
|
|
if (!is_array($values)) {
|
|
$values = explode("\n", trim($values));
|
|
}
|
|
foreach ($values as &$value) {
|
|
$value = str_repeat(' ', 4) . htmlspecialchars($value);
|
|
}
|
|
return implode("\n", $values);
|
|
}
|
|
|
|
/**
|
|
* Extract the contents of a PEM format certificate to an array.
|
|
*
|
|
* @param string $cert PEM format certificate.
|
|
*
|
|
* @return array All extractable information about the certificate.
|
|
*/
|
|
public function parseCert($cert)
|
|
{
|
|
$data = openssl_x509_parse($cert, false);
|
|
if (!$data) {
|
|
throw new Horde_Crypt_Exception(sprintf(Horde_Crypt_Translation::t("Error parsing S/MIME certficate: %s"), openssl_error_string()));
|
|
}
|
|
|
|
$details = array(
|
|
'extensions' => $data['extensions'],
|
|
'issuer' => $data['issuer'],
|
|
'serialNumber' => $data['serialNumber'],
|
|
'subject' => $data['subject'],
|
|
'validity' => array(
|
|
'notafter' => new DateTime('@' . $data['validTo_time_t']),
|
|
'notbefore' => new DateTime('@' . $data['validFrom_time_t'])
|
|
),
|
|
'version' => $data['version']
|
|
);
|
|
|
|
// Add additional fields for BC purposes.
|
|
$details['certificate'] = $details;
|
|
|
|
$bc_changes = array(
|
|
'emailAddress' => 'Email',
|
|
'commonName' => 'CommonName',
|
|
'organizationName' => 'Organisation',
|
|
'organizationalUnitName' => 'OrganisationalUnit',
|
|
'countryName' => 'Country',
|
|
'stateOrProvinceName' => 'StateOrProvince',
|
|
'localityName' => 'Location',
|
|
'streetAddress' => 'StreetAddress',
|
|
'telephoneNumber' => 'TelephoneNumber',
|
|
'surname' => 'Surname',
|
|
'givenName' => 'GivenName'
|
|
);
|
|
foreach (array('issuer', 'subject') as $val) {
|
|
foreach (array_keys($details[$val]) as $key) {
|
|
if (isset($bc_changes[$key])) {
|
|
$details['certificate'][$val][$bc_changes[$key]] = $details[$val][$key];
|
|
unset($details['certificate'][$val][$key]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $details;
|
|
}
|
|
|
|
/**
|
|
* Decrypt an S/MIME signed message using a public key.
|
|
*
|
|
* @param string $text The text to be verified.
|
|
* @param array $params The parameters needed for verification.
|
|
*
|
|
* @return string The verification message.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
protected function _decryptSignature($text, $params)
|
|
{
|
|
throw new Horde_Crypt_Exception('_decryptSignature() ' . Horde_Crypt_Translation::t("not yet implemented"));
|
|
}
|
|
|
|
/**
|
|
* Check for the presence of the OpenSSL extension to PHP.
|
|
*
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function checkForOpenSSL()
|
|
{
|
|
if (!Horde_Util::extensionExists('openssl')) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("The openssl module is required for the Horde_Crypt_Smime:: class."));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract the email address from a public key.
|
|
*
|
|
* @param string $key The public key.
|
|
*
|
|
* @return mixed Returns the first email address found, or null if
|
|
* there are none.
|
|
*/
|
|
public function getEmailFromKey($key)
|
|
{
|
|
$key_info = openssl_x509_parse($key);
|
|
if (!is_array($key_info)) {
|
|
return null;
|
|
}
|
|
|
|
if (isset($key_info['subject'])) {
|
|
if (isset($key_info['subject']['Email'])) {
|
|
return $key_info['subject']['Email'];
|
|
} elseif (isset($key_info['subject']['emailAddress'])) {
|
|
return $key_info['subject']['emailAddress'];
|
|
}
|
|
}
|
|
|
|
// Check subjectAltName per http://www.ietf.org/rfc/rfc3850.txt
|
|
if (isset($key_info['extensions']['subjectAltName'])) {
|
|
$names = preg_split('/\s*,\s*/', $key_info['extensions']['subjectAltName'], -1, PREG_SPLIT_NO_EMPTY);
|
|
foreach ($names as $name) {
|
|
if (strpos($name, ':') === false) {
|
|
continue;
|
|
}
|
|
list($kind, $value) = explode(':', $name, 2);
|
|
if (Horde_String::lower($kind) == 'email') {
|
|
return $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Convert a PKCS 12 encrypted certificate package into a private key,
|
|
* public key, and any additional keys.
|
|
*
|
|
* @param string $pkcs12 The PKCS 12 data.
|
|
* @param array $params The parameters needed for parsing.
|
|
* <pre>
|
|
* Parameters:
|
|
* ===========
|
|
* 'sslpath' => The path to the OpenSSL binary. (REQUIRED)
|
|
* 'password' => The password to use to decrypt the data. (Optional)
|
|
* 'newpassword' => The password to use to encrypt the private key.
|
|
* (Optional)
|
|
* </pre>
|
|
*
|
|
* @return stdClass An object.
|
|
* 'private' - The private key in PEM format.
|
|
* 'public' - The public key in PEM format.
|
|
* 'certs' - An array of additional certs.
|
|
* @throws Horde_Crypt_Exception
|
|
*/
|
|
public function parsePKCS12Data($pkcs12, $params)
|
|
{
|
|
/* Check for availability of OpenSSL PHP extension. */
|
|
$this->checkForOpenSSL();
|
|
|
|
if (!isset($params['sslpath'])) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("No path to the OpenSSL binary provided. The OpenSSL binary is necessary to work with PKCS 12 data."));
|
|
}
|
|
$sslpath = escapeshellcmd($params['sslpath']);
|
|
|
|
/* Create temp files for input/output. */
|
|
$input = $this->_createTempFile('horde-smime');
|
|
$output = $this->_createTempFile('horde-smime');
|
|
|
|
$ob = new stdClass;
|
|
|
|
/* Write text to file */
|
|
file_put_contents($input, $pkcs12);
|
|
unset($pkcs12);
|
|
|
|
/* Extract the private key from the file first. */
|
|
$cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nocerts';
|
|
if (isset($params['password'])) {
|
|
$cmdline .= ' -passin stdin';
|
|
if (!empty($params['newpassword'])) {
|
|
$cmdline .= ' -passout stdin';
|
|
} else {
|
|
$cmdline .= ' -nodes';
|
|
}
|
|
} else {
|
|
$cmdline .= ' -nodes';
|
|
}
|
|
|
|
if ($fd = popen($cmdline, 'w')) {
|
|
fwrite($fd, $params['password'] . "\n");
|
|
if (!empty($params['newpassword'])) {
|
|
fwrite($fd, $params['newpassword'] . "\n");
|
|
}
|
|
pclose($fd);
|
|
} else {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to smime binary."));
|
|
}
|
|
|
|
$ob->private = trim(file_get_contents($output));
|
|
if (empty($ob->private)) {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Password incorrect"));
|
|
}
|
|
|
|
/* Extract the client public key next. */
|
|
$cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -clcerts';
|
|
if (isset($params['password'])) {
|
|
$cmdline .= ' -passin stdin';
|
|
}
|
|
|
|
if ($fd = popen($cmdline, 'w')) {
|
|
fwrite($fd, $params['password'] . "\n");
|
|
pclose($fd);
|
|
} else {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to smime binary."));
|
|
}
|
|
|
|
$ob->public = trim(file_get_contents($output));
|
|
|
|
/* Extract the CA public key next. */
|
|
$cmdline = $sslpath . ' pkcs12 -in ' . $input . ' -out ' . $output . ' -nokeys -cacerts';
|
|
if (isset($params['password'])) {
|
|
$cmdline .= ' -passin stdin';
|
|
}
|
|
|
|
if ($fd = popen($cmdline, 'w')) {
|
|
fwrite($fd, $params['password'] . "\n");
|
|
pclose($fd);
|
|
} else {
|
|
throw new Horde_Crypt_Exception(Horde_Crypt_Translation::t("Error while talking to smime binary."));
|
|
}
|
|
|
|
$ob->certs = trim(file_get_contents($output));
|
|
|
|
return $ob;
|
|
}
|
|
|
|
/**
|
|
* The Content-Type parameters PHP's openssl_pkcs7_* functions return are
|
|
* deprecated. Fix these headers to the correct ones (see RFC 2311).
|
|
*
|
|
* @param string $text The PKCS7 data.
|
|
* @param string $type Is this 'message' or 'signature' data?
|
|
*
|
|
* @return string The PKCS7 data with the correct Content-Type parameter.
|
|
*/
|
|
protected function _fixContentType($text, $type)
|
|
{
|
|
if ($type == 'message') {
|
|
$from = 'application/x-pkcs7-mime';
|
|
$to = 'application/pkcs7-mime';
|
|
} else {
|
|
$from = 'application/x-pkcs7-signature';
|
|
$to = 'application/pkcs7-signature';
|
|
}
|
|
return str_replace('Content-Type: ' . $from, 'Content-Type: ' . $to, $text);
|
|
}
|
|
|
|
/**
|
|
* Create a temporary file that will be deleted at the end of this
|
|
* process.
|
|
*
|
|
* @param string $descrip Description string to use in filename.
|
|
* @param boolean $delete Delete the file automatically?
|
|
*
|
|
* @return string Filename of a temporary file.
|
|
*/
|
|
protected function _createTempFile($descrip = 'horde-crypt', $delete = true)
|
|
{
|
|
return Horde_Util::getTempFile($descrip, $delete, $this->_params['temp'], true);
|
|
}
|
|
|
|
}
|