From 09c17b1a3175ecd75f58bb5d2b89bac56dc2cadf Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sat, 19 Aug 2017 00:42:36 -0500 Subject: [PATCH] ASN1 / X509: update to use DateTime instead of unix time --- phpseclib/File/ASN1.php | 80 +++++++++++++++++++++++++++++++++++++---- phpseclib/File/X509.php | 68 +++++++++++++++++++++++++++++------ 2 files changed, 131 insertions(+), 17 deletions(-) diff --git a/phpseclib/File/ASN1.php b/phpseclib/File/ASN1.php index 7ce28bdd..732bd9c6 100644 --- a/phpseclib/File/ASN1.php +++ b/phpseclib/File/ASN1.php @@ -554,7 +554,9 @@ class File_ASN1 break; case FILE_ASN1_TYPE_UTC_TIME: case FILE_ASN1_TYPE_GENERALIZED_TIME: - $current['content'] = $this->_decodeTime(substr($content, $content_pos), $tag); + $current['content'] = class_exists('DateTime') ? + $this->_decodeDateTime(substr($content, $content_pos), $tag) : + $this->_decodeUnixTime(substr($content, $content_pos), $tag); default: } @@ -788,10 +790,20 @@ class File_ASN1 return isset($this->oids[$decoded['content']]) ? $this->oids[$decoded['content']] : $decoded['content']; case FILE_ASN1_TYPE_UTC_TIME: case FILE_ASN1_TYPE_GENERALIZED_TIME: - if (isset($mapping['implicit'])) { - $decoded['content'] = $this->_decodeTime($decoded['content'], $decoded['type']); + if (class_exists('DateTime')) { + if (isset($mapping['implicit'])) { + $decoded['content'] = $this->_decodeDateTime($decoded['content'], $decoded['type']); + } + if (!$decoded['content']) { + return false; + } + return $decoded['content']->format($this->format); + } else { + if (isset($mapping['implicit'])) { + $decoded['content'] = $this->_decodeUnixTime($decoded['content'], $decoded['type']); + } + return @date($this->format, $decoded['content']); } - return @date($this->format, $decoded['content']); case FILE_ASN1_TYPE_BIT_STRING: if (isset($mapping['mapping'])) { $offset = ord($decoded['content'][0]); @@ -1040,7 +1052,12 @@ class File_ASN1 case FILE_ASN1_TYPE_GENERALIZED_TIME: $format = $mapping['type'] == FILE_ASN1_TYPE_UTC_TIME ? 'y' : 'Y'; $format.= 'mdHis'; - $value = @gmdate($format, strtotime($source)) . 'Z'; + if (!class_exists('DateTime')) { + $value = @gmdate($format, strtotime($source)) . 'Z'; + } else { + $date = new DateTime($source, new DateTimeZone('GMT')); + $value = $date->format($format) . 'Z'; + } break; case FILE_ASN1_TYPE_BIT_STRING: if (isset($mapping['mapping'])) { @@ -1202,7 +1219,7 @@ class File_ASN1 } /** - * BER-decode the time + * BER-decode the time (using UNIX time) * * Called by _decode_ber() and in the case of implicit tags asn1map(). * @@ -1211,7 +1228,7 @@ class File_ASN1 * @param int $tag * @return string */ - function _decodeTime($content, $tag) + function _decodeUnixTime($content, $tag) { /* UTCTime: http://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 @@ -1250,6 +1267,55 @@ class File_ASN1 return @$mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year) + $timezone; } + + /** + * BER-decode the time (using DateTime) + * + * Called by _decode_ber() and in the case of implicit tags asn1map(). + * + * @access private + * @param string $content + * @param int $tag + * @return string + */ + function _decodeDateTime($content, $tag) + { + /* UTCTime: + http://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 + http://www.obj-sys.com/asn1tutorial/node15.html + + GeneralizedTime: + http://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 + http://www.obj-sys.com/asn1tutorial/node14.html */ + + $format = 'YmdHis'; + + if ($tag == FILE_ASN1_TYPE_UTC_TIME) { + // https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=28 says "the seconds + // element shall always be present" but none-the-less I've seen X509 certs where it isn't and if the + // browsers parse it phpseclib ought to too + if (preg_match('#^(\d{10})(Z|[+-]\d{4})$#', $content, $matches)) { + $content = $matches[1] . '00' . $matches[2]; + } + $prefix = substr($content, 0, 2) >= 50 ? '19' : '20'; + $content = $prefix . $content; + } elseif (strpos($content, '.') !== false) { + $format.= '.u'; + } + + if ($content[strlen($content) - 1] == 'Z') { + $content = substr($content, 0, -1) . '+0000'; + } + + if (strpos($content, '-') !== false || strpos($content, '+') !== false) { + $format.= 'O'; + } + + // error supression isn't necessary as of PHP 7.0: + // http://php.net/manual/en/migration70.other-changes.php + return @DateTime::createFromFormat($format, $content); + } + /** * Set the time format * diff --git a/phpseclib/File/X509.php b/phpseclib/File/X509.php index e6c84438..d6eaf47b 100644 --- a/phpseclib/File/X509.php +++ b/phpseclib/File/X509.php @@ -2114,7 +2114,9 @@ class File_X509 } if (!isset($date)) { - $date = time(); + $date = class_exists('DateTime') ? + new DateTime($date, new DateTimeZone(date_default_timezone_get())) : + time(); } $notBefore = $this->currentCert['tbsCertificate']['validity']['notBefore']; @@ -2123,9 +2125,17 @@ class File_X509 $notAfter = $this->currentCert['tbsCertificate']['validity']['notAfter']; $notAfter = isset($notAfter['generalTime']) ? $notAfter['generalTime'] : $notAfter['utcTime']; + if (class_exists('DateTime')) { + $notBefore = new DateTime($notBefore, new DateTimeZone(@date_default_timezone_get())); + $notAfter = new DateTime($notAfter, new DateTimeZone(@date_default_timezone_get())); + } else { + $notBefore = @strtotime($notBefore); + $notAfter = @strtotime($notAfter); + } + switch (true) { - case $date < @strtotime($notBefore): - case $date > @strtotime($notAfter): + case $date < $notBefore: + case $date > $notAfter: return false; } @@ -3385,7 +3395,15 @@ class File_X509 */ function _timeField($date) { - $year = @gmdate("Y", @strtotime($date)); // the same way ASN1.php parses this + if (is_object($date) && strtolower(get_class($date)) == 'file_asn1_element') { + return $date; + } + if (!class_exists('DateTime')) { + $year = @gmdate("Y", @strtotime($date)); // the same way ASN1.php parses this + } else { + $dateObj = new DateTime($date, new DateTimeZone('GMT')); + $year = $dateObj->format('Y'); + } if ($year < 2050) { return array('utcTime' => $date); } else { @@ -3450,8 +3468,16 @@ class File_X509 return false; } - $startDate = !empty($this->startDate) ? $this->startDate : @date('D, d M Y H:i:s O'); - $endDate = !empty($this->endDate) ? $this->endDate : @date('D, d M Y H:i:s O', strtotime('+1 year')); + if (!class_exists('DateTime')) { + $startDate = !empty($this->startDate) ? $this->startDate : @date('D, d M Y H:i:s O'); + $endDate = !empty($this->endDate) ? $this->endDate : @date('D, d M Y H:i:s O', strtotime('+1 year')); + } else { + $startDate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O'); + + $endDate = new DateTime('+1 year', new DateTimeZone(@date_default_timezone_get())); + $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O'); + } if (!empty($this->serialNumber)) { $serialNumber = $this->serialNumber; } else { @@ -3724,7 +3750,12 @@ class File_X509 $currentCert = isset($this->currentCert) ? $this->currentCert : null; $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; - $thisUpdate = !empty($this->startDate) ? $this->startDate : @date('D, d M Y H:i:s O'); + if (!class_exists('DateTime')) { + $thisUpdate = !empty($this->startDate) ? $this->startDate : @date('D, d M Y H:i:s O'); + } else { + $thisUpdate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $thisUpdate = !empty($this->startDate) ? $this->startDate : $thisUpdate->format('D, d M Y H:i:s O'); + } if (isset($crl->currentCert) && is_array($crl->currentCert) && isset($crl->currentCert['tbsCertList'])) { $this->currentCert = $crl->currentCert; @@ -3876,7 +3907,12 @@ class File_X509 */ function setStartDate($date) { - $this->startDate = @date('D, d M Y H:i:s O', @strtotime($date)); + if (class_exists('DateTime')) { + $date = new DateTime($date); + $this->startDate = $date->format('D, d M Y H:i:s O'); + } else { + $this->startDate = @date('D, d M Y H:i:s O', @strtotime($date)); + } } /** @@ -3900,7 +3936,12 @@ class File_X509 $temp = chr(FILE_ASN1_TYPE_GENERALIZED_TIME) . $asn1->_encodeLength(strlen($temp)) . $temp; $this->endDate = new File_ASN1_Element($temp); } else { - $this->endDate = @date('D, d M Y H:i:s O', @strtotime($date)); + if (class_exists('DateTime')) { + $date = new DateTime($date); + $this->endDate = $date->format('D, d M Y H:i:s O'); + } else { + $this->endDate = @date('D, d M Y H:i:s O', @strtotime($date)); + } } } @@ -4640,9 +4681,16 @@ class File_X509 return false; } + if (!class_exists('DateTime')) { + $revocationDate = @date('D, d M Y H:i:s O'); + } else { + $revocationDate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $revocationDate = $revocationDate->format('D, d M Y H:i:s O'); + } + $i = count($rclist); $rclist[] = array('userCertificate' => $serial, - 'revocationDate' => $this->_timeField(@date('D, d M Y H:i:s O'))); + 'revocationDate' => $this->_timeField($revocationDate)); return $i; }