From 1bdd6422d6f72e2a29af5171bc89f93aad4527d8 Mon Sep 17 00:00:00 2001 From: Vincent JARDIN Date: Mon, 19 Apr 2021 18:49:13 +0000 Subject: [PATCH] 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 --- ext/openssl/openssl.c | 134 +++++++++++++--- ext/openssl/php_openssl.h | 3 + .../openssl_pkey_get_public_rfc7512.phpt | 147 ++++++++++++++++++ ext/openssl/xp_ssl.c | 23 ++- 4 files changed, 283 insertions(+), 24 deletions(-) create mode 100644 ext/openssl/tests/openssl_pkey_get_public_rfc7512.phpt diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 7ebf99453cc7..55f55985fdee 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -56,6 +56,7 @@ #include #include #include +#include /* Common */ #include @@ -515,7 +516,6 @@ struct php_x509_request { /* {{{ */ static X509 *php_openssl_x509_from_param(zend_object *cert_obj, zend_string *cert_str); static X509 *php_openssl_x509_from_zval(zval *val, bool *free_cert); static X509_REQ *php_openssl_csr_from_param(zend_object *csr_obj, zend_string *csr_str); -static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len); static int php_openssl_is_private_key(EVP_PKEY* pkey); static X509_STORE * php_openssl_setup_verify(zval * calist); @@ -933,6 +933,46 @@ static void php_openssl_dispose_config(struct php_x509_request * req) /* {{{ */ } /* }}} */ +static ENGINE *php_openssl_make_pkcs11_engine(const bool warn) /* {{{ */ +{ + char *verbose = NULL; + ENGINE *engine; + + engine = ENGINE_by_id("pkcs11"); + if (engine == NULL) { + if (warn) { + php_error_docref(NULL, E_WARNING, "Cannot load PKCS11 engine"); + } + php_openssl_store_errors(); + return NULL; + } + verbose = getenv("OPENSSL_ENGINE_VERBOSE"); + if (verbose) { + if (!ENGINE_ctrl_cmd_string(engine, "VERBOSE", NULL, 0)) { + ENGINE_free(engine); + php_openssl_store_errors(); + return NULL; + } + } else { + if (!ENGINE_ctrl_cmd_string(engine, "QUIET", NULL, 0)) { + ENGINE_free(engine); + php_openssl_store_errors(); + return NULL; + } + } + if (!ENGINE_init(engine)) { + if (warn) { + php_error_docref(NULL, E_WARNING, "Cannot init PKCS11 engine"); + } + php_openssl_store_errors(); + return NULL; + } + ENGINE_free(engine); + + return engine; +} +/* }}} */ + #if defined(PHP_WIN32) || PHP_OPENSSL_API_VERSION >= 0x10100 #define PHP_OPENSSL_RAND_ADD_TIME() ((void) 0) #else @@ -1384,9 +1424,9 @@ PHP_FUNCTION(openssl_get_cert_locations) } /* }}} */ -static X509 *php_openssl_x509_from_str(zend_string *cert_str) { +X509 *php_openssl_x509_from_str(zend_string *cert_str) { X509 *cert = NULL; - BIO *in; + BIO *in = NULL; if (ZSTR_LEN(cert_str) > 7 && memcmp(ZSTR_VAL(cert_str), "file://", sizeof("file://") - 1) == 0) { 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) { return NULL; } cert = PEM_read_bio_X509(in, NULL, NULL, NULL); + } else if (ZSTR_LEN(cert_str) > 7 && memcmp(ZSTR_VAL(cert_str), "pkcs11:", sizeof("pkcs11:") - 1) == 0) { + ENGINE *engine = php_openssl_make_pkcs11_engine(true); + struct { + const char *s_slot_cert_id; + X509 *cert; + } parms = { + .s_slot_cert_id = ZSTR_VAL(cert_str), + .cert = NULL, + }; + int force_login = 0; + + if (!engine) { + return NULL; + } + + if (!ENGINE_ctrl_cmd(engine, "LOAD_CERT_CTRL", 0, &parms, NULL, force_login)) { + ENGINE_finish(engine); + php_openssl_store_errors(); + return NULL; + } + ENGINE_finish(engine); + if (parms.cert == NULL) { + php_openssl_store_errors(); + return NULL; + } + cert = parms.cert; } else { in = BIO_new_mem_buf(ZSTR_VAL(cert_str), (int) ZSTR_LEN(cert_str)); if (in == NULL) { @@ -3477,7 +3543,7 @@ static int php_openssl_pem_password_cb(char *buf, int size, int rwflag, void *us } /* }}} */ -static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len) +EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *passphrase, size_t passphrase_len) { EVP_PKEY *key = NULL; X509 *cert = NULL; @@ -3549,6 +3615,8 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas } else if (Z_TYPE_P(val) == IS_OBJECT && Z_OBJCE_P(val) == php_openssl_certificate_ce) { cert = php_openssl_certificate_from_obj(Z_OBJ_P(val))->x509; } else { + ENGINE *engine = NULL; + /* force it to be a string and check if it refers to a file */ /* passing non string values leaks, object uses toString, it returns NULL * See bug38255.phpt @@ -3566,13 +3634,27 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas TMP_CLEAN; } } + if (Z_STRLEN_P(val) > 7 && memcmp(Z_STRVAL_P(val), "pkcs11:", sizeof("pkcs11:") - 1) == 0) { + engine = php_openssl_make_pkcs11_engine(true); + if (engine == NULL) { + TMP_CLEAN; + } + } /* it's an X509 file/cert of some kind, and we need to extract the data from that */ if (public_key) { - cert = php_openssl_x509_from_str(Z_STR_P(val)); + if (engine) { + key = ENGINE_load_public_key(engine, Z_STRVAL_P(val), NULL, NULL); + ENGINE_finish(engine); + engine = NULL; + } + /* val could be a certificate (file, pkcs11:, etc., let's try to extract the key) */ + if (!key) { + cert = php_openssl_x509_from_str(Z_STR_P(val)); + } if (cert) { free_cert = 1; - } else { + } else if (!key) { /* not a X509 certificate, try to retrieve public key */ BIO* in; if (filename) { @@ -3589,26 +3671,32 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas } } else { /* we want the private key */ - BIO *in; - - if (filename) { - in = BIO_new_file(filename, PHP_OPENSSL_BIO_MODE_R(PKCS7_BINARY)); + if (engine) { + key = ENGINE_load_private_key(engine, Z_STRVAL_P(val), NULL, NULL); + ENGINE_finish(engine); + engine = NULL; } else { - in = BIO_new_mem_buf(Z_STRVAL_P(val), (int)Z_STRLEN_P(val)); - } + BIO *in; - if (in == NULL) { - TMP_CLEAN; - } - if (passphrase == NULL) { - key = PEM_read_bio_PrivateKey(in, NULL, NULL, NULL); - } else { - struct php_openssl_pem_password password; - password.key = passphrase; - password.len = passphrase_len; - key = PEM_read_bio_PrivateKey(in, NULL, php_openssl_pem_password_cb, &password); + if (filename) { + in = BIO_new_file(filename, PHP_OPENSSL_BIO_MODE_R(PKCS7_BINARY)); + } else { + in = BIO_new_mem_buf(Z_STRVAL_P(val), (int)Z_STRLEN_P(val)); + } + + if (in == NULL) { + TMP_CLEAN; + } + if (passphrase == NULL) { + key = PEM_read_bio_PrivateKey(in, NULL, NULL, NULL); + } else { + struct php_openssl_pem_password password; + password.key = passphrase; + password.len = passphrase_len; + key = PEM_read_bio_PrivateKey(in, NULL, php_openssl_pem_password_cb, &password); + } + BIO_free(in); } - BIO_free(in); } } diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h index 664e33c5f35d..0e720a897759 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -108,6 +108,9 @@ PHP_OPENSSL_API zend_string* php_openssl_decrypt( const char *iv, size_t iv_len, const char *tag, zend_long tag_len, const char *aad, size_t aad_len); +PHP_OPENSSL_API EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, + char *passphrase, size_t passphrase_len); +PHP_OPENSSL_API X509 *php_openssl_x509_from_str(zend_string *cert_str); /* OpenSSLCertificate class */ diff --git a/ext/openssl/tests/openssl_pkey_get_public_rfc7512.phpt b/ext/openssl/tests/openssl_pkey_get_public_rfc7512.phpt new file mode 100644 index 000000000000..ec4074974859 --- /dev/null +++ b/ext/openssl/tests/openssl_pkey_get_public_rfc7512.phpt @@ -0,0 +1,147 @@ +--TEST-- +openssl_pkey_get_public(), openss_pkey_get_private(), openssl_x509_read() with RFC7512 URI +--SKIPIF-- + 0) die("skip couldn't locate openssl binary"); +exec('softhsm2-util --version', $out, $code); +if ($code > 0) die("skip couldn't locate softhsm2-util binary"); +exec('pkcs11-tool --show-info --module ' . $PKCS11_MODULE_PATH, $out, $code); +if ($code > 0) die("skip couldn't locate pkcs11-tool binary"); +exec('pkcs11-dump info ' . $PKCS11_MODULE_PATH, $out, $code); +if ($code > 0) die("skip couldn't locate pkcs11-dump binary"); +?> +--FILE-- + ['pipe','w'], + 2 => ['pipe','w'], + ], + $pipes + ); + + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[2]); + + return proc_close($proc); +} + +$PKCS11_MODULE_PATH="/usr/lib/softhsm/libsofthsm2.so"; +putenv("PKCS11_MODULE_PATH=".$PKCS11_MODULE_PATH); + +$SOFTHSM2_CONF=tempnam(sys_get_temp_dir(), 'softhsm2'); +$SOFTHSM2_TOKENDIR=sprintf("%s.dir", $SOFTHSM2_CONF); +mkdir($SOFTHSM2_TOKENDIR); +$PHP11_PIN=123456; +$PHP11_SOPIN=12345678; + +file_put_contents( + $SOFTHSM2_CONF, + sprintf( + "directories.tokendir = %s" . PHP_EOL. + "objectstore.backend = file" . PHP_EOL. + "log.level = DEBUG" . PHP_EOL. + "slots.removable = false" . PHP_EOL. + "slots.mechanisms = ALL", + $SOFTHSM2_TOKENDIR + ) +); + +putenv(sprintf("SOFTHSM2_CONF=%s", $SOFTHSM2_CONF)); +sexec("softhsm2-util --show-slots | grep ^Slot | cut -d ' ' -f 2", $out); +$INIT11_SLOT=(int)$out[0]; +if ($INIT11_SLOT != 0) { + echo "Error slot"; + exec("softhsm2-util --show-slots", $out); + var_dump($out); + exit(1); +} + +sexec(sprintf("softhsm2-util --init-token --free --slot %d --label TestVJToken --pin %s --so-pin %s", + $INIT11_SLOT, $PHP11_PIN, $PHP11_SOPIN), $out); + +/* XXX custom slot is always the first one */ +sexec(sprintf("pkcs11-dump slotlist %s 2>/dev/null | grep SoftHSM | head -1 | cut -f 1", + $PKCS11_MODULE_PATH), $PHP11_SLOT); +if (!is_string($PHP11_SLOT)) { + echo "Cannot detect the slot" . PHP_EOL; + exit(1); +} +$PHP11_SLOT=(int)$PHP11_SLOT; + +/* + * Most of these features can be supported natively by PHP, but + * the purpose is to focus on RFC7512, so let's use the system tools. + */ +$PRIVKEYPEM=$SOFTHSM2_TOKENDIR . '.key.priv.pem'; +$PRIVKEYDER=$SOFTHSM2_TOKENDIR . '.key.priv.der'; +$PUBKEYDER=$SOFTHSM2_TOKENDIR . '.key.pub.der'; +$CERTDER=$SOFTHSM2_TOKENDIR . '.cert.der'; +sexec(sprintf("openssl genrsa -out %s 2048", $PRIVKEYPEM), $genkey); +sexec(sprintf("openssl rsa -inform PEM -in %s -outform DER -out %s", $PRIVKEYPEM, $PRIVKEYDER), $pem2der); + +/* extract the public key */ +sexec(sprintf("openssl rsa -in %s -outform DER -pubout -out %s", $PRIVKEYPEM, $PUBKEYDER), $extract); + +/* let's import these keys */ +sexec(sprintf("pkcs11-tool --login --pin %s --write-object %s --type privkey --label VJPrivKey --module %s", + $PHP11_PIN, $PRIVKEYDER, $PKCS11_MODULE_PATH), $importpriv); + +sexec(sprintf("pkcs11-tool --login --pin %s --write-object %s --type pubkey --label VJPubKey --module %s", + $PHP11_PIN, $PUBKEYDER, $PKCS11_MODULE_PATH), $importpub); + +/* let's build the x509 */ +sexec(sprintf("openssl req -new -x509 -subj '/CN=MyCertVJ' ". + "-engine pkcs11 -keyform engine -key 'pkcs11:object=VJPrivKey;type=private;pin-value=%s' ". + "-outform DER -out %s", $PHP11_PIN, $CERTDER), $req); +sexec(sprintf("pkcs11-tool --login --pin %s --write-object %s --type cert --label VJCert --module %s", + $PHP11_PIN, $CERTDER, $PKCS11_MODULE_PATH), $importcert); + +/* let's start the tests */ + +$key = openssl_pkey_get_private(sprintf("pkcs11:object=VJPrivKey;type=private;pin-value=%s", $PHP11_PIN)); +if (!($key instanceof OpenSSLAsymmetricKey)) { + echo "Private Key NOK" . PHP_EOL; + exit(1); +} +echo "Private Key OK" . PHP_EOL; + +$key = openssl_pkey_get_public(sprintf("pkcs11:object=VJPubKey;type=public")); +if (!($key instanceof OpenSSLAsymmetricKey)) { + echo "Public Key NOK" . PHP_EOL; + exit(1); +} +echo "Public Key OK" . PHP_EOL; + +$cert = openssl_x509_read(sprintf("pkcs11:object=VJCert;type=cert")); +if (!($cert instanceof OpenSSLCertificate)) { + echo "Cert NOK" . PHP_EOL; + exit(1); +} +echo "Cert OK" . PHP_EOL; +$certArray=openssl_x509_parse($cert); + +if ($certArray['name'] !== "/CN=MyCertVJ") { + echo "Cert content NOK" . PHP_EOL; + exit(1); +} +echo "Cert content OK" . PHP_EOL; + +?> +--EXPECT-- +Private Key OK +Public Key OK +Cert OK +Cert content OK diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index b17706617c8e..8346525f6a28 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -938,6 +938,8 @@ static int php_openssl_set_local_cert(SSL_CTX *ctx, php_stream *stream) /* {{{ * if (certfile) { char resolved_path_buff[MAXPATHLEN]; const char *private_key = NULL; + X509 *cert = NULL; + int ctx_set = 0; if (VCWD_REALPATH(certfile, resolved_path_buff)) { /* a certificate to use for authentication */ @@ -948,8 +950,20 @@ static int php_openssl_set_local_cert(SSL_CTX *ctx, php_stream *stream) /* {{{ * certfile); return FAILURE; } + ctx_set = 1; + /* val is still local_cert/certfile since GET_VER_OPT_STRING("local_cert", certfile) */ + } else if ((cert = php_openssl_x509_from_str(Z_STR_P(val))) != NULL) { + if (SSL_CTX_use_certificate(ctx, cert) != 1) { + X509_free(cert); + php_error_docref(NULL, E_WARNING, + "Invalid local cert `%s'; Check your device", + certfile); + return FAILURE; + } + ctx_set = 1; + } + if (ctx_set) { GET_VER_OPT_STRING("local_pk", private_key); - if (private_key) { char resolved_path_buff_pk[MAXPATHLEN]; if (VCWD_REALPATH(private_key, resolved_path_buff_pk)) { @@ -957,6 +971,13 @@ static int php_openssl_set_local_cert(SSL_CTX *ctx, php_stream *stream) /* {{{ * php_error_docref(NULL, E_WARNING, "Unable to set private key file `%s'", resolved_path_buff_pk); return FAILURE; } + } else if (GET_VER_OPT("local_pk")) /* fill val with local_pk if any */ { + EVP_PKEY *pkey = php_openssl_pkey_from_zval(val /* local_pk */, 0 /* not public */, NULL, 0); + if (SSL_CTX_use_PrivateKey(ctx, pkey) != 1) { + EVP_PKEY_free(pkey); + php_error_docref(NULL, E_WARNING, "Unable to set private key `%s'", private_key); + return FAILURE; + } } } else { if (SSL_CTX_use_PrivateKey_file(ctx, resolved_path_buff, SSL_FILETYPE_PEM) != 1) {