[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