modules/vhosts/include/certs.php
1ab9db55
 <?php
c208bd90
 /*
 This file belongs to the Webinterface of schokokeks.org Hosting
 
5effc2bd
 Written 2008-2014 by schokokeks.org Hosting, namely
c208bd90
   Bernd Wurst <bernd@schokokeks.org>
   Hanno Böck <hanno@schokokeks.org>
 
 To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
 
 You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see 
 http://creativecommons.org/publicdomain/zero/1.0/
 
 Nevertheless, in case you use a significant part of this code, we ask (but not require, see the license) that you keep the authors' names in place and return your changes to the public. We would be especially happy if you tell us what you're going to do with this code.
 */
1ab9db55
 
 require_once('inc/base.php');
1319a6d9
 require_once('inc/security.php');
1ab9db55
 
 define("CERT_OK", 0);
 define("CERT_INVALID", 1);
 define("CERT_NOCHAIN", 2);
 
 function user_certs()
 {
   $uid = (int) $_SESSION['userinfo']['uid'];
44996558
   $result = db_query("SELECT id, valid_from, valid_until, subject, cn FROM vhosts.certs WHERE uid=? ORDER BY cn", array($uid));
1ab9db55
   $ret = array();
f1f231f5
   while ($i = $result->fetch())
1ab9db55
     $ret[] = $i;
   DEBUG($ret);
   return $ret;
 }
 
1319a6d9
 function user_csr()
 {
   $uid = (int) $_SESSION['userinfo']['uid'];
44996558
   $result = db_query("SELECT id, created, hostname, bits FROM vhosts.csr WHERE uid=? ORDER BY hostname", array($uid));
1319a6d9
   $ret = array();
f1f231f5
   while ($i = $result->fetch())
1319a6d9
     $ret[] = $i;
   DEBUG($ret);
   return $ret;
 }
1ab9db55
 
 function cert_details($id)
 {
   $id = (int) $id;
   $uid = (int) $_SESSION['userinfo']['uid'];
   
bc103625
   $result = db_query("SELECT id, lastchange, valid_from, valid_until, subject, cn, chain, cert, `key` FROM vhosts.certs WHERE uid=:uid AND id=:id", array(":uid" => $uid, ":id" => $id));
f1f231f5
   if ($result->rowCount() != 1)
5bfd0ff2
     system_failure("Ungültiges Zertifikat #{$id}");
f1f231f5
   return $result->fetch();
1ab9db55
 }
 
bc103625
 function cert_is_letsencrypt($id)
 {
   $details = cert_details($id);
3860b011
   if ($details['chain'] == config('letsencrypt_chain')) {
bc103625
     return true;
   }
   return false;
 }
 
1ab9db55
 
1319a6d9
 function csr_details($id)
 {
   $id = (int) $id;
   $uid = (int) $_SESSION['userinfo']['uid'];
   
44996558
   $result = db_query("SELECT id, created, hostname, bits, `replace`, csr, `key` FROM vhosts.csr WHERE uid=:uid AND id=:id", array(":uid" => $uid, ":id" => $id));
f1f231f5
   if ($result->rowCount() != 1)
1319a6d9
     system_failure("Ungültiger CSR");
f1f231f5
   return $result->fetch();
1319a6d9
 }
 
 
1ab9db55
 function get_available_CAs()
 {
   $path = '/etc/apache2/certs/cabundle/';
   $ret = glob($path.'*.pem');
   if (! $ret)
     system_failure("Konnte die CA-Zertifikate nicht laden");
   DEBUG($ret);
   return $ret;
 }
 
 
32ca1630
 function get_chain($cert)
 {
   $certdata = openssl_x509_parse($cert, true);
ee37ea57
   if ($certdata === FALSE) {
     system_failure("Das Zertifikat konnte nicht gelesen werden");
   }
   if (! isset($certdata['issuer']['CN'])) {
     return NULL;
   }
44996558
   $result = db_query("SELECT id FROM vhosts.certchain WHERE cn=?", array($certdata['issuer']['CN']));
f1f231f5
   if ($result->rowCount() > 0)
32ca1630
   {
f1f231f5
     $c = $result->fetch();
32ca1630
     //$chainfile = '/etc/apache2/certs/chains/'.$c['id'].'.pem';
     DEBUG("identified fitting certificate chain #".$c['id']);
     return $c['id'];
   }
 }
 
 
1ab9db55
 function validate_certificate($cert, $key)
ee37ea57
 { 
   // Lade private key 
   $seckey = openssl_get_privatekey($key);
   if ($seckey === FALSE) {
     system_failure("Der private Schlüssel konnte (ohne Passwort) nicht gelesen werden.");
   }
   // Lade public key
   $pubkey = openssl_get_publickey($cert);
   if ($pubkey === FALSE) {
     system_failure("In dem eingetragenen Zertifikat wurde kein öffentlicher Schlüssel gefunden.");
   }
   // Parse Details über den pubkey
   $certinfo = openssl_pkey_get_details($pubkey);
43284387
   DEBUG($certinfo);
ee37ea57
   if ($certinfo === FALSE) {
     system_failure("Der öffentliche Schlüssel des Zertifikats konnte nicht gelesen werden");
43284387
   }
ee37ea57
 
   // Apache unterstützt nur Schlüssel vom Typ RSA oder DSA
   if (! in_array($certinfo['type'], array(OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_DSA))) {
43284387
     system_failure("Dieser Schlüssel nutzt einen nicht unterstützten Algorithmus.");
   }
     
ee37ea57
   // Bei ECC-Keys treten kürzere Schlüssellängen auf, die können wir aktuell aber sowieso nicht unterstützen
   if ($certinfo['bits'] < 2048) {
     warning("Dieser Schlüssel hat eine sehr geringe Bitlänge und ist daher als nicht besonders sicher einzustufen!");
   }
 
   // Prüfe ob Key und Zertifikat zusammen passen
1ab9db55
   if (openssl_x509_check_private_key($cert, $key) !== true)
   {
416e339d
     DEBUG("Zertifikat und Key passen nicht zusammen: ".openssl_x509_check_private_key($cert, $key));
1ab9db55
     return CERT_INVALID;
   }
 
32ca1630
   $cacerts = array('/etc/ssl/certs');
42747c76
   $chain = (int) get_chain($cert);
32ca1630
   if ($chain)
   {
44996558
     $result = db_query("SELECT content FROM vhosts.certchain WHERE id=?", array($chain));
f1f231f5
     $tmp = $result->fetch();
42747c76
     $chaincert = $tmp['content'];
     $chainfile = tempnam(sys_get_temp_dir(), 'webinterface');
     $f = fopen($chainfile, "w");
     fwrite($f, $chaincert);
     fclose($f);
     $cacerts[] = $chainfile;
32ca1630
   }
1ab9db55
 
42747c76
   $valid = openssl_x509_checkpurpose($cert, X509_PURPOSE_SSL_SERVER, $cacerts);
   if ($chain) {
     unlink($chainfile);
   }
   if ($valid !== true)
1ab9db55
   { 
     DEBUG('certificate was not validated as a server certificate with the available chain');
     return CERT_NOCHAIN;
   }
 
   return CERT_OK;
 }
 
 
 function parse_cert_details($cert)
 {
   $certdata = openssl_x509_parse($cert, true);
   /* 
 name => /CN=*.bwurst.org
 validFrom_time_t => 1204118790
 validTo_time_t => 1267190790
 
 
   */
43284387
   DEBUG($certdata);
c575e470
   //return array('subject' => $certdata['name'], 'cn' => $certdata['subject']['CN'], 'valid_from' => date('Y-m-d', $certdata['validFrom_time_t']), 'valid_until' => date('Y-m-d', $certdata['validTo_time_t']));
aae5f16e
   $issuer = $certdata['issuer']['CN'];
   if (isset($certdata['issuer']['O'])) {
     $issuer = $certdata['issuer']['O'];
   }
   return array('subject' => $certdata['subject']['CN'].' / '.$issuer, 'cn' => $certdata['subject']['CN'], 'valid_from' => date('Y-m-d', $certdata['validFrom_time_t']), 'valid_until' => date('Y-m-d', $certdata['validTo_time_t']), 'issuer' => $certdata['issuer']['CN']);
1ab9db55
 }
 
 
 function save_cert($info, $cert, $key)
 {
0ba4f0f1
   openssl_pkey_export($key, $key);
   openssl_x509_export($cert, $cert);
1ab9db55
   $uid = (int) $_SESSION['userinfo']['uid'];
 
44996558
   db_query("INSERT INTO vhosts.certs (uid, subject, cn, valid_from, valid_until, chain, cert, `key`) VALUES (:uid, :subject, :cn, :valid_from, :valid_until, :chain, :cert, :key)", 
         array(":uid" => $uid, ":subject" => filter_input_general($info['subject']), ":cn" => filter_input_general($info['cn']), ":valid_from" => $info['valid_from'], 
               ":valid_until" => $info['valid_until'], ":chain" => get_chain($cert), ":cert" => $cert, ":key" => $key));
1ab9db55
 }
 
5bfd0ff2
 
da0e59cc
 function refresh_cert($id, $info, $cert, $key = NULL)
5bfd0ff2
 {
0ba4f0f1
   openssl_x509_export($cert, $cert);
440f489a
   $chain = get_chain($cert);
9a7d1594
 
5bfd0ff2
   $id = (int) $id;
   $oldcert = cert_details($id);
44996558
   $args = array(":subject" => filter_input_general($info['subject']),
                 ":cn" => filter_input_general($info['cn']),
                 ":cert" => $cert,
                 ":valid_from" => $info['valid_from'],
                 ":valid_until" => $info['valid_until'],
                 ":chain" => get_chain($cert),
                 ":id" => $id);
 
da0e59cc
   $keyop = '';
0ba4f0f1
   if ($key) {
     openssl_pkey_export($key, $key);
44996558
     $keyop = ", `key`=:key";
     $args[":key"] = $key;
0ba4f0f1
   }
44996558
   db_query("UPDATE vhosts.certs SET subject=:subject, cn=:cn, cert=:cert{$keyop}, valid_from=:valid_from, valid_until=:valid_until, chain=:chain WHERE id=:id", $args);
5bfd0ff2
 }
 
 
1ab9db55
 function delete_cert($id)
 {
   $uid = (int) $_SESSION['userinfo']['uid'];
   $id = (int) $id;
   
44996558
   db_query("DELETE FROM vhosts.certs WHERE uid=? AND id=?", array($uid, $id));
1ab9db55
 }
 
1319a6d9
 function delete_csr($id)
 {
   $uid = (int) $_SESSION['userinfo']['uid'];
   $id = (int) $id;
   
44996558
   db_query("DELETE FROM vhosts.csr WHERE uid=? AND id=?", array($uid, $id));
1319a6d9
 }
 
 
eaac3bfa
 function split_cn($cn)
 {
   $domains = array();
   if (strstr($cn, ',') or strstr($cn, "\n")) {
     $domains = preg_split("/[, \n]+/", $cn);
     DEBUG("Domains:");
     DEBUG($domains);
   } else {
     $domains[] = $cn;
   }
   for ($i=0;$i!=count($domains);$i++) {
     $domains[$i] = filter_input_hostname($domains[$i], true);
   }
   return $domains;
 }
 
1319a6d9
 function create_csr($cn, $bits)
 {
eaac3bfa
   $domains = split_cn($cn);
   $tmp = array();
   foreach ($domains as $dom) {
     $tmp[] = 'DNS:'.$dom;
   }
   $SAN = "[ v3_req ]\nsubjectAltName = ".implode(', ', $tmp);
   DEBUG($SAN);
   $cn = $domains[0];
1319a6d9
   $bits = (int) $bits;
   if ($bits == 0)
     $bits = 4096;
 
   $keyfile = tempnam(ini_get('upload_tmp_dir'), 'key');
   $csrfile = tempnam(ini_get('upload_tmp_dir'), 'csr');
   $config = tempnam(ini_get('upload_tmp_dir'), 'config');
 
   DEBUG("key: ".$keyfile." / csr: ".$csrfile." / config: ".$config);
 
   $c = fopen($config, "w");
   fwrite($c, "[req]
 default_bits = {$bits}
 default_keyfile = {$keyfile}
 encrypt_key = no
 distinguished_name      = req_distinguished_name
eaac3bfa
 req_extensions = v3_req
1319a6d9
 
 [ req_distinguished_name ]
 countryName                     = Country Name (2 letter code)
f0e987a5
 countryName_default             = 
1319a6d9
 stateOrProvinceName             = State or Province Name (full name)
f0e987a5
 stateOrProvinceName_default     = 
1319a6d9
 localityName                    = Locality Name (eg, city)
f0e987a5
 localityName_default            = 
1319a6d9
 0.organizationName              = Organization Name (eg, company)
f0e987a5
 0.organizationName_default      = 
1319a6d9
 
 commonName = Common Name
 commonName_default = {$cn}
eaac3bfa
 {$SAN}
1319a6d9
 ");
   fclose($c);
 
   $output = '';
13a6c656
   $cmdline = "openssl req -sha256 -new -batch -config {$config} -out {$csrfile}";
1319a6d9
   $retval = 0;
   exec($cmdline, $output, $retval);
   DEBUG($output);
   DEBUG($retval);
   if ($retval != 0)
   {
9a7d1594
     system_failure("Die Erzeugung des CSR ist fehlgeschlagen. Ausgabe des OpenSSL-Befehls: ".print_r($output, true));
1319a6d9
   }
   
   $csr = file_get_contents($csrfile);
   $key = file_get_contents($keyfile);
 
   unlink($csrfile);
   unlink($keyfile);
   unlink($config);
 
   return array($csr, $key);
 }
 
 
 
733161de
 function save_csr($cn, $bits, $replace=NULL)
1319a6d9
 {
101aced0
   if (! $cn) {
     system_failure("Sie müssen einen Domainname eingeben!");
   }
eaac3bfa
   $domains = split_cn($cn);
   $cn = $domains[0];
1319a6d9
   $csr = NULL;
   $key = NULL;
eaac3bfa
   list($csr, $key) = create_csr(implode(',',$domains), $bits);
1319a6d9
   
   $uid = (int) $_SESSION['userinfo']['uid'];
44996558
   db_query("INSERT INTO vhosts.csr (uid, hostname, bits, `replace`, csr, `key`) VALUES (:uid, :cn, :bits, :replace, :csr, :key)",
eaac3bfa
            array(":uid" => $uid, ":cn" => $cn, ":bits" => $bits, 
44996558
                  ":replace" => $replace, ":csr" => $csr, ":key" => $key));
   $id = db_insert_id();
1319a6d9
   return $id;  
 }