Skip to content

Commit

Permalink
Major refactor
Browse files Browse the repository at this point in the history
- Rename RootCaBundleBuilder to CaRootPemBundle
- Separate MozillaCertData class
- Better OOP design / easier to test
- Increase unit test coverage
  • Loading branch information
EvanDotPro committed Sep 15, 2012
1 parent 729f034 commit 78f7044
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 24,356 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.
most Linux distributions' PHP packages, else you need to ensure you compile
using --with-openssl[=DIR].

### CLI Root CA Bundle Updater
### CLI root CA bundle updater

[update-ca-bundle](https://github.com/EvanDotPro/Sslurp/blob/master/bin/update-ca-bundle)
is a handy command-line tool for fetching and building a PEM certificate bundle
Expand Down
7 changes: 4 additions & 3 deletions autoload_classmap.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?php
// Generated by ZF2's ./bin/classmap_generator.php
return array(
'Sslurp\Module' => __DIR__ . '/Module.php',
'Sslurp\X509Certificate' => __DIR__ . '/src/Sslurp/X509Certificate.php',
'Sslurp\RootCaBundleBuilder' => __DIR__ . '/src/Sslurp/RootCaBundleBuilder.php',
'Sslurp\AbstractCaRootData' => __DIR__ . '/src/Sslurp/AbstractCaRootData.php',
'Sslurp\MozillaCertData' => __DIR__ . '/src/Sslurp/MozillaCertData.php',
'Sslurp\CaRootPemBundle' => __DIR__ . '/src/Sslurp/CaRootPemBundle.php',
'Sslurp\X509Certificate' => __DIR__ . '/src/Sslurp/X509Certificate.php',
);
8 changes: 6 additions & 2 deletions bin/update-ca-bundle
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ if (isset($opts['o']) && is_array($opts['o'])) {
$opts['o'] = $opts['o'][0];
} elseif (!isset($opts['o']) && isset($argv[1])) {
$opts['o'] = $argv[1];
} elseif (!isset($opts['o'])) {
$opts['o'] = null;
}

$caBundleBuilder = new Sslurp\RootCaBundleBuilder();
$caBundle = $caBundleBuilder->getUpdatedRootCaBundle();
if ($opts['o'] == '-') $opts['o'] = null;

$caRootPemBundle = new Sslurp\CaRootPemBundle();
$caBundle = $caRootPemBundle->getContent();

if (!$caBundle) {
echo "Sorry, there was an error building the latest root CA bundle.\n";
Expand Down
67 changes: 67 additions & 0 deletions src/Sslurp/AbstractCaRootData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/**
* This file is part of Sslurp.
* https://github.com/EvanDotPro/Sslurp
*
* (c) Evan Coury <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sslurp;

use DateTime;
use DateTimeZone;

abstract class AbstractCaRootData
{
/**
* The CVS version ID
*
* @var string
*/
private $version = null;

/**
* The date/time of the version commit
*
* @var DateTime
*/
private $dateTime = null;

/**
* Get the version number
*
* @return string
*/
public function getVersion()
{
if ($this->version === null) {
if (preg_match('/^#?\s?(CVS_ID\s+\".*\")/m', $this->getContent(), $match)) {
$parts = explode(' ', $match[1]);
$this->version = $parts[6];
$this->dateTime = new DateTime($parts[9] . ' ' . $parts[10], new DateTimeZone('UTC'));
} else {
throw new \RuntimeException('Unable to detect CVS version ID.');
}
}

return $this->version;
}

/**
* Get the date/time of the last update
*
* @return DateTime
*/
public function getDateTime()
{
if ($this->dateTime === null) {
$this->getVersion();
}

return $this->dateTime;
}

abstract protected function getContent();
}
124 changes: 124 additions & 0 deletions src/Sslurp/CaRootPemBundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php
/**
* This file is part of Sslurp.
* https://github.com/EvanDotPro/Sslurp
*
* (c) Evan Coury <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sslurp;

class CaRootPemBundle extends AbstractCaRootData
{
/**
* The content of the PEM bundle
*
* @var string
*/
private $pemContent = null;

public function __construct($pemContent = null, MozillaCertData $mozCertData = null)
{
$this->pemContent = $pemContent;
$this->mozCertData = $mozCertData ?: new MozillaCertData();
}

/**
* Return the content of the PEM bundle
*/
public function getContent()
{
if ($this->pemContent === null) {
$this->pemContent = $this->getUpdatedCaRootBundle();
}
return $this->pemContent;
}

public function isLatest()
{
return $this->getVersion() == $this->mozCertData->getVersion();
}

public function getUpdatedCaRootBundle()
{
return $this->buildBundle($this->mozCertData->getContent());
}

public function getMozillaCertData()
{
return $this->mozCertData;
}

protected function buildBundle($rawCertData)
{
$rawCertData = explode("\n", $rawCertData);
$currentDate = defined('SSLURP_OVERRIDE_DATETIME') ? SSLURP_OVERRIDE_DATETIME : date(DATE_RFC822);
$caBundle = <<<EOT
##
## Bundle of CA Root Certificates
##
## Generated $currentDate
## Generated with Sslurp (https://github.com/EvanDotPro/Sslurp)
##
## This is a bundle of X.509 certificates of public Certificate Authorities
## (CA). These were automatically extracted from Mozilla's root certificates
## file (certdata.txt). This file can be found in the mozilla source tree:
## '/mozilla/security/nss/lib/ckfw/builtins/certdata.txt'
##
## It contains the certificates in PEM format and therefore
## can be directly used with curl / libcurl / php_curl, or with
## an Apache+mod_ssl webserver for SSL client authentication.
## Just configure this file as the SSLCACertificateFile.
##
EOT;
$caName = '';
while (($line = array_shift($rawCertData)) !== null) {
if (preg_match('/^#|^\s*$/', $line)) {
continue;
}

$line = rtrim($line);

if (preg_match('/^(CVS_ID\s+\".*\")/', $line, $match)) {
$caBundle .= "# {$match[1]}\n";
}

if (preg_match('/^CKA_LABEL\s+[A-Z0-9]+\s+\"(.*)\"/', $line, $match)) {
$caName = $match[1];
}

if (preg_match('/^CKA_VALUE MULTILINE_OCTAL/', $line)) {
$data = '';
while ($line = array_shift($rawCertData)) {
if (preg_match('/^END/', $line)) {
break;
}

$line = rtrim($line);
$octets = explode('\\', $line);
array_shift($octets);

foreach ($octets as $oct) {
$data .= chr(octdec($oct));
}
}

$caBundle .= $this->buildPemString($caName, $data);
}
}

return $caBundle;
}

protected function buildPemString($caName, $data)
{
return "\n{$caName}\n"
. str_repeat('=', strlen($caName)) . "\n"
. "-----BEGIN CERTIFICATE-----\n"
. chunk_split(base64_encode($data), 76, "\n")
. "-----END CERTIFICATE-----\n";
}
}
159 changes: 159 additions & 0 deletions src/Sslurp/MozillaCertData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php
/**
* This file is part of Sslurp.
* https://github.com/EvanDotPro/Sslurp
*
* (c) Evan Coury <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Sslurp;

class MozillaCertData extends AbstractCaRootData
{
// mxr.mozilla.org cert expires Nov 28th, 2013
const MOZILLA_MXR_SSL_PIN = '47cac6d8f2c2363675e6f433970f27523824d0ec';

/**
* certdata.txt contents
*
* @var string
*/
private $certData = null;

/**
* Stream context
*
* @var resource
*/
private $context = null;

/**
* @param string $certData Used for unit testing
*/
public function __construct($certData = null)
{
$this->certData = $certData;
}

/**
* Get the raw certdata.txt contents from mxr.mozilla.org
*
* @return string
*/
public function getContent()
{
if ($this->certData === null) {
$this->certData = $this->fetchLatestCertData();
}

return $this->certData;
}

/**
* Get the stream context for the TCP connection to the server.
*
* If no stream context is set, will create a default one.
*
* @return resource
*/
public function getStreamContext()
{
if (! $this->context) {
$this->context = stream_context_create(array('ssl' => array(
'capture_peer_cert' => true,
'verify_peer' => true,
'allow_self_signed' => false,
'cafile' => $this->getRootCaBundlePath(),
'CN_match' => 'mxr.mozilla.org',
)));
}

return $this->context;
}

protected function fetchLatestCertData()
{
$ctx = $this->getStreamContext();

$fp = stream_socket_client('ssl:https://mxr.mozilla.org:443', $errNo, $errStr, 30, STREAM_CLIENT_CONNECT, $ctx);

if (!$fp) {
throw new \RuntimeException($errStr, $errNo);
}

$headers = "GET /mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1 HTTP/1.1\r\n";
$headers .= "Host: mxr.mozilla.org\r\n";
$headers .= "Connection: close\r\n";
$headers .= "Accept: */*\r\n";
fwrite($fp, "{$headers}\r\n"); // send request

$response = '';
while (!feof($fp)) {
$response .= fgets($fp);
}
fclose($fp);

$params = stream_context_get_params($ctx);
$cert = new X509Certificate($params['options']['ssl']['peer_certificate']);
$pin = $cert->getPin();

if ($pin !== static::MOZILLA_MXR_SSL_PIN) {
if (time() > 1383282000) { // If it's November 1st, 2013 or later (mxr.mozilla.org cert expires Nov 28th, 2013)
echo "WARNING: mxr.mozilla.org certificate pin may be out of date. " .
"If you see this, message, please file an issue at https://github.com/EvanDotPro/Sslurp/issues\n";
} else {
echo "ERROR: Certificate pin for mxr.mozilla.org did NOT match expected value!\n\n";
echo 'Expected: ' . static::MOZILLA_MXR_SSL_PIN . "\n";
echo "Received: {$pin}\n";
exit(1);
}
}

return $this->getResponseBody($response);
}

protected function getResponseBody($string)
{
$lines = explode("\r\n", $string);

$isHeader = true;
$headers = $content = array();

while ($lines) {
$nextLine = array_shift($lines);

if ($isHeader && $nextLine == '') {
$isHeader = false;
continue;
}
if ($isHeader) {
$headers[] = $nextLine;
} else {
$content[] = $nextLine;
}
}

return implode("\r\n", $content);
}

protected function getRootCaBundlePath()
{
$caBundlePaths = array(
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/certs/ca-certificates.crt',
'/etc/ssl/ca-bundle.pem',
'/usr/share/ssl/certs/ca-bundle.crt',
__DIR__ . '/../../data/Equifax_Secure_Ca.pem',
);

foreach ($caBundlePaths as $caBundle) {
if (is_readable($caBundle)) {
break;
}
}

return $caBundle;
}
}
Loading

0 comments on commit 78f7044

Please sign in to comment.