Commit b036b436 authored by Lukas Fleischer's avatar Lukas Fleischer
Browse files

Add support for multiple SSH public keys



Attaching more than one SSH public key to the same account is useful,
e.g. if one uses different machines to access the AUR SSH interface.
Multiple keys can now be specified by adding multiple lines to the text
area on the account edit form.

Implements FS#45469.
Signed-off-by: Lukas Fleischer's avatarLukas Fleischer <lfleischer@archlinux.org>
Acked-by: Leonidas Spyropoulos's avatarLeonidas Spyropoulos <artafinde@gmail.com>
parent dbe56342
......@@ -47,8 +47,10 @@ db = mysql.connector.connect(host=aur_db_host, user=aur_db_user,
unix_socket=aur_db_socket, buffered=True)
cur = db.cursor()
cur.execute("SELECT Username, AccountTypeID FROM Users WHERE SSHPubKey = %s " +
"AND Suspended = 0", (keytype + " " + keytext,))
cur.execute("SELECT Users.Username, Users.AccountTypeID FROM Users " +
"INNER JOIN SSHPubKeys ON SSHPubKeys.UserID = Users.ID "
"WHERE SSHPubKeys.PubKey = %s AND Users.Suspended = 0",
(keytype + " " + keytext,))
if cur.rowcount != 1:
exit(1)
......
......@@ -33,7 +33,6 @@ CREATE TABLE Users (
LangPreference VARCHAR(5) NOT NULL DEFAULT 'en',
IRCNick VARCHAR(32) NOT NULL DEFAULT '',
PGPKey VARCHAR(40) NULL DEFAULT NULL,
SSHPubKey VARCHAR(4096) NULL DEFAULT NULL,
LastLogin BIGINT UNSIGNED NOT NULL DEFAULT 0,
LastLoginIPAddress INTEGER UNSIGNED NOT NULL DEFAULT 0,
InactivityTS BIGINT UNSIGNED NOT NULL DEFAULT 0,
......@@ -53,6 +52,17 @@ INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd) VALUES (
3, 1, 'user', 'user@localhost', MD5('user'));
-- SSH public keys used for the aurweb SSH/Git interface.
--
CREATE TABLE SSHPubKeys (
UserID INTEGER UNSIGNED NOT NULL,
Fingerprint VARCHAR(44) NOT NULL,
PubKey VARCHAR(4096) NOT NULL,
PRIMARY KEY (Fingerprint),
FOREIGN KEY (UserID) REFERENCES Users(ID) ON DELETE CASCADE
) ENGINE = InnoDB;
-- Track Users logging in/out of AUR web site.
--
CREATE TABLE Sessions (
......
......@@ -3,10 +3,16 @@ want to keep the package contents, please create a backup before starting the
upgrade process and import the source tarballs into the Git repositories
afterwards.
1. Add a field for the SSH public key to the Users table:
1. Add a table to store SSH public keys:
----
ALTER TABLE Users ADD COLUMN SSHPubKey VARCHAR(4096) NULL DEFAULT NULL;
CREATE TABLE SSHPubKeys (
UserID INTEGER UNSIGNED NOT NULL,
Fingerprint VARCHAR(44) NOT NULL,
PubKey VARCHAR(4096) NOT NULL,
PRIMARY KEY (Fingerprint),
FOREIGN KEY (UserID) REFERENCES Users(ID) ON DELETE CASCADE
) ENGINE = InnoDB;
----
2. Create a new user and configure Git/SSH as described in INSTALL.
......
......@@ -16,6 +16,7 @@ $need_userinfo = array(
if (in_array($action, $need_userinfo)) {
$row = account_details(in_request("ID"), in_request("U"));
$PK = implode("\n", account_get_ssh_keys($row["ID"]));
}
if ($action == "AccountInfo") {
......@@ -59,7 +60,7 @@ if (isset($_COOKIE["AURSID"])) {
display_account_form("UpdateAccount", $row["Username"],
$row["AccountTypeID"], $row["Suspended"], $row["Email"],
"", "", $row["RealName"], $row["LangPreference"],
$row["IRCNick"], $row["PGPKey"], $row["SSHPubKey"],
$row["IRCNick"], $row["PGPKey"], $PK,
$row["InactivityTS"] ? 1 : 0, $row["ID"]);
} else {
print __("You do not have permission to edit this account.");
......
......@@ -53,7 +53,7 @@ function html_format_pgp_fingerprint($fingerprint) {
* @param string $L The language preference of the displayed user
* @param string $I The IRC nickname of the displayed user
* @param string $K The PGP key fingerprint of the displayed user
* @param string $PK The SSH public key of the displayed user
* @param string $PK The list of SSH public keys
* @param string $J The inactivity status of the displayed user
* @param string $UID The user ID of the displayed user
*
......@@ -83,7 +83,7 @@ function display_account_form($A,$U="",$T="",$S="",$E="",$P="",$C="",$R="",
* @param string $L The language preference of the user
* @param string $I The IRC nickname of the user
* @param string $K The PGP fingerprint of the user
* @param string $PK The SSH public key of the user
* @param string $PK The list of public SSH keys
* @param string $J The inactivity status of the user
* @param string $UID The user ID of the modified account
*
......@@ -149,12 +149,32 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$P="",$C="",
}
if (!$error && !empty($PK)) {
if (valid_ssh_pubkey($PK)) {
$tokens = explode(" ", $PK);
$PK = $tokens[0] . " " . $tokens[1];
} else {
$error = __("The SSH public key is invalid.");
$ssh_keys = array_filter(array_map('trim', explode("\n", $PK)));
$ssh_fingerprints = array();
foreach ($ssh_keys as &$ssh_key) {
if (!valid_ssh_pubkey($ssh_key)) {
$error = __("The SSH public key is invalid.");
break;
}
$ssh_fingerprint = ssh_key_fingerprint($ssh_key);
if (!$ssh_fingerprint) {
$error = __("The SSH public key is invalid.");
break;
}
$tokens = explode(" ", $ssh_key);
$ssh_key = $tokens[0] . " " . $tokens[1];
$ssh_fingerprints[] = $ssh_fingerprint;
}
/*
* Destroy last reference to prevent accidentally overwriting
* an array element.
*/
unset($ssh_key);
}
if (isset($_COOKIE['AURSID'])) {
......@@ -203,22 +223,24 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$P="",$C="",
"<strong>", htmlspecialchars($E,ENT_QUOTES), "</strong>");
}
}
if (!$error && !empty($PK)) {
if (!$error && count($ssh_keys) > 0) {
/*
* Check whether the SSH public key is available.
* Check whether any of the SSH public keys is already in use.
* TODO: Fix race condition.
*/
$q = "SELECT COUNT(*) FROM Users ";
$q.= "WHERE SSHPubKey = " . $dbh->quote($PK);
$q = "SELECT Fingerprint FROM SSHPubKeys ";
$q.= "WHERE Fingerprint IN (";
$q.= implode(',', array_map(array($dbh, 'quote'), $ssh_fingerprints));
$q.= ")";
if ($TYPE == "edit") {
$q.= " AND ID != " . intval($UID);
$q.= " AND UserID != " . intval($UID);
}
$result = $dbh->query($q);
$row = $result->fetch(PDO::FETCH_NUM);
if ($row[0]) {
if ($row) {
$error = __("The SSH public key, %s%s%s, is already in use.",
"<strong>", htmlspecialchars($PK, ENT_QUOTES), "</strong>");
"<strong>", htmlspecialchars($row[0], ENT_QUOTES), "</strong>");
}
}
......@@ -247,13 +269,11 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$P="",$C="",
$L = $dbh->quote($L);
$I = $dbh->quote($I);
$K = $dbh->quote(str_replace(" ", "", $K));
$PK = empty($PK) ? "NULL" : $dbh->quote($PK);
$q = "INSERT INTO Users (AccountTypeID, Suspended, ";
$q.= "InactivityTS, Username, Email, Passwd, Salt, ";
$q.= "RealName, LangPreference, IRCNick, PGPKey, ";
$q.= "SSHPubKey) ";
$q.= "RealName, LangPreference, IRCNick, PGPKey) ";
$q.= "VALUES (1, 0, 0, $U, $E, $P, $salt, $R, $L, ";
$q.= "$I, $K, $PK)";
$q.= "$I, $K)";
$result = $dbh->exec($q);
if (!$result) {
print __("Error trying to create account, %s%s%s.",
......@@ -261,6 +281,9 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$P="",$C="",
return;
}
$uid = $dbh->lastInsertId();
account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints);
print __("The account, %s%s%s, has been successfully created.",
"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
print "<p>\n";
......@@ -321,10 +344,12 @@ function process_account_form($TYPE,$A,$U="",$T="",$S="",$E="",$P="",$C="",
$q.= ", LangPreference = " . $dbh->quote($L);
$q.= ", IRCNick = " . $dbh->quote($I);
$q.= ", PGPKey = " . $dbh->quote(str_replace(" ", "", $K));
$q.= ", SSHPubKey = " . $dbh->quote($PK);
$q.= ", InactivityTS = " . $inactivity_ts;
$q.= " WHERE ID = ".intval($UID);
$result = $dbh->exec($q);
account_set_ssh_keys($UID, $ssh_keys, $ssh_fingerprints);
if (!$result) {
print __("No changes were made to the account, %s%s%s.",
"<strong>", htmlspecialchars($U,ENT_QUOTES), "</strong>");
......@@ -1194,3 +1219,93 @@ function can_edit_account($acctinfo) {
$uid = $acctinfo['ID'];
return has_credential(CRED_ACCOUNT_EDIT, array($uid));
}
/*
* Compute the fingerprint of an SSH key.
*
* @param string $ssh_key The SSH public key to retrieve the fingerprint for
*
* @return string The SSH key fingerprint
*/
function ssh_key_fingerprint($ssh_key) {
$tmpfile = tempnam(sys_get_temp_dir(), "aurweb");
file_put_contents($tmpfile, $ssh_key);
/*
* The -l option of ssh-keygen can be used to show the fingerprint of
* the specified public key file. Expected output format:
*
* 2048 SHA256:uBBTXmCNjI2CnLfkuz9sG8F+e9/T4C+qQQwLZWIODBY user@host (RSA)
*
* ... where 2048 is the key length, the second token is the actual
* fingerprint, followed by the key comment and the key type.
*/
$cmd = "/usr/bin/ssh-keygen -l -f " . escapeshellarg($tmpfile);
exec($cmd, $out, $ret);
if ($ret !== 0 || count($out) !== 1) {
return false;
}
unlink($tmpfile);
$tokens = explode(' ', $out[0]);
if (count($tokens) != 4) {
return false;
}
$tokens = explode(':', $tokens[1]);
if (count($tokens) != 2 || $tokens[0] != 'SHA256') {
return false;
}
return $tokens[1];
}
/*
* Get the SSH public keys associated with an account.
*
* @param int $uid The user ID of the account to retrieve the keys for.
*
* @return array An array representing the keys
*/
function account_get_ssh_keys($uid) {
$dbh = DB::connect();
$q = "SELECT PubKey FROM SSHPubKeys WHERE UserID = " . intval($uid);
$result = $dbh->query($q);
if ($result) {
return $result->fetchAll(PDO::FETCH_COLUMN, 0);
} else {
return array();
}
}
/*
* Set the SSH public keys associated with an account.
*
* @param int $uid The user ID of the account to assign the keys to.
* @param array $ssh_keys The SSH public keys.
* @param array $ssh_fingerprints The corresponding SSH key fingerprints.
*
* @return bool Boolean flag indicating success or failure.
*/
function account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints) {
$dbh = DB::connect();
$q = sprintf("DELETE FROM SSHPubKeys WHERE UserID = %d", $uid);
$dbh->exec($q);
$ssh_fingerprint = reset($ssh_fingerprints);
foreach ($ssh_keys as $ssh_key) {
$q = sprintf(
"INSERT INTO SSHPubKeys (UserID, Fingerprint, PubKey) " .
"VALUES (%d, %s, %s)", $uid,
$dbh->quote($ssh_fingerprint), $dbh->quote($ssh_key)
);
$dbh->exec($q);
$ssh_fingerprint = next($ssh_fingerprints);
}
return true;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment