4 * This file is part of the civicrm-cxn-rpc package.
6 * Copyright (c) CiviCRM LLC <info@civicrm.org>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this package.
12 namespace Civi\Cxn\Rpc
;
14 use Civi\Cxn\Rpc\Exception\ExpiredCertException
;
15 use Civi\Cxn\Rpc\Exception\InvalidCertException
;
16 use Civi\Cxn\Rpc\Http\HttpInterface
;
17 use Civi\Cxn\Rpc\Http\PhpHttp
;
20 * Class DefaultCertificateValidator
21 * @package Civi\Cxn\Rpc
23 * The default certificate validator will:
24 * - Check that the certificate is signed by canonical CA.
25 * - Check that the certificate has not been revoked by the canonical CA
26 * (using the CRL URL of the CA).
28 * Validating the CRL requires issuing HTTP requests. To improve performance,
29 * consider replacing the default $http instance (PhpHttp) with something
30 * that supports caching.
32 class DefaultCertificateValidator
implements CertificateValidatorInterface
{
35 * Specify that content should be auto-loaded via HTTP.
37 const AUTOLOAD
= '*auto*';
41 * The CA certificate (PEM-encoded).
42 * Use DefaultCertificateValidator::AUTOLOAD to use the bundled CiviRootCA.
48 * The URL for downloading the CRL.
49 * Use DefaultCertificateValidator::AUTOLOAD to extract from $caCert.
51 protected $crlUrl = DefaultCertificateValidator
::AUTOLOAD
;
56 * Use DefaultCertificateValidator::AUTOLOAD to download via HTTP.
62 * The certificate which signs CRLs (PEM-encoded).
63 * Use DefaultCertificateValidator::AUTOLOAD to download via HTTP.
65 protected $crlDistCert;
68 * @var HttpInterface|string
69 * The service to use when autoloading data.
70 * Use DefaultCertificateValidator::AUTOLOAD to download via HTTP.
75 * @param string $caCertPem
76 * @param string $crlDistCertPem
77 * @param string $crlPem
78 * @param HttpInterface|string $http
80 public function __construct(
81 $caCertPem = DefaultCertificateValidator
::AUTOLOAD
,
82 $crlDistCertPem = DefaultCertificateValidator
::AUTOLOAD
,
83 $crlPem = DefaultCertificateValidator
::AUTOLOAD
,
84 $http = DefaultCertificateValidator
::AUTOLOAD
) {
86 $this->caCert
= $caCertPem;
87 $this->crlDistCert
= $crlDistCertPem;
93 * Determine whether an X.509 certificate is currently valid.
95 * @param string $certPem
96 * PEM-encoded certificate.
97 * @throws InvalidCertException
98 * Invalid certificates are reported as exceptions.
100 public function validateCert($certPem) {
101 if ($this->getCaCert()) {
102 self
::validate($certPem, $this->getCaCert(), $this->getCrl(), $this->getCrlDistCert());
106 protected static function validate($certPem, $caCertPem, $crlPem = NULL, $crlDistCertPem = NULL) {
107 $caCertObj = X509Util
::loadCACert($caCertPem);
109 $certObj = new \
File_X509();
110 $certObj->loadCA($caCertPem);
112 if ($crlPem !== NULL) {
113 $crlObj = new \
File_X509();
114 if ($crlDistCertPem) {
115 $crlDistCertObj = X509Util
::loadCrlDistCert($crlDistCertPem, NULL, $caCertPem);
116 if ($crlDistCertObj->getSubjectDN(FILE_X509_DN_STRING
) !== $caCertObj->getSubjectDN(FILE_X509_DN_STRING
)) {
117 throw new InvalidCertException(sprintf("CRL distributor (%s) does not act on behalf of this CA (%s)",
118 $crlDistCertObj->getSubjectDN(FILE_X509_DN_STRING
),
119 $caCertObj->getSubjectDN(FILE_X509_DN_STRING
)
123 self
::validate($crlDistCertPem, $caCertPem);
125 catch (InvalidCertException
$ie) {
126 throw new InvalidCertException("CRL distributor has an invalid certificate", 0, $ie);
128 $crlObj->loadCA($crlDistCertPem);
130 $crlObj->loadCA($caCertPem);
131 $crlObj->loadCRL($crlPem);
132 if (!$crlObj->validateSignature()) {
133 throw new InvalidCertException("CRL signature is invalid");
137 $parsedCert = $certObj->loadX509($certPem);
138 if ($crlPem !== NULL) {
139 if (empty($parsedCert)) {
140 throw new InvalidCertException("Identity is invalid. Empty certificate.");
142 if (empty($parsedCert['tbsCertificate']['serialNumber'])) {
143 throw new InvalidCertException("Identity is invalid. No serial number.");
145 $revoked = $crlObj->getRevoked($parsedCert['tbsCertificate']['serialNumber']->toString());
146 if (!empty($revoked)) {
147 throw new InvalidCertException("Identity is invalid. Certificate revoked.");
151 if (!$certObj->validateSignature()) {
152 throw new InvalidCertException("Identity is invalid. Certificate is not signed by proper CA.");
154 if (!$certObj->validateDate(Time
::getTime())) {
155 throw new ExpiredCertException("Identity is invalid. Certificate expired.");
162 public function getCaCert() {
163 if ($this->caCert
=== self
::AUTOLOAD
) {
164 $this->caCert
= file_get_contents(Constants
::getCert());
166 return $this->caCert
;
170 * @param string $caCert
173 public function setCaCert($caCert) {
174 $this->caCert
= $caCert;
179 * Determine the CRL URL which corresponds to this CA.
181 public function getCrlUrl() {
182 if ($this->crlUrl
=== self
::AUTOLOAD
) {
183 $this->crlUrl
= NULL; // Default if we can't find something else.
184 $caCertObj = X509Util
::loadCACert($this->getCaCert());
185 // There can be multiple DPs, but in practice CiviRootCA only has one.
186 $crlDPs = $caCertObj->getExtension('id-ce-cRLDistributionPoints');
187 if (is_array($crlDPs)) {
188 foreach ($crlDPs as $crlDP) {
189 foreach ($crlDP['distributionPoint']['fullName'] as $fullName) {
190 if (isset($fullName['uniformResourceIdentifier'])) {
191 $this->crlUrl
= $fullName['uniformResourceIdentifier'];
198 return $this->crlUrl
;
202 * @param string $crlUrl
205 public function setCrlUrl($crlUrl) {
206 $this->crlUrl
= $crlUrl;
213 public function getCrlDistCert() {
214 if ($this->crlDistCert
=== self
::AUTOLOAD
) {
215 if ($this->getCrlUrl()) {
216 $url = preg_replace('/\.crl/', '/dist.crt', $this->getCrlUrl());
217 list ($headers, $blob, $code) = $this->getHttp()->send('GET', $url, '');
219 throw new \
RuntimeException("Certificate validation failed. Cannot load CRL distribution certificate: $url");
221 $this->crlDistCert
= $blob;
224 $this->crlDistCert
= NULL;
227 return $this->crlDistCert
;
231 * @param string $crlDistCert
234 public function setCrlDistCert($crlDistCert) {
235 $this->crlDistCert
= $crlDistCert;
242 public function getCrl() {
243 if ($this->crl
=== self
::AUTOLOAD
) {
244 $url = $this->getCrlUrl();
246 list ($headers, $blob, $code) = $this->getHttp()->send('GET', $url, '');
248 throw new \
RuntimeException("Certificate validation failed. Cannot load CRL: $url");
263 public function setCrl($crl) {
269 * @return HttpInterface
271 public function getHttp() {
272 if ($this->http
=== self
::AUTOLOAD
) {
273 $this->http
= new PhpHttp();
279 * @param HttpInterface $http
282 public function setHttp($http) {