[aur-dev] [PATCH v2] Add support for multiple SSH public keys

Lukas Fleischer lfleischer at archlinux.org
Fri Jun 26 13:22:18 UTC 2015


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 <lfleischer at archlinux.org>
---
Changes since v1:

* Remove whitespace from SSH public keys before processing.
* Make sure SSH key fingerprints are unique.
* Only use one SQL query when checking for duplicates.

 git-interface/git-auth.py |   6 +-
 schema/aur-schema.sql     |  12 +++-
 upgrading/4.0.0.txt       |  10 ++-
 web/html/account.php      |   3 +-
 web/lib/acctfuncs.inc.php | 153 ++++++++++++++++++++++++++++++++++++++++------
 5 files changed, 159 insertions(+), 25 deletions(-)

diff --git a/git-interface/git-auth.py b/git-interface/git-auth.py
index b67d9de..c7de777 100755
--- a/git-interface/git-auth.py
+++ b/git-interface/git-auth.py
@@ -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)
diff --git a/schema/aur-schema.sql b/schema/aur-schema.sql
index 5a2e5c5..594a804 100644
--- a/schema/aur-schema.sql
+++ b/schema/aur-schema.sql
@@ -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 at 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 (
diff --git a/upgrading/4.0.0.txt b/upgrading/4.0.0.txt
index 637c4b9..74e167b 100644
--- a/upgrading/4.0.0.txt
+++ b/upgrading/4.0.0.txt
@@ -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.
diff --git a/web/html/account.php b/web/html/account.php
index 0bb145c..c447de3 100644
--- a/web/html/account.php
+++ b/web/html/account.php
@@ -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.");
diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php
index 6b7d227..417ee6d 100644
--- a/web/lib/acctfuncs.inc.php
+++ b/web/lib/acctfuncs.inc.php
@@ -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 at 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;
+}
-- 
2.4.4


More information about the aur-dev mailing list