DaPortal

<?php //$Id$
//Copyright (c) 2015-2016 Pierre Pronchery <khorben@defora.org>
//This file is part of DeforaOS Web DaPortal
//
//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, version 3 of the License.
//
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with this program. If not, see <http://www.gnu.org/licenses/>.
//PKIContent
abstract class PKIContent extends ContentMulti
{
//public
//methods
//essential
//PKIContent::PKIContent
public function __construct(Engine $engine, Module $module,
$properties = FALSE)
{
//fields
$this->fields['country'] = 'Country';
$this->fields['state'] = 'State';
$this->fields['locality'] = 'Locality';
$this->fields['organization'] = 'Organization';
$this->fields['section'] = 'Section';
$this->fields['email'] = 'e-mail';
$this->fields['parent'] = 'Parent CA';
$this->fields['signed'] = 'Signed';
//let PKI content be public by default
$this->setPublic(TRUE);
$this->set('parent', FALSE);
$this->set('signed', FALSE);
parent::__construct($engine, $module, $properties);
}
//accessors
//PKIContent::canSubmit
public function canSubmit(Engine $engine, Request $request = NULL,
&$error = FALSE)
{
$class = static::$class;
if(parent::canSubmit($engine, $request, $error) === FALSE)
return FALSE;
if($request !== NULL)
{
if(($title = $request->get('title')) === FALSE
|| strlen($title) == 0
|| strpos($title, '/') !== FALSE
|| $title == '..')
{
$error = _('Invalid name');
return FALSE;
}
if($this->getSubject($request) === FALSE)
{
$error = _('Invalid subject');
return FALSE;
}
if(($parent = $request->get('parent')) !== FALSE
&& CAPKIContent::load($engine,
$this->getModule(),
$parent) === FALSE)
{
$error = _('Invalid parent');
return FALSE;
}
if($class::loadFromName($engine, $this->getModule(),
$title, $parent) !== FALSE)
{
$error = _('Duplicate name');
return FALSE;
}
}
return TRUE;
}
//PKIContent::getParent
public function getParent(Engine $engine)
{
if(($parent = $this->get('parent')) === FALSE)
return FALSE;
return CAPKIContent::load($engine, $this->getModule(), $parent);
}
//PKIContent::getSubject
public function getSubject(Request $request = NULL)
{
$ret = '';
$fields = array('country' => 'C', 'state' => 'ST',
'locality' => 'L', 'organization' => 'O',
'section' => 'OU', 'cn' => 'CN',
'email' => 'emailAddress');
$s = ($request !== NULL) ? $request : $this;
foreach($fields as $field => $key)
{
switch($field)
{
case 'cn':
$value = $this->getTitle(); //XXX
break;
case 'country':
if(($value = $s->get($field)) === FALSE)
break;
if(strlen($value) != 0
&& strlen($value) != 2)
return FALSE;
break;
default:
$value = $s->get($field);
break;
}
if($value !== FALSE && strlen($value) > 0)
{
if(strchr($value, '/') !== FALSE)
//XXX escape slashes instead?
return FALSE;
$ret.='/'.$key.'='.$value;
}
}
return (strlen($ret) > 0) ? $ret : FALSE;
}
//useful
//PKIContent::displayContent
public function displayContent(Engine $engine, Request $request = NULL)
{
$parent = $this->getParent($engine);
$columns = array('title' => '', 'value' => '');
$fields = array('country' => _('Country: '),
'state' => _('State: '), 'locality' => _('Locality: '),
'organization' => _('Organization: '),
'section' => _('Section: '),
'email' => _('e-mail: '));
$vbox = new PageElement('vbox');
if($parent !== FALSE)
{
$expander = $vbox->append('expander', array(
'title' => _('Parent CA')));
$expander->append($parent->displayContent($engine));
}
if($this->get('signed') === FALSE)
$vbox->append('dialog', array('type' => 'warning',
'text' => sprintf(
_('This %s is not signed'),
static::$text_content)));
$view = $vbox->append('treeview', array('columns' => $columns));
foreach($fields as $k => $v)
{
if(($value = $this->get($k)) === FALSE
|| strlen($value) == 0)
continue;
$view->append('row', array('title' => $v,
'value' => $value));
}
return $vbox;
}
//PKIContent::form
public function form(Engine $engine, Request $request)
{
return parent::form($engine, $request);
}
protected function _formSubmit(Engine $engine, Request $request)
{
$countries = array('AF' => _('Afghanistan'),
'AX' => _('Åland'),
'AL' => _('Albania'),
'DZ' => _('Algeria'),
'AS' => _('American Samoa'),
'AD' => _('Andorra'),
'AO' => _('Angola'),
'AI' => _('Anguilla'),
'AQ' => _('Antarctica'),
'AG' => _('Antigua and Barbuda'),
'AR' => _('Argentina'),
'AM' => _('Armenia'),
'AW' => _('Aruba'),
'AU' => _('Australia'),
'AT' => _('Austria'),
'AZ' => _('Azerbaijan'),
'BS' => _('Bahamas'),
'BH' => _('Bahrain'),
'BD' => _('Bangladesh'),
'BB' => _('Barbados'),
'BY' => _('Belarus'),
'BE' => _('Belgium'),
'BZ' => _('Belize'),
'BJ' => _('Benin'),
'BM' => _('Bermuda'),
'BT' => _('Bhutan'),
'BO' => _('Bolivia'),
'BQ' => _('Bonaire, Sint Eustatius and Saba'),
'BA' => _('Bosnia and Herzegovina'),
'BW' => _('Botswana'),
'BV' => _('Bouvet Island'),
'BR' => _('Brazil'),
'IO' => _('British Indian Ocean Territory'),
'BN' => _('Brunei Darussalam'),
'BG' => _('Bulgaria'),
'BF' => _('Burkina Faso'),
'BI' => _('Burundi'),
'KH' => _('Cambodia'),
'CM' => _('Cameroon'),
'CA' => _('Canada'),
'CV' => _('Cape Verde'),
'KY' => _('Cayman Islands'),
'CF' => _('Central African Republic'),
'TD' => _('Chad'),
'CL' => _('Chile'),
'CN' => _('China'),
'CX' => _('Christmas Island'),
'CC' => _('Cocos (Keeling) Islands'),
'CO' => _('Colombia'),
'KM' => _('Comoros'),
'CG' => _('Congo (Brazzaville)'),
'CD' => _('Congo (Kinshasa)'),
'CK' => _('Cook Islands'),
'CR' => _('Costa Rica'),
'CI' => _('Côte d\'Ivoire'),
'HR' => _('Croatia'),
'CU' => _('Cuba'),
'CW' => _('Curaçao'),
'CY' => _('Cyprus'),
'CZ' => _('Czech Republic'),
'DK' => _('Denmark'),
'DJ' => _('Djibouti'),
'DM' => _('Dominica'),
'DO' => _('Dominican Republic'),
'EC' => _('Ecuador'),
'EG' => _('Egypt'),
'SV' => _('El Salvador'),
'GQ' => _('Equatorial Guinea'),
'ER' => _('Eritrea'),
'EE' => _('Estonia'),
'ET' => _('Ethiopia'),
'FK' => _('Falkland Islands'),
'FO' => _('Faroe Islands'),
'FJ' => _('Fiji'),
'FI' => _('Finland'),
'FR' => _('France'),
'GF' => _('French Guiana'),
'PF' => _('French Polynesia'),
'TF' => _('French Southern Lands'),
'GA' => _('Gabon'),
'GM' => _('Gambia'),
'GE' => _('Georgia'),
'DE' => _('Germany'),
'GH' => _('Ghana'),
'GI' => _('Gibraltar'),
'GR' => _('Greece'),
'GL' => _('Greenland'),
'GD' => _('Grenada'),
'GP' => _('Guadeloupe'),
'GU' => _('Guam'),
'GT' => _('Guatemala'),
'GG' => _('Guernsey'),
'GN' => _('Guinea'),
'GW' => _('Guinea-Bissau'),
'GY' => _('Guyana'),
'HT' => _('Haiti'),
'HM' => _('Heard and McDonald Islands'),
'HN' => _('Honduras'),
'HK' => _('Hong Kong'),
'HU' => _('Hungary'),
'IS' => _('Iceland'),
'IN' => _('India'),
'ID' => _('Indonesia'),
'IR' => _('Iran'),
'IQ' => _('Iraq'),
'IE' => _('Ireland'),
'IM' => _('Isle of Man'),
'IL' => _('Israel'),
'IT' => _('Italy'),
'JM' => _('Jamaica'),
'JP' => _('Japan'),
'JE' => _('Jersey'),
'JO' => _('Jordan'),
'KZ' => _('Kazakhstan'),
'KE' => _('Kenya'),
'KI' => _('Kiribati'),
'KP' => _('Korea, North'),
'KR' => _('Korea, South'),
'KW' => _('Kuwait'),
'KG' => _('Kyrgyzstan'),
'LA' => _('Laos'),
'LV' => _('Latvia'),
'LB' => _('Lebanon'),
'LS' => _('Lesotho'),
'LR' => _('Liberia'),
'LY' => _('Libya'),
'LI' => _('Liechtenstein'),
'LT' => _('Lithuania'),
'LU' => _('Luxembourg'),
'MO' => _('Macau'),
'MK' => _('Macedonia'),
'MG' => _('Madagascar'),
'MW' => _('Malawi'),
'MY' => _('Malaysia'),
'MV' => _('Maldives'),
'ML' => _('Mali'),
'MT' => _('Malta'),
'MH' => _('Marshall Islands'),
'MQ' => _('Martinique'),
'MR' => _('Mauritania'),
'MU' => _('Mauritius'),
'YT' => _('Mayotte'),
'MX' => _('Mexico'),
'FM' => _('Micronesia'),
'MD' => _('Moldova'),
'MC' => _('Monaco'),
'MN' => _('Mongolia'),
'ME' => _('Montenegro'),
'MS' => _('Montserrat'),
'MA' => _('Morocco'),
'MZ' => _('Mozambique'),
'MM' => _('Myanmar'),
'NA' => _('Namibia'),
'NR' => _('Nauru'),
'NP' => _('Nepal'),
'NL' => _('Netherlands'),
'NC' => _('New Caledonia'),
'NZ' => _('New Zealand'),
'NI' => _('Nicaragua'),
'NE' => _('Niger'),
'NG' => _('Nigeria'),
'NU' => _('Niue'),
'NF' => _('Norfolk Island'),
'MP' => _('Northern Mariana Islands'),
'NO' => _('Norway'),
'OM' => _('Oman'),
'PK' => _('Pakistan'),
'PW' => _('Palau'),
'PS' => _('Palestine'),
'PA' => _('Panama'),
'PG' => _('Papua New Guinea'),
'PY' => _('Paraguay'),
'PE' => _('Peru'),
'PH' => _('Philippines'),
'PN' => _('Pitcairn'),
'PL' => _('Poland'),
'PT' => _('Portugal'),
'PR' => _('Puerto Rico'),
'QA' => _('Qatar'),
'RE' => _('Reunion'),
'RO' => _('Romania'),
'RU' => _('Russian Federation'),
'RW' => _('Rwanda'),
'BL' => _('Saint Barthélemy'),
'SH' => _('Saint Helena'),
'KN' => _('Saint Kitts and Nevis'),
'LC' => _('Saint Lucia'),
'MF' => _('Saint Martin (French part)'),
'PM' => _('Saint Pierre and Miquelon'),
'VC' => _('Saint Vincent and the Grenadines'),
'WS' => _('Samoa'),
'SM' => _('San Marino'),
'ST' => _('Sao Tome and Principe'),
'SA' => _('Saudi Arabia'),
'SN' => _('Senegal'),
'RS' => _('Serbia'),
'SC' => _('Seychelles'),
'SL' => _('Sierra Leone'),
'SG' => _('Singapore'),
'SX' => _('Sint Maarten'),
'SK' => _('Slovakia'),
'SI' => _('Slovenia'),
'SB' => _('Solomon Islands'),
'SO' => _('Somalia'),
'ZA' => _('South Africa'),
'GS' => _('South Georgia and South Sandwich Islands'),
'SS' => _('South Sudan'),
'ES' => _('Spain'),
'LK' => _('Sri Lanka'),
'SD' => _('Sudan'),
'SR' => _('Suriname'),
'SJ' => _('Svalbard and Jan Mayen Islands'),
'SZ' => _('Swaziland'),
'SE' => _('Sweden'),
'CH' => _('Switzerland'),
'SY' => _('Syria'),
'TW' => _('Taiwan'),
'TJ' => _('Tajikistan'),
'TZ' => _('Tanzania'),
'TH' => _('Thailand'),
'TL' => _('Timor-Leste'),
'TG' => _('Togo'),
'TK' => _('Tokelau'),
'TO' => _('Tonga'),
'TT' => _('Trinidad and Tobago'),
'TN' => _('Tunisia'),
'TR' => _('Turkey'),
'TM' => _('Turkmenistan'),
'TC' => _('Turks and Caicos Islands'),
'TV' => _('Tuvalu'),
'UG' => _('Uganda'),
'UA' => _('Ukraine'),
'AE' => _('United Arab Emirates'),
'GB' => _('United Kingdom'),
'UM' => _('United States Minor Outlying Islands'),
'US' => _('United States of America'),
'UY' => _('Uruguay'),
'UZ' => _('Uzbekistan'),
'VU' => _('Vanuatu'),
'VA' => _('Vatican City'),
'VE' => _('Venezuela'),
'VN' => _('Vietnam'),
'VG' => _('Virgin Islands, British'),
'VI' => _('Virgin Islands, U.S.'),
'WF' => _('Wallis and Futuna Islands'),
'EH' => _('Western Sahara'),
'YE' => _('Yemen'),
'ZM' => _('Zambia'),
'ZW' => _('Zimbabwe'));
$keysizes = array('' => 'Default', 1024 => 1024, 2048 => 2048,
4096 => 4096);
$days = array('' => 'Default', 365 => '1 year',
730 => '2 years', 1095 => '3 years',
1460 => '4 years', 2825 => '5 years',
2190 => '6 years', 2555 => '7 years',
2920 => '8 years', 3285 => '9 years',
3650 => '10 years');
$vbox = new PageElement('vbox');
$vbox->append('entry', array('name' => 'title',
'text' => _('Name: '),
'placeholder' => _('Name'),
'value' => $request->get('title')));
$country = $vbox->append('combobox', array('name' => 'country',
'editable' => TRUE,
'text' => _('Country: '), 'size' => 2,
'placeholder' => _('Country'),
'value' => $request->get('country')));
//countries
asort($countries);
foreach($countries as $value => $text)
$country->append('label', array('text' => $text,
'value' => $value));
$vbox->append('entry', array('name' => 'state',
'text' => _('State: '),
'placeholder' => _('State'),
'value' => $request->get('state')));
$vbox->append('entry', array('name' => 'locality',
'text' => _('Locality: '),
'placeholder' => _('Locality'),
'value' => $request->get('locality')));
$vbox->append('entry', array('name' => 'organization',
'text' => _('Organization: '),
'placeholder' => _('Organization'),
'value' => $request->get('organization')));
$vbox->append('entry', array('name' => 'section',
'text' => _('Section: '),
'placeholder' => _('Section'),
'value' => $request->get('section')));
$vbox->append('entry', array('name' => 'email',
'text' => _('e-mail: '),
'placeholder' => _('e-mail'),
'value' => $request->get('email')));
//key size
$keysize = $vbox->append('combobox', array('name' => 'keysize',
'text' => _('Key size: '),
'value' => $request->get('keysize')));
foreach($keysizes as $value => $text)
$keysize->append('label', array('text' => $text,
'value' => $value));
//expiration
$expiration = $vbox->append('combobox', array('name' => 'days',
'text' => _('Expiration: '),
'value' => $request->get('days')));
foreach($days as $value => $text)
$expiration->append('label', array('text' => $text,
'value' => $value));
//signing
if($request->getID() !== FALSE)
$vbox->append('checkbox', array('name' => 'sign',
'value' => $request->get('sign')
? TRUE : FALSE,
'text' => _('Sign')));
return $vbox;
}
protected function _formUpdate(Engine $engine, Request $request)
{
//FIXME really implement
return parent::_formUpdate($engine, $request);
}
//PKIContent::renew
public function renew(Engine $engine, $content = FALSE,
&$error = FALSE)
{
$parent = $this->getParent($engine);
if($content !== FALSE || $parent === FALSE)
{
$error = _('Unsupported operation');
return FALSE;
}
return $parent->renew($engine, $this, $error);
}
//PKIContent::revoke
public function revoke(Engine $engine, $content = FALSE,
&$error = FALSE)
{
$parent = $this->getParent($engine);
if($content !== FALSE || $parent === FALSE)
{
$error = _('Unsupported operation');
return FALSE;
}
return $parent->revoke($engine, $this, $error);
}
//PKIContent::sign
public function sign(Engine $engine, $content = FALSE,
&$error = FALSE)
{
$parent = $this->getParent($engine);
if($content !== FALSE || $parent === FALSE)
{
$error = _('Unsupported operation');
return FALSE;
}
return $parent->sign($engine, $this, $error);
}
//PKIContent::loadFromName
static public function loadFromName(Engine $engine, Module $module,
$name, $parent = FALSE)
{
if(($res = static::_loadFromName($engine, $module, $name,
$parent)) === FALSE)
return FALSE;
return static::loadFromProperties($engine, $module, $res);
}
static protected function _loadFromName(Engine $engine, Module $module,
$name, $parent)
{
$database = $engine->getDatabase();
$query = ($parent !== FALSE)
? static::$query_load_by_title_parent
: static::$query_load_by_title_parent_null;
$args = array('module_id' => $module->getID(),
'title' => $name);
if($parent !== FALSE)
$args['parent'] = $parent->getID();
if(($res = $database->query($engine, $query, $args)) === FALSE
|| count($res) != 1)
return FALSE;
return $res->current();
}
//protected
//methods
//accessors
//PKIContent::getRoot
protected function getRoot(Engine $engine)
{
global $config;
if($this->root !== FALSE)
return $this->root;
$module = $this->getModule();
$section = 'module::'.$module->getName(); //XXX
if(($this->root = $config->get($section, 'root')) === FALSE)
return $engine->log(LOG_ERR, 'The PKI root folder is'
.' not configured');
return $this->root;
}
//CAPKIContent::getRootCA
protected function getRootCA(Engine $engine, $parent = FALSE)
{
if($parent === FALSE)
$parent = $this->getParent($engine);
if($parent === FALSE)
//FIXME add support for self-signed certificates
return FALSE;
return $parent->getRootCA($engine);
}
//useful
//PKIContent::createCertificate
protected function createCertificate(Engine $engine,
Request $request = NULL, $parent = FALSE, $days = FALSE,
$keysize = FALSE, &$error = FALSE)
{
$root = $this->getRootCA($engine, $parent);
$subject = $this->getSubject($request);
//enforce reasonable defaults
if($days === FALSE)
$days = 365;
if($keysize === FALSE)
$keysize = 4096;
//check parameters
if($root === FALSE || $subject === FALSE || !is_numeric($days)
|| !is_numeric($keysize))
{
$error = _('Invalid arguments to create certificate');
return FALSE;
}
switch(static::$class)
{
case 'CAPKIContent':
$x509 = ($parent !== FALSE) ? '' : ' -x509';
$extensions = '';
$keyout = $root.'/private/cakey.pem';
$out = ($parent !== FALSE) ? $root.'/cacert.csr'
: $root.'/cacert.pem';
break;
case 'CAClientPKIContent':
$x509 = ' -x509';
$extensions = ' -extensions usr_cert';
$keyout = $root.'/private/'.$this->getTitle().'.key';
$out = $root.'/newcerts/'.$this->getTitle().'.pem';
break;
case 'CAServerPKIContent':
$x509 = ' -x509';
$extensions = ' -extensions srv_cert';
$keyout = $root.'/private/'.$this->getTitle().'.key';
$out = $root.'/newcerts/'.$this->getTitle().'.pem';
break;
default:
$error = _('Invalid class to create certificate');
return FALSE;
}
$opensslcnf = $root.'/openssl.cnf';
if(file_exists($keyout) || file_exists($out))
{
$error = _('Could not generate the certificate');
return $engine->log(LOG_ERR,
'Could not generate the certificate');
}
$days = ' -days '.escapeshellarg($days);
$keysize = ' -newkey rsa:'.escapeshellarg($keysize);
$cmd = 'openssl req -batch -nodes -new'.$x509.$days.$keysize
.' -config '.escapeshellarg($opensslcnf).$extensions
.' -keyout '.escapeshellarg($keyout)
.' -out '.escapeshellarg($out)
.' -subj '.escapeshellarg($subject);
$res = -1;
$engine->log(LOG_DEBUG, 'Executing: '.$cmd);
$stdout = defined('STDOUT')
? STDOUT : fopen('php://stdout', 'w');
$fds = array(1 => $stdout, 2 => $stdout);
if(($fp = proc_open($cmd, $fds, $pipes)) === FALSE
|| ($res = proc_close($fp)) != 0)
{
$error = _('Could not generate the certificate');
return $engine->log(LOG_ERR,
'Could not generate the certificate');
}
return TRUE;
}
//PKIContent::createSigningRequest
protected function createSigningRequest(Engine $engine, $parent = FALSE,
&$error = FALSE)
{
$root = $this->getRootCA($engine, $parent);
//check parameters
if($root === FALSE)
{
$error = _('Invalid arguments to signing request');
return FALSE;
}
switch(static::$class)
{
case 'CAClientPKIContent':
case 'CAServerPKIContent':
$in = $root.'/newcerts/'.$this->getTitle().'.pem';
$out = $root.'/newreqs/'.$this->getTitle().'.csr';
$signkey = $root.'/private/'.$this->getTitle().'.key';
break;
default:
$error = _('Invalid class to signing request');
return FALSE;
}
$opensslcnf = $root.'/openssl.cnf';
if(!file_exists($in) || file_exists($out)
|| !file_exists($signkey))
{
$error = _('Could not generate the signing request');
return $engine->log(LOG_ERR,
'Could not generate the signing request');
}
$cmd = 'openssl x509 -x509toreq'
.' -in '.escapeshellarg($in)
.' -out '.escapeshellarg($out)
.' -signkey '.escapeshellarg($signkey);
$res = -1;
$engine->log(LOG_DEBUG, 'Executing: '.$cmd);
$fds = array(1 => STDOUT, 2 => STDOUT);
if(($fp = proc_open($cmd, $fds, $pipes)) === FALSE
|| ($res = proc_close($fp)) != 0)
{
$error = _('Could not generate the signing request');
return $engine->log(LOG_ERR,
'Could not generate the signing request');
}
return TRUE;
}
//private
//properties
private $root = FALSE;
}
?>