From ac6507bfde8b3253c7c3d42d02970e9a7ca08707 Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Thu, 2 Oct 2025 09:01:49 -0400 Subject: [PATCH 1/5] 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 87440f68f06a..830da2a84ad3 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -2088,6 +2088,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) { @@ -2099,7 +2177,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; @@ -2107,12 +2186,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) @@ -2150,8 +2230,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; } @@ -2229,8 +2326,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 c7fdf82c7861..1fff2aaed778 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -90,7 +90,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 92552ca807ae00f8fb2a2bfc1e6fe74c403df068 Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Wed, 29 Oct 2025 12:02:05 -0400 Subject: [PATCH 2/5] 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 830da2a84ad3..5499a7598e71 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -2096,7 +2096,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 37ac456021d6651ac8a03e35449a3efc5c2b91f8 Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Wed, 29 Oct 2025 13:32:56 -0400 Subject: [PATCH 3/5] 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 1fff2aaed778..cc4a6b8e22f8 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 */ #include "zend_attributes.h" #include "zend_constants.h" From 64d94bafd94ce1327404318977977ddf9a8e7ee2 Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Tue, 11 Aug 2026 10:20:17 -0400 Subject: [PATCH 4/5] 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 5499a7598e71..99e13de9d73c 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -2088,13 +2088,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; @@ -2102,6 +2102,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; @@ -2160,11 +2161,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) @@ -2177,8 +2177,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; @@ -2186,17 +2186,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; @@ -2230,26 +2231,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) { @@ -2326,11 +2322,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 cc4a6b8e22f8..d3d2d03c7d6b 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 */ #include "zend_attributes.h" #include "zend_constants.h" @@ -90,10 +90,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) From 7c5c85b4ab6c19ad4c4621aefd78d27054c9fd0c Mon Sep 17 00:00:00 2001 From: Steve Wall Date: Thu, 10 Sep 2026 14:30:59 -0400 Subject: [PATCH 5/5] add missing code --- ext/openssl/openssl.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index 99e13de9d73c..26431952efc4 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -2102,7 +2102,10 @@ static int php_openssl_parse_validity_range(zval *validity, time_t *notBefore, t time_t from = -1; time_t to = -1; - if (Z_TYPE_P(validity) != IS_ARRAY || + if (Z_TYPE_P(validity) != IS_ARRAY) { + php_error_docref(NULL, E_WARNING, "Incorrect type, not an array"); + return FAILURE; + } if ((tmp = zend_hash_index_find(Z_ARRVAL_P(validity), 2)) != NULL) { php_error_docref(NULL, E_WARNING, "Too many timestamps"); return FAILURE;