From c952738750e034a36360f41a5829734efa1539ac Mon Sep 17 00:00:00 2001 From: Deon George Date: Wed, 8 Oct 2014 23:17:00 +1100 Subject: [PATCH] Added SSL --- application/bootstrap.php | 1 + .../classes/Controller/User/Welcome.php | 30 +- application/classes/Model/Account.php | 24 ++ application/classes/ORM.php | 34 ++ application/views/account/user/edit.php | 18 +- application/views/ssl/user/add.php | 35 ++ modules/ssl/classes/Controller/Admin/Ssl.php | 149 ++++++++ modules/ssl/classes/Controller/SSL.php | 14 + modules/ssl/classes/Controller/User/Ssl.php | 96 +++++ modules/ssl/classes/Model/SSL.php | 339 ++++++++++++++++++ modules/ssl/classes/Model/SSL/CA.php | 162 +++++++++ modules/ssl/config/openssl.cnf | 284 +++++++++++++++ modules/ssl/config/ssl.php | 22 ++ modules/ssl/messages/models/ssl_ca.php | 22 ++ modules/ssl/views/ssl/admin/add_edit.php | 85 +++++ modules/ssl/views/ssl/user/add.php | 20 ++ modules/ssl/views/ssl/user/view.php | 87 +++++ 17 files changed, 1411 insertions(+), 11 deletions(-) create mode 100644 application/classes/Model/Account.php create mode 100644 application/classes/ORM.php create mode 100644 application/views/ssl/user/add.php create mode 100644 modules/ssl/classes/Controller/Admin/Ssl.php create mode 100644 modules/ssl/classes/Controller/SSL.php create mode 100644 modules/ssl/classes/Controller/User/Ssl.php create mode 100644 modules/ssl/classes/Model/SSL.php create mode 100644 modules/ssl/classes/Model/SSL/CA.php create mode 100644 modules/ssl/config/openssl.cnf create mode 100644 modules/ssl/config/ssl.php create mode 100644 modules/ssl/messages/models/ssl_ca.php create mode 100644 modules/ssl/views/ssl/admin/add_edit.php create mode 100644 modules/ssl/views/ssl/user/add.php create mode 100644 modules/ssl/views/ssl/user/view.php diff --git a/application/bootstrap.php b/application/bootstrap.php index 3a060c4..69941ee 100644 --- a/application/bootstrap.php +++ b/application/bootstrap.php @@ -119,6 +119,7 @@ Kohana::$config->attach(new Config_File); * Enable modules. Modules are referenced by a relative or absolute path. */ Kohana::modules(array( + 'ssl' => MODPATH.'ssl', // SSL Management Module 'tsm' => MODPATH.'tsm', // TSM Module // 'lnauth' => MODPATH.'lnauth', // lnAuth Base Authentication Tools 'lnapp' => MODPATH.'lnapp', // lnApp Base Application Tools diff --git a/application/classes/Controller/User/Welcome.php b/application/classes/Controller/User/Welcome.php index 68af41d..6d5cf5b 100644 --- a/application/classes/Controller/User/Welcome.php +++ b/application/classes/Controller/User/Welcome.php @@ -12,6 +12,10 @@ class Controller_User_Welcome extends Controller_Welcome { protected $auth_required = TRUE; + protected $secure_actions = array( + 'index'=>0, + ); + public function action_index() { $n = ORM::factory('ADMIN')->where('EMAIL_ADDRESS','=',$this->ao->email)->find_all(); if (! $n->count()) @@ -31,7 +35,29 @@ class Controller_User_Welcome extends Controller_Welcome { )); Block::factory() - ->title(sprintf('Your ADMINs in TSMs : %s',$this->ao->name())) + ->title(sprintf('Your ADMINs in TSM : %s',$this->ao->name())) + ->title_icon('icon-info-sign') + ->span(9) + ->body($output); + + $n = $this->ao->ssl->find_all(); + if (! $n->count()) + $output = 'You have no currently SSL Certificates, would you like to '.HTML::anchor(URL::link('user','ssl/add'),'add').' one?'; + else + $output = Table::factory() + ->data($n) + ->columns(array( + 'id'=>'ID', + 'dn()'=>'Cert', + 'valid_to(TRUE)'=>'Expires', + 'issuer_cn()'=>'Issuer', + )) + ->prepend(array( + 'id'=>array('url'=>URL::link('user','ssl/view/')), + )); + + Block::factory() + ->title(sprintf('Your SSL Certificates for TSM : %s',$this->ao->name())) ->title_icon('icon-info-sign') ->span(9) ->body($output); @@ -54,7 +80,7 @@ class Controller_User_Welcome extends Controller_Welcome { )); Block::factory() - ->title(sprintf('Your NODES in TSMs : %s',$this->ao->name())) + ->title(sprintf('Your NODES in TSM : %s',$this->ao->name())) ->title_icon('icon-info-sign') ->span(9) ->body($output); diff --git a/application/classes/Model/Account.php b/application/classes/Model/Account.php new file mode 100644 index 0000000..5478069 --- /dev/null +++ b/application/classes/Model/Account.php @@ -0,0 +1,24 @@ +array('model'=>'SSL','far_key'=>'id','foreign_key'=>'account_id'), + ); + + public function id() { + if (! $this->prefix) + throw HTTP_Exception::factory(501,'Your prefix is missing, please contact an admin'); + + return strlen($this->prefix) > 1 ? $this->prefix : sprintf('%s%06d',$this->prefix,$this->id); + } +} +?> diff --git a/application/classes/ORM.php b/application/classes/ORM.php new file mode 100644 index 0000000..891d408 --- /dev/null +++ b/application/classes/ORM.php @@ -0,0 +1,34 @@ +where('status','=',TRUE); + } + + /** + * Function help to find records that are active + */ + public function list_active() { + return $this->_where_active()->find_all(); + } + + public function where_active() { + return $this->_where_active(); + } +} +?> diff --git a/application/views/account/user/edit.php b/application/views/account/user/edit.php index 285a635..56d0e82 100644 --- a/application/views/account/user/edit.php +++ b/application/views/account/user/edit.php @@ -1,9 +1,9 @@
Account Details - display('email'),array('label'=>'Email','class'=>'col-md-3','placeholder'=>'Email Address','type'=>'email','required','data-error'=>'Invalid EMAIL address')); ?> + display('email'),array('label'=>'Email','divclass'=>'col-md-3','placeholder'=>'Email Address','type'=>'email','required','data-error'=>'Invalid EMAIL address')); ?> - display('company'),array('label'=>'Company','class'=>'col-md-3','placeholder'=>'Company Name','required')); ?> + display('company'),array('label'=>'Company','divclass'=>'col-md-3','placeholder'=>'Company Name','required')); ?>
@@ -12,33 +12,33 @@ display('title'),array('class'=>'form-control','required','nocg'=>TRUE)); ?>
- display('first_name'),array('class'=>'form-control col-md-2','placeholder'=>'First Name','required','nocg'=>TRUE)); ?> + display('first_name'),array('class'=>'form-control','placeholder'=>'First Name','required','nocg'=>TRUE)); ?>
- display('last_name'),array('class'=>'form-control col-md-2','placeholder'=>'Last Name','required','nocg'=>TRUE)); ?> + display('last_name'),array('class'=>'form-control','placeholder'=>'Last Name','required','nocg'=>TRUE)); ?>
- display('address1'),array('class'=>'col-md-6','placeholder'=>'Address Line 1','required')); ?> + display('address1'),array('divclass'=>'col-md-6','placeholder'=>'Address Line 1','required')); ?> - display('address2'),array('class'=>'col-md-6','placeholder'=>'Address Line 2')); ?> + display('address2'),array('divclass'=>'col-md-6','placeholder'=>'Address Line 2')); ?>
- display('city'),array('label'=>'City','placeholder'=>'City','required','nocg'=>TRUE)); ?> + display('city'),array('class'=>'form-control','label'=>'City','placeholder'=>'City','required','nocg'=>TRUE)); ?>
- display('state'),array('label'=>'','class'=>'input-mini','placeholder'=>'State','required','nocg'=>TRUE)); ?> + display('state'),array('class'=>'form-control','label'=>'','class'=>'input-mini','placeholder'=>'State','required','nocg'=>TRUE)); ?>
- display('zip'),array('label'=>'','class'=>'input-mini','placeholder'=>'Post Code','required','nocg'=>TRUE)); ?> + display('zip'),array('class'=>'form-control','label'=>'','class'=>'input-mini','placeholder'=>'Post Code','required','nocg'=>TRUE)); ?>
diff --git a/application/views/ssl/user/add.php b/application/views/ssl/user/add.php new file mode 100644 index 0000000..590fb82 --- /dev/null +++ b/application/views/ssl/user/add.php @@ -0,0 +1,35 @@ +
+ SSL Certificate Details + +

To use SSL with a Tivoli Storage Manager Client, you need to receive a certificate from this service.

+

Please do the following:

+

    +
  1. Open up a command prompt, and depending on your operating system, change to your BA Client BIN directory. For example:
    +
    +
    Linux
    +
    cd /opt/tivoli/tsm/client/ba/bin
    +
    Windows
    +
    cd "C:\Program Files\Tivoli\TSM\baclient"
    +
    +
  2. +
  3. Create your SSL Certificate Store dsmsert.kdb with the following command:
    + gsk8capicmd_64 -keydb -create -db dsmcert.kdb -type kdb -stash

    +
  4. +
  5. Create a Certificate Signing Request using the following command:
    + gsk8capicmd_64 -certreq -create -db dsmcert.kdb -stashed -label 'TSM-SL01' -dn 'O=IBM,cn=id(); ?>' -size 2048 -file id(); ?>.CSR

    +
  6. + +
  7. Paste the contents of your CSR file here:
    + 'col-md-6','label'=>'CSR','placeholder'=>'Certificate Sign Request','style'=>'font-family: monospace;','cols'=>61,'rows'=>15)); ?> +
  8. + +
  9. Submit your CSR to be signed, you'll receive a certificate once approved
  10. +
+
+ +
+
+ + +
+
diff --git a/modules/ssl/classes/Controller/Admin/Ssl.php b/modules/ssl/classes/Controller/Admin/Ssl.php new file mode 100644 index 0000000..1fad9f9 --- /dev/null +++ b/modules/ssl/classes/Controller/Admin/Ssl.php @@ -0,0 +1,149 @@ +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()); + } +} +?> diff --git a/modules/ssl/classes/Controller/SSL.php b/modules/ssl/classes/Controller/SSL.php new file mode 100644 index 0000000..6a142d1 --- /dev/null +++ b/modules/ssl/classes/Controller/SSL.php @@ -0,0 +1,14 @@ + diff --git a/modules/ssl/classes/Controller/User/Ssl.php b/modules/ssl/classes/Controller/User/Ssl.php new file mode 100644 index 0000000..c689ddf --- /dev/null +++ b/modules/ssl/classes/Controller/User/Ssl.php @@ -0,0 +1,96 @@ +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)); + } +} +?> diff --git a/modules/ssl/classes/Model/SSL.php b/modules/ssl/classes/Model/SSL.php new file mode 100644 index 0000000..3342e2d --- /dev/null +++ b/modules/ssl/classes/Model/SSL.php @@ -0,0 +1,339 @@ +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 .= '
'; + $output .= '
'; + $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 .= ''; + } + + $output .= Form::button('download','Download',array('class'=>'btn btn-default','nocg'=>TRUE)); + + if ($passwd_len) { + $output .= ''; + $output .= '
'; + $output .= '
'; + $output .= '
'; + } + + $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; + } +} +?> diff --git a/modules/ssl/classes/Model/SSL/CA.php b/modules/ssl/classes/Model/SSL/CA.php new file mode 100644 index 0000000..f65067e --- /dev/null +++ b/modules/ssl/classes/Model/SSL/CA.php @@ -0,0 +1,162 @@ +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; + } +} +?> diff --git a/modules/ssl/config/openssl.cnf b/modules/ssl/config/openssl.cnf new file mode 100644 index 0000000..90bb26e --- /dev/null +++ b/modules/ssl/config/openssl.cnf @@ -0,0 +1,284 @@ +# OpenSSL example configuration file. + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] + +# We can add new OIDs in here for use by 'ca' and 'req'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +dir = . # Where everything is kept +certs = $dir # Where the issued certs are kept +crl_dir = $dir # Where the issued crl are kept +database = $dir/index.txt # database index file. +new_certs_dir = $dir # default place for new certs. + +certificate = $dir/ca.crt # The CA certificate +serial = $dir/serial # The current serial number +crl = $dir/crl.pem # The current CRL +private_key = $dir/ca.key # The private key +RANDFILE = $dir/.rand # private random number file + +x509_extensions = usr_cert # The extentions to add to the cert + +# Extension copying option: use with caution. +copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +crl_extensions = crl_ext + +default_days = 375 # how long to certify for +default_crl_days = 30 # how long before next CRL +default_md = sha1 # which md to use. +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = optional +stateOrProvinceName = optional +organizationName = supplied +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ policy_o ] +countryName = match +stateOrProvinceName = supplied +organizationName = supplied +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ policy_ou ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = supplied +commonName = supplied +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extentions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString. +# utf8only: only UTF8Strings. +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings +# so use this option with caution! +string_mask = nombstr + +req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = AU +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = VIC + +localityName = Locality Name (eg, city) +localityName_default = Melbourne + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = IBM Australia +#0.organizationName_default = $ENV::KEY_ORG + +# we can do this but it is not needed normally :-) +#1.organizationName = Second Organization Name (eg, company) +#1.organizationName_default = World Wide Web Pty Ltd + +organizationalUnitName = Organizational Unit Name (eg, section) +organizationalUnitName_default = CSI Data Software +#organizationalUnitName_default = $ENV::KEY_OU +#organizationalUnitName_default = + +commonName = Common Name (eg, your name or your server\'s hostname) +#commonName_default = $ENV::KEY_CN +commonName_max = 64 + +#emailAddress = Email Address +#emailAddress_default = $ENV::KEY_EMAIL +#emailAddress_max = 40 + +# SET-ex3 = SET extension number 3 +#subjectAltName = Alternative Names + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +# unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +#basicConstraints = CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy + +# Copy subject details +issuerAltName = issuer:copy,URI:https://www.csidata.co +authorityInfoAccess = OCSP;URI:https://www.csidata.co/ssl/ocsp,caIssuers;URI:https://www.csidata.co/ssl/ca +crlDistributionPoints = URI:https://www.csidata.co/ssl/crl + +## Should add --extensions client to client certs +[ client ] + +nsCertType = client,email,objsign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always +issuerAltName = issuer:copy,URI:https://www.csidata.co +authorityInfoAccess = OCSP;URI:https://www.csidata.co/ssl/ocsp,caIssuers;URI:https://www.csidata.co/ssl/ca +crlDistributionPoints = URI:https://www.csidata.co/ssl/crl + +## Should add --extensions server to server certs +[ server ] + +nsCertType = server +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer:always +issuerAltName = issuer:copy,URI:https://www.csidata.co +authorityInfoAccess = OCSP;URI:https://www.csidata.co/ssl/ocsp,caIssuers;URI:https://www.csidata.co/ssl/ca +crlDistributionPoints = URI:https://www.csidata.co/ssl/crl + +[ v3_req ] + +# Extensions to add to a certificate request +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +[ v3_ca ] +basicConstraints = critical,CA:true,pathlen:0 + +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA, emailCA + +issuerAltName = issuer:copy,URI:https://www.csidata.co +authorityInfoAccess = OCSP;URI:https://www.csidata.co/ssl/ocsp,caIssuers;URI:https://www.csidata.co/ssl/ca +crlDistributionPoints = URI:https://www.csidata.co/ssl/crl + +[ v3_ca_ou ] +basicConstraints = critical,CA:true,pathlen:0 + +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA, emailCA + +issuerAltName = issuer:copy,URI:https://www.csidata.co +authorityInfoAccess = OCSP;URI:https://www.csidata.co/ssl/ocsp,caIssuers;URI:https://www.csidata.co/ssl/ca +crlDistributionPoints = URI:https://www.csidata.co/ssl/crl + +[ v3_ca_o ] +basicConstraints = critical,CA:true,pathlen:0 + +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA, emailCA + +issuerAltName = issuer:copy,URI:https://www.csidata.co +authorityInfoAccess = OCSP;URI:https://www.csidata.co/ssl/ocsp,caIssuers;URI:https://www.csidata.co/ssl/ca +crlDistributionPoints = URI:https://www.csidata.co/ssl/crl + +[ v3_ca_ca ] +basicConstraints = critical,CA:true,pathlen:1 + +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer:always +keyUsage = cRLSign, keyCertSign +nsCertType = sslCA, emailCA + +issuerAltName = issuer:copy,URI:https://www.csidata.co +authorityInfoAccess = OCSP;URI:https://www.csidata.co/ssl/ocsp,caIssuers;URI:https://www.csidata.co/ssl/ca +crlDistributionPoints = URI:https://www.csidata.co/ssl/crl + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier = keyid:always,issuer:always diff --git a/modules/ssl/config/ssl.php b/modules/ssl/config/ssl.php new file mode 100644 index 0000000..df4a90d --- /dev/null +++ b/modules/ssl/config/ssl.php @@ -0,0 +1,22 @@ + 7, + + // Min password length for exports + 'minpass_length' => 0, + + // Location to openssl config + 'config' => 'openssl.cnf', +); +?> diff --git a/modules/ssl/messages/models/ssl_ca.php b/modules/ssl/messages/models/ssl_ca.php new file mode 100644 index 0000000..b4ae1f0 --- /dev/null +++ b/modules/ssl/messages/models/ssl_ca.php @@ -0,0 +1,22 @@ +array( + 'isCert'=>'This is not a valid certificate', + 'isCA'=>'This is certificate is not a Certificate Authority certificate', + ), + 'ssl_ca_id'=>array( + 'rule_parentExist'=>'The parent certificate doesnt exist, please define it first', + ), +); +?> diff --git a/modules/ssl/views/ssl/admin/add_edit.php b/modules/ssl/views/ssl/admin/add_edit.php new file mode 100644 index 0000000..0dbfd94 --- /dev/null +++ b/modules/ssl/views/ssl/admin/add_edit.php @@ -0,0 +1,85 @@ + +
+
+ SSL CA Certificate Edit/Update + +
+
Subject
+
dn(); ?>
+ +
Subject Key ID
+
ski(); ?>
+ +
Serial Number
+
serial(); ?>
+ + isRoot()) AND ! $o->isRoot()) : ?> +
Issuer
+
+ validParent()) : ?> + ssl_ca_id,$o->issuer()); ?> + + issuer(); ?> + +
+ +
Issuer Key ID
+
aki_keyid(); ?>
+ +
Issuer Serial
+
aki_serial(); ?>
+ +
Issuer Valid
+
validParent(TRUE); ?>
+ + +
Status
+
Root Ceritificate
+ + +
Path Length
+
ca_path_len(); ?>
+ +
Hash
+
hash(); ?>
+ +
Valid From
+
valid_from(TRUE); ?>
+ +
Valid To
+
valid_to(TRUE); ?>
+ +
Hash
+
hash(); ?>
+ +
Version
+
version(); ?>
+ +
Key Algorithm
+
algorithm(); ?>
+
+
+
+ + +
+
+ CSR Data + csr,array('divclass'=>'col-md-12','placeholder'=>'Certificate Sign Request','style'=>'font-family: monospace; font-size: 95%;','rows'=>Form::textarea_rows($o->csr))); ?> + + Private Key Data + pk,array('divclass'=>'col-md-12','placeholder'=>'Private Key','style'=>'font-family: monospace; font-size: 95%;','rows'=>Form::textarea_rows($o->pk))); ?> +
+ +
+ Certificate Data + cert,array('divclass'=>'col-md-12','placeholder'=>'Public Certificate','style'=>'font-family: monospace; font-size: 95%;','rows'=>Form::textarea_rows($o->cert))); ?> +
+
+ +
+
+ + +
+
diff --git a/modules/ssl/views/ssl/user/add.php b/modules/ssl/views/ssl/user/add.php new file mode 100644 index 0000000..8d1aebc --- /dev/null +++ b/modules/ssl/views/ssl/user/add.php @@ -0,0 +1,20 @@ +
+ SSL Certificate Details + +

To use SSL with this application, you need to receive a certificate from this service.

+

Please do the following:

+

    +
  1. Paste the contents of your CSR file here:
    + 'col-md-6','label'=>'CSR','placeholder'=>'Certificate Sign Request','style'=>'font-family: monospace;','cols'=>61,'rows'=>15)); ?> +
  2. + +
  3. Submit your CSR to be signed, you'll receive a certificate once approved
  4. +
+
+ +
+
+ + +
+
diff --git a/modules/ssl/views/ssl/user/view.php b/modules/ssl/views/ssl/user/view.php new file mode 100644 index 0000000..e710293 --- /dev/null +++ b/modules/ssl/views/ssl/user/view.php @@ -0,0 +1,87 @@ +
+ SSL Details + +
+
Subject
+
dn(); ?>
+ + cert) : ?> +
Subject Key ID
+
ski(); ?>
+ +
Serial Number
+
serial(); ?>
+ +
Issuer
+
+ validCA()) : ?> + ca->id,$o->issuer()); ?> + + issuer(); ?> + +
+ +
Issuer Key ID
+
aki_keyid(); ?>
+ +
Issuer Serial
+
aki_serial(); ?>
+ +
Valid From
+
valid_from(TRUE); ?>
+ +
Valid To
+
valid_to(TRUE); ?>
+ +
Hash
+
hash(); ?>
+ +
Version
+
version(); ?>
+ +
Algorithm
+
algorithm(); ?>
+ + +
Status
+
Waiting to be signed.
+ +
+ + cert) : ?> +
+ Certificate Chain + + data($o->list_ca()) + ->columns(array( + 'id'=>'ID', + 'subject_cn()'=>'Cert', + 'valid_to(TRUE)'=>'Expires', + 'issuer_cn()'=>'Issuer', + )) + ->prepend(array( + 'id'=>array('url'=>URL::link('admin','ssl/edit/')), + )); ?> + +
+ +
+ Certificate + +
cert ? $o->cert : $o->csr; ?>
+ + download_button(); + + if ($ao=Auth::instance()->get_user() AND ($ao->isAdmin()) AND $o->service->status AND ($o->valid_to()-(Kohana::$config->load('ssl.min_renew_days')*86400) <= time()) AND $o->service->paid_to() > time()) : + echo Form::open(URL::link('admin','ssl/renew/'.$o->service->id)); + echo Form::button('submit','Renew',array('class'=>'btn btn-primary')); + endif + ?> + + +