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

Leonidas Spyropoulos artafinde at gmail.com
Fri Jun 26 13:53:08 UTC 2015


On 26/06/15, Lukas Fleischer wrote:
> 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

Acked-by

-- 


More information about the aur-dev mailing list