From df1a2290b6e6d77ad17ccbc985f1054e960f2260 Mon Sep 17 00:00:00 2001 From: James Troup Date: Sat, 27 Nov 2004 13:32:16 +0000 Subject: [PATCH] 2004-08-04 James Troup * jennifer (check_files): check for unknown component before checking for NEWness.2004-06-17 James Troup * jennifer (check_dsc): s/dsc_whitespace_rules/signing_rules/. * tea (check_dscs): likewise. * utils.py (parse_changes): s/dsc_whitespace_rules/signing_rules/, change from boolean to a variable with 3 possible values, 0 and 1 as before, -1 means don't require a signature. Makes parse_changes() useful for parsing arbitary RFC822-style files, e.g. 'Release' files. (check_signature): add support for detached signatures by passing the files the signature is for as an optional third argument. s/filename/sig_filename/g. Add a fourth optional argument to choose the keyring(s) to use. Don't os.path.basename() the sig_filename before checking it for taint. (re_taint_free): allow '/'. --- jennifer | 15 ++++---- tea | 4 +- utils.py | 109 ++++++++++++++++++++++++++++++++----------------------- 3 files changed, 73 insertions(+), 55 deletions(-) diff --git a/jennifer b/jennifer index bb1fff7d..e9f575c5 100755 --- a/jennifer +++ b/jennifer @@ -2,7 +2,7 @@ # Checks Debian packages from Incoming # Copyright (C) 2000, 2001, 2002, 2003, 2004 James Troup -# $Id: jennifer,v 1.51 2004-06-17 15:01:18 troup Exp $ +# $Id: jennifer,v 1.52 2004-11-27 13:32:16 troup Exp $ # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -45,7 +45,7 @@ re_strip_revision = re.compile(r"-([^-]+)$"); ################################################################################ # Globals -jennifer_version = "$Revision: 1.51 $"; +jennifer_version = "$Revision: 1.52 $"; Cnf = None; Options = None; @@ -533,16 +533,13 @@ def check_files(): if files[file]["component"] == source: files[file]["original component"] = source; files[file]["component"] = dest; + # Ensure the component is valid for the target suite if Cnf.has_key("Suite:%s::Components" % (suite)) and \ files[file]["component"] not in Cnf.ValueList("Suite::%s::Components" % (suite)): reject("unknown component `%s' for suite `%s'." % (files[file]["component"], suite)); continue; - # See if the package is NEW - if not Katie.in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file): - files[file]["new"] = 1; - # Validate the component component = files[file]["component"]; component_id = db_access.get_component_id(component); @@ -550,6 +547,10 @@ def check_files(): reject("file '%s' has unknown component '%s'." % (file, component)); continue; + # See if the package is NEW + if not Katie.in_override_p(files[file]["package"], files[file]["component"], suite, files[file].get("dbtype",""), file): + files[file]["new"] = 1; + # Validate the priority if files[file]["priority"].find('/') != -1: reject("file '%s' has invalid priority '%s' [contains '/']." % (file, files[file]["priority"])); @@ -617,7 +618,7 @@ def check_dsc(): # Parse the .dsc file try: - dsc.update(utils.parse_changes(dsc_filename, dsc_whitespace_rules=1)); + dsc.update(utils.parse_changes(dsc_filename, signing_rules=1)); except utils.cant_open_exc: # if not -n copy_to_holding() will have done this for us... if Options["No-Action"]: diff --git a/tea b/tea index 6ef35883..4671272f 100755 --- a/tea +++ b/tea @@ -2,7 +2,7 @@ # Various different sanity checks # Copyright (C) 2000, 2001, 2002, 2003, 2004 James Troup -# $Id: tea,v 1.29 2004-09-01 07:52:15 rmurray Exp $ +# $Id: tea,v 1.30 2004-11-27 13:32:16 troup Exp $ # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -126,7 +126,7 @@ def check_dscs(): for line in list_file.readlines(): file = line[:-1]; try: - utils.parse_changes(file, dsc_whitespace_rules=1); + utils.parse_changes(file, signing_rules=1); except utils.invalid_dsc_format_exc, line: utils.warn("syntax error in .dsc file '%s', line %s." % (file, line)); count += 1; diff --git a/utils.py b/utils.py index 3d83da01..b2a2ae3e 100644 --- a/utils.py +++ b/utils.py @@ -2,7 +2,7 @@ # Utility functions # Copyright (C) 2000, 2001, 2002, 2003, 2004 James Troup -# $Id: utils.py,v 1.69 2004-06-24 00:41:39 troup Exp $ +# $Id: utils.py,v 1.70 2004-11-27 13:32:16 troup Exp $ ################################################################################ @@ -40,7 +40,7 @@ re_issource = re.compile (r"(.+)_(.+?)\.(orig\.tar\.gz|diff\.gz|tar\.gz|dsc)$"); re_single_line_field = re.compile(r"^(\S*)\s*:\s*(.*)"); re_multi_line_field = re.compile(r"^\s(.*)"); -re_taint_free = re.compile(r"^[-+~\.\w]+$"); +re_taint_free = re.compile(r"^[-+~/\.\w]+$"); re_parse_maintainer = re.compile(r"^\s*(\S.*\S)\s*\<([^\>]+)\>"); @@ -137,24 +137,27 @@ def extract_component_from_section(section): ################################################################################ -# Parses a changes file and returns a dictionary where each field is a -# key. The mandatory first argument is the filename of the .changes -# file. - -# dsc_whitespace_rules is an optional boolean argument which defaults -# to off. If true, it turns on strict format checking to avoid -# allowing in source packages which are unextracable by the -# inappropriately fragile dpkg-source. -# -# The rules are: -# -# o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----" -# followed by any PGP header data and must end with a blank line. -# -# o The data section must end with a blank line and must be followed by -# "-----BEGIN PGP SIGNATURE-----". - -def parse_changes(filename, dsc_whitespace_rules=0): +def parse_changes(filename, signing_rules=0): + """Parses a changes file and returns a dictionary where each field is a +key. The mandatory first argument is the filename of the .changes +file. + +signing_rules is an optional argument: + + o If signing_rules == -1, no signature is required. + o If signing_rules == 0 (the default), a signature is required. + o If signing_rules == 1, it turns on the same strict format checking + as dpkg-source. + +The rules for (signing_rules == 1)-mode are: + + o The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----" + followed by any PGP header data and must end with a blank line. + + o The data section must end with a blank line and must be followed by + "-----BEGIN PGP SIGNATURE-----". +""" + error = ""; changes = {}; @@ -181,7 +184,7 @@ def parse_changes(filename, dsc_whitespace_rules=0): index += 1; line = indexed_lines[index]; if line == "": - if dsc_whitespace_rules: + if signing_rules == 1: index += 1; if index > num_of_lines: raise invalid_dsc_format_exc, index; @@ -196,13 +199,13 @@ def parse_changes(filename, dsc_whitespace_rules=0): break; if line.startswith("-----BEGIN PGP SIGNED MESSAGE"): inside_signature = 1; - if dsc_whitespace_rules: + if signing_rules == 1: while index < num_of_lines and line != "": index += 1; line = indexed_lines[index]; continue; # If we're not inside the signed data, don't process anything - if not inside_signature: + if signing_rules >= 0 and not inside_signature: continue; slf = re_single_line_field.match(line); if slf: @@ -224,7 +227,7 @@ def parse_changes(filename, dsc_whitespace_rules=0): continue; error += line; - if dsc_whitespace_rules and inside_signature: + if signing_rules == 1 and inside_signature: raise invalid_dsc_format_exc, index; changes_in.close(); @@ -852,24 +855,38 @@ def gpgv_get_status_output(cmd, status_read, status_write): ############################################################ -def check_signature (filename, reject): +def check_signature (sig_filename, reject, data_filename="", keyrings=None): """Check the signature of a file and return the fingerprint if the signature is valid or 'None' if it's not. The first argument is the filename whose signature should be checked. The second argument is a reject function and is called when an error is found. The reject() function must allow for two arguments: the first is the error message, the second is an optional prefix string. It's possible for reject() -to be called more than once during an invocation of check_signature().""" +to be called more than once during an invocation of check_signature(). +The third argument is optional and is the name of the files the +detached signature applies to. The fourth argument is optional and is +a *list* of keyrings to use. +""" # Ensure the filename contains no shell meta-characters or other badness - if not re_taint_free.match(os.path.basename(filename)): - reject("!!WARNING!! tainted filename: '%s'." % (filename)); - return 0; + if not re_taint_free.match(sig_filename): + reject("!!WARNING!! tainted signature filename: '%s'." % (sig_filename)); + return None; + + if data_filename and not re_taint_free.match(data_filename): + reject("!!WARNING!! tainted data filename: '%s'." % (data_filename)); + return None; + + if not keyrings: + keyrings = (Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"]) + # Build the command line + status_read, status_write = os.pipe(); + cmd = "gpgv --status-fd %s" % (status_write); + for keyring in keyrings: + cmd += " --keyring %s" % (keyring); + cmd += " %s %s" % (sig_filename, data_filename); # Invoke gpgv on the file - status_read, status_write = os.pipe(); - cmd = "gpgv --status-fd %s --keyring %s --keyring %s %s" \ - % (status_write, Cnf["Dinstall::PGPKeyring"], Cnf["Dinstall::GPGKeyring"], filename); (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write); # Process the status-fd output @@ -896,35 +913,35 @@ to be called more than once during an invocation of check_signature().""" # If we failed to parse the status-fd output, let's just whine and bail now if internal_error: - reject("internal error while performing signature check on %s." % (filename)); + reject("internal error while performing signature check on %s." % (sig_filename)); reject(internal_error, ""); reject("Please report the above errors to the Archive maintainers by replying to this mail.", ""); return None; # Now check for obviously bad things in the processed output if keywords.has_key("SIGEXPIRED"): - reject("The key used to sign %s has expired." % (filename)); + reject("The key used to sign %s has expired." % (sig_filename)); bad = 1; if keywords.has_key("KEYREVOKED"): - reject("The key used to sign %s has been revoked." % (filename)); + reject("The key used to sign %s has been revoked." % (sig_filename)); bad = 1; if keywords.has_key("BADSIG"): - reject("bad signature on %s." % (filename)); + reject("bad signature on %s." % (sig_filename)); bad = 1; if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"): - reject("failed to check signature on %s." % (filename)); + reject("failed to check signature on %s." % (sig_filename)); bad = 1; if keywords.has_key("NO_PUBKEY"): args = keywords["NO_PUBKEY"]; if len(args) >= 1: key = args[0]; - reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, filename)); + reject("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename)); bad = 1; if keywords.has_key("BADARMOR"): - reject("ASCII armour of signature was corrupt in %s." % (filename)); + reject("ASCII armour of signature was corrupt in %s." % (sig_filename)); bad = 1; if keywords.has_key("NODATA"): - reject("no signature found in %s." % (filename)); + reject("no signature found in %s." % (sig_filename)); bad = 1; if bad: @@ -932,7 +949,7 @@ to be called more than once during an invocation of check_signature().""" # Next check gpgv exited with a zero return code if exit_status: - reject("gpgv failed while checking %s." % (filename)); + reject("gpgv failed while checking %s." % (sig_filename)); if status.strip(): reject(prefix_multi_line_string(status, " [GPG status-fd output:] "), ""); else: @@ -941,20 +958,20 @@ to be called more than once during an invocation of check_signature().""" # Sanity check the good stuff we expect if not keywords.has_key("VALIDSIG"): - reject("signature on %s does not appear to be valid [No VALIDSIG]." % (filename)); + reject("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename)); bad = 1; else: args = keywords["VALIDSIG"]; if len(args) < 1: - reject("internal error while checking signature on %s." % (filename)); + reject("internal error while checking signature on %s." % (sig_filename)); bad = 1; else: fingerprint = args[0]; if not keywords.has_key("GOODSIG"): - reject("signature on %s does not appear to be valid [No GOODSIG]." % (filename)); + reject("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename)); bad = 1; if not keywords.has_key("SIG_ID"): - reject("signature on %s does not appear to be valid [No SIG_ID]." % (filename)); + reject("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename)); bad = 1; # Finally ensure there's not something we don't recognise @@ -964,7 +981,7 @@ to be called more than once during an invocation of check_signature().""" for keyword in keywords.keys(): if not known_keywords.has_key(keyword): - reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], filename)); + reject("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename)); bad = 1; if bad: -- 2.39.5