def import_clean_key(self, key): """Import the clean key we exported in export_clean_key() to our temp keyring.""" # Import the export of our public key and the given public key for x in [self.signer, key]: PiusUtil.debug("importing %s" % x) import_opts = ["import-minimal"] if x == self.signer: import_opts.append("keep-ownertrust") path = self._tmpfile_path("%s.asc" % x) cmd = ([self.gpg] + GPG_BASE_OPTS + GPG_QUIET_OPTS + [ "--keyring", self.tmp_keyring, "--import-options", ",".join(import_opts), "--import", path, ]) try: self._run_and_check_status(cmd) except GpgUnknownError as e: if x == self.signer: print("\n\nERROR: Didn't find the signing key on keyring") sys.exit(1) raise e
def export_clean_key(self, key): """Export clean key from the users' KeyID.""" # Export our public key and the given public key for x in [self.signer, key]: PiusUtil.debug("exporting %s" % x) path = self._tmpfile_path("%s.asc" % x) self._export_key(self.keyring, [x], path)
def _check_gpg2(self): cmd = [self.gpg, "--version"] PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=self.null, stdout=subprocess.PIPE, stderr=self.null, text=True, ) v = None for line in gpg.stdout: # On Linux this looks like: # gpg (GnuPG) 2.1.8 # On Mac this looks like: # gpg (GnuPG/MacGPG2) 2.0.28 m = re.match(r"^gpg \(GnuPG.*\) ([0-9\.]+)$", line) if m: v = m.group(1) if not v: print("ERROR: Could not determine gpg version\n") sys.exit(1) if not v.startswith("2."): print("ERROR: PIUS requires GnuPG 2.x or later\n") sys.exit(1)
def gpg_wait_for_string(self, fd, string): """Look for a specific string on the status-fd.""" line = "" while line not in (string, ): PiusUtil.debug("Waiting for line %s" % string) raw_line = fd.readline() if not raw_line: raise GpgUnknownError("gpg output unexpectedly ended") line = raw_line.strip() PiusUtil.debug("got line %s" % line)
def write_file(self, data): """Separated out for easier unittesting""" if not os.path.exists(PiusUtil.statedir()): os.mkdir(PiusUtil.statedir(), 0o750) if not os.path.isdir(PiusUtil.statedir()): print("WARNING: There is a %s which is not a directory." " Not storing state." % PiusUtil.statedir()) return if os.path.exists(self.signed_keys_db): shutil.copy(self.signed_keys_db, self.signed_keys_db + ".save") with open(self.signed_keys_db, "w") as fp: fp.write(json.dumps(data))
def check_fingerprint(self, key): """Prompt the user to see if they have verified this fingerprint.""" cmd = ([self.gpg] + GPG_BASE_OPTS + GPG_QUIET_OPTS + ["--keyring", self.keyring, "--fingerprint", key]) PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=self.null, stdout=subprocess.PIPE, stderr=self.null, close_fds=True, text=True, ) output = gpg.stdout.read() output = output.strip() retval = gpg.wait() if retval != 0: print("WARNING: Keyid %s not valid, skipping." % key) return False print(output) while True: ans = input( "\nHave you verified this user/key, and if so, what level" " do you want to sign at?\n 0-3, Show again, Next, Help," " or Quit? [0|1|2|3|s|n|h|q] (default: n) ") print() if ans == "y": print( "'Yes' is no longer a valid answer, please specify a level " "to sign at.") elif ans in ("n", "N", ""): return False elif ans in ("s", "S"): print(output) elif ans in ("0", "1", "2", "3"): return ans elif ans in ("?", "h", "H"): self._print_cert_levels() elif ans in ("q", "Q"): print("Dying at user request") sys.exit(1)
def _run_and_check_status(self, cmd, shell=False): """Helper function for running a gpg call that requires no input but that we want to make sure succeeded.""" PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=self.null, stderr=self.null, shell=shell, close_fds=(not shell), ) retval = gpg.wait() if retval != 0: # We don't catch this, but that's fine, if this errors, a stack # trace is what we want raise GpgUnknownError( "'%s' exited with %d" % (" ".join(cmd) if isinstance(cmd, list) else cmd, retval))
def load_signed_keys(self): PiusUtil.handle_path_migration( self.signed_keys_db, [ os.path.join(x, self.kSIGNED_KEYS_DB_NAME) for x in PiusUtil.previous_statedirs() ], ) if not os.path.exists(self.signed_keys_db): return dict() with open(self.signed_keys_db, "r") as fp: data = fp.read() # We have had multiple versions of the state file. # # v1 was just one key per line, each key was "signed" # # v2 was a hash taking the form: # key: (SIGNED | WILL_NOTSIGN | NOT_SIGNED) # # v3 was the first one with a version identifier in it. It includes # a `meta` entry for the version and any other future metadata. # The data itself takes the format of: # key: { # 'OUTBOUND': (SIGNED | WILL_NOTSIGN | NOT_SIGNED | None), # 'OUTBOUND': (None | Ignore), # } try: signstate = json.loads(data) if "_meta" in signstate and signstate["_meta"]["version"] == 3: PiusUtil.debug("Loading v3 PIUS statefile") del (signstate["_meta"]) elif "_meta" not in signstate: PiusUtil.debug("Loading v2 PIUS statefile") signstate = self.convert_from_v2(signstate) except ValueError: PiusUtil.debug("Loading v1 PIUS statefile") signstate = self.convert_from_v1(data) return signstate
def get_all_keyids(self): """Given a keyring, get all the KeyIDs from it.""" PiusUtil.debug("extracting all keyids from keyring") cmd = ([self.gpg] + GPG_BASE_OPTS + [ "--keyring", self.keyring, "--no-options", "--with-colons", "--fingerprint", "--fixed-list-mode", ]) PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=self.null, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) # We use 'pub' instead of 'fpr' to support old crufty keys too... pub_re = re.compile("^pub:") uid_re = re.compile("^uid:") key_tuples = [] name = keyid = None for line in gpg.stdout: if pub_re.match(line): lineparts = line.split(":") keyid = lineparts[4] elif keyid and uid_re.match(line): lineparts = line.split(":") name = lineparts[9] PiusUtil.debug("Got id %s for %s" % (keyid, name)) key_tuples.append((name, keyid)) name = keyid = None # sort the list if self.sort_keyring: keyids = [i[1] for i in sorted(key_tuples)] else: keyids = [i[1] for i in key_tuples] return keyids
def encrypt_and_sign_file(self, infile, outfile, keyid): """Encrypt and sign a file. Used for PGP/Mime email generation.""" cmd = ([self.gpg] + GPG_BASE_OPTS + GPG_QUIET_OPTS + GPG_FD_OPTS + [ "--keyring", self.tmp_keyring, "--no-options", "--always-trust", "-u", self.force_signer, "-aes", "-r", keyid, "-r", self.signer, "--output", outfile, infile, ]) PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self.null, close_fds=True, text=True, ) # For some reason when using agent an initial enter is needed gpg.stdin.write("\n") skippable = [ PiusSigner.GPG_USERID, PiusSigner.GPG_NEED_PASS, PiusSigner.GPG_GOOD_PASS, PiusSigner.GPG_SIG_BEG, PiusSigner.GPG_SIG_CREATED, PiusSigner.GPG_PROGRESS, PiusSigner.GPG_PINENTRY_LAUNCHED, PiusSigner.GPG_WARN_VERSION, ] while True: PiusUtil.debug("Waiting for response") line = gpg.stdout.readline().strip() PiusUtil.debug("Got %s" % line) if PiusSigner.GPG_ENC_BEG in line: PiusUtil.debug("Got GPG_ENC_BEG") continue elif PiusSigner.GPG_ENC_END in line: PiusUtil.debug("Got GPG_ENC_END") break elif PiusSigner.GPG_ENC_INV in line: PiusUtil.debug("Got GPG_ENC_INV") raise EncryptionKeyError elif PiusSigner.GPG_KEY_CONSIDERED in line: PiusUtil.debug("Got KEY_CONSIDERED") continue elif (PiusSigner.GPG_KEY_EXP in line or PiusSigner.GPG_SIG_EXP in line): # These just mean we passed a given key/sig that's expired, # there may be ones left that are good. We cannot report an # error until we get a ENC_INV. PiusUtil.debug("Got GPG_KEY/SIG_EXP") continue elif any([s in line for s in skippable]): PiusUtil.debug("Got skippable stuff") continue else: raise EncryptionUnknownError(line) retval = gpg.wait() if retval != 0: raise EncryptionUnknownError("Return code was %s" % retval)
def sign_uid(self, key, index, level): """Sign a single UID of a key.""" cmd = ( [self.gpg] + GPG_BASE_OPTS + GPG_QUIET_OPTS + GPG_FD_OPTS + ["--keyring", self.tmp_keyring, "-u", self.force_signer] + self.policy_opts() + [ "--default-cert-level", level, "--no-ask-cert-level", "--edit-key", key, ]) # NB: keep the `--edit-key <key>` at the very end of this list! PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self.null, close_fds=True, text=True, bufsize=1, ) PiusUtil.debug("Waiting for prompt") self.gpg_wait_for_string(gpg.stdout, PiusSigner.GPG_PROMPT) PiusUtil.debug("Selecting UID %d" % index) gpg.stdin.write("%s\n" % str(index)) PiusUtil.debug("Waiting for ack") self.gpg_wait_for_string(gpg.stdout, PiusSigner.GPG_ACK) PiusUtil.debug("Running sign subcommand") self.gpg_wait_for_string(gpg.stdout, PiusSigner.GPG_PROMPT) PiusUtil.debug("Sending sign command") gpg.stdin.write("sign\n") self.gpg_wait_for_string(gpg.stdout, PiusSigner.GPG_ACK) while True: PiusUtil.debug("Waiting for response") line = gpg.stdout.readline() PiusUtil.debug("Got %s" % line) if PiusSigner.GPG_ALREADY_SIGNED in line: print(" UID already signed") gpg.stdin.write("quit\n") return False elif PiusSigner.GPG_KEY_CONSIDERED in line: PiusUtil.debug("Got KEY_CONSIDERED") continue elif (PiusSigner.GPG_KEY_EXP in line or PiusSigner.GPG_SIG_EXP in line): # The user has an expired signing or encryption key, keep going PiusUtil.debug("Got GPG_KEY/SIG_EXP") continue elif PiusSigner.GPG_PROMPT in line: # Unfortunately PGP doesn't give us anything parsable in this # case. It just gives us another prompt. We give the most likely # problem. Best we can do. print( " ERROR: GnuPG won't let us sign, this probably means it" " can't find a secret key, which most likely means that the" " keyring you are using doesn't have _your_ _public_ key on" " it.") gpg.stdin.write("quit\n") raise NoSelfKeyError elif PiusSigner.GPG_CONFIRM in line: # This is what we want break else: print(" ERROR: GnuPG reported an unknown error") gpg.stdin.write("quit\n") # Don't raise an exception, it's not probably just this UID... return False PiusUtil.debug("Confirming signing") gpg.stdin.write("Y\n") self.gpg_wait_for_string(gpg.stdout, PiusSigner.GPG_ACK) # # gpg-agent doesn't always work as well as we like. Of the problems: # * It can't always pop up an X window reliably (pinentry problems) # * It doesn't seem able to figure out the best pinetry program # to use in many situations # * Sometimes it silently fails in odd ways # # So this chunk of code will follow gpg through as many tries as # gpg-agent is willing to give and then inform the user of an error and # raise an exception. # # Since we're here, we also handle the highly unlikely case where the # verified cached passphrase doesn't work. # while True: line = gpg.stdout.readline() PiusUtil.debug("Got %s" % line) # gpg1 + gpgagent1 reported BAD_PASSPHRASE for both the agent the # wrong passphrase, and for canceling the prompt. # # gpg2.0 + gpgagent2.0 seems to do MISSING_PASSPHRASE and # BAD_PASSPHRASE for the respective answers # # gpg2.1 + gpgagent2.1 seems to just do ERROR if "ERROR" in line: print(" ERROR: Agent reported an error.") raise AgentError if "MISSING_PASSPHRASE" in line: print(" ERROR: Agent didn't provide passphrase to PGP.") raise AgentError if "BAD_PASSPHRASE" in line: line = gpg.stdout.readline() PiusUtil.debug("Got %s" % line) if "USERID_HINT" in line: continue print(" ERROR: Agent reported the passphrase was incorrect.") raise AgentError if "GOOD_PASSPHRASE" in line: break if PiusSigner.GPG_PROMPT in line: break PiusUtil.debug("Saving key") gpg.stdin.write("save\n") gpg.wait() return True
def cleanup(self): """Cleanup all our temp files.""" PiusUtil.clean_files([self.tmp_keyring, ("%s~" % self.tmp_keyring)])
def clean_clean_key(self, key): """Delete the "clean" unsigned key which we exported temporarily.""" # Remove the temporary exports of the public keys paths = [self._tmpfile_path("%s.asc" % x) for x in [self.signer, key]] PiusUtil.clean_files(paths)
def export_signed_uid(self, key, filename): """Export the signed UID form working keyring.""" PiusUtil.debug("exporting %s" % key) self._export_key(self.tmp_keyring, [key], filename)
def __init__(self): self.signed_keys_db = os.path.join(PiusUtil.statedir(), self.kSIGNED_KEYS_DB_NAME) self.state = {} self._load() self.modified = False
def encrypt_signed_uid(self, key, filename): """Encrypt the file we exported the signed UID to.""" (base, ext) = os.path.splitext(os.path.basename(filename)) enc_file = "%s_ENCRYPTED%s" % (base, ext) enc_path = self._outfile_path(enc_file) if os.path.exists(enc_path): os.unlink(enc_path) cmd = ([self.gpg] + GPG_BASE_OPTS + GPG_QUIET_OPTS + GPG_FD_OPTS + [ "--keyring", self.tmp_keyring, "--always-trust", "--armor", "-r", key, "--output", enc_path, "-e", filename, ]) PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=self.null, close_fds=True, ) # Must send a blank line... gpg.stdin.write("\n") while True: PiusUtil.debug("Waiting for response") line = gpg.stdout.readline().strip() PiusUtil.debug("Got %s" % line) if PiusSigner.GPG_ENC_BEG in line: PiusUtil.debug("Got GPG_ENC_BEG") continue elif PiusSigner.GPG_ENC_COMPLIANT_MODE in line: PiusUtil.debug("Got ENCRYPTION_COMPLIANCE_MODE") continue elif PiusSigner.GPG_ENC_END in line: PiusUtil.debug("Got GPG_ENC_END") break elif PiusSigner.GPG_ENC_INV in line: PiusUtil.debug("Got GPG_ENC_INV") raise EncryptionKeyError elif (PiusSigner.GPG_KEY_EXP in line or PiusSigner.GPG_SIG_EXP in line): # These just mean we passed a given key/sig that's expired, # there may be ones left that are good. We cannot report an # error until we get a ENC_INV. PiusUtil.debug("Got GPG_KEY_EXP") continue elif PiusSigner.GPG_KEY_CONSIDERED in line: PiusUtil.debug("Got KEY_CONSIDERED") continue elif PiusSigner.GPG_PROGRESS in line: PiusUtil.debug("Got skippable stuff") continue else: raise EncryptionUnknownError(line) gpg.wait() return enc_file
def get_uids(self, key): """Get all UIDs on a given key.""" cmd = ([self.gpg] + GPG_BASE_OPTS + GPG_QUIET_OPTS + GPG_FD_OPTS + [ "--keyring", self.keyring, "--no-options", "--with-colons", "--edit-key", key, ]) PiusUtil.logcmd(cmd) gpg = subprocess.Popen( cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, bufsize=1, text=True, ) gpg.stdin.write("\n") uids = [] unique_files = [] while True: line = gpg.stdout.readline().strip() # skip the things we don't care about... if not line: PiusUtil.debug("breaking, EOF") break if line == PiusSigner.GPG_PROMPT: PiusUtil.debug("got to command prompt") break # Parse the line... PiusUtil.debug("Got a line %s" % line) fields = line.split(":") if fields[0] != "uid": continue status = fields[1] uid = fields[9] index = int(fields[13].split(",")[0]) PiusUtil.debug("Got UID %s with status %s" % (uid, status)) # If we can we capture an email address is saved for # emailing off signed keys (not yet implemented), and # also for the ID for that UID. # # If we can't, then we grab what we can and make it the # id and blank out the email. # # For the normal case (have email), we'll be storing each email # twice but that's OK since it means that email is *always* a valid # email or None and id is *always* a valid identifier match = re.search(".* <(.*)>", uid) if match: email = match.group(1) PiusUtil.debug("got email %s" % email) filename = re.sub("@", "_at_", email) filename = "%s__%s" % (key, filename) uid = email else: # but if it doesn't have an email, do the right thing email = None PiusUtil.debug("no email") uid = re.sub(" ", "_", uid) uid = re.sub("'", "", uid) filename = "%s__%s" % (key, uid) # Append the UID we're signing with in case people sign with 2 # keys in succession: filename = "%s__%s" % (filename, self.signer) if filename in unique_files: PiusUtil.debug("Filename is a duplicate") count = 2 while True: test = "%s_%s" % (filename, count) PiusUtil.debug("Trying %s" % test) if test not in unique_files: PiusUtil.debug("%s worked!" % test) filename = test break else: count += 1 else: PiusUtil.debug("%s isn't in %s" % (filename, repr(unique_files))) # NOTE: Make sure to append the file BEFORE adding the extension # since that's what we test against above! unique_files.append(filename) filename += ".asc" uids.append({ "email": email, "file": self._outfile_path(filename), "status": status, "id": uid, "index": index, }) # sometimes it wants a save here. I don't know why. We can quit and # check for a save prompt, and then hit no, but we have to make sure # it's still running or we'll hang. It's just easier to issue a 'save' # instead of a quit. Also, write() + wait() causes a hang in py3 even # though there's nothing in any of the pipes, so we use communicate() # here which does the right thing gpg.stdin.write("save\n") PiusUtil.debug("waiting") gpg.wait() return uids