Added SSL

This commit is contained in:
Deon George
2014-10-08 23:17:00 +11:00
parent 62992c1a0e
commit c952738750
17 changed files with 1411 additions and 11 deletions

View File

@@ -0,0 +1,149 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides Reseller SSL functions
*
* @package SSL
* @category Controllers/Admin
* @author Deon George
* @copyright (c) 2009-2014 Deon George
* @license http://dev.leenooks.net/license.html
*/
class Controller_Admin_SSL extends Controller_SSL {
protected $auth_required = TRUE;
protected $secure_actions = array(
'add'=>3,
'edit'=>3,
'list'=>3,
'listchildca'=>3,
'listchildcrt'=>3,
'renew'=>3,
);
public function action_add() {
Block::factory()
->type('form-horizontal')
->title('Add/View SSL CA')
->title_icon('icon-wrench')
->body($this->add_edit());
}
public function action_edit() {
list($id,$output) = Table::page(__METHOD__);
Block::factory()
->type('form-horizontal')
->title(sprintf('%s: %s',_('Add/View SSL CA'),$id))
->title_icon('icon-wrench')
->body($this->add_edit($id,$output));
}
public function action_list() {
Block::factory()
->title('SSL CA Certificates')
->title_icon('icon-th-list')
->body(Table::factory()
->data(ORM::factory('SSL_CA')->find_all())
->columns(array(
'id'=>'ID',
'subject_cn()'=>'Cert',
'valid_to(TRUE)'=>'Expires',
'validParent(TRUE)'=>'Valid',
'count_ca_child(FALSE)'=>'cCA',
'count_ssl_child(FALSE)'=>'Crts',
'issuer()'=>'Issuer',
))
->prepend(array(
'id'=>array('url'=>URL::link('admin','ssl/edit/')),
))
);
}
public function action_listchildca() {
list($id,$output) = Table::page(__METHOD__);
$sco = ORM::factory('SSL_CA',$id);
if ($sco->list_ca_child())
Block::factory()
->title(sprintf('SSL CA Certificates for CA: %s',$sco->dn()))
->title_icon('icon-th-list')
->body(Table::factory()
->data($sco->where_active()->list_ca_child())
->columns(array(
'id'=>'ID',
'subject_cn()'=>'Cert',
'ski()'=>'Identifier',
'valid_to(TRUE)'=>'Expires',
'validParent(TRUE)'=>'Valid',
'count_ca_child(FALSE)'=>'cCA',
'count_ssl_child(FALSE)'=>'Crts',
))
->prepend(array(
'id'=>array('url'=>URL::link('admin','ssl/edit/')),
))
);
if ($sco->list_ssl_child())
$this->action_listchildcrt();
}
public function action_listchildcrt() {
list($id,$output) = Table::page(__METHOD__);
$sco = ORM::factory('SSL_CA',$id);
Block::factory()
->title(sprintf('SSL Certificates for CA: %s',$sco->dn()))
->title_icon('icon-th-list')
->body(Table::factory()
->jssort('crt')
->data($sco->list_ssl_child())
->columns(array(
'id'=>'ID',
'subject_cn()'=>'Cert',
'ski()'=>'Identifier',
'valid_to(TRUE)'=>'Expires',
'validCA(TRUE)'=>'Valid',
))
->prepend(array(
'id'=>array('url'=>URL::link('user','ssl/view/')),
))
);
}
public function action_renew() {
$so = ORM::factory('SSL',$this->request->param('id'));
if (! $so->loaded() OR ! Auth::instance()->authorised($so->account))
throw HTTP_Exception::factory(403,'Service either doesnt exist, or you are not authorised to see it');
$so->sign();
HTTP::redirect(URL::link('user','ssl/view/'.$so->id));
}
private function add_edit($id=NULL,$output='') {
$sco = ORM::factory('SSL_CA',$id);
if ($this->request->post()) {
if (! $sco->account_id)
$sco->account_id = (string)Auth::instance()->get_user();
// Set our values, so that our filters have data
$sco->values($this->request->post());
// To trigger our filter to get the correct parent
$sco->ssl_ca_id = -1;
if ($sco->changed() AND ! $this->save($sco))
$sco->reload();
if ($sco->saved())
HTTP::redirect(URL::link('admin','ssl/edit/'.$sco->id));
}
return View::factory('ssl/admin/add_edit')
->set('o',$sco)
->set('mode',$this->request->action());
}
}
?>

View File

@@ -0,0 +1,14 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides SSL management
*
* @package SSL
* @category Controllers
* @author Deon George
* @copyright (c) 2009-2014 Deon George
* @license http://dev.leenooks.net/license.html
*/
class Controller_SSL extends Controller_TemplateDefault {
}
?>

View File

@@ -0,0 +1,96 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides User SSL functions
*
* @package SSL
* @category Controllers/User
* @author Deon George
* @copyright (c) 2009-2013 Deon George
* @license http://dev.leenooks.net/license.html
*/
class Controller_User_SSL extends Controller_SSL {
protected $auth_required = TRUE;
protected $secure_actions = array(
'add'=>0,
'download'=>0,
'view'=>0,
);
public function action_add() {
if ($this->request->post()) {
$so = ORM::factory('SSL');
$so->account_id = (string)Auth::instance()->get_user();
// Set our values, so that our filters have data
$so->values($this->request->post());
$this->save($so);
if ($so->saved())
HTTP::redirect(URL::link('user','ssl/view/'.$so->id));
}
Block::factory()
->type('form-horizontal')
->title('Add/Edit Record')
->title_icon('fa-wrench')
->body(View::factory('ssl/user/add')->set('o',$this->ao));
}
public function action_download() {
$passwd_len = Kohana::$config->load('ssl')->minpass_length;
$so = ORM::factory('SSL',$this->request->post('sid'));
if (! $so->loaded() OR ! Auth::instance()->authorised($so->account))
throw HTTP_Exception::factory(403,'SSL either doesnt exist, or you are not authorised to see it');
if ($passwd_len) {
$passwd = $this->request->post('passwd');
if (strlen($passwd) < $passwd_len) {
SystemMessage::add(array(
'title'=>_('Validation failed'),
'type'=>'danger',
'body'=>_('Your requested password is too short.'),
));
HTTP::redirect(URL::link('user','ssl/view/'.$so->id));
}
}
$this->auto_render = FALSE;
$this->response->headers('Content-Type','plain/text');
$this->response->headers('Content-Disposition','attachment; filename="'.$this->ao->id().'.crt"');
$this->response->body($so->cert);
}
public function action_view() {
$so = ORM::factory('SSL',$this->request->param('id'));
if (! $so->loaded() OR ! Auth::instance()->authorised($so->account))
throw HTTP_Exception::factory(403,'SSL either doesnt exist, or you are not authorised to see it');
if ($this->request->post()) {
$so->account_id = (string)Auth::instance()->get_user();
// Set our values, so that our filters have data
$so->values($this->request->post());
if ($so->changed() AND ! $this->save($so))
$so->reload();
if ($so->saved())
HTTP::redirect(URL::link('user','ssl/view/'.$so->id));
}
Block::factory()
->title('SSL Certificate')
->title_icon('fa-certificate')
->body(View::factory('ssl/user/view')->set('o',$so));
}
}
?>

View File

@@ -0,0 +1,339 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class supports SSL Certificates
*
* @package SSL
* @category Models
* @author Deon George
* @copyright (c) 2009-2013 Deon George
* @license http://dev.leenooks.net/license.html
*/
class Model_SSL extends ORM {
protected $_cert_details = NULL;
// Relationships
protected $_belongs_to = array(
'account'=>array(),
);
protected $_has_one = array(
'ca'=>array('model'=>'SSL_CA','far_key'=>'ssl_ca_id','foreign_key'=>'id'),
);
protected $_display_filters = array(
'csr'=>array(
array('Model_SSL::subject_csr',array(':value')),
),
'cert'=>array(
array('Model_SSL::subject_cert',array(':value')),
),
);
protected $_save_message = TRUE;
/**
* Parse our AuthorityKeyIndentifier Extension to extract information
* @param $key Return just that index
*/
private function _aki($key=NULL) {
$result = array();
$aki = $this->_extensions('authorityKeyIdentifier');
if (! $aki)
return '';
foreach (explode("\n",preg_replace("/\n$/",'',$aki)) as $x) {
if (! $x)
continue;
if (strstr($x,':')) {
list($a,$b) = explode(':',$x,2);
$result[strtolower($a)] = $b;
}
}
return is_null($key) ? $result : Arr::get($result,$key);
}
/**
* This function will convert a large decimal number into hex
* @param $number Large decimal number
*/
private static function _dec_to_hex($number) {
$hex = array();
if ($number == 0)
return '00';
while ($number > 0) {
if ($number == 0) {
array_push($hex, '0');
} else {
$x = (int) ($number/16);
array_push($hex,strtoupper(dechex((int)($number-($x*16)))));
$number = $x;
}
}
return preg_replace('/^:/','',preg_replace('/(..)/',":$1",implode(array_reverse($hex))));
}
/**
* Parse our Sign Certifcate to extract information
* @param $key Return just that index
*/
private function _details($key=NULL) {
if (! $this->cert)
return array();
return is_null($key) ? $this->_cert_details : Arr::get($this->_cert_details,$key,array());
}
private static function _dn(array $array) {
$result = '';
$i = 0;
foreach ($array as $k=>$v) {
if ($i++)
$result .= ',';
$result .= sprintf('%s=%s',$k,$v);
}
return $result;
}
/**
* Parse our Sign Certifcate Extensions to extract information
* @param $key Return just that index
*/
protected function _extensions($key=NULL) {
$result = Arr::get($this->_cert_details,'extensions');
return is_null($key) ? $result : Arr::get($result,$key);
}
// We want to inject the SSL object into this Model
protected function _load_values(array $values) {
parent::_load_values($values);
$this->_cert_details = openssl_x509_parse($this->cert);
return $this;
}
public function aki_dirname() {
return $this->_aki('dirname');
}
public function aki_keyid() {
return $this->_aki('keyid');
}
public function aki_serial() {
return $this->_aki('serial');
}
public function algorithm() {
if (! $this->cert)
return NULL;
$e = '';
openssl_x509_export(openssl_x509_read($this->cert),$e,FALSE);
// @todo There must be a nice way to get this?
return (preg_match('/^\s+Signature Algorithm:\s*(.*)\s*$/m',$e,$match)) ? $match[1] : _('Unknown');
}
public function download_button() {
if ($this->valid_to() < time())
return '';
$passwd_len = Kohana::$config->load('ssl')->minpass_length;
$output = Form::open(URL::link('user','ssl/download'),array('class'=>'form-inline','data-toggle'=>'validator','role'=>'form'));
$output .= Form::hidden('sid',$this->id);
if ($passwd_len) {
$output .= '<div class="form-group">';
$output .= '<div class="input-group">';
$output .= Form::password('passwd','',array('class'=>'form-control','placeholder'=>_('Choose a password'),'nocg'=>TRUE,'pattern'=>'.{'.$passwd_len.',}','data-error'=>"Minimum ${passwd_len} characters",'required'));
$output .= '<span class="input-group-btn">';
}
$output .= Form::button('download','Download',array('class'=>'btn btn-default','nocg'=>TRUE));
if ($passwd_len) {
$output .= '</span>';
$output .= '</div>';
$output .= '<div class="help-block with-errors"></div>';
$output .= '</div>';
}
$output .= Form::close();
return $output;
}
public function dn() {
return $this->display($this->signed() ? 'cert' : 'csr');
}
public function hash() {
return Arr::get($this->_cert_details,'hash');
}
public function issuer() {
return self::_dn(Arr::get($this->_cert_details,'issuer',array()));
}
public function serial() {
return $this->_dec_to_hex(Arr::get($this->_cert_details,'serialNumber'));
}
/**
* (Re)Sign an SSL Certificate
*/
public function sign($force=FALSE) {
$ssl_config = Kohana::$config->load('ssl');
$ssl_conf = Kohana::find_file('config',$ssl_config->config,'');
$ssl_conf = array_pop($ssl_conf);
// If our certificate is not old enough skip
if ($this->valid_to() > time()+$ssl_config->min_renew_days*86400 AND ! $force)
return FALSE;
$today = mktime(0,0,0,date('n'),date('j'),date('Y'));
$days = (int)$this->account->renew;
if (! $this->ca->pk)
throw HTTP_Exception::factory(400,'Unable to sign, missing Private Key for CA :ca',array(':ca'=>$this->ca->subject_cn()));
$res = openssl_csr_sign($this->csr,$this->ca->cert,$this->ca->pk,$days,array(
'config'=>$ssl_conf,
'x509_extensions'=>'client',
'digest_alg'=>'sha1',
),time());
if ($res AND openssl_x509_export($res,$cert)) {
$this->cert = $cert;
$this->save();
return TRUE;
} else {
/*
echo Debug::vars(array(
'csr'=>$this->csr,
'ca'=>$this->ca->cert,
'caid'=>$this->ca->id,
'days'=>$days,
'ssl'=>$ssl_conf,
'x509e'=>'client',
// 'command'=>sprintf('openssl ca -days %s -cert /tmp/ssl_ca.crt -keyfile /tmp/ssl_ca.key -out /tmp/ssl.crt -in /tmp/ssl.csr -extensions %s -config %s',$days,'client',$ssl_conf),
));
file_put_contents('/tmp/ssl.csr',$this->csr);
file_put_contents('/tmp/ssl_ca.key',$this->ca->pk);
file_put_contents('/tmp/ssl_ca.crt',$this->ca->cert);
*/
throw HTTP_Exception::factory(501,'Error Creating SSL Certificate :error',array(':error'=>openssl_error_string()));
}
}
private function signed() {
return $this->_cert_details ? TRUE : FALSE;
}
public function ski() {
return $this->_extensions('subjectKeyIdentifier');
}
public function issuer_cn() {
return Arr::get($this->cert ? Arr::get($this->_cert_details,'issuer') : array(),'CN');
}
public function subject_cn() {
return $this->cert ? Arr::get(Arr::get($this->_cert_details,'subject'),'CN') : self::subject_csr($this->csr);
}
public static function subject_cert($cert) {
try {
return self::_dn(Arr::get(openssl_x509_parse($cert),'subject'));
} catch (exception $e) {
return 'Invalid Cert';
}
}
public static function subject_csr($csr) {
try {
return self::_dn(openssl_csr_get_subject($csr));
} catch (exception $e) {
return 'Invalid CSR';
}
}
public function validCA($format=FALSE) {
return StaticList_YesNo::get(($this->_cert_details AND $this->ssl_ca_id AND $this->aki_keyid()==$this->ca->ski()) ? $this->ca->validParent() : FALSE,$format);
}
public function valid_from($format=FALSE) {
$k = Arr::get($this->_cert_details,'validFrom_time_t');
if (! $k)
return NULL;
return $format ? Site::Date($k) : $k;
}
public function valid_to($format=FALSE) {
$k = Arr::get($this->_cert_details,'validTo_time_t');
if (! $k)
return NULL;
return $format ? Site::Date($k) : $k;
}
// If we change the SSL certificate, we need to reload our SSL object
public function values(array $values, array $expected = NULL) {
parent::values($values,$expected);
if (array_key_exists('cert',$this->_changed))
$this->_cert_details = openssl_x509_parse($this->cert);
return $this;
}
public function version() {
return Arr::get($this->_cert_details,'version');
}
public function list_ca() {
$result = array();
if (! $this->validCA())
return $result;
$x = $this;
while (! is_null($x->ssl_ca_id) AND $x->id != $x->ssl_ca_id AND $x=$x->ca)
array_push($result,$x);
return $result;
}
/**
* Return all our CA Certs for this certificate
*/
public function list_ca_crts() {
$result = array();
foreach ($this->list_ca() as $so)
array_push($result,$so->cert);
return $result;
}
}
?>

View File

@@ -0,0 +1,162 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class supports SSL
*
* @package SSL
* @category Models
* @author Deon George
* @copyright (c) 2009-2013 Deon George
* @license http://dev.leenooks.net/license.html
*/
class Model_SSL_CA extends Model_SSL {
// Relationships
protected $_belongs_to = array(
'parent'=>array('model'=>'ssl_ca','foreign_key'=>'ssl_ca_id'),
);
protected $_has_many = array(
'child'=>array('model'=>'ssl_ca','far_key'=>'id','foreign_key'=>'ssl_ca_id'),
'ssl'=>array('model'=>'ssl','far_key'=>'id','foreign_key'=>'ssl_ca_id'),
);
protected $_display_filters = array(
'cert'=>array(
array('Model_SSL::subject_cert',array(':value')),
),
);
public function filters() {
return array(
'ssl_ca_id'=>array(
array(array($this,'filter_getParent')),
)
);
}
public function rules() {
return Arr::merge(parent::rules(),array(
'cert'=>array(
array('not_empty'),
array(array($this,'isCert')),
array(array($this,'isCA')),
),
'ssl_ca_id'=>array(
array(array($this,'rule_parentExist')),
),
));
}
private function _bc() {
return $this->_extensions('basicConstraints');
}
public function ca_path_len() {
$m = array();
$x = preg_match('/.*pathlen:\s*([0-9]+).*$/',$this->_bc(),$m);
return isset($m[1]) ? (int)$m[1] : 0;
}
/**
* Filter to find the parent SSL_CA
*
* @notes This filter only runs when the value passed is -1
*/
public function filter_getParent() {
// This cannot be an array
if (count(func_get_args()) != 1)
return NULL;
$x = func_get_args();
$x = array_pop($x);
// This filter only runs when our value is -1
if ($x != -1)
return $x;
foreach (ORM::factory($this->_object_name)->find_all() as $so)
if ($so->ski() == $this->aki_keyid())
return $so->id;
// If we got here, we couldnt find it
return $this->isRoot() ? NULL : $x;
}
public function isCA() {
return preg_match('/CA:TRUE/',$this->_bc()) ? TRUE : FALSE;
}
public function isCert() {
return $this->_cert_details ? TRUE : FALSE;
}
public function isRoot() {
return $this->aki_keyid() == $this->ski();
}
public function rule_parentExist() {
// Our ssl_ca_id should have been populated by filter_GetParent().
return ($this->ssl_ca_id > 0) OR $this->isRoot();
}
/**
* Make sure we have our parent in the DB too
*/
public function validParent($format=FALSE) {
$result = NULL;
// If we are a root cert, we are valid
if (is_null($this->ssl_ca_id) AND $this->isRoot())
return StaticList_YesNo::get(TRUE,$format);
return StaticList_YesNo::get($this->aki_keyid() == $this->parent->ski(),$format);
}
/**
* List the child CA certs
*/
public function count_ca_child($children=FALSE) {
$result = 0;
if ($children)
foreach ($this->list_ca_child() as $cao)
$result += $cao->count_ca_child($children);
return $result+count($this->list_ca_child());
}
public function count_ssl_child($children=FALSE) {
$result = 0;
if ($children)
foreach ($this->list_ssl_child() as $cao)
$result += $cao->ca->count_ssl_child($children);
return $result+count($this->list_ssl_child());
}
public function list_ca_child() {
$result = array();
foreach ($this->child->find_all() as $co) {
// Ignore me if this is the root certificate
if ($co->id == $this->id)
continue;
array_push($result,$co);
}
return $result;
}
public function list_ssl_child() {
$result = array();
foreach ($this->ssl->find_all() as $co)
if ($co->validCA())
array_push($result,$co);
return $result;
}
}
?>