Skip to content

Commit 1bdd642

Browse files
committed
HSM: OpenSSL, add RFC7512
Add RFC7512 URIs with openssl_pkey_get_public(), openssl_pkey_get_private() and openssl_x509_read(), support When a URI based on the RFC7512 is used, the private key should be loaded from the engine instead of using a local file. Reviewed-by: Jakub Zelenka <bukka@php.net>
1 parent c4fe9fb commit 1bdd642

File tree

4 files changed

+283
-24
lines changed

4 files changed

+283
-24
lines changed

ext/openssl/openssl.c

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
#include <openssl/ssl.h>
5757
#include <openssl/pkcs12.h>
5858
#include <openssl/cms.h>
59+
#include <openssl/engine.h>
5960

6061
/* Common */
6162
#include <time.h>
@@ -515,7 +516,6 @@ struct php_x509_request { /* {{{ */
515516
static X509 *php_openssl_x509_from_param(zend_object *cert_obj, zend_string *cert_str);
516517
static X509 *php_openssl_x509_from_zval(zval *val, bool *free_cert);
517518
static X509_REQ *php_openssl_csr_from_param(zend_object *csr_obj, zend_string *csr_str);
518-
static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len);
519519

520520
static int php_openssl_is_private_key(EVP_PKEY* pkey);
521521
static X509_STORE * php_openssl_setup_verify(zval * calist);
@@ -933,6 +933,46 @@ static void php_openssl_dispose_config(struct php_x509_request * req) /* {{{ */
933933
}
934934
/* }}} */
935935

936+
static ENGINE *php_openssl_make_pkcs11_engine(const bool warn) /* {{{ */
937+
{
938+
char *verbose = NULL;
939+
ENGINE *engine;
940+
941+
engine = ENGINE_by_id("pkcs11");
942+
if (engine == NULL) {
943+
if (warn) {
944+
php_error_docref(NULL, E_WARNING, "Cannot load PKCS11 engine");
945+
}
946+
php_openssl_store_errors();
947+
return NULL;
948+
}
949+
verbose = getenv("OPENSSL_ENGINE_VERBOSE");
950+
if (verbose) {
951+
if (!ENGINE_ctrl_cmd_string(engine, "VERBOSE", NULL, 0)) {
952+
ENGINE_free(engine);
953+
php_openssl_store_errors();
954+
return NULL;
955+
}
956+
} else {
957+
if (!ENGINE_ctrl_cmd_string(engine, "QUIET", NULL, 0)) {
958+
ENGINE_free(engine);
959+
php_openssl_store_errors();
960+
return NULL;
961+
}
962+
}
963+
if (!ENGINE_init(engine)) {
964+
if (warn) {
965+
php_error_docref(NULL, E_WARNING, "Cannot init PKCS11 engine");
966+
}
967+
php_openssl_store_errors();
968+
return NULL;
969+
}
970+
ENGINE_free(engine);
971+
972+
return engine;
973+
}
974+
/* }}} */
975+
936976
#if defined(PHP_WIN32) || PHP_OPENSSL_API_VERSION >= 0x10100
937977
#define PHP_OPENSSL_RAND_ADD_TIME() ((void) 0)
938978
#else
@@ -1384,9 +1424,9 @@ PHP_FUNCTION(openssl_get_cert_locations)
13841424
}
13851425
/* }}} */
13861426

1387-
static X509 *php_openssl_x509_from_str(zend_string *cert_str) {
1427+
X509 *php_openssl_x509_from_str(zend_string *cert_str) {
13881428
X509 *cert = NULL;
1389-
BIO *in;
1429+
BIO *in = NULL;
13901430

13911431
if (ZSTR_LEN(cert_str) > 7 && memcmp(ZSTR_VAL(cert_str), "file://", sizeof("file://") - 1) == 0) {
13921432
if (php_openssl_open_base_dir_chk(ZSTR_VAL(cert_str) + (sizeof("file://") - 1))) {
@@ -1399,6 +1439,32 @@ static X509 *php_openssl_x509_from_str(zend_string *cert_str) {
13991439
return NULL;
14001440
}
14011441
cert = PEM_read_bio_X509(in, NULL, NULL, NULL);
1442+
} else if (ZSTR_LEN(cert_str) > 7 && memcmp(ZSTR_VAL(cert_str), "pkcs11:", sizeof("pkcs11:") - 1) == 0) {
1443+
ENGINE *engine = php_openssl_make_pkcs11_engine(true);
1444+
struct {
1445+
const char *s_slot_cert_id;
1446+
X509 *cert;
1447+
} parms = {
1448+
.s_slot_cert_id = ZSTR_VAL(cert_str),
1449+
.cert = NULL,
1450+
};
1451+
int force_login = 0;
1452+
1453+
if (!engine) {
1454+
return NULL;
1455+
}
1456+
1457+
if (!ENGINE_ctrl_cmd(engine, "LOAD_CERT_CTRL", 0, &parms, NULL, force_login)) {
1458+
ENGINE_finish(engine);
1459+
php_openssl_store_errors();
1460+
return NULL;
1461+
}
1462+
ENGINE_finish(engine);
1463+
if (parms.cert == NULL) {
1464+
php_openssl_store_errors();
1465+
return NULL;
1466+
}
1467+
cert = parms.cert;
14021468
} else {
14031469
in = BIO_new_mem_buf(ZSTR_VAL(cert_str), (int) ZSTR_LEN(cert_str));
14041470
if (in == NULL) {
@@ -3477,7 +3543,7 @@ static int php_openssl_pem_password_cb(char *buf, int size, int rwflag, void *us
34773543
}
34783544
/* }}} */
34793545

3480-
static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len)
3546+
EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len)
34813547
{
34823548
EVP_PKEY *key = NULL;
34833549
X509 *cert = NULL;
@@ -3549,6 +3615,8 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas
35493615
} else if (Z_TYPE_P(val) == IS_OBJECT && Z_OBJCE_P(val) == php_openssl_certificate_ce) {
35503616
cert = php_openssl_certificate_from_obj(Z_OBJ_P(val))->x509;
35513617
} else {
3618+
ENGINE *engine = NULL;
3619+
35523620
/* force it to be a string and check if it refers to a file */
35533621
/* passing non string values leaks, object uses toString, it returns NULL
35543622
* See bug38255.phpt
@@ -3566,13 +3634,27 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas
35663634
TMP_CLEAN;
35673635
}
35683636
}
3637+
if (Z_STRLEN_P(val) > 7 && memcmp(Z_STRVAL_P(val), "pkcs11:", sizeof("pkcs11:") - 1) == 0) {
3638+
engine = php_openssl_make_pkcs11_engine(true);
3639+
if (engine == NULL) {
3640+
TMP_CLEAN;
3641+
}
3642+
}
35693643
/* it's an X509 file/cert of some kind, and we need to extract the data from that */
35703644
if (public_key) {
3571-
cert = php_openssl_x509_from_str(Z_STR_P(val));
3645+
if (engine) {
3646+
key = ENGINE_load_public_key(engine, Z_STRVAL_P(val), NULL, NULL);
3647+
ENGINE_finish(engine);
3648+
engine = NULL;
3649+
}
3650+
/* val could be a certificate (file, pkcs11:, etc., let's try to extract the key) */
3651+
if (!key) {
3652+
cert = php_openssl_x509_from_str(Z_STR_P(val));
3653+
}
35723654

35733655
if (cert) {
35743656
free_cert = 1;
3575-
} else {
3657+
} else if (!key) {
35763658
/* not a X509 certificate, try to retrieve public key */
35773659
BIO* in;
35783660
if (filename) {
@@ -3589,26 +3671,32 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas
35893671
}
35903672
} else {
35913673
/* we want the private key */
3592-
BIO *in;
3593-
3594-
if (filename) {
3595-
in = BIO_new_file(filename, PHP_OPENSSL_BIO_MODE_R(PKCS7_BINARY));
3674+
if (engine) {
3675+
key = ENGINE_load_private_key(engine, Z_STRVAL_P(val), NULL, NULL);
3676+
ENGINE_finish(engine);
3677+
engine = NULL;
35963678
} else {
3597-
in = BIO_new_mem_buf(Z_STRVAL_P(val), (int)Z_STRLEN_P(val));
3598-
}
3679+
BIO *in;
35993680

3600-
if (in == NULL) {
3601-
TMP_CLEAN;
3602-
}
3603-
if (passphrase == NULL) {
3604-
key = PEM_read_bio_PrivateKey(in, NULL, NULL, NULL);
3605-
} else {
3606-
struct php_openssl_pem_password password;
3607-
password.key = passphrase;
3608-
password.len = passphrase_len;
3609-
key = PEM_read_bio_PrivateKey(in, NULL, php_openssl_pem_password_cb, &password);
3681+
if (filename) {
3682+
in = BIO_new_file(filename, PHP_OPENSSL_BIO_MODE_R(PKCS7_BINARY));
3683+
} else {
3684+
in = BIO_new_mem_buf(Z_STRVAL_P(val), (int)Z_STRLEN_P(val));
3685+
}
3686+
3687+
if (in == NULL) {
3688+
TMP_CLEAN;
3689+
}
3690+
if (passphrase == NULL) {
3691+
key = PEM_read_bio_PrivateKey(in, NULL, NULL, NULL);
3692+
} else {
3693+
struct php_openssl_pem_password password;
3694+
password.key = passphrase;
3695+
password.len = passphrase_len;
3696+
key = PEM_read_bio_PrivateKey(in, NULL, php_openssl_pem_password_cb, &password);
3697+
}
3698+
BIO_free(in);
36103699
}
3611-
BIO_free(in);
36123700
}
36133701
}
36143702

ext/openssl/php_openssl.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ PHP_OPENSSL_API zend_string* php_openssl_decrypt(
108108
const char *iv, size_t iv_len,
109109
const char *tag, zend_long tag_len,
110110
const char *aad, size_t aad_len);
111+
PHP_OPENSSL_API EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key,
112+
char *passphrase, size_t passphrase_len);
113+
PHP_OPENSSL_API X509 *php_openssl_x509_from_str(zend_string *cert_str);
111114

112115
/* OpenSSLCertificate class */
113116

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
--TEST--
2+
openssl_pkey_get_public(), openss_pkey_get_private(), openssl_x509_read() with RFC7512 URI
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded("openssl"))
6+
die("skip");
7+
if (!function_exists("proc_open")) die("skip no proc_open");
8+
$PKCS11_MODULE_PATH="/usr/lib/softhsm/libsofthsm2.so";
9+
exec('openssl help', $out, $code);
10+
if ($code > 0) die("skip couldn't locate openssl binary");
11+
exec('softhsm2-util --version', $out, $code);
12+
if ($code > 0) die("skip couldn't locate softhsm2-util binary");
13+
exec('pkcs11-tool --show-info --module ' . $PKCS11_MODULE_PATH, $out, $code);
14+
if ($code > 0) die("skip couldn't locate pkcs11-tool binary");
15+
exec('pkcs11-dump info ' . $PKCS11_MODULE_PATH, $out, $code);
16+
if ($code > 0) die("skip couldn't locate pkcs11-dump binary");
17+
?>
18+
--FILE--
19+
<?php
20+
21+
/* simple exec */
22+
function sexec($cmd, &$stdout=null, &$stderr=null) {
23+
$proc = proc_open(
24+
$cmd,
25+
[
26+
1 => ['pipe','w'],
27+
2 => ['pipe','w'],
28+
],
29+
$pipes
30+
);
31+
32+
$stdout = stream_get_contents($pipes[1]);
33+
fclose($pipes[1]);
34+
35+
$stderr = stream_get_contents($pipes[2]);
36+
fclose($pipes[2]);
37+
38+
return proc_close($proc);
39+
}
40+
41+
$PKCS11_MODULE_PATH="/usr/lib/softhsm/libsofthsm2.so";
42+
putenv("PKCS11_MODULE_PATH=".$PKCS11_MODULE_PATH);
43+
44+
$SOFTHSM2_CONF=tempnam(sys_get_temp_dir(), 'softhsm2');
45+
$SOFTHSM2_TOKENDIR=sprintf("%s.dir", $SOFTHSM2_CONF);
46+
mkdir($SOFTHSM2_TOKENDIR);
47+
$PHP11_PIN=123456;
48+
$PHP11_SOPIN=12345678;
49+
50+
file_put_contents(
51+
$SOFTHSM2_CONF,
52+
sprintf(
53+
"directories.tokendir = %s" . PHP_EOL.
54+
"objectstore.backend = file" . PHP_EOL.
55+
"log.level = DEBUG" . PHP_EOL.
56+
"slots.removable = false" . PHP_EOL.
57+
"slots.mechanisms = ALL",
58+
$SOFTHSM2_TOKENDIR
59+
)
60+
);
61+
62+
putenv(sprintf("SOFTHSM2_CONF=%s", $SOFTHSM2_CONF));
63+
sexec("softhsm2-util --show-slots | grep ^Slot | cut -d ' ' -f 2", $out);
64+
$INIT11_SLOT=(int)$out[0];
65+
if ($INIT11_SLOT != 0) {
66+
echo "Error slot";
67+
exec("softhsm2-util --show-slots", $out);
68+
var_dump($out);
69+
exit(1);
70+
}
71+
72+
sexec(sprintf("softhsm2-util --init-token --free --slot %d --label TestVJToken --pin %s --so-pin %s",
73+
$INIT11_SLOT, $PHP11_PIN, $PHP11_SOPIN), $out);
74+
75+
/* XXX custom slot is always the first one */
76+
sexec(sprintf("pkcs11-dump slotlist %s 2>/dev/null | grep SoftHSM | head -1 | cut -f 1",
77+
$PKCS11_MODULE_PATH), $PHP11_SLOT);
78+
if (!is_string($PHP11_SLOT)) {
79+
echo "Cannot detect the slot" . PHP_EOL;
80+
exit(1);
81+
}
82+
$PHP11_SLOT=(int)$PHP11_SLOT;
83+
84+
/*
85+
* Most of these features can be supported natively by PHP, but
86+
* the purpose is to focus on RFC7512, so let's use the system tools.
87+
*/
88+
$PRIVKEYPEM=$SOFTHSM2_TOKENDIR . '.key.priv.pem';
89+
$PRIVKEYDER=$SOFTHSM2_TOKENDIR . '.key.priv.der';
90+
$PUBKEYDER=$SOFTHSM2_TOKENDIR . '.key.pub.der';
91+
$CERTDER=$SOFTHSM2_TOKENDIR . '.cert.der';
92+
sexec(sprintf("openssl genrsa -out %s 2048", $PRIVKEYPEM), $genkey);
93+
sexec(sprintf("openssl rsa -inform PEM -in %s -outform DER -out %s", $PRIVKEYPEM, $PRIVKEYDER), $pem2der);
94+
95+
/* extract the public key */
96+
sexec(sprintf("openssl rsa -in %s -outform DER -pubout -out %s", $PRIVKEYPEM, $PUBKEYDER), $extract);
97+
98+
/* let's import these keys */
99+
sexec(sprintf("pkcs11-tool --login --pin %s --write-object %s --type privkey --label VJPrivKey --module %s",
100+
$PHP11_PIN, $PRIVKEYDER, $PKCS11_MODULE_PATH), $importpriv);
101+
102+
sexec(sprintf("pkcs11-tool --login --pin %s --write-object %s --type pubkey --label VJPubKey --module %s",
103+
$PHP11_PIN, $PUBKEYDER, $PKCS11_MODULE_PATH), $importpub);
104+
105+
/* let's build the x509 */
106+
sexec(sprintf("openssl req -new -x509 -subj '/CN=MyCertVJ' ".
107+
"-engine pkcs11 -keyform engine -key 'pkcs11:object=VJPrivKey;type=private;pin-value=%s' ".
108+
"-outform DER -out %s", $PHP11_PIN, $CERTDER), $req);
109+
sexec(sprintf("pkcs11-tool --login --pin %s --write-object %s --type cert --label VJCert --module %s",
110+
$PHP11_PIN, $CERTDER, $PKCS11_MODULE_PATH), $importcert);
111+
112+
/* let's start the tests */
113+
114+
$key = openssl_pkey_get_private(sprintf("pkcs11:object=VJPrivKey;type=private;pin-value=%s", $PHP11_PIN));
115+
if (!($key instanceof OpenSSLAsymmetricKey)) {
116+
echo "Private Key NOK" . PHP_EOL;
117+
exit(1);
118+
}
119+
echo "Private Key OK" . PHP_EOL;
120+
121+
$key = openssl_pkey_get_public(sprintf("pkcs11:object=VJPubKey;type=public"));
122+
if (!($key instanceof OpenSSLAsymmetricKey)) {
123+
echo "Public Key NOK" . PHP_EOL;
124+
exit(1);
125+
}
126+
echo "Public Key OK" . PHP_EOL;
127+
128+
$cert = openssl_x509_read(sprintf("pkcs11:object=VJCert;type=cert"));
129+
if (!($cert instanceof OpenSSLCertificate)) {
130+
echo "Cert NOK" . PHP_EOL;
131+
exit(1);
132+
}
133+
echo "Cert OK" . PHP_EOL;
134+
$certArray=openssl_x509_parse($cert);
135+
136+
if ($certArray['name'] !== "/CN=MyCertVJ") {
137+
echo "Cert content NOK" . PHP_EOL;
138+
exit(1);
139+
}
140+
echo "Cert content OK" . PHP_EOL;
141+
142+
?>
143+
--EXPECT--
144+
Private Key OK
145+
Public Key OK
146+
Cert OK
147+
Cert content OK

0 commit comments

Comments
 (0)