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) $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 { $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; } 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); 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) $action=1; 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() { 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;"); $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) { 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 signinf 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; $err->log("ssl","export"); $f=$this->get_list(); $str=" "; $db->query("SELECT COUNT(*) AS cnt FROM certificates WHERE uid='$cuid' AND status!=".self::STATUS_EXPIRED); while ($db->next_record()) { $str.=" ".($db->Record["id"])."\n"; $str.=" ".($db->Record["sslcsr"])."\n"; $str.=" ".($db->Record["sslkey"])."\n"; $str.=" ".($db->Record["sslcrt"])."\n"; $str.=" ".($db->Record["sslchain"])."\n"; } $str.=" \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) { 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 / * value is the value 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,$value) { 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)."', value='".addslashes($value)."', 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 $err,$cuid,$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.")."\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") { $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; } } $validstart=0; $validend=0; $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 */