<?php
require_once($INC . "/sql.inc.php");
require_once($INC . "/state.inc.php");
require_once($INC . "/prof.inc.php");
require_once($INC . "/mail.inc.php");
require_once($INC . "/events.inc.php");
require_once($INC . "/cache.inc.php");
if (!isset($rg_max_ssh_keys))
$rg_max_ssh_keys = 10;
$rg_keys_error = "";
function rg_keys_set_error($str)
{
global $rg_keys_error;
$rg_keys_error = $str;
rg_log($str);
}
function rg_keys_error()
{
global $rg_keys_error;
return $rg_keys_error;
}
/*
* Events functions
*/
$rg_keys_functions = array(
1000 => "rg_keys_event_new",
1001 => "rg_keys_event_del",
1002 => "rg_keys_event_regen",
1003 => "rg_keys_event_notify_user",
// new style
'rg_keys_event_regen' => 'rg_keys_event_regen'
);
rg_event_register_functions($rg_keys_functions);
/*
* Event for adding a new key
*/
function rg_keys_event_new($db, $event)
{
$ret = array();
$event['op'] = "new";
// mark keys dirty
$ret[] = array_merge($event, array(
'category' => 'rg_keys_event_regen',
'prio' => 10)
);
// notify user
$ret[] = array_merge($event, array("category" => 1003, "prio" => 100));
return $ret;
}
/*
* Event for deleting a key
*/
function rg_keys_event_del($db, $event)
{
$ret = array();
$event['type'] = 1;
$event['op'] = "del";
// mark keys dirty
$ret[] = array_merge($event, array(
'category' => 'rg_keys_event_regen',
'prio' => 10)
);
// notify user
$ret[] = array_merge($event, array("category" => 1003, "prio" => 100));
return $ret;
}
/*
* Regenerate keyring.
* We ignore requests that were inserted in queue after we already
* regenerated the keys.
*/
function rg_keys_event_regen($db, $event)
{
rg_log("keys_event_regen");
$last = rg_cache_get("key::last_regen_time");
if ($last === FALSE)
$last = 0;
if ($event['itime'] < $last) {
rg_log("DEBUG: event itime(" . $event['itime'] . ") < last($last)."
. " Skip regeneration of keys.");
} else {
$r = rg_keys_regen($db);
if ($r === FALSE)
return FALSE;
}
return array();
}
/*
* Notify user that a new key was added to the keyring
*/
function rg_keys_event_notify_user($db, $event)
{
rg_prof_start("keys_event_notify_user");
rg_log("keys_event_notify_user: event=" . rg_array2string($event));
// TODO: del: Maybe add also the statistics.
// TODO: del: Do not forget that here we have a list
// TODO: del: Take care: we already deleted the keys. We cannot inspect
// them anymore! Maybe put info in the event.
$ret = FALSE;
while (1) {
$r = rg_mail_template("mail/user/key/" . $event['op'], $event);
if ($r === FALSE)
break;
$ret = array();
break;
}
rg_prof_end("keys_event_notify_user");
return $ret;
}
/*
* Returns TRUE if the key is too weak by the admin standards
* @ki - output of rg_keys_info()
*/
function rg_keys_weak($db, $ki)
{
$ret = array('ok' => 0, 'weak' => 1);
if (strcmp($ki['type'], 'ssh-rsa') == 0) {
$min = rg_state_get($db, 'ssh_key_min_bits_rsa');
if ($min === FALSE) {
rg_keys_set_error('cannot lookup state');
return $ret;
}
if ($ki['bits'] < $min) {
rg_keys_set_error('RSA key has less than '
. $min . ' bits (' . $ki['bits'] . ')');
$ret['ok'] = 1;
return $ret;
}
} else if (strcmp($ki['type'], 'ssh-dss') == 0) {
$r = rg_state_get($db, 'ssh_key_allow_dsa');
if ($r === FALSE) {
rg_keys_set_error('cannot lookup state');
return $ret;
}
if ($r != 1) {
rg_keys_set_error('DSA keys are not allowed');
$ret['ok'] = 1;
return $ret;
}
} else if (strncmp($ki['type'], 'ecdsa-', 6) == 0) {
$min = rg_state_get($db, 'ssh_key_min_bits_ecdsa');
if ($min === FALSE) {
rg_keys_set_error('cannot lookup state');
return $ret;
}
if ($ki['bits'] < $min) {
rg_keys_set_error('ECDSA key has less than '
. $min . ' bits (' . $ki['bits'] . ')');
$ret['ok'] = 1;
return $ret;
}
}
$ret['ok'] = 1;
$ret['weak'] = 0;
return $ret;
}
/*
* Extracts info about a ssh key
*/
function rg_keys_info($key)
{
rg_prof_start("keys_info");
rg_log_enter('rg_keys_info key=' . $key);
$ret = array();
$ret['ok'] = 0;
while(1) {
if (empty($key)) {
rg_keys_set_error('you did not uploaded the key');
break;
}
if (strpos($key, "PRIVATE KEY") !== FALSE) {
rg_keys_set_error("private instead of public key");
break;
}
// We must have at least key type and the key
$t = explode(' ', $key, 2);
if (!isset($t[1])) {
rg_keys_set_error("malformed ssh key (missing fields)");
break;
}
$ret['type'] = $t[0];
if ((strncmp($ret['type'], 'ssh-', 4) != 0)
&& (strncmp($ret['type'], 'ecdsa-', 6) != 0)) {
rg_log('key: ' . $key);
rg_keys_set_error("key does not start with ssh- or ecdsa-");
break;
}
// We try to detect the key because spaces may mess up things
$ret['comment'] = '';
$error = TRUE;
$off = 0;
while (1) {
rg_log("DEBUG: off=$off");
// -1 signals that we used the whole string
if ($off == -1)
break;
$pos = strpos($t[1], ' ', $off);
if ($pos === FALSE) {
$ret['key'] = $t[1];
$off = -1;
} else {
$ret['key'] = substr($t[1], 0, $pos);
$ret['key'] = str_replace(' ', '', $ret['key']);
$off = $pos + 1;
}
rg_log("DEBUG: pos=$pos off=$off key=" . $ret['key']);
$d = base64_decode($ret['key']);
if ($d === FALSE) {
rg_keys_set_error("malformed ssh key (base64 failed)");
continue;
}
$d_len = strlen($d);
rg_log("d=" . bin2hex($d));
rg_log("d_len=$d_len");
if (strlen($d) < 4) {
rg_keys_set_error("key is too short (< 4)");
continue;
}
// First, we have the length of the string 'ssh-*'
$_t = unpack('N', $d);
$len = $_t[1];
rg_log_ml("len=$len");
if ($d_len < 4 + $len) {
rg_keys_set_error("key is too short");
continue;
}
$type2 = substr($d, 4, $len);
rg_log("DEBUG: type2=$type2");
if (strcasecmp($ret['type'], $type2) != 0) {
rg_keys_set_error('key type mismatch: ' . $ret['type'] . ' != ' . $type2);
break;
}
$bits_div = 1;
$bits_sub = 1;
$fixes = array();
if (strcasecmp($ret['type'], 'ssh-rsa') == 0) {
// OK
$count = 2;
$bits_pos = 1;
} else if (strcasecmp($ret['type'], 'ssh-dss') == 0) {
// Always 1024 - OK
$count = 4;
$bits_pos = 3;
$bits_sub = 0;
} else if (strncasecmp($ret['type'], 'ecdsa-', 6) == 0) {
// Possible: 256, 384, 521 - OK
$count = 2;
$bits_pos = 1;
$bits_div = 2;
$fixes[528] = 521;
} else if (strcasecmp($ret['type'], 'ssh-ed25519') == 0) {
// Always 256 - OK
$count = 1;
$bits_pos = 0;
$bits_sub = 0;
} else {
rg_log('Strange key type: ' . $ret['type']);
// Probably this is a new key type, just consider it valid
$_t = explode(' ', $key, 3);
$ret['key'] = $_t[1];
if (isset($_t[2]))
$ret['comment'] = trim($_t[2]);
$error = FALSE;
break;
}
$have_all_chunks = TRUE;
$used = 4 + $len;
for ($i = 0; $i < $count; $i++) {
if ($d_len < $used + 4) {
rg_keys_set_error('key is too short (chunk length)');
$have_all_chunks = FALSE;
break;
}
$_t = unpack('N', substr($d, $used, 4));
$xlen = $_t[1];
rg_log("xlen=$xlen bits_sub=$bits_sub bits_div=$bits_div");
//rg_log('bin: ' . bin2hex(substr($d, $used + 4, $xlen)));
//rg_log('ascii: ' . substr($d, $used + 4, $xlen));
if ($d_len < $used + 4 + $xlen) {
rg_keys_set_error("key is too short (chunk body)");
$have_all_chunks = FALSE;
break;
}
if ($i == $bits_pos) {
$ret['bits'] = (($xlen - $bits_sub) / $bits_div) * 8;
if (isset($fixes[$ret['bits']]))
$ret['bits'] = $fixes[$ret['bits']];
}
$used += 4 + $xlen;
}
if ($have_all_chunks === FALSE)
continue;
$ret['comment'] = trim(substr($t[1], $off));
$error = FALSE;
break;
}
if ($error)
break;
$digest = md5($d);
$a = array();
for ($i = 0; $i < 16; $i++)
$a[] = substr($digest, $i * 2, 2);
$ret['fingerprint_md5'] = implode(":", $a);
$_x = base64_encode(hash('sha256', $d, TRUE));
$ret['fingerprint_sha256'] = rtrim($_x, "=");
$ret['ok'] = 1;
break;
}
rg_log_exit();
rg_prof_end("keys_info");
return $ret;
}
/*
* Remove keys from database for user 'ui'
*/
function rg_keys_remove($db, $ui, $list)
{
rg_prof_start("keys_remove");
rg_log_enter("keys_remove: list=" . rg_array2string($list));
$ret = FALSE;
while (1) {
$my_list = array();
foreach ($list as $key_id => $junk)
$my_list[] = sprintf("%u", $key_id);
$params = array("uid" => $ui['uid']);
$sql_list = implode(", ", $my_list);
$sql = "DELETE FROM keys"
. " WHERE uid = @@uid@@"
. " AND key_id IN (" . $sql_list . ")";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error("cannot delete keys"
. " (" . rg_sql_error() . ")");
break;
}
rg_sql_free_result($res);
$event = array(
'category' => 1001,
'prio' => 50,
'ui' => $ui,
'keys' => implode(',', $my_list));
$r = rg_event_add($db, $event);
if ($r !== TRUE) {
rg_keys_set_error("cannot add event"
. " (" . rg_event_error() . ")");
break;
}
$key = 'user' . '::' . $ui['uid'] . '::' . 'keys';
foreach ($my_list as $_key_id)
rg_cache_unset($key . '::' . $_key_id,
RG_SOCKET_NO_WAIT);
rg_event_signal_daemon('', 0);
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end("keys_remove");
return $ret;
}
/*
* Count the number of keys per user
*/
function rg_keys_count($db, $uid)
{
rg_prof_start("keys_count");
$ret = FALSE;
while (1) {
$params = array("uid" => $uid);
$sql = "SELECT COUNT(*) AS count FROM keys"
. " WHERE uid = @@uid@@";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error("cannot query (" . rg_sql_error() . ")");
break;
}
$row = rg_sql_fetch_array($res);
rg_sql_free_result($res);
$ret = $row['count'];
break;
}
rg_prof_end("keys_count");
return $ret;
}
/*
* Returns the maximum number of keys allowed per user
*/
function rg_keys_max($db)
{
global $rg_max_ssh_keys;
$r = rg_state_get($db, 'max_ssh_keys');
if (($r === FALSE) || empty($r))
return $rg_max_ssh_keys;
return $r;
}
/*
* Adds a key
* Returns the key_id of the key.
*/
function rg_keys_add($db, $ui, $key)
{
rg_prof_start("keys_add");
rg_log_enter("keys_add: key=$key");
$ret = FALSE;
$do_rollback = 0;
while (1) {
$itime = time();
$ki = rg_keys_info($key);
if ($ki['ok'] != 1)
break;
$r = rg_keys_weak($db, $ki);
if ($r['ok'] != 1)
break;
if ($r['weak'] != 0)
break;
// Check if we are over the maximum
// the config after update may not have this defined.
$no_of_keys = rg_keys_count($db, $ui['uid']);
if ($no_of_keys === FALSE)
break;
if ($no_of_keys >= rg_keys_max($db)) {
rg_keys_set_error("too many keys"
. " (" . $no_of_keys . "); please delete some");
break;
}
$r = rg_sql_begin($db);
if ($r !== TRUE) {
rg_keys_set_error("cannot start transaction"
. " (" . rg_sql_error() . ")");
break;
}
$do_rollback = 1;
$params = array(
'itime' => $itime,
'uid' => $ui['uid'],
'key' => $ki['type'] . ' ' . $ki['key']
. ' ' . $ki['comment'],
'count' => 0,
'first_use' => 0,
'fingerprint_sha256' => $ki['fingerprint_sha256']);
$sql = "INSERT INTO keys (itime, uid, key"
. ", fingerprint_sha256)"
. " VALUES (@@itime@@, @@uid@@, @@key@@"
. ", @@fingerprint_sha256@@)"
. " RETURNING key_id";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error("cannot insert key"
. " (" . rg_sql_error() . ")");
break;
}
$row = rg_sql_fetch_array($res);
$key_id = $row['key_id'];
rg_sql_free_result($res);
$event = array(
'category' => 1000,
'prio' => 50,
'ui' => $ui,
'ki' => $ki,
'key_id' => $key_id);
$r = rg_event_add($db, $event);
if ($r !== TRUE) {
rg_keys_set_error("cannot add event"
. " (" . rg_event_error() . ")");
break;
}
$r = rg_sql_commit($db);
if ($r !== TRUE) {
rg_keys_set_error("cannot commit transaction"
. " (" . rg_sql_error() . ")");
break;
}
$do_rollback = 0;
$_key = 'user' . '::' . $ui['uid'] . '::'
. 'keys' . '::' . $key_id;
rg_cache_merge($_key, $params, RG_SOCKET_NO_WAIT);
rg_event_signal_daemon('', 0);
$ret = $key_id;
break;
}
if ($do_rollback == 1)
rg_sql_rollback($db);
rg_log_exit();
rg_prof_end("keys_add");
return $ret;
}
/*
* Update first_use, last_use, last_ip and count
*/
function rg_keys_update_use($db, $uid, $key_id, $ip, $cmd)
{
// The build system will not update table 'keys'
if ($key_id == 0)
return TRUE;
rg_prof_start("keys_update_use");
rg_log_enter("keys_update_use: uid=$uid key_id=$key_id"
. ", ip=$ip, cmd=$cmd");
$ret = FALSE;
while (1) {
$now = time();
$update_first_use = TRUE;
$update_last_use = TRUE;
$key = 'user' . '::' . $uid . '::' . 'keys' . '::' . $key_id;
$c = rg_cache_get($key);
if ($c !== FALSE) {
if (isset($c['first_use']) && ($c['first_use'] > 0))
$update_first_use = FALSE;
// We will not update the field if is too soon
if (isset($c['last_use'])
&& (strcmp($ip, $c['last_ip']) == 0)
&& (strcmp($cmd, $c['last_cmd']) == 0)
&& ($now - $c['last_use'] < 60))
$update_last_use = FALSE;
}
$params = array(
'now' => $now,
'key_id' => $key_id,
'ip' => $ip,
'last_cmd' => $cmd);
if ($update_first_use) {
$sql = "UPDATE keys SET first_use = @@now@@"
. " WHERE first_use = 0"
. " AND key_id = @@key_id@@";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error("cannot update key's first use");
break;
}
rg_sql_free_result($res);
rg_cache_set($key . '::' . 'first_use', $now,
RG_SOCKET_NO_WAIT);
}
if ($update_last_use) {
$sql = "UPDATE keys SET last_use = @@now@@"
. ", last_ip = @@ip@@"
. ", last_cmd = @@last_cmd@@"
. ", count = count + 1"
. " WHERE key_id = @@key_id@@";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error("cannot update key"
. " (" . rg_sql_error() . ")");
break;
}
rg_sql_free_result($res);
$a = array(
'last_use' => $now,
'last_ip' => $ip,
'last_cmd' => $cmd);
rg_cache_merge($key, $a, RG_SOCKET_NO_WAIT);
}
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end("keys_update_use");
return $ret;
}
/*
* Outputs a line for authorized_keys file
*/
function rg_keys_output_line($i)
{
global $rg_scripts;
global $rg_ssh_paras;
return 'command="'
. $rg_scripts . '/scripts/remote.sh'
. ' ' . $i['uid']
. ' ' . $i['key_id']
. ' ' . $i['flags']
. '"'
. ',' . $rg_ssh_paras
. ' ' . trim($i['key']) . "\n";
}
/*
* Regenerates authorized_keys files
*/
function rg_keys_regen($db)
{
global $php_errormsg;
global $rg_keys_file;
global $rg_scripts;
rg_prof_start("keys_regen");
$now = time();
$ret = FALSE;
while (1) {
// create .ssh folder if does not exists
$dir = dirname($rg_keys_file);
if (!file_exists($dir)) {
if (!@mkdir($dir, 0700, TRUE)) {
rg_keys_set_error("cannot create dir [$dir] ($php_errormsg)");
break;
}
chown($dir, "rocketgit");
chgrp($dir, "rocketgit");
}
$akp = rg_state_get($db, 'AuthorizedKeysCommand');
if ($akp === FALSE) {
rg_keys_set_error('cannot get state of AuthorizedKeysCommand');
break;
}
if ($akp == 1) {
if (file_exists($rg_keys_file))
unlink($rg_keys_file);
$ret = TRUE;
break;
}
$tmp = $rg_keys_file . ".tmp";
$f = @fopen($tmp, "w");
if ($f === FALSE) {
rg_keys_set_error("cannot open file $tmp ($php_errormsg)");
break;
}
if (chmod($tmp, 0600) === FALSE) {
rg_keys_set_error("cannot chmod tmp file ($php_errmsg)");
fclose($f);
break;
}
chown($tmp, "rocketgit");
chgrp($tmp, "rocketgit");
$list = array();
$sql = "SELECT key_id, uid, key FROM keys ORDER BY count DESC";
$res = rg_sql_query($db, $sql);
if ($res === FALSE) {
rg_keys_set_error('cannot query keys table');
break;
}
while (($row = rg_sql_fetch_array($res))) {
$row['flags'] = 'N';
$list[] = $row;
}
rg_sql_free_result($res);
$sql = 'SELECT id, who, ssh_key FROM workers';
$res = rg_sql_query($db, $sql);
if ($res === FALSE) {
rg_keys_set_error('cannot query workers table');
break;
}
while (($row = rg_sql_fetch_array($res))) {
$a = array(
'key_id' => $row['id'],
'uid' => $row['who'],
'key' => $row['ssh_key'],
'flags' => 'W'
);
$list[] = $a;
}
rg_sql_free_result($res);
$errors = 0;
foreach ($list as $row) {
// Ignore invalid keys
$ki = rg_keys_info($row['key']);
if ($ki['ok'] != 1)
continue;
// Ignore weak keys
$r = rg_keys_weak($db, $ki);
if ($r['ok'] != 1)
continue;
if ($r['weak'] != 0)
continue;
//rg_log("Writing key [" . $row['key'] . "] for uid " . $row['uid']);
$buf = rg_keys_output_line($row);
if (@fwrite($f, $buf) === FALSE) {
rg_keys_set_error("cannot write; disk space problems? ($php_errormsg)");
$errors = 1;
break;
}
}
fclose($f);
if ($errors == 1) {
unlink($tmp);
break;
}
if (@rename($tmp, $rg_keys_file) === FALSE) {
rg_keys_set_error("cannot rename $tmp to $rg_keys_file ($php_errormsg)");
unlink($tmp);
break;
}
rg_cache_set("key::last_regen_time", $now, RG_SOCKET_NO_WAIT);
$ret = TRUE;
break;
}
rg_prof_end("keys_regen");
return $ret;
}
/*
* List keys
*/
function rg_keys_list($db, $ui)
{
rg_prof_start("keys_list");
rg_log_enter("keys_list: uid=" . $ui['uid']);
$ret = FALSE;
while (1) {
$params = array("uid" => $ui['uid']);
$sql = "SELECT * FROM keys WHERE uid = @@uid@@"
. " ORDER BY itime DESC";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error('cannot select from db');
break;
}
$ret = array();
while (($row = rg_sql_fetch_array($res))) {
$ki = rg_keys_info($row['key']);
if ($ki['ok'] != 1) {
rg_internal_error("Invalid key in db!");
continue;
}
$r = rg_keys_weak($db, $ki);
if ($r['ok'] != 1)
continue;
$ki['weak'] = $r['weak'];
$t = $ki;
$t['key_id'] = $row['key_id'];
if ($row['itime'] == 0)
$t['itime'] = "N/A";
else
$t['itime'] = gmdate("Y-m-d H:i", $row['itime']);
if ($row['first_use'] == 0)
$t['first_use'] = "N/A";
else
$t['first_use'] = gmdate("Y-m-d H:i", $row['first_use']);
if (empty($row['last_ip']))
$t['last_ip'] = "N/A";
else
$t['last_ip'] = $row['last_ip'];
if ($row['last_use'] == 0)
$t['last_use'] = "N/A";
else
$t['last_use'] = gmdate("Y-m-d H:i", $row['last_use']);
if (empty($row['last_cmd']))
$t['last_cmd'] = "N/A";
else
$t['last_cmd'] = $row['last_cmd'];
$t['count'] = $row['count'];
$ret[] = $t;
}
rg_sql_free_result($res);
break;
}
rg_log_exit();
rg_prof_end("keys_list");
return $ret;
}
/*
* Search a key by fingerprint
* Used for OpenSSH (rg_authorize script)
*/
function rg_keys_search_by_fingerprint($db, $fp)
{
rg_prof_start('keys_search_by_fingerprint');
$ret = array('ok' => 0, 'list' => array());
while (1) {
$params = array('fp' => $fp);
$sql = 'SELECT key_id, uid, key FROM keys'
. ' WHERE fingerprint_sha256 = @@fp@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error('cannot select from keys table');
break;
}
while (($row = rg_sql_fetch_array($res))) {
$row['flags'] = 'N';
$ret['list'][] = $row;
}
rg_sql_free_result($res);
$sql = 'SELECT id, who, ssh_key FROM workers'
. ' WHERE fingerprint_sha256 = @@fp@@';
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_keys_set_error('cannot select from workers table');
break;
}
while (($row = rg_sql_fetch_array($res))) {
$row2 = array(
'key_id' => $row['id'],
'uid' => $row['who'],
'key' => $row['ssh_key'],
'flags' => 'W'
);
$ret['list'][] = $row2;
}
rg_sql_free_result($res);
$ret['ok'] = 1;
break;
}
rg_prof_end('keys_search_by_fingerprint');
return $ret;
}
?>