#!/usr/bin/env python3.7 # -*- mode:python; coding:utf-8 -*- # # NAME # pkgsign -- tool for use with FreeBSD pkg-repo(8) that utilizes # ssh-agent for private key management when signing # repositories # # LICENSE # Copyright (c) 2011 lars at oddbit dot com # Copyright (c) 2021 anrxc at sysphere dot org # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # SYNOPSIS # /usr/sbin/pkg repo /path/to/repository signing_command: pkgsign [FINGERPRINT] # /usr/sbin/pkg repo /path/to/repository signing_command: ssh signing-server pkgsign [FINGERPRINT] # # FILES # $HOME/.ssh/ssh-agent.info # $HOME/.gnupg/gpg-agent.info # /usr/local/etc/ssl/public/[FINGERPRINT].pub # import paramiko.agent from sys import stdin from sys import stdout from os import environ from binascii import hexlify from sys import argv as sysargv from struct import unpack as strunpack try: # For obtaining fingerprints from an agent _KDUMP = sysargv[1] == "--dump" and 1 # For experimenting with keys and signatures _DEBUG = sysargv[1] == "--debug" and 1 except IndexError: _KDUMP = 0 _DEBUG = 0 try: if _KDUMP < 1: if _DEBUG > 0: _PKGID = stdin.readline().strip() _KEYID = sysargv[2] else: _PKGID = stdin.readline().strip() _KEYID = sysargv[1] except IndexError: raise SystemExit("ERROR: key fingerprint missing from command line, aborting") if "SSH_AUTH_SOCK" not in environ: import re ## gpg-agent untested, at some point in 2013 SSH support was broken ## for months and if such a thing is allowed to happen then best to ## rely on ssh-agent #agent_info = open("%s/.gnupg/gpg-agent.info" % environ["HOME"], "r") agent_info = open("%s/.ssh/ssh-agent.info" % environ["HOME"], "r") agent_format = re.compile("SSH_AUTH_SOCK=(?P[^;]+).*SSH_AGENT_PID=(?P\d+)", re.MULTILINE | re.DOTALL) match = agent_format.search(agent_info.read()) agent_info.close() localenv = match.groupdict() environ["SSH_AGENT_PID"] = localenv["PID"] environ["SSH_AUTH_SOCK"] = localenv["SOCK"] if _DEBUG > 0: print("INFO: SSH_AGENT_PID is %s " % environ["SSH_AGENT_PID"]) print("INFO: SSH_AUTH_SOCK is %s " % environ["SSH_AUTH_SOCK"]) agent = paramiko.agent.Agent() agent_keys = agent.get_keys() if len(agent_keys) == 0: if _DEBUG > 0: print("ERROR: no key(s) found in ssh-agent, aborting") raise SystemExit(1) if _KDUMP > 0: for key in agent_keys: print("INFO: found ssh-agent key %s" % hexlify(key.get_fingerprint()).decode()) raise SystemExit(0) for key in agent_keys: if _DEBUG > 0: print("INFO: found ssh-agent key %s" % hexlify(key.get_fingerprint()).decode()) if _KEYID.encode() == hexlify(key.get_fingerprint()): if _DEBUG > 0: print("INFO: key match found, signing with %s" % _KEYID) # RSA sign flags: 0 (sha1), 2 (sha256), 4 (sha512) # - for widespread use upstream must accept the flags support patch try: raw_sig = key.sign_ssh_data(_PKGID, 2) except TypeError: raise SystemExit("ERROR: agent.py missing flags support, see patch at the bottom") # Strip fields with the algorithm name and length of the signature sig_parts = [] while raw_sig: len = strunpack('>I', raw_sig[:4])[0] bits = raw_sig[4:len+4] sig_parts.append(bits) raw_sig = raw_sig[len+4:] sig = sig_parts[1] # To convert key.get_base64() to pkcs8 would be more code than this # entire thing. To use ssh-keygen instead we need a temporary file as # it can't read it from stdin when performing a conversion. In the end # it is much simpler to just read it from a pregenerated file. pub_key = open("/usr/local/etc/ssl/public/%s.pub" % _KEYID, "r") # Print data in order that pkg-repo(8) is expecting print("SIGNATURE") # - flush to ensure order and prevent pkg-repo(8) segfault stdout.flush() # - write the signature raw bytes that pkg-repo(8) is expecting stdout.buffer.write(sig) stdout.flush() print() stdout.flush() print("CERT") stdout.flush() print(pub_key.read().strip()) stdout.flush() print("END") stdout.flush() pub_key.close() # For validating signatures against/with openssl pkeyutl # - generate: echo -n "[HASH]" | openssl dgst -sign [PRIVATEKEY] -sha256 -binary >signature-cmp if _DEBUG > 0: # - validate: echo -n "[HASH]" | openssl sha256 -binary | \ # openssl pkeyutl -verify -sigfile signature-[HASH] -pubin \ # -inkey [PUBLICKEY] -pkeyopt digest:sha256 open("signature-%s" % _PKGID, "wb").write(sig) raise SystemExit(0) else: if _DEBUG > 0: print("WARN: key mismatch, continuing search...") continue if _DEBUG > 0: raise SystemExit("ERROR: no matching key(s) found for signing, aborting") else: raise SystemExit(1) # Subject: [PATCH] paramiko agent RSA sign flags support # #--- paramiko/agent.py 2021-01-15 23:03:50.387801224 +0100 #+++ paramiko/agent.py 2021-01-15 23:04:34.667800388 +0100 #@@ -407,12 +407,12 @@ # def get_name(self): # return self.name # #- def sign_ssh_data(self, data): #+ def sign_ssh_data(self, data, flags=0): # msg = Message() # msg.add_byte(cSSH2_AGENTC_SIGN_REQUEST) # msg.add_string(self.blob) # msg.add_string(data) #- msg.add_int(0) #+ msg.add_int(flags) # ptype, result = self.agent._send_message(msg) # if ptype != SSH2_AGENT_SIGN_RESPONSE: # raise SSHException("key cannot be used for signing")