From d51d5efa65af6cef587d5d7f3348eb37d97d7085 Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Thu, 2 Oct 2025 09:01:49 -0400 Subject: [PATCH 1/4] Support specifying an exact date/time range for validity when signing a CSR This changes the `days` parameter of `openssl_csr_sign()` to a `validity` parameter, which can be either an integer specifying the number of days the certificate is to be valid for (compatible with current usage), or it can be an array of two integer or string values, representing the notBefore and notAfter times to use for the certificate. If they are integers or numeric strings, they are to be a time_t value. If they are non-numeric strings, they are to be an ASN.1 timestamp (YYMMDDHHMMSSZ or YYYYMMDDHHMMSSZ). --- ext/openssl/openssl.c | 112 ++++++++++++++++++++++++++++++++-- ext/openssl/openssl.stub.php | 2 +- ext/openssl/openssl_arginfo.h | 2 +- 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 80c0a8bab073..5ae6b8fd381c 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -2091,6 +2091,84 @@ PHP_FUNCTION(openssl_csr_export) } /* }}} */ +/* {{{ parse_time_range */ +/* convert an array of either integers or strings to a pair of time_t values + * representing the notBefore and notAfter times for a certificate. + * If the array values are strings, they must either be a valid numeric string + * representing the unix timestamp, or they must be an ASN.1 timestamp. + */ +static int parse_time_range(zval *validity, time_t *from_time, time_t *to_time) { + zval *tmp; + long lval; + double dval; + ASN1_TIME *t; + time_t from = -1; + time_t to = -1; + + if ((tmp = zend_hash_index_find(Z_ARRVAL_P(validity), 2)) != NULL) { + php_error_docref(NULL, E_WARNING, "Too many timestamps"); + return FAILURE; + } + if ((tmp = zend_hash_index_find(Z_ARRVAL_P(validity), 1)) == NULL) { + php_error_docref(NULL, E_WARNING, "Too few timestamps"); + return FAILURE; + } + if ((tmp = zend_hash_index_find(Z_ARRVAL_P(validity), 0)) != NULL && + ((Z_TYPE_P(tmp) == IS_LONG) || (Z_TYPE_P(tmp) == IS_STRING))) { + if (Z_TYPE_P(tmp) == IS_LONG) { + from = Z_LVAL_P(tmp); + } else if (Z_TYPE_P(tmp) == IS_STRING) { + switch (is_numeric_string(Z_STRVAL_P(tmp), Z_STRLEN_P(tmp), &lval, &dval, 0)) { + case IS_LONG: + from = lval; + break; + case IS_DOUBLE: + from = (int) dval; + break; + default: + t = ASN1_UTCTIME_new(); + if (ASN1_TIME_set_string(t, Z_STRVAL_P(tmp))) { + from = php_openssl_asn1_time_to_time_t(t); + } + ASN1_UTCTIME_free(t); + } + } + } + if (from == -1) { + php_error_docref(NULL, E_WARNING, "Invalid certificate start timestamp"); + return FAILURE; + } + if ((tmp = zend_hash_index_find(Z_ARRVAL_P(validity), 1)) != NULL && + ((Z_TYPE_P(tmp) == IS_LONG) || (Z_TYPE_P(tmp) == IS_STRING))) { + if (Z_TYPE_P(tmp) == IS_LONG) { + to = Z_LVAL_P(tmp); + } else { + switch (is_numeric_string(Z_STRVAL_P(tmp), Z_STRLEN_P(tmp), &lval, &dval, 0)) { + case IS_LONG: + to = lval; + break; + case IS_DOUBLE: + to = (int) dval; + break; + default: + t = ASN1_UTCTIME_new(); + if (ASN1_TIME_set_string(t, Z_STRVAL_P(tmp))) { + to = php_openssl_asn1_time_to_time_t(t); + } + ASN1_UTCTIME_free(t); + } + } + } + if (to == -1) { + php_error_docref(NULL, E_WARNING, "Invalid certificate end timestamp"); + return FAILURE; + } + *from_time = from; + *to_time = to; + return SUCCESS; +} +/* }}} */ + /* {{{ Signs a cert with another CERT */ PHP_FUNCTION(openssl_csr_sign) { @@ -2102,7 +2180,8 @@ PHP_FUNCTION(openssl_csr_sign) zend_object *cert_obj; zend_string *cert_str; zval *zpkey, *args = NULL; - zend_long num_days; + zend_long num_days = -1; + zval *validity; zend_long serial = Z_L(0); zend_string *serial_hex = NULL; X509 *cert = NULL, *new_cert = NULL; @@ -2110,12 +2189,13 @@ PHP_FUNCTION(openssl_csr_sign) int i; bool new_cert_used = false; struct php_x509_request req; + time_t from_time = -1, to_time = -1; ZEND_PARSE_PARAMETERS_START(4, 7) Z_PARAM_OBJ_OF_CLASS_OR_STR(csr_obj, php_openssl_request_ce, csr_str) Z_PARAM_OBJ_OF_CLASS_OR_STR_OR_NULL(cert_obj, php_openssl_certificate_ce, cert_str) Z_PARAM_ZVAL(zpkey) - Z_PARAM_LONG(num_days) + Z_PARAM_ZVAL(validity) Z_PARAM_OPTIONAL Z_PARAM_ARRAY_OR_NULL(args) Z_PARAM_LONG(serial) @@ -2153,8 +2233,25 @@ PHP_FUNCTION(openssl_csr_sign) goto cleanup; } - if (num_days < 0 || num_days > LONG_MAX / 86400) { - php_error_docref(NULL, E_WARNING, "Days must be between 0 and %ld", LONG_MAX / 86400); + /* If 'validity' is an integer, it is the number of days the certificate + * will be valid for, starting from right now. + * If it is an array, it is expected to contain two values, the + * starting time and the ending time for the validity period. Each of + * the values are expected to be either a numeric value representing a + * unix timestamp, or a string containing an ASN.1 timestamp. + */ + if (Z_TYPE_P(validity) == IS_LONG) { + num_days = Z_LVAL_P(validity); + if (num_days < 0 || num_days > LONG_MAX / 86400) { + php_error_docref(NULL, E_WARNING, "Days must be between 0 and %ld", LONG_MAX / 86400); + goto cleanup; + } + } else if (Z_TYPE_P(validity) == IS_ARRAY) { + if (parse_time_range(validity, &from_time, &to_time) != SUCCESS) { + goto cleanup; + } + } else { + php_error_docref(NULL, E_WARNING, "Fourth parameter must be integer or array"); goto cleanup; } @@ -2232,8 +2329,11 @@ PHP_FUNCTION(openssl_csr_sign) php_openssl_store_errors(); goto cleanup; } - if (!X509_gmtime_adj(X509_getm_notBefore(new_cert), 0) - || !X509_gmtime_adj(X509_getm_notAfter(new_cert), 60*60*24*num_days)) { + if (num_days == -1) { + ASN1_TIME_set(X509_getm_notBefore(new_cert), from_time); + ASN1_TIME_set(X509_getm_notAfter(new_cert), to_time); + } else if (!X509_gmtime_adj(X509_getm_notBefore(new_cert), 0) + || !X509_gmtime_adj(X509_getm_notAfter(new_cert), 60*60*24*num_days)) { php_openssl_store_errors(); goto cleanup; } diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 3d3fa3ea634f..54747a42818e 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -562,7 +562,7 @@ function openssl_csr_export(OpenSSLCertificateSigningRequest|string $csr, &$outp /** * @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key */ -function openssl_csr_sign(OpenSSLCertificateSigningRequest|string $csr, OpenSSLCertificate|string|null $ca_certificate, #[\SensitiveParameter] $private_key, int $days, ?array $options = null, int $serial = 0, ?string $serial_hex = null): OpenSSLCertificate|false {} +function openssl_csr_sign(OpenSSLCertificateSigningRequest|string $csr, OpenSSLCertificate|string|null $ca_certificate, #[\SensitiveParameter] $private_key, int|array $validity, ?array $options = null, int $serial = 0): OpenSSLCertificate|false {} /** * @param OpenSSLAsymmetricKey|null $private_key diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index b1742dcb05f8..9950cd7d3268 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -87,7 +87,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_openssl_csr_sign, 0, 4, Open ZEND_ARG_OBJ_TYPE_MASK(0, csr, OpenSSLCertificateSigningRequest, MAY_BE_STRING, NULL) ZEND_ARG_OBJ_TYPE_MASK(0, ca_certificate, OpenSSLCertificate, MAY_BE_STRING|MAY_BE_NULL, NULL) ZEND_ARG_INFO(0, private_key) - ZEND_ARG_TYPE_INFO(0, days, IS_LONG, 0) + ZEND_ARG_TYPE_MASK(0, validity, MAY_BE_LONG|MAY_BE_ARRAY, NULL) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, serial, IS_LONG, 0, "0") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, serial_hex, IS_STRING, 1, "null") From 58289068ee30da5792d05c36e36beb5cf3939beb Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Wed, 29 Oct 2025 12:02:05 -0400 Subject: [PATCH 2/4] Fix long integer type --- ext/openssl/openssl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 5ae6b8fd381c..a2dc44d0d49f 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -2099,7 +2099,7 @@ PHP_FUNCTION(openssl_csr_export) */ static int parse_time_range(zval *validity, time_t *from_time, time_t *to_time) { zval *tmp; - long lval; + zend_long lval; double dval; ASN1_TIME *t; time_t from = -1; From 8e5cabd585e6fb1136ff2a28a5ec563191ba7e00 Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Wed, 29 Oct 2025 13:32:56 -0400 Subject: [PATCH 3/4] Fix merge error, update stubs hash --- ext/openssl/openssl.stub.php | 2 +- ext/openssl/openssl_arginfo.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 54747a42818e..defc7958f1fb 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -562,7 +562,7 @@ function openssl_csr_export(OpenSSLCertificateSigningRequest|string $csr, &$outp /** * @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key */ -function openssl_csr_sign(OpenSSLCertificateSigningRequest|string $csr, OpenSSLCertificate|string|null $ca_certificate, #[\SensitiveParameter] $private_key, int|array $validity, ?array $options = null, int $serial = 0): OpenSSLCertificate|false {} +function openssl_csr_sign(OpenSSLCertificateSigningRequest|string $csr, OpenSSLCertificate|string|null $ca_certificate, #[\SensitiveParameter] $private_key, int|array $validity, ?array $options = null, int $serial = 0, ?string $serial_hex = null): OpenSSLCertificate|false {} /** * @param OpenSSLAsymmetricKey|null $private_key diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index 9950cd7d3268..e6ac98b69a89 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit openssl.stub.php instead. - * Stub hash: 7cad995b734d69f98d489edb97a7878a4ea8f47e */ + * Stub hash: c742a9450641420aa95af97bf10dc2040722885c */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_x509_export_to_file, 0, 2, _IS_BOOL, 0) ZEND_ARG_OBJ_TYPE_MASK(0, certificate, OpenSSLCertificate, MAY_BE_STRING, NULL) From caf83878543061022c842d361ab5f3f732527c94 Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Tue, 11 Aug 2026 10:20:17 -0400 Subject: [PATCH 4/4] Don't remove 'days' parameter from 'openssl_csr_sign()', add a 'validity' at the end instead. --- ext/openssl/openssl.c | 54 ++++++++++++++++------------------- ext/openssl/openssl.stub.php | 2 +- ext/openssl/openssl_arginfo.h | 5 ++-- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index a2dc44d0d49f..87b9e952aebc 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -2091,13 +2091,13 @@ PHP_FUNCTION(openssl_csr_export) } /* }}} */ -/* {{{ parse_time_range */ +/* php_openssl_parse_validity_range */ /* convert an array of either integers or strings to a pair of time_t values * representing the notBefore and notAfter times for a certificate. * If the array values are strings, they must either be a valid numeric string * representing the unix timestamp, or they must be an ASN.1 timestamp. */ -static int parse_time_range(zval *validity, time_t *from_time, time_t *to_time) { +static int php_openssl_parse_validity_range(zval *validity, time_t *notBefore, time_t *notAfter) { zval *tmp; zend_long lval; double dval; @@ -2105,6 +2105,7 @@ static int parse_time_range(zval *validity, time_t *from_time, time_t *to_time) time_t from = -1; time_t to = -1; + if (Z_TYPE_P(validity) != IS_ARRAY || if ((tmp = zend_hash_index_find(Z_ARRVAL_P(validity), 2)) != NULL) { php_error_docref(NULL, E_WARNING, "Too many timestamps"); return FAILURE; @@ -2163,11 +2164,10 @@ static int parse_time_range(zval *validity, time_t *from_time, time_t *to_time) php_error_docref(NULL, E_WARNING, "Invalid certificate end timestamp"); return FAILURE; } - *from_time = from; - *to_time = to; + *notBefore = from; + *notAfter = to; return SUCCESS; } -/* }}} */ /* {{{ Signs a cert with another CERT */ PHP_FUNCTION(openssl_csr_sign) @@ -2180,8 +2180,8 @@ PHP_FUNCTION(openssl_csr_sign) zend_object *cert_obj; zend_string *cert_str; zval *zpkey, *args = NULL; - zend_long num_days = -1; - zval *validity; + zend_long num_days; + zval *validity = NULL; zend_long serial = Z_L(0); zend_string *serial_hex = NULL; X509 *cert = NULL, *new_cert = NULL; @@ -2189,17 +2189,18 @@ PHP_FUNCTION(openssl_csr_sign) int i; bool new_cert_used = false; struct php_x509_request req; - time_t from_time = -1, to_time = -1; + time_t notBefore = -1, notAfter = -1; - ZEND_PARSE_PARAMETERS_START(4, 7) + ZEND_PARSE_PARAMETERS_START(4, 8) Z_PARAM_OBJ_OF_CLASS_OR_STR(csr_obj, php_openssl_request_ce, csr_str) Z_PARAM_OBJ_OF_CLASS_OR_STR_OR_NULL(cert_obj, php_openssl_certificate_ce, cert_str) Z_PARAM_ZVAL(zpkey) - Z_PARAM_ZVAL(validity) + Z_PARAM_LONG(num_days) Z_PARAM_OPTIONAL Z_PARAM_ARRAY_OR_NULL(args) Z_PARAM_LONG(serial) Z_PARAM_STR_OR_NULL(serial_hex) + Z_PARAM_ARRAY_OR_NULL(validity) ZEND_PARSE_PARAMETERS_END(); RETVAL_FALSE; @@ -2233,26 +2234,21 @@ PHP_FUNCTION(openssl_csr_sign) goto cleanup; } - /* If 'validity' is an integer, it is the number of days the certificate - * will be valid for, starting from right now. - * If it is an array, it is expected to contain two values, the - * starting time and the ending time for the validity period. Each of - * the values are expected to be either a numeric value representing a - * unix timestamp, or a string containing an ASN.1 timestamp. + /* If 'validity' is present, 'days' will be ignored. + * 'validity' must contain two values, the starting time and the ending + * time for the validity period. Each of the values are expected to be + * either a numeric value representing a unix timestamp, or a string + * containing an ASN.1 timestamp. */ - if (Z_TYPE_P(validity) == IS_LONG) { - num_days = Z_LVAL_P(validity); - if (num_days < 0 || num_days > LONG_MAX / 86400) { - php_error_docref(NULL, E_WARNING, "Days must be between 0 and %ld", LONG_MAX / 86400); + if (validity != NULL) { + if (php_openssl_parse_validity_range(validity, ¬Before, ¬After) != SUCCESS) { goto cleanup; } - } else if (Z_TYPE_P(validity) == IS_ARRAY) { - if (parse_time_range(validity, &from_time, &to_time) != SUCCESS) { + } else { + if (num_days < 0 || num_days > LONG_MAX / 86400) { + php_error_docref(NULL, E_WARNING, "Days must be between 0 and %ld", LONG_MAX / 86400); goto cleanup; } - } else { - php_error_docref(NULL, E_WARNING, "Fourth parameter must be integer or array"); - goto cleanup; } if (PHP_SSL_REQ_PARSE(&req, args) == FAILURE) { @@ -2329,11 +2325,11 @@ PHP_FUNCTION(openssl_csr_sign) php_openssl_store_errors(); goto cleanup; } - if (num_days == -1) { - ASN1_TIME_set(X509_getm_notBefore(new_cert), from_time); - ASN1_TIME_set(X509_getm_notAfter(new_cert), to_time); + if (validity != NULL) { + ASN1_TIME_set(X509_getm_notBefore(new_cert), notBefore); + ASN1_TIME_set(X509_getm_notAfter(new_cert), notAfter); } else if (!X509_gmtime_adj(X509_getm_notBefore(new_cert), 0) - || !X509_gmtime_adj(X509_getm_notAfter(new_cert), 60*60*24*num_days)) { + || !X509_gmtime_adj(X509_getm_notAfter(new_cert), 60*60*24*num_days)) { php_openssl_store_errors(); goto cleanup; } diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index defc7958f1fb..df24eac3fcc2 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -562,7 +562,7 @@ function openssl_csr_export(OpenSSLCertificateSigningRequest|string $csr, &$outp /** * @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key */ -function openssl_csr_sign(OpenSSLCertificateSigningRequest|string $csr, OpenSSLCertificate|string|null $ca_certificate, #[\SensitiveParameter] $private_key, int|array $validity, ?array $options = null, int $serial = 0, ?string $serial_hex = null): OpenSSLCertificate|false {} +function openssl_csr_sign(OpenSSLCertificateSigningRequest|string $csr, OpenSSLCertificate|string|null $ca_certificate, #[\SensitiveParameter] $private_key, int $days, ?array $options = null, int $serial = 0, ?string $serial_hex = null, ?array $validity = null): OpenSSLCertificate|false {} /** * @param OpenSSLAsymmetricKey|null $private_key diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index e6ac98b69a89..bba7dad8231e 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit openssl.stub.php instead. - * Stub hash: c742a9450641420aa95af97bf10dc2040722885c */ + * Stub hash: 71dc392508471ef93af70eecd38bb906afcf0e26 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_x509_export_to_file, 0, 2, _IS_BOOL, 0) ZEND_ARG_OBJ_TYPE_MASK(0, certificate, OpenSSLCertificate, MAY_BE_STRING, NULL) @@ -87,10 +87,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_openssl_csr_sign, 0, 4, Open ZEND_ARG_OBJ_TYPE_MASK(0, csr, OpenSSLCertificateSigningRequest, MAY_BE_STRING, NULL) ZEND_ARG_OBJ_TYPE_MASK(0, ca_certificate, OpenSSLCertificate, MAY_BE_STRING|MAY_BE_NULL, NULL) ZEND_ARG_INFO(0, private_key) - ZEND_ARG_TYPE_MASK(0, validity, MAY_BE_LONG|MAY_BE_ARRAY, NULL) + ZEND_ARG_TYPE_INFO(0, days, IS_LONG, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 1, "null") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, serial, IS_LONG, 0, "0") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, serial_hex, IS_STRING, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, validity, IS_ARRAY, 1, "null") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_openssl_csr_new, 0, 2, OpenSSLCertificateSigningRequest, MAY_BE_BOOL)