Alternc  latest
Alternc logiel libre pour l'hébergement
m_ssl.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  ----------------------------------------------------------------------
5  AlternC - Web Hosting System
6  Copyright (C) 2000-2014 by the AlternC Development Team.
7  https://alternc.org/
8  ----------------------------------------------------------------------
9  LICENSE
10 
11  This program is free software; you can redistribute it and/or
12  modify it under the terms of the GNU General Public License (GPL)
13  as published by the Free Software Foundation; either version 2
14  of the License, or (at your option) any later version.
15 
16  This program is distributed in the hope that it will be useful,
17  but WITHOUT ANY WARRANTY; without even the implied warranty of
18  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  GNU General Public License for more details.
20 
21  To read the license please visit http://www.gnu.org/copyleft/gpl.html
22  ----------------------------------------------------------------------
23  Purpose of file: Manage SSL Certificates and HTTPS Hosting
24  ----------------------------------------------------------------------
25  */
26 
27 // -----------------------------------------------------------------
28 /**
29  * SSL Certificates management class
30  */
31 class m_ssl {
32 
33  const STATUS_PENDING = 0; // we have a key / csr, but no CRT
34  const STATUS_OK = 1; // we have the key, csr, crt, chain
35  const STATUS_EXPIRED = 99; // The certificate is now expired.
36 
37  public $error = "";
38 
39  // Includes one or more of those flags to see only those certificates
40  // when listing them:
41  const FILTER_PENDING = 1;
42  const FILTER_OK = 2;
43  const FILTER_EXPIRED = 4;
44 
45  const KEY_REPOSITORY = "/var/lib/alternc/ssl/private";
46  const SPECIAL_CERTIFICATE_ID_PATH = "/var/lib/alternc/ssl/special_id.json";
47 
48  // -----------------------------------------------------------------
49  /**
50  * Constructor
51  */
52  function m_ssl() {
53  global $L_FQDN;
54  $this->last_certificate_id=variable_get('last_certificate_id',0,'Latest certificate ID parsed by update_domains. Do not change this unless you know what you are doing');
55  $this->default_certificate_fqdn=variable_get('default_certificate_fqdn',$L_FQDN,'FQDN of the certificate we will use as a default one before getting a proper one through any provider. If unsure, keep the default');
56  }
57 
58 
59  // -----------------------------------------------------------------
60  /**
61  * Return the list of special FQDN for which we'd like to obtain a certificate too.
62  * (apart from sub+domaine from sub_domaines table)
63  * used by providers to get the certs they should generate
64  * also used by update_domaines to choose which cert to use for a specific fqdn
65  */
66  function get_fqdn_specials() {
67  global $L_FQDN;
68  $specials=array($L_FQDN);
69  $variables=array("fqdn_dovecot","fqdn_postfix","fqdn_proftpd","fqdn_mailman");
70  foreach($variables as $var) {
71  $value = variable_get($var,null);
72  if ($value && !in_array($value,$specials)) {
73  $specials[]=$value;
74  }
75  }
76  return $specials;
77  }
78 
79 
80  // -----------------------------------------------------------------
81  /**
82  * set expired certificates as such :
83  */
84  function expire_certificates() {
85  global $db;
86  $db->query("UPDATE certificates SET status=".self::STATUS_EXPIRED." WHERE status=".self::STATUS_OK." AND validend<NOW();");
87  }
88 
89 
90  // -----------------------------------------------------------------
91  /**
92  * Crontab launched every minute
93  * to search for new certificates and launch web_action="UPDATE"
94  */
95  function cron_new_certs() {
96  global $db,$msg,$dom;
97  $db->query("SELECT max(id) AS maxid FROM certificates;");
98  if (!$db->next_record()) {
99  $msg->raise("ERROR","ssl",_("FATAL: no certificates in certificates table, even the SnakeOil one??"));
100  return false;
101  }
102  $maxid=$db->Record["maxid"];
103  if ($maxid>$this->last_certificate_id) {
104  $db->query("SELECT id,fqdn,altnames,sslcrt FROM certificates WHERE id>?",array($this->last_certificate_id));
105  $certs=array();
106  // fill an array of fqdn/altnames
107  while ($db->next_record()) {
108  if (!$db->Record["sslcrt"]) continue; // skip NOT FINALIZED certificates !!
109 
110  $certs[]=array("id"=>$db->Record["id"],"fqdn"=>$db->Record["fqdn"]);
111  $altnames=explode("\n",$db->Record["altnames"]);
112  foreach($altnames as $altname) {
113  $certs[]=array("id"=>$db->Record["id"],"fqdn"=>$altname);
114  }
115  }
116 
117  // get the list of subdomains-id that match the following FQDN (or wildcard)
118  $updateids=array();
119  foreach($certs as $cert) {
120  $subids=$this->searchSubDomain($cert["fqdn"]);
121  foreach($subids as $subid) {
122  $updateids[$subid]=$cert["id"];
123  }
124  // if this fqdn match a special domain, update its certificate (and mark service for reloading)
125  $this->update_specials_match($cert["id"],$cert["fqdn"]);
126  }
127 
128  // update those subdomains
129  $dom->lock();
130  foreach($updateids as $id => $certid) {
131  $db->query("UPDATE sub_domaines SET web_action=? WHERE id=?;",array("UPDATE",$id));
132  $msg->raise("INFO","ssl",sprintf(_("Reloading domain %s as we have new certificate %s"),$id,$certid));
133  }
134  $dom->unlock();
135  $this->last_certificate_id=$maxid;
136  variable_set('last_certificate_id',$this->last_certificate_id);
137  }
138  }
139 
140 
141  function fqdnmatch($cert,$fqdn) {
142  if ($cert==$fqdn)
143  return true;
144  if (substr($cert,0,2)=="*." &&
145  substr($cert,2)==substr($fqdn,strpos($fqdn,".")+1) )
146  return true;
147  return false;
148  }
149 
150  // -----------------------------------------------------------------
151  /**
152  * update special system certificate that matches the cert fqdn:
153  */
154  function update_specials_match($id,$fqdn) {
155  global $L_FQDN;
156 
157  if ($this->fqdnmatch($fqdn,$L_FQDN)) {
158  // new certificate for the panel
159  $this->copycert("alternc-panel",$id);
160  exec("service apache2 reload");
161  }
162  $variables=array("fqdn_dovecot","fqdn_postfix","fqdn_proftpd","fqdn_mailman");
163  foreach($variables as $var) {
164  $value = variable_get($var,null);
165  if ($value) {
166  if ($this->fqdnmatch($fqdn,$value)) {
167  $this->copycert("alternc-".substr($var,5),$id);
168  exec("service ".substr($var,5)." reload");
169  }
170  }
171  }
172 
173  }
174 
175  // -----------------------------------------------------------------
176  /**
177  * copy a certificate (by its ID) to the system files
178  * set the correct permissions
179  * try to minimize zero-file-size risk or timing attack
180  */
181  private function copycert($target,$id) {
182  global $db,$msg;
183  $msg->raise("INFO","ssl",_("Copying system certificate $id on $target"));
184  $db->query("SELECT * FROM certificates WHERE id=?",array($id));
185  if (!$db->next_record()) return false;
186  if (!file_put_contents("/etc/ssl/certs/".$target.".pem.tmp",trim($db->Record["sslcrt"])."\n".trim($db->Record["sslchain"]))) {
187  $msg->raise("ERROR","ssl",_("Can't put file into /etc/ssl/certs/".$target.".pem.tmp, failing properly"));
188  return false;
189  }
190  chown("/etc/ssl/certs/".$target.".pem.tmp","root");
191  chgrp("/etc/ssl/certs/".$target.".pem.tmp","ssl-cert");
192  chmod("/etc/ssl/certs/".$target.".pem.tmp",0755);
193  if (!file_put_contents("/etc/ssl/private/".$target.".key.tmp",$db->Record["sslkey"])) {
194  $msg->raise("ERROR","ssl",_("Can't put file into /etc/ssl/private/".$target.".key.tmp, failing properly"));
195  @unlink("/etc/ssl/certs/".$target.".pem.tmp");
196  return false;
197  }
198  chown("/etc/ssl/private/".$target.".key.tmp","root");
199  chgrp("/etc/ssl/private/".$target.".key.tmp","ssl-cert");
200  chmod("/etc/ssl/private/".$target.".key.tmp",0750);
201 
202  rename("/etc/ssl/certs/".$target.".pem.tmp","/etc/ssl/certs/".$target.".pem");
203  rename("/etc/ssl/private/".$target.".key.tmp","/etc/ssl/private/".$target.".key");
204  return true;
205  }
206 
207 
208  // -----------------------------------------------------------------
209  /**
210  * search for a FQDN as a fqdn or a wildcard in all subdomains currently hosted
211  * return a list of subdomain-id
212  */
213  function searchSubDomain($fqdn) {
214  global $db;
215  $db->query("SELECT sd.id FROM sub_domaines sd, domaines_type dt WHERE dt.name=sd.type AND dt.only_dns=0 AND
216 (CONCAT(sd.sub,IF(sd.sub!='','.',''),sd.domaine)=?
217 OR CONCAT('*.',SUBSTRING(CONCAT(sd.sub,IF(sd.sub!='','.',''),sd.domaine),
218 INSTR(CONCAT(sd.sub,IF(sd.sub!='','.',''),sd.domaine),'.')+1))=?
219 );",
220  array($fqdn,$fqdn));
221  $ids=array();
222  while ($db->next_record()) {
223  $ids[]=$db->Record["id"];
224  }
225  return $ids;
226  }
227 
228 
229  // -----------------------------------------------------------------
230  /**
231  * delete old certificates (expired for more than a year)
232  */
234  global $db;
235  $db->query("SELECT c.id,sd.id AS used FROM certificates c LEFT JOIN sub_domaines sd ON sd.certificate_id=c.id WHERE c.status=".self::STATUS_EXPIRED." AND c.validend<DATE_SUB(NOW(), INTERVAL 12 MONTH) AND c.validend!='0000-00-00 00:00:00';");
236  while ($db->next_record()) {
237  if ($db->Record["used"]) {
238  continue; // this certificate is used (even though it's expired :/ )
239  }
240  $CRTDIR = self::KEY_REPOSITORY . "/" . floor($db->Record["id"]/1000);
241  @unlink($CRTDIR."/".$db->Record["id"].".pem");
242  @unlink($CRTDIR."/".$db->Record["id"].".key");
243  @unlink($CRTDIR."/".$db->Record["id"].".chain");
244  $d=opendir($CRTDIR);
245  $empty=true;
246  while (($c=readdir($d))!==false) {
247  if (is_file($CRTDIR."/".$c)) {
248  $empty=false;
249  break;
250  }
251  }
252  closedir($d);
253  if ($empty) {
254  rmdir($CRTDIR);
255  }
256  }
257  }
258 
259 
260  // -----------------------------------------------------------------
261  /** Return all the SSL certificates for an account (or the searched one)
262  * @param $filter an integer telling which certificate we want to see (see FILTER_* constants above)
263  * the default is showing all certificate, but only Pending and OK certificates, not expired
264  * when there is more than 10.
265  * @return array all the ssl certificate this user can use
266  * (each array is the content of the certificates table)
267  */
268  function get_list(&$filter = null) {
269  global $db, $msg, $cuid;
270  $msg->log("ssl", "get_list");
271  $this->expire_certificates();
272  $r = array();
273  // 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.
274  if (is_null($filter)) {
275  $filter = (self::FILTER_PENDING | self::FILTER_OK);
276  }
277  // filter the filter values :)
278  $filter = ($filter & (self::FILTER_PENDING | self::FILTER_OK | self::FILTER_EXPIRED));
279  // Here filter can't be null (and will be returned to the caller !)
280  $sql = "";
281  $sql = " uid='$cuid' ";
282  $sql.=" AND status IN (-1";
283  if ($filter & self::FILTER_PENDING) {
284  $sql.="," . self::STATUS_PENDING;
285  }
286  if ($filter & self::FILTER_OK) {
287  $sql.="," . self::STATUS_OK;
288  }
289  if ($filter & self::FILTER_EXPIRED) {
290  $sql.="," . self::STATUS_EXPIRED;
291  }
292  $sql.=") ";
293  $db->query("SELECT *, UNIX_TIMESTAMP(validstart) AS validstartts, UNIX_TIMESTAMP(validend) AS validendts FROM certificates WHERE $sql ORDER BY validstart DESC;");
294  if ($db->num_rows()) {
295  while ($db->next_record()) {
296  $r[] = $db->Record;
297  }
298  return $r;
299  } else {
300  $msg->raise("INFO", "ssl", _("No SSL certificates available"));
301  return array();
302  }
303  }
304 
305 
306 
307  // -----------------------------------------------------------------
308  /** Generate a new CSR, a new Private RSA Key, for FQDN.
309  * @param $fqdn string the FQDN of the domain name for which we want a CSR.
310  * a wildcard certificate must start by *.
311  * @param $provider string a provider if necessary
312  * @return integer the Certificate ID created in the MySQL database
313  * or false if an error occurred
314  */
315  function new_csr($fqdn, $provider="manual") {
316  global $db, $msg, $cuid;
317  $msg->log("ssl", "new_csr");
318  if (substr($fqdn, 0, 2) == "*.") {
319  $f = substr($fqdn, 2);
320  } else {
321  $f = $fqdn;
322  }
323  if (checkfqdn($f)) {
324  $msg->raise("ERROR","ssl", _("Bad FQDN domain name"));
325  return false;
326  }
327  putenv("OPENSSL_CONF=/etc/alternc/openssl.cnf");
328  $pkey = openssl_pkey_new();
329  if (!$pkey) {
330  $msg->raise("ERROR","ssl", _("Can't generate a private key (1)"));
331  return false;
332  }
333  $privKey = "";
334  if (!openssl_pkey_export($pkey, $privKey)) {
335  $msg->raise("ERROR","ssl", _("Can't generate a private key (2)"));
336  return false;
337  }
338  $dn = array("commonName" => $fqdn);
339  // override the (not taken from openssl.cnf) digest to use SHA-2 / SHA256 and not SHA-1 or MD5 :
340  $config = array("digest_alg" => "sha256");
341  $csr = openssl_csr_new($dn, $pkey, $config);
342  $csrout = "";
343  openssl_csr_export($csr, $csrout);
344  $db->query("INSERT INTO certificates SET uid=?, status=?, fqdn=?, altnames='', validstart=NOW(), sslcsr=?, sslkey=?, provider=?;",array($cuid, self::STATUS_PENDING, $fqdn, $csrout, $privKey, $provider));
345  if (!($id = $db->lastid())) {
346  $msg->raise("ERROR","ssl", _("Can't generate a CSR"));
347  return false;
348  }
349  return $id;
350  }
351 
352 
353  // -----------------------------------------------------------------
354  /** Return all informations of a given certificate for the current user.
355  * @param $id integer the certificate by id
356  * @param $anyuser integer if you want to search cert for any user, set this to true
357  * @return array all the informations of the current certificate as a hash.
358  */
359  function get_certificate($id, $anyuser=false) {
360  global $db, $msg, $cuid;
361  $msg->log("ssl", "get_certificate");
362  $id = intval($id);
363  $sql="";
364  if (!$anyuser) {
365  $sql=" AND uid='".intval($cuid)."' ";
366  }
367  $db->query("SELECT *, UNIX_TIMESTAMP(validstart) AS validstartts, UNIX_TIMESTAMP(validend) AS validendts FROM certificates WHERE id=? $sql;",array($id));
368  if (!$db->next_record()) {
369  $msg->raise("ERROR","ssl", _("Can't find this Certificate"));
370  return false;
371  }
372  return $db->Record;
373  }
374 
375 
376  // -----------------------------------------------------------------
377  /** Return paths to certificate, key, and chain for a certificate
378  * given it's ID.
379  * @param $id integer the certificate by id
380  * @return array cert, key, chain (not mandatory) with full path.
381  */
382  function get_certificate_path($id) {
383  global $db, $msg, $cuid;
384  $msg->log("ssl", "get_certificate_path",$id);
385  $id = intval($id);
386  $db->query("SELECT id FROM certificates WHERE id=?;",array($id));
387  if (!$db->next_record()) {
388  $msg->raise("ERROR","ssl", _("Can't find this Certificate"));
389  // Return cert 0 info :)
390  $id=0;
391  }
392  $chain=self::KEY_REPOSITORY."/".floor($id/1000)."/".$id.".chain";
393  if (!file_exists($chain))
394  $chain=false;
395 
396  return array(
397  "cert" => self::KEY_REPOSITORY."/".floor($id/1000)."/".$id.".pem",
398  "key" => self::KEY_REPOSITORY."/".floor($id/1000)."/".$id.".key",
399  "chain" => $chain
400  );
401  }
402 
403  // -----------------------------------------------------------------
404  /** Return all the valid certificates that can be used for a specific FQDN
405  * return the list of certificates by order of preference
406  * (the 2 last will be the default FQDN and the snakeoil if necessary)
407  * keys: id, provider, crt, chain, key, validstart, validend
408  */
409  function get_valid_certs($fqdn, $provider="") {
410  global $db, $msg, $cuid;
411  $this->expire_certificates();
412 
413  $db->query("SELECT *, UNIX_TIMESTAMP(validstart) AS validstartts, UNIX_TIMESTAMP(validend) AS validendts FROM certificates WHERE status=".self::STATUS_OK." ORDER BY validstart DESC;");
414 
415  $good=array(); // list of good certificates
416  $ugly=array(); // good but not with the right provider
417  $bad=array(); // our snakeoil
418 
419  $wildcard="*".substr($fqdn,strpos($fqdn,"."));
420  $defaultwild="*".substr($this->default_certificate_fqdn,strpos($this->default_certificate_fqdn,"."));
421 
422  while($db->next_record()) {
423  $found=false;
424  if ($db->Record["fqdn"]==$fqdn || $db->Record["fqdn"]==$wildcard) {
425  $found=true;
426 
427  } else {
428  $alts=explode("\n",$db->Record["altnames"]);
429  foreach($alts as $alt) {
430  if ($alt==$fqdn || $alt==$wildcard) {
431  $found=true;
432  break;
433  }
434  }
435  }
436  if ($found) {
437  if ($provider=="" || $provider==$db->Record["provider"]) {
438  $good[]=$db->Record;
439  } else {
440  $ugly[]=$db->Record;
441  }
442  }
443  // search for the default one, the one used by the panel
444  if (!count($bad)) {
445  $found=false;
446  if ($db->Record["fqdn"]==$this->default_certificate_fqdn || $db->Record["fqdn"]==$defaultwild) {
447  $found=true;
448  } else {
449  $alts=explode("\n",$db->Record["altnames"]);
450  foreach($alts as $alt) {
451  if ($alt==$this->default_certificate_fqdn || $alt==$defaultwild) {
452  $found=true;
453  break;
454  }
455  }
456  }
457  if ($found) {
458  $bad=$db->Record;
459  }
460  }
461  }
462  // add the one with the bad provider
463  if (count($ugly)) {
464  $good=array_merge($good,$ugly);
465  }
466  // add the panel/default one
467  if (count($bad)) {
468  $good[]=$bad;
469  }
470  // Add the Snakeoil : #0
471  $db->query("SELECT * FROM certificates WHERE id=0;");
472  if ($db->next_record()) {
473  $good[]=$db->Record;
474  }
475  return $good;
476  }
477 
478 
479  // -----------------------------------------------------------------
480  /** Import an existing ssl Key, Certificate and (maybe) a Chained Cert
481  * @param $key string the X.509 PEM-encoded RSA key
482  * @param $crt string the X.509 PEM-encoded certificate, which *must*
483  * be the one signing the private RSA key in $key (we will check that anyway...)
484  * @param $chain string the X.509 PEM-encoded list of SSL Certificate chain if intermediate authorities
485  * TODO: check that the chain is effectively a chain to the CRT ...
486  * @param $provider string the ssl cert provider
487  * @return integer the ID of the newly created certificate in the table
488  * or false if an error occurred
489  */
490  function import_cert($key, $crt, $chain = "", $provider = "") {
491  global $cuid, $msg, $db;
492  $msg->log("ssl", "import_cert");
493 
494  // Search for an existing cert: (first)
495  $db->query("SELECT id FROM certificates WHERE sslcrt=?;",array($crt));
496  if ($db->next_record()) {
497  $msg->raise("ERROR","ssl", _("Certificate already exists in database"));
498  return false;
499  }
500 
501  $result = $this->check_cert($crt, $chain, $key);
502  if ($result === false) {
503  $msg->raise("ERROR","ssl", $this->error);
504  return false;
505  }
506  list($crt, $chain, $key, $crtdata) = $result;
507 
508  $validstart = $crtdata['validFrom_time_t'];
509  $validend = $crtdata['validTo_time_t'];
510  $fqdn = $crtdata["subject"]["CN"];
511  $altnames = $this->parseAltNames($crtdata["extensions"]["subjectAltName"]);
512 
513  // Everything is PERFECT and has been thoroughly checked, let's insert those in the DB !
514  // The sslcsr column is required as it has no default value, giving it an empty value.
515  $db->query(
516  "INSERT INTO certificates SET uid=?, status=?, fqdn=?, altnames=?, validstart=FROM_UNIXTIME(?), validend=FROM_UNIXTIME(?), sslkey=?, sslcrt=?, sslchain=?, provider=?, sslcsr = '';",
517  array($cuid, self::STATUS_OK, $fqdn, $altnames, intval($validstart), intval($validend), $key, $crt, $chain, $provider)
518  );
519  if (!($id = $db->lastid())) {
520  $msg->log('ssl', 'impoert_cert', 'insert query failed (' . print_r($db->last_error(), TRUE) . ')');
521  $msg->raise("ERROR","ssl", _("Can't save the Key/Crt/Chain now. Please try later."));
522  return false;
523  }
524  return $id;
525  }
526 
527 
528  // -----------------------------------------------------------------
529  /** Import an ssl certificate into an existing certificate entry in the DB.
530  * (finalize an enrollment process)
531  * @param $certid integer the ID in the database of the SSL Certificate
532  * @param $crt string the X.509 PEM-encoded certificate, which *must*
533  * be the one signing the private RSA key in certificate $certid
534  * @param $chain string the X.509 PEM-encoded list of SSL Certificate chain if intermediate authorities
535  * @return integer the ID of the updated certificate in the table
536  * or false if an error occurred
537  */
538  function finalize($certid, $crt, $chain) {
539  global $cuid, $msg, $db;
540  $msg->log("ssl", "finalize");
541 
542  $certid = intval($certid);
543  $result = $this->check_cert($crt, $chain, "", $certid);
544  if ($result === false) {
545  $msg->raise("ERROR","ssl", $this->error);
546  return false;
547  }
548  list($crt, $chain, $key, $crtdata) = $result;
549 
550  $validstart = $crtdata['validFrom_time_t'];
551  $validend = $crtdata['validTo_time_t'];
552  $fqdn = $crtdata["subject"]["CN"];
553  $altnames = $this->parseAltNames($crtdata["extensions"]["subjectAltName"]);
554 
555  // Everything is PERFECT and has been thoroughly checked, let's insert those in the DB !
556  if (!$db->query(
557  "INSERT INTO certificates (status,fqdn,altnames,validstart,validend,sslcrt,sslchain,sslcsr)
558 SELECT ?,?,?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?, ?, sslcsr FROM certificate WHERE id=?;",
559  array(self::STATUS_OK, $fqdn, $altnames, $validstart, $validend, $crt, $chain, $certid)
560  )) {
561  $msg->raise("ERROR","ssl", _("Can't save the Crt/Chain now. Please try later."));
562  return false;
563  }
564  $newid=$db->lastid();
565  $db->query("DELETE FROM certificates WHERE id=?;",array($certid));
566  return $newid;
567  }
568 
569 
570  // -----------------------------------------------------------------
571  /** Function called by a hook when an AlternC member is deleted.
572  * @access private
573  * TODO: delete unused ssl certificates ?? > do this in the crontab.
574  */
575  function alternc_del_member() {
576  global $db, $msg, $cuid;
577  $msg->log("ssl", "alternc_del_member");
578  $db->query("UPDATE certificates SET uid=2000 WHERE uid=?;",array($cuid));
579  return true;
580  }
581 
582 
583  // -----------------------------------------------------------------
584  /** Launched by hosting_functions.sh launched by update_domaines.sh
585  * Action may be create/postinst/delete/enable/disable
586  * Change the template for this domain name to have the proper CERTIFICATE
587  * An algorithm determine the best possible certificate, which may be a BAD one
588  * (like a generic self-signed for localhost as a last chance)
589  */
590  public function updateDomain($action, $type, $fqdn, $mail = 0, $value = "") {
591  global $db, $msg, $dom;
592  $msg->log("ssl", "update_domain($action,$type,$fqdn)");
593 
594  // the domain type must be a "dns_only=false" one:
595  if (!($domtype=$dom->domains_type_get($type)) || $domtype["only_dns"]==true) {
596  return; // nothing to do : this domain type does not involve Vhosts
597  }
598 
599  // The 'vhost' type is overloaded with -http, -https, and -both.
600  // If the type starts with vhost, we should match vhost%
601  // Generally, only 'vhost' is used since the overloads are not enabled
602  // for use in the interface.
603  $type_match = $type;
604  if (substr($type, 0, 5) == 'vhost') {
605  $type_match = 'vhost%';
606  }
607  if ($action == "postinst") {
608  $msg->log("ssl", "update_domain:CREATE($action,$type,$fqdn)");
609  $offset = 0;
610  $found = false;
611  do { // try each subdomain (strtok-style) and search them in sub_domaines table:
612  $db->query(
613  "SELECT * FROM sub_domaines WHERE sub=? AND domaine=? AND web_action NOT IN ('','OK') AND type LIKE ?",
614  array(substr($fqdn, 0, $offset), substr($fqdn, $offset + ($offset != 0)), $type_match)
615  );
616  if ($db->next_record()) {
617  $found = true;
618  break;
619  }
620  $offset = strpos($fqdn, ".", $offset+1);
621  //No more dot, we prevent an infinite loop
622  if (!$offset) {
623  break;
624  }
625  } while (true);
626  if (!$found) {
627  echo "FATAL: didn't found fqdn $fqdn in sub_domaines table !\n";
628  return;
629  }
630  // found and $db point to it:
631  $subdom = $db->Record;
632  $TARGET_FILE = "/var/lib/alternc/apache-vhost/" . substr($subdom["compte"], -1) . "/" . $subdom["compte"] . "/" . $fqdn . ".conf";
633  $cert = $this->searchBestCert($subdom,$fqdn);
634  // $cert[crt/key/chain] are path to the proper files
635 
636  // edit apache conf file to set the certificate:
637  $s = file_get_contents($TARGET_FILE);
638  $s = str_replace("%%CRT%%", $cert["crt"], $s);
639  $s = str_replace("%%KEY%%", $cert["key"], $s);
640  if (isset($cert["chain"]) && $cert["chain"]) {
641  $s = str_replace("%%CHAINLINE%%", "SSLCertificateChainFile " . $cert["chain"], $s);
642  } else {
643  $s = str_replace("%%CHAINLINE%%", "", $s);
644  }
645  file_put_contents($TARGET_FILE, $s);
646  // Edit certif_hosts:
647  $db->query("UPDATE sub_domaines SET certificate_id=? WHERE id=?;",array($cert["id"], $subdom["id"]));
648  } // action==create
649 
650  }
651 
652 
653  // -----------------------------------------------------------------
654  /** Launched by hosting_functions.sh launched by update_domaines.sh
655  * Action may be create/postinst/delete/enable/disable
656  * Change the template for this domain name to have the proper CERTIFICATE
657  * An algorithm determine the best possible certificate, which may be a BAD one
658  * (like a generic self-signed for localhost as a last chance)
659  */
660  public function hook_updatedomains_web_before($subdomid) {
661  global $db, $msg, $dom;
662  $msg->log("ssl", "hook_updatedomains_web_before($subdomid)");
663 
664  $db->query("SELECT sd.*, dt.only_dns, dt.has_https_option, m.login FROM domaines_type dt, sub_domaines sd LEFT JOIN membres m ON m.uid=sd.compte WHERE dt.name=sd.type AND sd.web_action!='OK' AND id=?;",array($subdomid));
665  $db->next_record();
666  $subdom=$db->Record;
667  $domtype=$dom->domains_type_get($subdom["type"]);
668  // the domain type must be a "dns_only=false" one:
669  if ($domtype["only_dns"]==true) {
670  return; // nothing to do : this domain type does not involve Vhosts
671  }
672  $subdom["fqdn"]=$subdom["sub"].(($subdom["sub"])?".":"").$subdom["domaine"];
673 
674  list($cert) = $this->get_valid_certs($subdom["fqdn"], $subdom["provider"]);
675  $this->write_cert_file($cert);
676  // Edit certif_hosts:
677  $db->query("UPDATE sub_domaines SET certificate_id=? WHERE id=?;",array($cert["id"], $subdom["id"]));
678  }
679 
680 
681  // ----------------------------------------------------------------
682  /** Search for the best certificate for a user and a fqdn
683  * Return a hash with crt, key and maybe chain.
684  * they are the full path to the best certificate for this FQDN.
685  * if necessary, use "default_certificate_fqdn" or a "snakeoil"
686  * @param $subdom array the subdomain entry from sub_domaines table
687  * @param $fqdn string the fully qualified domain name to search for
688  * @return array an has with crt key chain
689  */
690  public function searchBestCert($subdom,$fqdn) {
691  global $db;
692 
693  // get the first good certificate:
694  list($cert) = $this->get_valid_certs($fqdn, $subdom["provider"]);
695  $this->write_cert_file($cert);
696  // we have the files, let's fill the output array :
697  $output=array(
698  "id" => $cert["id"],
699  "crt" => $CRTDIR . "/" . $cert["id"].".pem",
700  "key" => $CRTDIR . "/" . $cert["id"].".key",
701  );
702  if (file_exists($CRTDIR . "/" . $cert["id"].".chain")) {
703  $output["chain"] = $CRTDIR . "/" . $cert["id"].".chain";
704  }
705  return $output;
706  }
707 
708 
709  // -----------------------------------------------------------------
710  /** Write certificate file into KEY_REPOSITORY
711  * @param $cert array an array with ID sslcrt sslkey sslchain
712  */
713  function write_cert_file($cert) {
714  // we split the certificates by 1000
715  $CRTDIR = self::KEY_REPOSITORY . "/" . floor($cert["id"]/1000);
716  @mkdir($CRTDIR,0750,true);
717  // set the proper permissions on the Key Repository folder and children :
718  chown(self::KEY_REPOSITORY,"root");
719  chgrp(self::KEY_REPOSITORY,"ssl-cert");
720  chmod(self::KEY_REPOSITORY,0750);
721  chown($CRTDIR,"root");
722  chgrp($CRTDIR,"ssl-cert");
723  chmod($CRTDIR,0750);
724 
725  if (
726  !file_exists($CRTDIR . "/" . $cert["id"].".pem") ||
727  !file_exists($CRTDIR . "/" . $cert["id"].".key")) {
728  // write the files (first time we use a certificate)
729  file_put_contents($CRTDIR . "/" . $cert["id"].".pem", $cert["sslcrt"]);
730  file_put_contents($CRTDIR . "/" . $cert["id"].".key", $cert["sslkey"]);
731  // set the proper rights on those files :
732  chown($CRTDIR . "/" . $cert["id"].".pem","root");
733  chgrp($CRTDIR . "/" . $cert["id"].".pem","ssl-cert");
734  chmod($CRTDIR . "/" . $cert["id"].".pem",0640);
735  chown($CRTDIR . "/" . $cert["id"].".key","root");
736  chgrp($CRTDIR . "/" . $cert["id"].".key","ssl-cert");
737  chmod($CRTDIR . "/" . $cert["id"].".key",0640);
738  if (isset($cert["sslchain"]) && $cert["sslchain"]) {
739  file_put_contents($CRTDIR . "/" . $cert["id"] . ".chain", $cert["sslchain"]);
740  chown($CRTDIR . "/" . $cert["id"].".chain","root");
741  chgrp($CRTDIR . "/" . $cert["id"].".chain","ssl-cert");
742  chmod($CRTDIR . "/" . $cert["id"].".chain",0640);
743  }
744  }
745  }
746 
747 
748  // -----------------------------------------------------------------
749  /** Export every information for an AlternC's account
750  * @access private
751  * EXPERIMENTAL 'sid' function ;)
752  */
753  function alternc_export_conf() {
754  global $db, $msg, $cuid;
755  $msg->log("ssl", "export");
756  $str = " <ssl>";
757  $db->query("SELECT COUNT(*) AS cnt FROM certificates WHERE uid='$cuid' AND status!=" . self::STATUS_EXPIRED);
758  while ($db->next_record()) {
759  $str.=" <id>" . ($db->Record["id"]) . "</id>\n";
760  $str.=" <csr>" . ($db->Record["sslcsr"]) . "</key>\n";
761  $str.=" <key>" . ($db->Record["sslkey"]) . "<key>\n";
762  $str.=" <crt>" . ($db->Record["sslcrt"]) . "</crt>\n";
763  $str.=" <chain>" . ($db->Record["sslchain"]) . "<chain>\n";
764  }
765  $str.=" </ssl>\n";
766  return $str;
767  }
768 
769 
770  // -----------------------------------------------------------------
771  /** Returns the list of alternate names of an X.509 SSL Certificate
772  * from the attribute list.
773  * @param $str string the $crtdata["extensions"]["subjectAltName"] from openssl
774  * @return array an array of FQDNs
775  */
776  function parseAltNames($str) {
777  $mat = array();
778  if (preg_match_all("#DNS:([^,]*)#", $str, $mat, PREG_PATTERN_ORDER)) {
779  return implode("\n", $mat[1]);
780  } else {
781  return "";
782  }
783  }
784 
785  // -----------------------------------------------------------------
786  /** Check that a crt is a proper certificate
787  * @param $crt string an SSL Certificate
788  * @param $chain string is a list of certificates
789  * @param $key string is a rsa key associated with certificate
790  * @param $certid if no key is specified, use it from this certificate ID in the table
791  * @return array the crt, chain, key, crtdata(array) after a proper reformatting
792  * or false if an error occurred (in that case $this->error is filled)
793  */
794  function check_cert($crt, $chain, $key = "", $certid = null) {
795  global $db;
796  // Check that the key crt and chain are really SSL certificates and keys
797  $crt = trim(str_replace("\r\n", "\n", $crt)) . "\n";
798  $key = trim(str_replace("\r\n", "\n", $key)) . "\n";
799  $chain = trim(str_replace("\r\n", "\n", $chain)) . "\n";
800 
801  $this->error = "";
802  if (trim($key) == "" && !is_null($certid)) {
803  // find it in the DB :
804  $db->query("SELECT sslkey FROM certificates WHERE id=?;",array(intval($certid)));
805  if (!$db->next_record()) {
806  $this->error.=_("Can't find the private key in the certificate table, please check your form.");
807  return false;
808  }
809  $key = $db->f("sslkey");
810  $key = trim(str_replace("\r\n", "\n", $key)) . "\n";
811  }
812 
813  if (substr($crt, 0, 28) != "-----BEGIN CERTIFICATE-----\n" ||
814  substr($crt, -26, 26) != "-----END CERTIFICATE-----\n") {
815  $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";
816  }
817  if (trim($chain) &&
818  (substr($chain, 0, 28) != "-----BEGIN CERTIFICATE-----\n" ||
819  substr($chain, -26, 26) != "-----END CERTIFICATE-----\n")) {
820  $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";
821  }
822  if ((substr($key, 0, 32) != "-----BEGIN RSA PRIVATE KEY-----\n" ||
823  substr($key, -30, 30) != "-----END RSA PRIVATE KEY-----\n") &&
824  (substr($key, 0, 28) != "-----BEGIN PRIVATE KEY-----\n" ||
825  substr($key, -26, 26) != "-----END PRIVATE KEY-----\n")) {
826  $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";
827  }
828  if ($this->error) {
829  return false;
830  }
831 
832  // We split the chained certificates in individuals certificates :
833  $chains = array();
834  $status = 0;
835  $new = "";
836  $lines = explode("\n", $chain);
837  foreach ($lines as $line) {
838  if ($line == "-----BEGIN CERTIFICATE-----" && $status == 0) {
839  $status = 1;
840  $new = $line . "\n";
841  continue;
842  }
843  if ($line == "-----END CERTIFICATE-----" && $status == 1) {
844  $status = 0;
845  $new.=$line . "\n";
846  $chains[] = $new;
847  $new = "";
848  continue;
849  }
850  if ($status == 1) {
851  $new.=$line . "\n";
852  }
853  }
854  // here chains contains all the ssl certificates in the chained certs.
855  // Now we check those using Openssl functions (real check :) )
856  $rchains = array();
857  $i = 0;
858  foreach ($chains as $tmpcert) {
859  $i++;
860  $tmpr = openssl_x509_read($tmpcert);
861  if ($tmpr === false) {
862  $this->error.=sprintf(_("The %d-th certificate in the chain is invalid"), $i) . "<br>\n";
863  } else {
864  $rchains[] = $tmpr;
865  }
866  }
867  $rcrt = openssl_x509_read($crt);
868  $crtdata = openssl_x509_parse($crt);
869  if ($rcrt === false || $crtdata === false) {
870  $this->error.=_("The certificate is invalid.") . "<br>\n";
871  }
872 
873  $rkey = openssl_pkey_get_private($key);
874  if ($rkey === false) {
875  $this->error.=_("The private key is invalid.") . "<br>\n";
876  }
877  if (!$this->error) {
878  // check that the private key and the certificates are matching :
879  if (!openssl_x509_check_private_key($rcrt, $rkey)) {
880  $this->error.=_("The private key is not the one signed inside the certificate.") . "<br>\n";
881  }
882  }
883  if (!$this->error) {
884  // Everything is fine, let's recreate crt, chain, key from our internal OpenSSL structures:
885  if (!openssl_x509_export($rcrt, $crt)) {
886  $this->error.=_("Can't export your certificate as a string, please check its syntax.") . "<br>\n";
887  }
888  $chain = "";
889  foreach ($rchains as $r) {
890  if (!openssl_x509_export($r, $tmp)) {
891  $this->error.=_("Can't export one of your chained certificates as a string, please check its syntax.") . "<br>\n";
892  } else {
893  $chain.=$tmp;
894  }
895  }
896  if (!openssl_pkey_export($rkey, $key)) {
897  $this->error.=_("Can't export your private key as a string, please check its syntax.") . "<br>\n";
898  }
899  }
900  return array($crt, $chain, $key, $crtdata);
901  }
902 
903 }
904 
905 /* Class m_ssl */
SSL Certificates management class.
Definition: m_ssl.php:31
finalize($certid, $crt, $chain)
Import an ssl certificate into an existing certificate entry in the DB.
Definition: m_ssl.php:538
hook_updatedomains_web_before($subdomid)
Launched by hosting_functions.sh launched by update_domaines.sh Action may be create/postinst/delete/...
Definition: m_ssl.php:660
get_certificate_path($id)
Return paths to certificate, key, and chain for a certificate given it's ID.
Definition: m_ssl.php:382
const STATUS_EXPIRED
Definition: m_ssl.php:35
get_valid_certs($fqdn, $provider="")
Return all the valid certificates that can be used for a specific FQDN return the list of certificate...
Definition: m_ssl.php:409
const SPECIAL_CERTIFICATE_ID_PATH
Definition: m_ssl.php:46
$error
Definition: m_ssl.php:37
fqdnmatch($cert, $fqdn)
Definition: m_ssl.php:141
get_fqdn_specials()
Return the list of special FQDN for which we'd like to obtain a certificate too.
Definition: m_ssl.php:66
check_cert($crt, $chain, $key="", $certid=null)
Check that a crt is a proper certificate.
Definition: m_ssl.php:794
parseAltNames($str)
Returns the list of alternate names of an X.509 SSL Certificate from the attribute list.
Definition: m_ssl.php:776
cron_new_certs()
Crontab launched every minute to search for new certificates and launch web_action="UPDATE".
Definition: m_ssl.php:95
const STATUS_OK
Definition: m_ssl.php:34
expire_certificates()
set expired certificates as such :
Definition: m_ssl.php:84
get_list(&$filter=null)
Return all the SSL certificates for an account (or the searched one)
Definition: m_ssl.php:268
searchBestCert($subdom, $fqdn)
Search for the best certificate for a user and a fqdn Return a hash with crt, key and maybe chain.
Definition: m_ssl.php:690
write_cert_file($cert)
Write certificate file into KEY_REPOSITORY.
Definition: m_ssl.php:713
update_specials_match($id, $fqdn)
update special system certificate that matches the cert fqdn:
Definition: m_ssl.php:154
const FILTER_OK
Definition: m_ssl.php:42
copycert($target, $id)
copy a certificate (by its ID) to the system files set the correct permissions try to minimize zero-f...
Definition: m_ssl.php:181
searchSubDomain($fqdn)
search for a FQDN as a fqdn or a wildcard in all subdomains currently hosted return a list of subdoma...
Definition: m_ssl.php:213
alternc_export_conf()
Export every information for an AlternC's account @access private EXPERIMENTAL 'sid' function ;)
Definition: m_ssl.php:753
get_certificate($id, $anyuser=false)
Return all informations of a given certificate for the current user.
Definition: m_ssl.php:359
import_cert($key, $crt, $chain="", $provider="")
Import an existing ssl Key, Certificate and (maybe) a Chained Cert.
Definition: m_ssl.php:490
const STATUS_PENDING
Definition: m_ssl.php:33
const FILTER_EXPIRED
Definition: m_ssl.php:43
updateDomain($action, $type, $fqdn, $mail=0, $value="")
Launched by hosting_functions.sh launched by update_domaines.sh Action may be create/postinst/delete/...
Definition: m_ssl.php:590
new_csr($fqdn, $provider="manual")
Generate a new CSR, a new Private RSA Key, for FQDN.
Definition: m_ssl.php:315
delete_old_certificates()
delete old certificates (expired for more than a year)
Definition: m_ssl.php:233
const KEY_REPOSITORY
Definition: m_ssl.php:45
m_ssl()
Constructor.
Definition: m_ssl.php:52
alternc_del_member()
Function called by a hook when an AlternC member is deleted.
Definition: m_ssl.php:575
const FILTER_PENDING
Definition: m_ssl.php:41
$c
Definition: mem_param.php:46