From bbb2746657d3395cca2a494a6de83957ec5dbc86 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Tue, 6 Sep 2022 14:00:13 +0200 Subject: [PATCH 1/2] Support ssh-agent forwarding --- osc/connection.py | 71 ++++++++++++----------------------------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/osc/connection.py b/osc/connection.py index f81f3748..bfea0900 100644 --- a/osc/connection.py +++ b/osc/connection.py @@ -5,6 +5,7 @@ import re import subprocess import ssl import sys +import tempfile import time import http.client @@ -516,8 +517,10 @@ class SignatureAuthHandler(AuthHandlerBase): # value of `basic_auth_password` is only used as a hint if we should skip signature auth self.basic_auth_password = bool(basic_auth_password) + self.temp_pubkey = None + def list_ssh_agent_keys(self): - cmd = ['ssh-add', '-l'] + cmd = ['ssh-add', '-L'] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except OSError: @@ -525,30 +528,10 @@ class SignatureAuthHandler(AuthHandlerBase): return [] stdout, _ = proc.communicate() if proc.returncode == 0 and stdout.strip(): - return [self.get_fingerprint(line) for line in stdout.splitlines()] + return stdout.strip().splitlines() else: return [] - def is_ssh_private_keyfile(self, keyfile_path): - if not os.path.isfile(keyfile_path): - return False - with open(keyfile_path) as f: - try: - line = f.readline(100).strip() - except UnicodeDecodeError: - # skip binary files - return False - if line == "-----BEGIN RSA PRIVATE KEY-----": - return True - if line == "-----BEGIN OPENSSH PRIVATE KEY-----": - return True - return False - - def is_ssh_public_keyfile(self, keyfile_path): - if not os.path.isfile(keyfile_path): - return False - return keyfile_path.endswith(".pub") - @staticmethod def get_fingerprint(line): parts = line.strip().split(b" ") @@ -556,41 +539,16 @@ class SignatureAuthHandler(AuthHandlerBase): raise ValueError(f"Unable to retrieve ssh key fingerprint from line: {line}") return parts[1] - def list_ssh_dir_keys(self): - sshdir = os.path.expanduser('~/.ssh') - keys_in_home_ssh = {} - for keyfile in os.listdir(sshdir): - if keyfile.startswith(("agent-", "authorized_keys", "config", "known_hosts")): - # skip files that definitely don't contain keys - continue - - keyfile_path = os.path.join(sshdir, keyfile) - # public key alone may be sufficient because the private key - # can get loaded into ssh-agent from gpg (yubikey works this way) - is_public = self.is_ssh_public_keyfile(keyfile_path) - # skip private detection if we think the key is a public one already - is_private = False if is_public else self.is_ssh_private_keyfile(keyfile_path) - - if not is_public and not is_private: - continue - - cmd = ["ssh-keygen", "-lf", keyfile_path] - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, _ = proc.communicate() - if proc.returncode == 0: - fingerprint = self.get_fingerprint(stdout) - if fingerprint and (fingerprint not in keys_in_home_ssh or is_private): - # prefer path to a private key - keys_in_home_ssh[fingerprint] = keyfile_path - return keys_in_home_ssh - def guess_keyfile(self): + # `ssh-keygen -Y sign` requires a file with a key which is not available during ssh agent forwarding + # that's why we need to list ssh-agent's keys and store the first one into a temp file keys_in_agent = self.list_ssh_agent_keys() if keys_in_agent: - keys_in_home_ssh = self.list_ssh_dir_keys() - for fingerprint in keys_in_agent: - if fingerprint in keys_in_home_ssh: - return keys_in_home_ssh[fingerprint] + self.temp_pubkey = tempfile.NamedTemporaryFile() + self.temp_pubkey.write(keys_in_agent[0]) + self.temp_pubkey.flush() + return self.temp_pubkey.name + sshdir = os.path.expanduser('~/.ssh') keyfiles = ('id_ed25519', 'id_ed25519_sk', 'id_rsa', 'id_ecdsa', 'id_ecdsa_sk', 'id_dsa') for keyfile in keyfiles: @@ -614,6 +572,11 @@ class SignatureAuthHandler(AuthHandlerBase): cmd = ['ssh-keygen', '-Y', 'sign', '-f', keyfile, '-n', namespace, '-q'] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdout, _ = proc.communicate(data) + + if self.temp_pubkey: + self.temp_pubkey.close() + self.temp_pubkey = None + if proc.returncode: raise oscerr.OscIOError(None, 'ssh-keygen signature creation failed: %d' % proc.returncode) From 2496b3e987d15d394f1e17973e7735799071af9d Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 7 Sep 2022 10:09:20 +0200 Subject: [PATCH 2/2] Properly handle missing ssh-keygen and ssh-add --- osc/connection.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/osc/connection.py b/osc/connection.py index bfea0900..59c02f3c 100644 --- a/osc/connection.py +++ b/osc/connection.py @@ -2,6 +2,7 @@ import base64 import fcntl import os import re +import shutil import subprocess import ssl import sys @@ -510,6 +511,9 @@ class SignatureAuthHandler(AuthHandlerBase): self.user = user self.sshkey = sshkey + self.ssh_keygen_path = shutil.which("ssh-keygen") + self.ssh_add_path = shutil.which("ssh-add") + apiurl = conf.config["apiurl"] if conf.config["api_host_options"][apiurl].get("credentials_mgr_class", None) == "osc.credentials.TransientCredentialsManager": self.basic_auth_password = False @@ -520,12 +524,10 @@ class SignatureAuthHandler(AuthHandlerBase): self.temp_pubkey = None def list_ssh_agent_keys(self): - cmd = ['ssh-add', '-L'] - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - except OSError: - # ssh-add is not available + if not self.ssh_add_path: return [] + cmd = [self.ssh_add_path, '-L'] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, _ = proc.communicate() if proc.returncode == 0 and stdout.strip(): return stdout.strip().splitlines() @@ -569,7 +571,7 @@ class SignatureAuthHandler(AuthHandlerBase): keyfile = '~/.ssh/' + keyfile keyfile = os.path.expanduser(keyfile) - cmd = ['ssh-keygen', '-Y', 'sign', '-f', keyfile, '-n', namespace, '-q'] + cmd = [self.ssh_keygen_path, '-Y', 'sign', '-f', keyfile, '-n', namespace, '-q'] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdout, _ = proc.communicate(data) @@ -622,6 +624,12 @@ class SignatureAuthHandler(AuthHandlerBase): # prefer basic auth, but only if password is set return False + if not self.ssh_keygen_path: + if conf.config["debug"]: + msg = "Skipping signature auth because ssh-keygen is not available" + print(msg, file=sys.stderr) + return False + if not self.sshkey_known(): # ssh key not set, try to guess it self.sshkey = self.guess_keyfile()