2015-05-07 15:29:43 +00:00
< ? php
/*
----------------------------------------------------------------------
AlternC - Web Hosting System
Copyright ( C ) 2000 - 2014 by the AlternC Development Team .
https :// alternc . org /
----------------------------------------------------------------------
LICENSE
This program is free software ; you can redistribute it and / or
modify it under the terms of the GNU General Public License ( GPL )
as published by the Free Software Foundation ; either version 2
of the License , or ( at your option ) any later version .
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 .
To read the license please visit http :// www . gnu . org / copyleft / gpl . html
----------------------------------------------------------------------
Purpose of file : Manage SSL Certificates and HTTPS Hosting
----------------------------------------------------------------------
*/
// -----------------------------------------------------------------
/**
* SSL Certificates management class
*/
class m_ssl {
const STATUS_PENDING = 0 ; // we have a key / csr, but no CRT
const STATUS_OK = 1 ; // we have the key, csr, crt, chain
const STATUS_EXPIRED = 99 ; // The certificate is now expired.
public $error = " " ;
// Includes one or more of those flags to see only those certificates
// when listing them:
const FILTER_PENDING = 1 ;
const FILTER_OK = 2 ;
const FILTER_EXPIRED = 4 ;
const FILTER_SHARED = 8 ;
const SSL_INCRON_FILE = " /var/run/alternc-ssl/generate_certif_alias " ;
2015-05-12 15:33:05 +00:00
var $myDomainesTypes = array ( " vhost-ssl " , " vhost-mixssl " , " panel-ssl " , " roundcube-ssl " , " squirrelmail-ssl " , " php52-ssl " , " php52-mixssl " , " url-ssl " );
2015-05-07 15:29:43 +00:00
const KEY_REPOSITORY = " /var/lib/alternc/ssl/private " ;
// -----------------------------------------------------------------
/**
* Constructor
*/
function m_ssl () {
}
// -----------------------------------------------------------------
/**
* Hook to add the " ssl certificate " menu in the Panel
*/
function hook_menu () {
global $quota , $db , $cuid ;
$q = $quota -> getquota ( " ssl " );
$obj = null ;
if ( $q [ 't' ] > 0 ) {
$obj = array (
'title' => _ ( " SSL Certificates " ),
'ico' => 'images/ssl.png' ,
'link' => 'toggle' ,
'pos' => 130 ,
'links' => array (),
);
if ( $quota -> cancreate ( " ssl " )) {
$obj [ 'links' ][] = array (
'ico' => 'images/new.png' ,
'txt' => _ ( " New SSL certificate " ),
'url' => " ssl_new.php " ,
'class' => '' ,
);
}
// or admin shared >0 !
$db -> query ( " SELECT COUNT(*) AS cnt FROM certificates WHERE uid=' $cuid ' OR shared=1 " );
$used = $q [ 'u' ];
if ( $db -> next_record ()) {
$used = $db -> f ( " cnt " );
}
if ( $used > 0 ) { // if there are some SSL certificates
$obj [ 'links' ][] = array (
'txt' => _ ( " List SSL Certificates " ),
'url' => " ssl_list.php "
);
}
}
return $obj ;
}
// -----------------------------------------------------------------
/** Return all the SSL certificates for an account ( or the searched one )
* @ param $filter an integer telling which certificate we want to see ( see FILTER_ * constants above )
* the default is showing all certificate , but only Pending and OK certificates , not expired or shared one
* when there is more than 10.
* @ return array all the ssl certificate this user can use
* ( each array is the content of the certificates table )
*/
function get_list ( & $filter = null ) {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " get_list " );
2015-05-07 15:29:43 +00:00
// Expire expired certificates:
$db -> query ( " UPDATE certificates SET status= " . self :: STATUS_EXPIRED . " WHERE status= " . self :: STATUS_OK . " AND validend<NOW(); " );
$r = array ();
// If we have no filter, we filter by default on pending and ok certificates if there is more than 10 of them for the same user.
if ( is_null ( $filter )) {
$db -> query ( " SELECT count(*) AS cnt FROM certificates WHERE uid=' $cuid ' OR shared=1; " );
$db -> next_record ();
if ( $db -> f ( " cnt " ) > 10 ) {
$filter = ( self :: FILTER_PENDING | self :: FILTER_OK );
} else {
$filter = ( self :: FILTER_PENDING | self :: FILTER_OK | self :: FILTER_EXPIRED | self :: FILTER_SHARED );
}
}
// filter the filter values :)
$filter = ( $filter & ( self :: FILTER_PENDING | self :: FILTER_OK | self :: FILTER_EXPIRED | self :: FILTER_SHARED ));
// Here filter can't be null (and will be returned to the caller !)
$sql = " " ;
if ( $filter & self :: FILTER_SHARED ) {
$sql = " (uid=' $cuid ' OR shared=1) " ;
} else {
$sql = " uid=' $cuid ' " ;
}
$sql .= " AND status IN (-1 " ;
if ( $filter & self :: FILTER_PENDING ) {
$sql .= " , " . self :: STATUS_PENDING ;
}
if ( $filter & self :: FILTER_OK ) {
$sql .= " , " . self :: STATUS_OK ;
}
if ( $filter & self :: FILTER_EXPIRED ) {
$sql .= " , " . self :: STATUS_EXPIRED ;
}
$sql .= " ) " ;
$db -> query ( " SELECT *, UNIX_TIMESTAMP(validstart) AS validstartts, UNIX_TIMESTAMP(validend) AS validendts FROM certificates WHERE $sql ORDER BY shared, fqdn; " );
if ( $db -> num_rows ()) {
while ( $db -> next_record ()) {
$r [] = $db -> Record ;
}
return $r ;
} else {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " No SSL certificates available " ));
2015-05-07 15:29:43 +00:00
return array ();
}
}
// -----------------------------------------------------------------
/** Return all the Vhosts of this user using SSL certificates
* @ return array all the ssl certificate and hosts of this user
*/
function get_vhosts () {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " get_vhosts " );
2015-05-07 15:29:43 +00:00
$r = array ();
$db -> query ( " SELECT ch.*, UNIX_TIMESTAMP(c.validstart) AS validstartts, UNIX_TIMESTAMP(c.validend) AS validendts, sd.domaine, sd.sub "
. " FROM certif_hosts ch LEFT JOIN certificates c ON ch.certif=c.id "
. " , sub_domaines sd WHERE sd.id=ch.sub AND ch.uid= $cuid "
. " ORDER BY sd.domaine, sd.sub; " );
if ( $db -> num_rows ()) {
while ( $db -> next_record ()) {
$r [] = $db -> Record ;
}
return $r ;
} else {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " You currently have no hosting using SSL certificate " ));
2015-05-07 15:29:43 +00:00
return array ();
}
}
// -----------------------------------------------------------------
/** Generate a new CSR , a new Private RSA Key , for FQDN .
* @ param $fqdn string the FQDN of the domain name for which we want a CSR .
* a wildcard certificate must start by *.
* @ return integer the Certificate ID created in the MySQL database
* or false if an error occurred
*/
function new_csr ( $fqdn ) {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " new_csr " );
2015-05-07 15:29:43 +00:00
if ( substr ( $fqdn , 0 , 2 ) == " *. " ) {
$f = substr ( $fqdn , 2 );
} else {
$f = $fqdn ;
}
if ( checkfqdn ( $f )) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Bad FQDN domain name " ));
2015-05-07 15:29:43 +00:00
return false ;
}
putenv ( " OPENSSL_CONF=/etc/alternc/openssl.cnf " );
$pkey = openssl_pkey_new ();
if ( ! $pkey ) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't generate a private key (1) " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$privKey = " " ;
if ( ! openssl_pkey_export ( $pkey , $privKey )) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't generate a private key (2) " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$dn = array ( " commonName " => $fqdn );
// override the (not taken from openssl.cnf) digest to use SHA-2 / SHA256 and not SHA-1 or MD5 :
$config = array ( " digest_alg " => " sha256 " );
$csr = openssl_csr_new ( $dn , $pkey , $config );
$csrout = " " ;
openssl_csr_export ( $csr , $csrout );
$db -> query ( " INSERT INTO certificates SET uid=' $cuid ', status= " . self :: STATUS_PENDING . " , shared=0, fqdn=' " . addslashes ( $fqdn ) . " ', altnames='', validstart=NOW(), sslcsr=' " . addslashes ( $csrout ) . " ', sslkey=' " . addslashes ( $privKey ) . " '; " );
if ( ! ( $id = $db -> lastid ())) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't generate a CSR " ));
2015-05-07 15:29:43 +00:00
return false ;
}
return $id ;
}
// -----------------------------------------------------------------
/** Return all informations of a given certificate for the current user .
* @ return array all the informations of the current certificate as a hash .
*/
function get_certificate ( $id ) {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " get_certificate " );
2015-05-07 15:29:43 +00:00
$id = intval ( $id );
$db -> query ( " SELECT *, UNIX_TIMESTAMP(validstart) AS validstartts, UNIX_TIMESTAMP(validend) AS validendts FROM certificates WHERE (uid=' $cuid ' OR (shared=1 AND status= " . self :: STATUS_OK . " ) ) AND id=' $id '; " );
if ( ! $db -> next_record ()) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't find this Certificate " ));
2015-05-07 15:29:43 +00:00
return false ;
}
return $db -> Record ;
}
// -----------------------------------------------------------------
/** Delete a Certificate for the current user .
* @ return boolean TRUE if the certificate has been deleted successfully .
*/
function del_certificate ( $id ) {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " del_certificate " );
2015-05-07 15:29:43 +00:00
$id = intval ( $id );
$db -> query ( " SELECT * FROM certificates WHERE uid=' $cuid ' AND id=' $id '; " );
if ( ! $db -> next_record ()) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't find this Certificate " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$fqdn = $db -> Record [ " fqdn " ];
$altnames = $db -> Record [ " altnames " ];
$db -> query ( " DELETE FROM certificates WHERE uid=' $cuid ' AND id=' $id '; " );
// Update any existing VHOST using this cert/key
$this -> updateTrigger ( $fqdn , $altnames );
return true ;
}
// -----------------------------------------------------------------
/** Share ( or unshare ) an ssl certificate
* @ param $id integer the id of the certificate in the table .
* @ param $action integer share ( 1 ) or unshare ( 0 ) this certificate
* @ return boolean
*/
function share ( $id , $action = 1 ) {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " share " );
2015-05-07 15:29:43 +00:00
$id = intval ( $id );
$db -> query ( " SELECT * FROM certificates WHERE uid=' $cuid ' AND status= " . self :: STATUS_OK . " AND id=' $id '; " );
if ( ! $db -> next_record ()) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't find this Certificate " ));
2015-05-07 15:29:43 +00:00
return false ;
}
if ( $action ) {
$action = 1 ;
$this -> updateTrigger ( $db -> Record [ " fqdn " ], $db -> Record [ " altnames " ]);
} else {
$action = 0 ;
}
$db -> query ( " UPDATE certificates SET shared= $action WHERE id=' $id '; " );
return true ;
}
// -----------------------------------------------------------------
/** Return all the subdomains that can be ssl - enabled for the current account .
* @ return array of strings : all the subdomains .
* Excludes the one for which a cert is already available
*/
function get_new_advice () {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " get_new_advice " );
2015-05-07 15:29:43 +00:00
$r = array ();
// my certificates, either OK or PENDING (not expired) or the SHARED one (only OK then)
$db -> query ( " SELECT fqdn FROM certificates WHERE
( uid = '$cuid' AND status IN ( " . self::STATUS_PENDING . " , " . self::STATUS_OK . " ) )
OR ( shared = 1 AND status = " . self::STATUS_OK . " )
ORDER BY shared , fqdn ; " );
$r = array ();
while ( $db -> next_record ()) {
$r [] = $db -> f ( " fqdn " );
}
// Now we get all our subdomains for certain domaines_types
$db -> query ( " SELECT sub,domaine FROM sub_domaines WHERE compte=' $cuid ' AND type IN ('vhost', 'url', 'roundcube', 'squirrelmail', 'panel', 'php52'); " );
$advice = array ();
while ( $db -> next_record ()) {
$me = $db -> f ( " sub " );
if ( $me ) {
$me .= " . " ;
}
$me .= $db -> f ( " domaine " );
if ( ! in_array ( $me , $r ) && ! in_array ( $me , $advice )) {
$advice [] = $me ;
}
if ( ! in_array ( " *. " . $db -> f ( " domaine " ), $r ) && ! in_array ( " *. " . $db -> f ( " domaine " ), $advice )) {
$advice [] = " *. " . $db -> f ( " domaine " );
}
}
sort ( $advice );
return ( $advice );
}
// -----------------------------------------------------------------
/** Import an existing ssl Key , Certificate and ( maybe ) a Chained Cert
* @ param $key string the X . 509 PEM - encoded RSA key
* @ param $crt string the X . 509 PEM - encoded certificate , which * must *
* be the one signinf the private RSA key in $key
* @ param $chain string the X . 509 PEM - encoded list of SSL Certificate chain if intermediate authorities
* @ return integer the ID of the newly created certificate in the table
* or false if an error occurred
*/
function import_cert ( $key , $crt , $chain = " " ) {
2017-10-07 17:24:32 +00:00
global $cuid , $msg , $db ;
$msg -> log ( " ssl " , " import_cert " );
2015-05-07 15:29:43 +00:00
$result = $this -> check_cert ( $crt , $chain , $key );
if ( $result === false ) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , $this -> error );
2015-05-07 15:29:43 +00:00
return false ;
}
list ( $crt , $chain , $key , $crtdata ) = $result ;
$validstart = $crtdata [ 'validFrom_time_t' ];
$validend = $crtdata [ 'validTo_time_t' ];
$fqdn = $crtdata [ " subject " ][ " CN " ];
$altnames = $this -> parseAltNames ( $crtdata [ " extensions " ][ " subjectAltName " ]);
// Everything is PERFECT and has been thoroughly checked, let's insert those in the DB !
$sql = " INSERT INTO certificates SET uid=' $cuid ', status= " . self :: STATUS_OK . " , shared=0, fqdn=' " . addslashes ( $fqdn ) . " ', altnames=' " . addslashes ( $altnames ) . " ', validstart=FROM_UNIXTIME( " . intval ( $validstart ) . " ), validend=FROM_UNIXTIME( " . intval ( $validend ) . " ), sslkey=' " . addslashes ( $key ) . " ', sslcrt=' " . addslashes ( $crt ) . " ', sslchain=' " . addslashes ( $chain ) . " '; " ;
$db -> query ( $sql );
if ( ! ( $id = $db -> lastid ())) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't save the Key/Crt/Chain now. Please try later. " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$this -> updateTrigger ( $fqdn , $altnames );
return $id ;
}
// -----------------------------------------------------------------
/** Import an ssl certificate into an existing certificate entry in the DB .
* ( finalize an enrollment process )
* @ param $certid integer the ID in the database of the SSL Certificate
* @ param $crt string the X . 509 PEM - encoded certificate , which * must *
* be the one signing the private RSA key in certificate $certid
* @ param $chain string the X . 509 PEM - encoded list of SSL Certificate chain if intermediate authorities
* @ return integer the ID of the updated certificate in the table
* or false if an error occurred
*/
function finalize ( $certid , $crt , $chain ) {
2017-10-07 17:24:32 +00:00
global $cuid , $msg , $db ;
$msg -> log ( " ssl " , " finalize " );
2015-05-07 15:29:43 +00:00
$certid = intval ( $certid );
$result = $this -> check_cert ( $crt , $chain , " " , $certid );
if ( $result === false ) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , $this -> error );
2015-05-07 15:29:43 +00:00
return false ;
}
list ( $crt , $chain , $key , $crtdata ) = $result ;
$validstart = $crtdata [ 'validFrom_time_t' ];
$validend = $crtdata [ 'validTo_time_t' ];
$fqdn = $crtdata [ " subject " ][ " CN " ];
$altnames = $this -> parseAltNames ( $crtdata [ " extensions " ][ " subjectAltName " ]);
// Everything is PERFECT and has been thoroughly checked, let's insert those in the DB !
$sql = " UPDATE certificates SET status= " . self :: STATUS_OK . " , shared=0, fqdn=' " . addslashes ( $fqdn ) . " ', altnames=' " . addslashes ( $altnames ) . " ', validstart=FROM_UNIXTIME( " . intval ( $validstart ) . " ), validend=FROM_UNIXTIME( " . intval ( $validend ) . " ), sslcrt=' " . addslashes ( $crt ) . " ', sslchain=' " . addslashes ( $chain ) . " ' WHERE id=' $certid ' ; " ;
if ( ! $db -> query ( $sql )) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't save the Crt/Chain now. Please try later. " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$this -> updateTrigger ( $fqdn , $altnames );
return $certid ;
}
// -----------------------------------------------------------------
/** Function called by a hook when an AlternC member is deleted .
* @ access private
* TODO : delete unused ssl certificates ? ? > do this in the crontab .
*/
function alternc_del_member () {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " alternc_del_member " );
2015-05-07 15:29:43 +00:00
$db -> query ( " UPDATE certificates SET ssl_action='DELETE' WHERE uid=' $cuid ' " );
return true ;
}
// -----------------------------------------------------------------
/** Hook which returns the used quota for the $name service for the current user .
* @ param $name string name of the quota
* @ return integer the number of service used or false if an error occured
* @ access private
*/
function hook_quota_get () {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " getquota " );
2015-05-07 15:29:43 +00:00
$q = Array ( " name " => " ssl " , " description " => _ ( " SSL Certificates " ), " used " => 0 );
$db -> query ( " SELECT COUNT(*) AS cnt FROM certificates WHERE uid=' $cuid ' AND status!= " . self :: STATUS_EXPIRED );
if ( $db -> next_record ()) {
$q [ 'used' ] = $db -> f ( " cnt " );
}
return $q ;
}
// -----------------------------------------------------------------
/** Launched by functions in this class
* when a certificate is validated , expired or shared .
* so that existing vhost using expired or self - signed certificates
* may have the chance to use a proper one automagically
* @ param string $fqdn the FQDN of the certificate
* @ param string $altnames any alternative names this certificate may have .
*/
public function updateTrigger ( $fqdn , $altnames = " " ) {
global $db ;
$fqdns = array ( $fqdn );
$an = explode ( " \n " , $altnames );
foreach ( $an as $a )
if ( trim ( $a ))
$fqdns [] = trim ( $a );
$db -> query ( " UPDATE sub_domaines SET web_action='UPDATE' WHERE "
. " if(LENGTH(sub)>0,CONCAT(sub,'.',domaine),domaine) IN (' " . implode ( " ',' " , $fqdns ) . " ') "
. " AND type LIKE '%ssl'; " );
}
// -----------------------------------------------------------------
/** Launched by hosting_functions . sh launched by update_domaines . sh
* Action may be create / postinst / delete / enable / disable
* Change the template for this domain name to have the proper CERTIFICATE
* An algorithm determine the best possible certificate , which may be a BAD one
* ( like a generic admin - shared or self - signed for localhost as a last chance )
*/
public function updateDomain ( $action , $type , $fqdn , $mail = 0 , $value = " " ) {
2017-10-07 17:24:32 +00:00
global $db , $msg ;
$msg -> log ( " ssl " , " update_domain( $action , $type , $fqdn ) " );
2015-05-07 15:29:43 +00:00
if ( ! in_array ( $type , $this -> myDomainesTypes )) {
return ; // nothing to do : the type is not our to start with ;)
}
if ( $action == " postinst " ) {
2017-10-07 17:24:32 +00:00
$msg -> log ( " ssl " , " update_domain:CREATE( $action , $type , $fqdn ) " );
2015-05-07 15:29:43 +00:00
$offset = 0 ;
$found = false ;
do { // try each subdomain (strtok-style) and search them in sub_domaines table:
$db -> query ( " SELECT * FROM sub_domaines WHERE "
. " sub=' " . substr ( $fqdn , 0 , $offset ) . " ' AND domaine=' " . substr ( $fqdn , $offset + ( $offset != 0 )) . " ' "
. " AND web_action NOT IN ('','OK') AND type=' " . $type . " '; " );
if ( $db -> next_record ()) {
$found = true ;
break ;
}
$offset = strpos ( $fqdn , " . " , $offset );
} while ( true );
if ( ! $found ) {
echo " FATAL: didn't found fqdn $fqdn in sub_domaines table ! \n " ;
return ;
}
// found and $db point to it:
$subdom = $db -> Record ;
$TARGET_FILE = " /var/lib/alternc/apache-vhost/ " . substr ( $subdom [ " compte " ], - 1 ) . " / " . $subdom [ " compte " ] . " / " . $fqdn . " .conf " ;
$cert = $this -> searchBestCert ( $subdom [ " compte " ], $fqdn );
// DEBUG echo "Return from searchBestCert(" . $subdom["compte"] . "," . $fqdn . ") is "; print_r($cert);
// Save crt/key/chain into KEY_REPOSITORY
$CRTDIR = self :: KEY_REPOSITORY . " / " . $subdom [ " compte " ];
@ mkdir ( $CRTDIR );
// Don't *overwrite* existing self-signed certificates in KEY_REPOSITORY
if ( isset ( $cert [ " selfsigned " ]) &&
file_exists ( $CRTDIR . " / " . $fqdn . " .crt " ) &&
file_exists ( $CRTDIR . " / " . $fqdn . " .key " )) {
echo " Self-Signed certificate reused... \n " ;
} else {
file_put_contents ( $CRTDIR . " / " . $fqdn . " .crt " , $cert [ " sslcrt " ]);
file_put_contents ( $CRTDIR . " / " . $fqdn . " .key " , $cert [ " sslkey " ]);
if ( isset ( $cert [ " sslchain " ]) && $cert [ " sslchain " ]) {
file_put_contents ( $CRTDIR . " / " . $fqdn . " .chain " , $cert [ " sslchain " ]);
}
}
// edit apache conf file to set the certificate:
$s = file_get_contents ( $TARGET_FILE );
$s = str_replace ( " %%CRT%% " , $CRTDIR . " / " . $fqdn . " .crt " , $s );
$s = str_replace ( " %%KEY%% " , $CRTDIR . " / " . $fqdn . " .key " , $s );
if ( isset ( $cert [ " sslchain " ]) && $cert [ " sslchain " ]) {
$s = str_replace ( " %%CHAINLINE%% " , " SSLCertificateChainFile " . $CRTDIR . " / " . $fqdn . " .chain " , $s );
} else {
$s = str_replace ( " %%CHAINLINE%% " , " " , $s );
}
file_put_contents ( $TARGET_FILE , $s );
// Edit certif_hosts:
$db -> query ( " DELETE FROM certif_hosts WHERE sub= " . $subdom [ " id " ] . " ; " );
$db -> query ( " INSERT INTO certif_hosts SET "
. " sub= " . intval ( $subdom [ " id " ]) . " , "
. " certif= " . intval ( $cert [ " id " ]) . " , "
. " uid= " . intval ( $subdom [ " compte " ]) . " ; " );
} // action==create
if ( $action == " delete " ) {
2017-10-07 17:24:32 +00:00
$msg -> log ( " ssl " , " update_domain:DELETE( $action , $type , $fqdn ) " );
2015-05-07 15:29:43 +00:00
$offset = 0 ;
$found = false ;
do { // try each subdomain (strtok-style) and search them in sub_domaines table:
$db -> query ( " SELECT * FROM sub_domaines WHERE "
. " sub=' " . substr ( $fqdn , 0 , $offset ) . " ' AND domaine=' " . substr ( $fqdn , $offset + ( $offset != 0 )) . " ' "
. " AND web_action NOT IN ('','OK') AND type=' " . $type . " '; " );
if ( $db -> next_record ()) {
$found = true ;
break ;
}
$offset = strpos ( $fqdn , " . " , $offset );
} while ( true );
if ( ! $found ) {
echo " FATAL: didn't found fqdn $fqdn in sub_domaines table ! \n " ;
return ;
}
// found and $db point to it:
$subdom = $db -> Record ;
$db -> query ( " DELETE FROM certif_hosts WHERE sub= " . $subdom [ " id " ] . " ; " );
}
}
// ----------------------------------------------------------------
/** Search for the best certificate for a user and a fqdn
* Return a hash with sslcrt , sslkey and maybe sslchain .
* return ANYWAY : if necessary , return a newly created ( and stored in KEY_REPOSITORY localhost self - signed certificate ...
*/
public function searchBestCert ( $uid , $fqdn ) {
global $db ;
$uid = intval ( $uid );
// 1st search for a valid certificate in my account or shared by the admin:
// the ORDER BY make it so that we try VALID then EXPIRED one (sad)
$wildcard = " *. " . substr ( $fqdn , strpos ( $fqdn , " . " ) + 1 );
$db -> query ( " SELECT * FROM certificates WHERE (status= " . self :: STATUS_OK . " OR status= " . self :: STATUS_EXPIRED . " ) "
. " AND (uid= " . $uid . " OR shared=1) "
. " AND (fqdn=' " . $fqdn . " ' OR fqdn=' " . $wildcard . " ' OR altnames LIKE '% " . $fqdn . " %') "
. " ORDER BY (validstart<=NOW() AND validend>=NOW()) DESC, validstart DESC " );
while ( $db -> next_record ()) {
// name
if ( $db -> Record [ " fqdn " ] == $fqdn ) {
return $db -> Record ;
}
// or alternative names
$altnames = explode ( " \n " , $db -> Record [ " altnames " ]);
foreach ( $altnames as $altname ) {
if ( trim ( $altname ) == $fqdn ) {
return $db -> Record ;
}
}
// or wildcard
if ( $db -> Record [ " fqdn " ] == $wildcard ) {
return $db -> Record ;
}
}
// not found, we generate a one-time self-signed certificate for this host.
$crt = $this -> selfSigned ( $fqdn );
$crt [ " uid " ] = $uid ;
return $crt ;
}
// -----------------------------------------------------------------
/** Export every information for an AlternC ' s account
* @ access private
* EXPERIMENTAL 'sid' function ;)
*/
function alternc_export_conf () {
2017-10-07 17:24:32 +00:00
global $db , $msg , $cuid ;
$msg -> log ( " ssl " , " export " );
2015-05-07 15:29:43 +00:00
$str = " <ssl> " ;
$db -> query ( " SELECT COUNT(*) AS cnt FROM certificates WHERE uid=' $cuid ' AND status!= " . self :: STATUS_EXPIRED );
while ( $db -> next_record ()) {
$str .= " <id> " . ( $db -> Record [ " id " ]) . " </id> \n " ;
$str .= " <csr> " . ( $db -> Record [ " sslcsr " ]) . " </key> \n " ;
$str .= " <key> " . ( $db -> Record [ " sslkey " ]) . " <key> \n " ;
$str .= " <crt> " . ( $db -> Record [ " sslcrt " ]) . " </crt> \n " ;
$str .= " <chain> " . ( $db -> Record [ " sslchain " ]) . " <chain> \n " ;
}
$str .= " </ssl> \n " ;
return $str ;
}
// -----------------------------------------------------------------
/** Returns the list of alternate names of an X . 509 SSL Certificate
* from the attribute list .
* @ param $str string the $crtdata [ " extensions " ][ " subjectAltName " ] from openssl
* @ return array an array of FQDNs
*/
function parseAltNames ( $str ) {
$mat = array ();
if ( preg_match_all ( " #DNS:([^,]*)# " , $str , $mat , PREG_PATTERN_ORDER )) {
return implode ( " \n " , $mat [ 1 ]);
} else {
return " " ;
}
}
// -----------------------------------------------------------------
/** Add ( immediately ) a global alias to the HTTP
* certif_alias table and add it to apache configuration
* by launching a incron action .
* name is the name of the alias , starting by /
* content is the content of the filename stored at this location
* If an alias with the same name already exists , return false .
* if the alias has been properly defined , return true .
* @ return boolean
*/
function alias_add ( $name , $content ) {
2017-10-07 17:24:32 +00:00
global $msg , $cuid , $db ;
2015-05-07 15:29:43 +00:00
$db -> query ( " SELECT name FROM certif_alias WHERE name=' " . addslashes ( $name ) . " '; " );
if ( $db -> next_record ()) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Alias already exists " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$db -> query ( " INSERT INTO certif_alias SET name=' " . addslashes ( $name ) . " ', content=' " . addslashes ( $content ) . " ', uid= " . intval ( $cuid ) . " ; " );
touch ( self :: SSL_INCRON_FILE );
return true ;
}
// -----------------------------------------------------------------
/** Removes ( immediately ) a global alias to the HTTP
* certif_alias table and add it to apache configuration
* by launching a incron action .
* name is the name of the alias , starting by /
* @ return boolean
*/
function alias_del ( $name ) {
2017-10-07 17:24:32 +00:00
global $msg , $cuid , $db ;
2015-05-07 15:29:43 +00:00
$db -> query ( " SELECT name FROM certif_alias WHERE name=' " . addslashes ( $name ) . " ' AND uid= " . intval ( $cuid ) . " ; " );
if ( ! $db -> next_record ()) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Alias not found " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$db -> query ( " DELETE FROM certif_alias WHERE name=' " . addslashes ( $name ) . " ' AND uid= " . intval ( $cuid ) . " ; " );
touch ( self :: SSL_INCRON_FILE );
return true ;
}
// -----------------------------------------------------------------
/** Check that a crt is a proper certificate
* @ param $crt string an SSL Certificate
* @ param $chain string is a list of certificates
* @ param $key string is a rsa key associated with certificate
* @ param $certid if no key is specified , use it from this certificate ID in the table
* @ return array the crt , chain , key , crtdata ( array ) after a proper reformatting
* or false if an error occurred ( in that case $this -> error is filled )
*/
function check_cert ( $crt , $chain , $key = " " , $certid = null ) {
global $db ;
// Check that the key crt and chain are really SSL certificates and keys
$crt = trim ( str_replace ( " \r \n " , " \n " , $crt )) . " \n " ;
$key = trim ( str_replace ( " \r \n " , " \n " , $key )) . " \n " ;
$chain = trim ( str_replace ( " \r \n " , " \n " , $chain )) . " \n " ;
$this -> error = " " ;
if ( trim ( $key ) == " " && ! is_null ( $certid )) {
// find it in the DB :
$db -> query ( " SELECT sslkey FROM certificates WHERE id= " . intval ( $certid ) . " ; " );
if ( ! $db -> next_record ()) {
$this -> error .= _ ( " Can't find the private key in the certificate table, please check your form. " );
return false ;
}
$key = $db -> f ( " sslkey " );
$key = trim ( str_replace ( " \r \n " , " \n " , $key )) . " \n " ;
}
if ( substr ( $crt , 0 , 28 ) != " -----BEGIN CERTIFICATE----- \n " ||
substr ( $crt , - 26 , 26 ) != " -----END CERTIFICATE----- \n " ) {
$this -> error .= _ ( " The certificate must begin by BEGIN CERTIFICATE and end by END CERTIFICATE lines. Please check you pasted it in PEM form. " ) . " <br> \n " ;
}
2015-05-12 15:14:19 +00:00
if ( trim ( $chain ) &&
2015-05-07 15:29:43 +00:00
( substr ( $chain , 0 , 28 ) != " -----BEGIN CERTIFICATE----- \n " ||
substr ( $chain , - 26 , 26 ) != " -----END CERTIFICATE----- \n " )) {
$this -> error .= _ ( " The chained certificate must begin by BEGIN CERTIFICATE and end by END CERTIFICATE lines. Please check you pasted it in PEM form. " ) . " <br> \n " ;
}
if (( substr ( $key , 0 , 32 ) != " -----BEGIN RSA PRIVATE KEY----- \n " ||
substr ( $key , - 30 , 30 ) != " -----END RSA PRIVATE KEY----- \n " ) &&
( substr ( $key , 0 , 28 ) != " -----BEGIN PRIVATE KEY----- \n " ||
substr ( $key , - 26 , 26 ) != " -----END PRIVATE KEY----- \n " )) {
$this -> error .= _ ( " The private key must begin by BEGIN (RSA )PRIVATE KEY and end by END (RSA )PRIVATE KEY lines. Please check you pasted it in PEM form. " ) . " <br> \n " ;
}
if ( $this -> error ) {
return false ;
}
// We split the chained certificates in individuals certificates :
$chains = array ();
$status = 0 ;
$new = " " ;
$lines = explode ( " \n " , $chain );
foreach ( $lines as $line ) {
if ( $line == " -----BEGIN CERTIFICATE----- " && $status == 0 ) {
$status = 1 ;
$new = $line . " \n " ;
continue ;
}
if ( $line == " -----END CERTIFICATE----- " && $status == 1 ) {
$status = 0 ;
$new .= $line . " \n " ;
$chains [] = $new ;
$new = " " ;
continue ;
}
if ( $status == 1 ) {
$new .= $line . " \n " ;
}
}
// here chains contains all the ssl certificates in the chained certs.
// Now we check those using Openssl functions (real check :) )
$rchains = array ();
$i = 0 ;
foreach ( $chains as $tmpcert ) {
$i ++ ;
$tmpr = openssl_x509_read ( $tmpcert );
if ( $tmpr === false ) {
$this -> error .= sprintf ( _ ( " The %d-th certificate in the chain is invalid " ), $i ) . " <br> \n " ;
} else {
$rchains [] = $tmpr ;
}
}
$rcrt = openssl_x509_read ( $crt );
$crtdata = openssl_x509_parse ( $crt );
if ( $rcrt === false || $crtdata === false ) {
$this -> error .= _ ( " The certificate is invalid. " ) . " <br> \n " ;
}
$rkey = openssl_pkey_get_private ( $key );
if ( $rkey === false ) {
$this -> error .= _ ( " The private key is invalid. " ) . " <br> \n " ;
}
if ( ! $this -> error ) {
// check that the private key and the certificates are matching :
if ( ! openssl_x509_check_private_key ( $rcrt , $rkey )) {
$this -> error .= _ ( " The private key is not the one signed inside the certificate. " ) . " <br> \n " ;
}
}
if ( ! $this -> error ) {
// Everything is fine, let's recreate crt, chain, key from our internal OpenSSL structures:
if ( ! openssl_x509_export ( $rcrt , $crt )) {
$this -> error .= _ ( " Can't export your certificate as a string, please check its syntax. " ) . " <br> \n " ;
}
$chain = " " ;
foreach ( $rchains as $r ) {
if ( ! openssl_x509_export ( $r , $tmp )) {
$this -> error .= _ ( " Can't export one of your chained certificates as a string, please check its syntax. " ) . " <br> \n " ;
} else {
$chain .= $tmp ;
}
}
if ( ! openssl_pkey_export ( $rkey , $key )) {
$this -> error .= _ ( " Can't export your private key as a string, please check its syntax. " ) . " <br> \n " ;
}
}
return array ( $crt , $chain , $key , $crtdata );
}
// -----------------------------------------------------------------
/** Generate a self - signed certificate
*
* @ param string $fqdn the fully qualified domain name to set as commonName for the certificate
* @ return hash an array similar to a certificate DB row containing everything ( sslcrt , sslcsr , sslkey , sslchain )
*/
private function selfSigned ( $fqdn ) {
2017-10-07 17:24:32 +00:00
global $msg ;
2015-05-07 15:29:43 +00:00
putenv ( " OPENSSL_CONF=/etc/alternc/openssl.cnf " );
$pkey = openssl_pkey_new ();
if ( ! $pkey ) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't generate a private key (1) " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$privKey = " " ;
if ( ! openssl_pkey_export ( $pkey , $privKey )) {
2017-10-07 17:53:30 +00:00
$msg -> raise ( " ERROR " , " ssl " , _ ( " Can't generate a private key (2) " ));
2015-05-07 15:29:43 +00:00
return false ;
}
$dn = array ( " commonName " => $fqdn );
// override the (not taken from openssl.cnf) digest to use SHA-2 / SHA256 and not SHA-1 or MD5 :
$config = array ( " digest_alg " => " sha256 " );
$csr = openssl_csr_new ( $dn , $pkey , $config );
$csrout = " " ;
openssl_csr_export ( $csr , $csrout );
$crt = openssl_csr_sign ( $csr , null , $pkey , 3650 , $config );
$crtout = " " ;
openssl_x509_export ( $crt , $crtout );
return array ( " id " => 0 , " status " => 1 , " shared " => 0 , " fqdn " => $fqdn , " altnames " => " " ,
" validstart " => date ( " Y-m-d H:i:s " ), " validend " => date ( " Y-m-d H:i:s " , time () + 86400 * 10 * 365.249 ),
" sslcsr " => $csrout , " sslcrt " => $crtout , " sslkey " => $privKey , " sslchain " => " " ,
" selfsigned " => true ,
);
}
function dummy () {
_ ( " Locally hosted forcing HTTPS " );
_ ( " Locally hosted HTTP and HTTPS " );
_ ( " HTTPS AlternC panel access " );
_ ( " HTTPS Roundcube Webmail " );
_ ( " HTTPS Squirrelmail Webmail " );
_ ( " php52 forcing HTTPS " );
_ ( " php52 HTTP and HTTPS " );
}
}
/* Class m_ssl */