Skip to content

openssl: certificate fingerprinting support #464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 8, 2013
133 changes: 133 additions & 0 deletions ext/openssl/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_openssl_x509_export, 0, 0, 2)
ZEND_ARG_INFO(0, notext)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_openssl_x509_fingerprint, 0, 0, 1)
ZEND_ARG_INFO(0, x509)
ZEND_ARG_INFO(0, method)
ZEND_ARG_INFO(0, raw_output)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_openssl_x509_check_private_key, 0)
ZEND_ARG_INFO(0, cert)
ZEND_ARG_INFO(0, key)
Expand Down Expand Up @@ -443,6 +449,7 @@ const zend_function_entry openssl_functions[] = {
PHP_FE(openssl_x509_checkpurpose, arginfo_openssl_x509_checkpurpose)
PHP_FE(openssl_x509_check_private_key, arginfo_openssl_x509_check_private_key)
PHP_FE(openssl_x509_export, arginfo_openssl_x509_export)
PHP_FE(openssl_x509_fingerprint, arginfo_openssl_x509_fingerprint)
PHP_FE(openssl_x509_export_to_file, arginfo_openssl_x509_export_to_file)

/* PKCS12 funcs */
Expand Down Expand Up @@ -1665,6 +1672,121 @@ PHP_FUNCTION(openssl_x509_export)
}
/* }}} */

static int php_openssl_x509_fingerprint(X509 *peer, const char *method, zend_bool raw, char **out, int *out_len)
{
unsigned char md[EVP_MAX_MD_SIZE];
const EVP_MD *mdtype;
int n;

if (!(mdtype = EVP_get_digestbyname(method))) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unknown signature algorithm");
return FAILURE;
} else if (!X509_digest(peer, mdtype, md, &n)) {
php_error_docref(NULL TSRMLS_CC, E_ERROR, "Could not generate signature");
return FAILURE;
}

if (raw) {
*out_len = n;
*out = estrndup(md, n);
} else {
*out_len = n * 2;
*out = emalloc(*out_len + 1);

make_digest_ex(*out, md, n);
}

return SUCCESS;
}

static int php_x509_fingerprint_cmp(X509 *peer, const char *method, const char *expected)
{
char *fingerprint;
int fingerprint_len;
int result = -1;

if (php_openssl_x509_fingerprint(peer, method, 0, &fingerprint, &fingerprint_len) == SUCCESS) {
result = strcmp(expected, fingerprint);
efree(fingerprint);
}

return result;
}

static zend_bool php_x509_fingerprint_match(X509 *peer, zval *val)
{
if (Z_TYPE_P(val) == IS_STRING) {
const char *method = NULL;

switch (Z_STRLEN_P(val)) {
case 32:
method = "md5";
break;

case 40:
method = "sha1";
break;
}

return method && php_x509_fingerprint_cmp(peer, method, Z_STRVAL_P(val)) == 0;
} else if (Z_TYPE_P(val) == IS_ARRAY) {
HashPosition pos;
zval **current;
char *key;
uint key_len;
ulong key_index;

for (zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(val), &pos);
zend_hash_get_current_data_ex(Z_ARRVAL_P(val), (void **)&current, &pos) == SUCCESS;
zend_hash_move_forward_ex(Z_ARRVAL_P(val), &pos)
) {
int key_type = zend_hash_get_current_key_ex(Z_ARRVAL_P(val), &key, &key_len, &key_index, 0, &pos);

if (key_type == HASH_KEY_IS_STRING
&& Z_TYPE_PP(current) == IS_STRING
&& php_x509_fingerprint_cmp(peer, key, Z_STRVAL_PP(current)) != 0
) {
return 0;
}
}
return 1;
}
return 0;
}

PHP_FUNCTION(openssl_x509_fingerprint)
{
X509 *cert;
zval **zcert;
long certresource;
zend_bool raw_output = 0;
char *method = "sha1";
int method_len;

char *fingerprint;
int fingerprint_len;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z|sb", &zcert, &method, &method_len, &raw_output) == FAILURE) {
return;
}

cert = php_openssl_x509_from_zval(zcert, 0, &certresource TSRMLS_CC);
if (cert == NULL) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "cannot get cert from parameter 1");
RETURN_FALSE;
}

if (php_openssl_x509_fingerprint(cert, method, raw_output, &fingerprint, &fingerprint_len) == SUCCESS) {
RETVAL_STRINGL(fingerprint, fingerprint_len, 0);
} else {
RETVAL_FALSE;
}

if (certresource == -1 && cert) {
X509_free(cert);
}
}

/* {{{ proto bool openssl_x509_check_private_key(mixed cert, mixed key)
Checks if a private key corresponds to a CERT */
PHP_FUNCTION(openssl_x509_check_private_key)
Expand Down Expand Up @@ -4865,6 +4987,17 @@ int php_openssl_apply_verification_policy(SSL *ssl, X509 *peer, php_stream *stre

/* if the cert passed the usual checks, apply our own local policies now */

if (GET_VER_OPT("peer_fingerprint")) {
if (Z_TYPE_PP(val) == IS_STRING || Z_TYPE_PP(val) == IS_ARRAY) {
if (!php_x509_fingerprint_match(peer, *val)) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Peer fingerprint doesn't match");
return FAILURE;
}
} else {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Expected peer fingerprint must be a string or an array");
}
}

name = X509_get_subject_name(peer);

/* Does the common name match ? (used primarily for https://) */
Expand Down
1 change: 1 addition & 0 deletions ext/openssl/php_openssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ PHP_FUNCTION(openssl_x509_free);
PHP_FUNCTION(openssl_x509_parse);
PHP_FUNCTION(openssl_x509_checkpurpose);
PHP_FUNCTION(openssl_x509_export);
PHP_FUNCTION(openssl_x509_fingerprint);
PHP_FUNCTION(openssl_x509_export_to_file);
PHP_FUNCTION(openssl_x509_check_private_key);

Expand Down
62 changes: 62 additions & 0 deletions ext/openssl/tests/openssl_peer_fingerprint.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
Testing peer fingerprint on connection
--SKIPIF--
<?php
if (!extension_loaded("openssl")) die("skip");
if (!function_exists('pcntl_fork')) die("skip no fork");
--FILE--
<?php
$context = stream_context_create();

stream_context_set_option($context, 'ssl', 'local_cert', __DIR__ . "/bug54992.pem");
stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
$server = stream_socket_server('ssl://127.0.0.1:64321', $errno, $errstr,
STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context);


$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
$contextC = stream_context_create(
array(
'ssl' => array(
'verify_peer' => true,
'cafile' => __DIR__ . '/bug54992-ca.pem',
'capture_peer_cert' => true,
'peer_fingerprint' => '81cafc260aa8d82956ebc6212a362ece',
)
)
);
// should be: 81cafc260aa8d82956ebc6212a362ecc
var_dump(stream_socket_client("ssl://127.0.0.1:64321", $errno, $errstr, 1,
STREAM_CLIENT_CONNECT, $contextC));

$contextC = stream_context_create(
array(
'ssl' => array(
'verify_peer' => true,
'cafile' => __DIR__ . '/bug54992-ca.pem',
'capture_peer_cert' => true,
'peer_fingerprint' => array(
'sha256' => '78ea579f2c3b439359dec5dac9d445108772927427c4780037e87df3799a0aa0',
),
)
)
);

var_dump(stream_socket_client("ssl://127.0.0.1:64321", $errno, $errstr, 1,
STREAM_CLIENT_CONNECT, $contextC));
} else {
@pcntl_wait($status);
@stream_socket_accept($server, 1);
@stream_socket_accept($server, 1);
}
--EXPECTF--
Warning: stream_socket_client(): Peer fingerprint doesn't match in %s on line %d

Warning: stream_socket_client(): Failed to enable crypto in %s on line %d

Warning: stream_socket_client(): unable to connect to ssl://127.0.0.1:64321 (Unknown error) in %s on line %d
bool(false)
resource(9) of type (stream)
47 changes: 47 additions & 0 deletions ext/openssl/tests/openssl_x509_fingerprint.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--TEST--
Testing openssl_x509_fingerprint()
--SKIPIF--
<?php
if (!extension_loaded("openssl")) die("skip");
?>
--FILE--
<?php

$cert = "file://" . dirname(__FILE__) . "/cert.crt";

echo "** Testing with no parameters **\n";
var_dump(openssl_x509_fingerprint());

echo "** Testing default functionality **\n";
var_dump(openssl_x509_fingerprint($cert));

echo "** Testing hash method md5 **\n";
var_dump(openssl_x509_fingerprint($cert, 'md5'));

echo "**Testing raw output md5 **\n";
var_dump(bin2hex(openssl_x509_fingerprint($cert, 'md5', true)));

echo "** Testing bad certification **\n";
var_dump(openssl_x509_fingerprint('123'));
echo "** Testing bad hash method **\n";
var_dump(openssl_x509_fingerprint($cert, 'xx45'));
--EXPECTF--
** Testing with no parameters **

Warning: openssl_x509_fingerprint() expects at least 1 parameter, 0 given in %s on line %d
NULL
** Testing default functionality **
string(40) "6e6fd1ea10a5a23071d61c728ee9b40df6dbc33c"
** Testing hash method md5 **
string(32) "ac77008e172897e06c0b065294487a67"
**Testing raw output md5 **
string(32) "ac77008e172897e06c0b065294487a67"
** Testing bad certification **

Warning: openssl_x509_fingerprint(): cannot get cert from parameter 1 in %s on line %d
bool(false)
** Testing bad hash method **

Warning: openssl_x509_fingerprint(): Unknown signature algorithm in %s on line %d
bool(false)