1
0
mirror of https://github.com/openSUSE/osc.git synced 2024-12-28 02:36:15 +01:00

Merge pull request #1135 from dmach/ssh-agent-forwarding

Support ssh-agent forwarding
This commit is contained in:
Daniel Mach 2022-09-08 10:32:03 +02:00 committed by GitHub
commit 39c00d55c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -2,9 +2,11 @@ import base64
import fcntl import fcntl
import os import os
import re import re
import shutil
import subprocess import subprocess
import ssl import ssl
import sys import sys
import tempfile
import time import time
import http.client import http.client
@ -509,6 +511,9 @@ class SignatureAuthHandler(AuthHandlerBase):
self.user = user self.user = user
self.sshkey = sshkey self.sshkey = sshkey
self.ssh_keygen_path = shutil.which("ssh-keygen")
self.ssh_add_path = shutil.which("ssh-add")
apiurl = conf.config["apiurl"] apiurl = conf.config["apiurl"]
if conf.config["api_host_options"][apiurl].get("credentials_mgr_class", None) == "osc.credentials.TransientCredentialsManager": if conf.config["api_host_options"][apiurl].get("credentials_mgr_class", None) == "osc.credentials.TransientCredentialsManager":
self.basic_auth_password = False self.basic_auth_password = False
@ -516,39 +521,19 @@ class SignatureAuthHandler(AuthHandlerBase):
# value of `basic_auth_password` is only used as a hint if we should skip signature auth # 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.basic_auth_password = bool(basic_auth_password)
self.temp_pubkey = None
def list_ssh_agent_keys(self): def list_ssh_agent_keys(self):
cmd = ['ssh-add', '-l'] if not self.ssh_add_path:
try:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError:
# ssh-add is not available
return [] return []
cmd = [self.ssh_add_path, '-L']
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, _ = proc.communicate() stdout, _ = proc.communicate()
if proc.returncode == 0 and stdout.strip(): if proc.returncode == 0 and stdout.strip():
return [self.get_fingerprint(line) for line in stdout.splitlines()] return stdout.strip().splitlines()
else: else:
return [] 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 @staticmethod
def get_fingerprint(line): def get_fingerprint(line):
parts = line.strip().split(b" ") parts = line.strip().split(b" ")
@ -556,41 +541,16 @@ class SignatureAuthHandler(AuthHandlerBase):
raise ValueError(f"Unable to retrieve ssh key fingerprint from line: {line}") raise ValueError(f"Unable to retrieve ssh key fingerprint from line: {line}")
return parts[1] 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): 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() keys_in_agent = self.list_ssh_agent_keys()
if keys_in_agent: if keys_in_agent:
keys_in_home_ssh = self.list_ssh_dir_keys() self.temp_pubkey = tempfile.NamedTemporaryFile()
for fingerprint in keys_in_agent: self.temp_pubkey.write(keys_in_agent[0])
if fingerprint in keys_in_home_ssh: self.temp_pubkey.flush()
return keys_in_home_ssh[fingerprint] return self.temp_pubkey.name
sshdir = os.path.expanduser('~/.ssh') sshdir = os.path.expanduser('~/.ssh')
keyfiles = ('id_ed25519', 'id_ed25519_sk', 'id_rsa', 'id_ecdsa', 'id_ecdsa_sk', 'id_dsa') keyfiles = ('id_ed25519', 'id_ed25519_sk', 'id_rsa', 'id_ecdsa', 'id_ecdsa_sk', 'id_dsa')
for keyfile in keyfiles: for keyfile in keyfiles:
@ -611,9 +571,14 @@ class SignatureAuthHandler(AuthHandlerBase):
keyfile = '~/.ssh/' + keyfile keyfile = '~/.ssh/' + keyfile
keyfile = os.path.expanduser(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) proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
stdout, _ = proc.communicate(data) stdout, _ = proc.communicate(data)
if self.temp_pubkey:
self.temp_pubkey.close()
self.temp_pubkey = None
if proc.returncode: if proc.returncode:
raise oscerr.OscIOError(None, 'ssh-keygen signature creation failed: %d' % proc.returncode) raise oscerr.OscIOError(None, 'ssh-keygen signature creation failed: %d' % proc.returncode)
@ -659,6 +624,12 @@ class SignatureAuthHandler(AuthHandlerBase):
# prefer basic auth, but only if password is set # prefer basic auth, but only if password is set
return False 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(): if not self.sshkey_known():
# ssh key not set, try to guess it # ssh key not set, try to guess it
self.sshkey = self.guess_keyfile() self.sshkey = self.guess_keyfile()