<?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', // new new style 'keys_event_new' => 'rg_keys_event_new', 'keys_event_del' => 'rg_keys_event_del', 'keys_event_regen' => 'rg_keys_event_regen', 'keys_event_notify_user' => 'rg_keys_event_notify_user' ); 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' => 'keys_event_notify_user', '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' => 'keys_event_notify_user', '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_uint($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_uint($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_uint($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) { $key = trim($key); $key = str_replace("\n", ' ', $key); 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)); $ret['key'] = trim($ret['key']); $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' => 'keys_event_del', '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 intval($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' => 'keys_event_new', '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 . ' ' . $i['key'] . "\n"; } /* * Regenerates authorized_keys files */ function rg_keys_regen($db) { global $rg_keys_file; global $rg_scripts; rg_prof_start("keys_regen"); $now = time(); $ret = FALSE; while (1) { $akp = rg_state_get($db, 'AuthorizedKeysCommand'); if ($akp === FALSE) { rg_keys_set_error('cannot get state of AuthorizedKeysCommand'); break; } $akp = intval($akp); if ($akp == 1) { if (file_exists($rg_keys_file)) unlink($rg_keys_file); $ret = TRUE; break; } // 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] (" . rg_php_err() . ")"); break; } chown($dir, "rocketgit"); chgrp($dir, "rocketgit"); } $tmp = $rg_keys_file . ".tmp"; $f = @fopen($tmp, "w"); if ($f === FALSE) { rg_keys_set_error("cannot open file $tmp (" . rg_php_err() . ")"); break; } if (chmod($tmp, 0600) === FALSE) { rg_keys_set_error("cannot chmod tmp file (" . rg_php_err() . ")"); 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? (" . rg_php_err() . ")"); $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 (" . rg_php_err() . ")"); 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; } ?>