AlternC/ssl/panel/class/m_ssl.php

572 lines
24 KiB
PHP
Raw Normal View History

<?php
2015-02-10 15:10:54 +00:00
/*
----------------------------------------------------------------------
AlternC - Web Hosting System
Copyright (C) 2000-2014 by the AlternC Development Team.
https://alternc.org/
----------------------------------------------------------------------
LICENSE
2015-02-10 15:10:54 +00:00
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.
2015-02-10 15:10:54 +00:00
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.
2015-02-10 15:10:54 +00:00
To read the license please visit http://www.gnu.org/copyleft/gpl.html
----------------------------------------------------------------------
Purpose of file: Manage SSL Certificates and HTTPS Hosting
----------------------------------------------------------------------
2015-02-10 15:10:54 +00:00
*/
2015-02-10 15:10:54 +00:00
// -----------------------------------------------------------------
/**
* SSL Certificates management class
*/
class m_ssl {
2015-02-10 15:10:54 +00:00
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";
// -----------------------------------------------------------------
/**
* 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) {
global $db, $err, $cuid;
$err->log("ssl", "get_list");
$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) {
2015-02-10 15:10:54 +00:00
$sql.="," . self::STATUS_PENDING;
}
if ($filter & self::FILTER_OK) {
2015-02-10 15:10:54 +00:00
$sql.="," . self::STATUS_OK;
}
if ($filter & self::FILTER_EXPIRED) {
2015-02-10 15:10:54 +00:00
$sql.="," . self::STATUS_EXPIRED;
}
2015-02-10 15:10:54 +00:00
$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 {
$err->raise("ssl", _("No SSL certificates available"));
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) {
global $db, $err, $cuid;
$err->log("ssl", "new_csr");
if (substr($fqdn, 0, 2) == "*.") {
$f = substr($fqdn, 2);
} else {
$f = $fqdn;
}
if (checkfqdn($f)) {
$err->raise("ssl", _("Bad FQDN domain name"));
return false;
}
putenv("OPENSSL_CONF=/etc/alternc/openssl.cnf");
$pkey = openssl_pkey_new();
if (!$pkey) {
$err->raise("ssl", _("Can't generate a private key (1)"));
return false;
}
$privKey = "";
2015-02-10 15:10:54 +00:00
if (!openssl_pkey_export($pkey, $privKey)) {
$err->raise("ssl", _("Can't generate a private key (2)"));
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 = "";
2015-02-10 15:10:54 +00:00
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())) {
$err->raise("ssl", _("Can't generate a CSR"));
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) {
global $db, $err, $cuid;
$err->log("ssl", "get_certificate");
$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()) {
$err->raise("ssl", _("Can't find this Certifcate"));
return false;
}
return $db->Record;
}
// -----------------------------------------------------------------
/** 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) {
global $db, $err, $cuid;
$err->log("ssl", "share");
$id = intval($id);
$db->query("SELECT id FROM certificates WHERE uid='$cuid' AND status=" . self::STATUS_OK . " AND id='$id';");
if (!$db->next_record()) {
$err->raise("ssl", _("Can't find this Certifcate"));
return false;
}
if ($action) {
2015-02-10 15:10:54 +00:00
$action = 1;
} else {
2015-02-10 15:10:54 +00:00
$action = 0;
}
2015-02-10 15:10:54 +00:00
$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() {
global $db, $err, $cuid;
$err->log("ssl", "get_new_advice");
$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;");
2015-02-10 15:10:54 +00:00
$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) {
2015-02-10 15:10:54 +00:00
$me.=".";
}
2015-02-10 15:10:54 +00:00
$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 = "") {
global $cuid, $err, $db;
$err->log("ssl", "import_cert");
$result = $this->check_cert($crt, $chain, $key);
if ($result === false) {
$err->raise("ssl", $this->error);
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())) {
$err->raise("ssl", _("Can't save the Key/Crt/Chain now. Please try later."));
return false;
}
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) {
global $cuid, $err, $db;
$err->log("ssl", "finalize");
$certid = intval($certid);
$result = $this->check_cert($crt, $chain, "", $certid);
if ($result === false) {
$err->raise("ssl", $this->error);
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)) {
$err->raise("ssl", _("Can't save the Crt/Chain now. Please try later."));
return false;
}
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() {
global $db, $err, $cuid;
$err->log("ssl", "alternc_del_member");
$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() {
global $db, $err, $cuid;
$err->log("ssl", "getquota");
$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;
}
// -----------------------------------------------------------------
/** Export every information for an AlternC's account
* @access private
* EXPERIMENTAL 'sid' function ;)
*/
function alternc_export_conf() {
global $db, $err, $cuid;
2015-02-10 15:10:54 +00:00
$err->log("ssl", "export");
$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();
2015-02-10 15:10:54 +00:00
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) {
global $err, $cuid, $db;
$db->query("SELECT name FROM certif_alias WHERE name='" . addslashes($name) . "';");
if ($db->next_record()) {
$err->raise("ssl", _("Alias already exists"));
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) {
global $err, $cuid, $db;
$db->query("SELECT name FROM certif_alias WHERE name='" . addslashes($name) . "' AND uid=" . intval($cuid) . ";");
if (!$db->next_record()) {
$err->raise("ssl", _("Alias not found"));
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;
2015-02-10 15:10:54 +00:00
// 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.") . "\n";
}
if ($chain &&
(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.") . "\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.") . "\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) . "\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.") . "\n";
}
$rkey = openssl_pkey_get_private($key);
if ($rkey === false) {
$this->error.=_("The private key is invalid.") . "\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.") . "\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.") . "\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.") . "\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.") . "\n";
}
}
return array($crt, $chain, $key, $crtdata);
}
// check_cert
}
/* Class m_ssl */