################################################################################
-def create_hash(where, files, hashname, hashfunc):
- """
- create_hash extends the passed files dict with the given hash by
- iterating over all files on disk and passing them to the hashing
- function given.
- """
-
- rejmsg = []
- for f in files.keys():
- try:
- file_handle = open_file(f)
- except CantOpenError:
- rejmsg.append("Could not open file %s for checksumming" % (f))
- continue
-
- files[f][hash_key(hashname)] = hashfunc(file_handle)
-
- file_handle.close()
- return rejmsg
-
-################################################################################
-
-def check_hash(where, files, hashname, hashfunc):
- """
- check_hash checks the given hash in the files dict against the actual
- files on disk. The hash values need to be present consistently in
- all file entries. It does not modify its input in any way.
- """
-
- rejmsg = []
- for f in files.keys():
- try:
- with open_file(f) as file_handle:
- # Check for the hash entry, to not trigger a KeyError.
- if not files[f].has_key(hash_key(hashname)):
- rejmsg.append("%s: misses %s checksum in %s" % (f, hashname,
- where))
- continue
-
- # Actually check the hash for correctness.
- if hashfunc(file_handle) != files[f][hash_key(hashname)]:
- rejmsg.append("%s: %s check failed in %s" % (f, hashname,
- where))
- except CantOpenError:
- # TODO: This happens when the file is in the pool.
- # warn("Cannot open file %s" % f)
- continue
- return rejmsg
-
-################################################################################
-
-def check_size(where, files):
- """
- check_size checks the file sizes in the passed files dict against the
- files on disk.
- """
-
- rejmsg = []
- for f in files.keys():
- try:
- entry = os.stat(f)
- except OSError as exc:
- if exc.errno == errno.ENOENT:
- # TODO: This happens when the file is in the pool.
- continue
- raise
-
- actual_size = entry[stat.ST_SIZE]
- size = int(files[f]["size"])
- if size != actual_size:
- rejmsg.append("%s: actual file size (%s) does not match size (%s) in %s"
- % (f, actual_size, size, where))
- return rejmsg
-
-################################################################################
-
def check_dsc_files(dsc_filename, dsc, dsc_files):
"""
Verify that the files listed in the Files field of the .dsc are
################################################################################
-def check_hash_fields(what, manifest):
- """
- check_hash_fields ensures that there are no checksum fields in the
- given dict that we do not know about.
- """
-
- rejmsg = []
- hashes = map(lambda x: x[0], known_hashes)
- for field in manifest:
- if field.startswith("checksums-"):
- hashname = field.split("-",1)[1]
- if hashname not in hashes:
- rejmsg.append("Unsupported checksum field for %s "\
- "in %s" % (hashname, what))
- return rejmsg
-
-################################################################################
-
-def _ensure_changes_hash(changes, format, version, files, hashname, hashfunc):
- if format >= version:
- # The version should contain the specified hash.
- func = check_hash
-
- # Import hashes from the changes
- rejmsg = parse_checksums(".changes", files, changes, hashname)
- if len(rejmsg) > 0:
- return rejmsg
- else:
- # We need to calculate the hash because it can't possibly
- # be in the file.
- func = create_hash
- return func(".changes", files, hashname, hashfunc)
-
-# We could add the orig which might be in the pool to the files dict to
-# access the checksums easily.
-
-def _ensure_dsc_hash(dsc, dsc_files, hashname, hashfunc):
- """
- ensure_dsc_hashes' task is to ensure that each and every *present* hash
- in the dsc is correct, i.e. identical to the changes file and if necessary
- the pool. The latter task is delegated to check_hash.
- """
-
- rejmsg = []
- if not dsc.has_key('Checksums-%s' % (hashname,)):
- return rejmsg
- # Import hashes from the dsc
- parse_checksums(".dsc", dsc_files, dsc, hashname)
- # And check it...
- rejmsg.extend(check_hash(".dsc", dsc_files, hashname, hashfunc))
- return rejmsg
-
-################################################################################
-
-def parse_checksums(where, files, manifest, hashname):
- rejmsg = []
- field = 'checksums-%s' % hashname
- if not field in manifest:
- return rejmsg
- for line in manifest[field].split('\n'):
- if not line:
- break
- clist = line.strip().split(' ')
- if len(clist) == 3:
- checksum, size, checkfile = clist
- else:
- rejmsg.append("Cannot parse checksum line [%s]" % (line))
- continue
- if not files.has_key(checkfile):
- # TODO: check for the file's entry in the original files dict, not
- # the one modified by (auto)byhand and other weird stuff
- # rejmsg.append("%s: not present in files but in checksums-%s in %s" %
- # (file, hashname, where))
- continue
- if not files[checkfile]["size"] == size:
- rejmsg.append("%s: size differs for files and checksums-%s entry "\
- "in %s" % (checkfile, hashname, where))
- continue
- files[checkfile][hash_key(hashname)] = checksum
- for f in files.keys():
- if not files[f].has_key(hash_key(hashname)):
- rejmsg.append("%s: no entry in checksums-%s in %s" % (f, hashname, where))
- return rejmsg
-
-################################################################################
-
# Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
################################################################################
-# see https://bugs.debian.org/619131
-def build_package_list(dsc, session = None):
- if not dsc.has_key("package-list"):
- return {}
-
- packages = {}
-
- for line in dsc["package-list"].split("\n"):
- if not line:
- break
-
- fields = line.split()
- name = fields[0]
- package_type = fields[1]
- (section, component) = extract_component_from_section(fields[2])
- priority = fields[3]
-
- # Validate type if we have a session
- if session and get_override_type(package_type, session) is None:
- # Maybe just warn and ignore? exit(1) might be a bit hard...
- utils.fubar("invalid type (%s) in Package-List." % (package_type))
-
- if name not in packages or packages[name]["type"] == "dsc":
- packages[name] = dict(priority=priority, section=section, type=package_type, component=component, files=[])
-
- return packages
-
-################################################################################
-
def send_mail (message, filename="", whitelists=None):
"""sendmail wrapper, takes _either_ a message string or a file as arguments
################################################################################
-def cc_fix_changes (changes):
- o = changes.get("architecture", "")
- if o:
- del changes["architecture"]
- changes["architecture"] = {}
- for j in o.split():
- changes["architecture"][j] = 1
-
-def changes_compare (a, b):
- """ Sort by source name, source version, 'have source', and then by filename """
- try:
- a_changes = parse_changes(a)
- except:
- return -1
-
- try:
- b_changes = parse_changes(b)
- except:
- return 1
-
- cc_fix_changes (a_changes)
- cc_fix_changes (b_changes)
-
- # Sort by source name
- a_source = a_changes.get("source")
- b_source = b_changes.get("source")
- q = cmp (a_source, b_source)
- if q:
- return q
-
- # Sort by source version
- a_version = a_changes.get("version", "0")
- b_version = b_changes.get("version", "0")
- q = apt_pkg.version_compare(a_version, b_version)
- if q:
- return q
-
- # Sort by 'have source'
- a_has_source = a_changes["architecture"].get("source")
- b_has_source = b_changes["architecture"].get("source")
- if a_has_source and not b_has_source:
- return -1
- elif b_has_source and not a_has_source:
- return 1
-
- # Fall back to sort by filename
- return cmp(a, b)
-
-################################################################################
-
def find_next_free (dest, too_many=100):
extra = 0
orig_dest = dest
################################################################################
-def validate_changes_file_arg(filename, require_changes=1):
- """
- 'filename' is either a .changes or .dak file. If 'filename' is a
- .dak file, it's changed to be the corresponding .changes file. The
- function then checks if the .changes file a) exists and b) is
- readable and returns the .changes filename if so. If there's a
- problem, the next action depends on the option 'require_changes'
- argument:
-
- - If 'require_changes' == -1, errors are ignored and the .changes
- filename is returned.
- - If 'require_changes' == 0, a warning is given and 'None' is returned.
- - If 'require_changes' == 1, a fatal error is raised.
-
- """
- error = None
-
- orig_filename = filename
- if filename.endswith(".dak"):
- filename = filename[:-4]+".changes"
-
- if not filename.endswith(".changes"):
- error = "invalid file type; not a changes file"
- else:
- if not os.access(filename,os.R_OK):
- if os.path.exists(filename):
- error = "permission denied"
- else:
- error = "file not found"
-
- if error:
- if require_changes == 1:
- fubar("%s: %s." % (orig_filename, error))
- elif require_changes == 0:
- warn("Skipping %s - %s" % (orig_filename, error))
- return None
- else: # We only care about the .dak file
- return filename
- else:
- return filename
-
-################################################################################
-
-def real_arch(arch):
- return (arch != "source" and arch != "all")
-
-################################################################################
-
def join_with_commas_and(list):
if len(list) == 0: return "nothing"
if len(list) == 1: return list[0]
################################################################################
-def gpgv_get_status_output(cmd, status_read, status_write):
- """
- Our very own version of commands.getouputstatus(), hacked to support
- gpgv's status fd.
- """
-
- cmd = ['/bin/sh', '-c', cmd]
- p2cread, p2cwrite = os.pipe()
- c2pread, c2pwrite = os.pipe()
- errout, errin = os.pipe()
- pid = os.fork()
- if pid == 0:
- # Child
- os.close(0)
- os.close(1)
- os.dup(p2cread)
- os.dup(c2pwrite)
- os.close(2)
- os.dup(errin)
- for i in range(3, 256):
- if i != status_write:
- try:
- os.close(i)
- except:
- pass
- try:
- os.execvp(cmd[0], cmd)
- finally:
- os._exit(1)
-
- # Parent
- os.close(p2cread)
- os.dup2(c2pread, c2pwrite)
- os.dup2(errout, errin)
-
- output = status = ""
- while 1:
- i, o, e = select.select([c2pwrite, errin, status_read], [], [])
- more_data = []
- for fd in i:
- r = os.read(fd, 8196)
- if len(r) > 0:
- more_data.append(fd)
- if fd == c2pwrite or fd == errin:
- output += r
- elif fd == status_read:
- status += r
- else:
- fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
- if not more_data:
- pid, exit_status = os.waitpid(pid, 0)
- try:
- os.close(status_write)
- os.close(status_read)
- os.close(c2pread)
- os.close(c2pwrite)
- os.close(p2cwrite)
- os.close(errin)
- os.close(errout)
- except:
- pass
- break
-
- return output, status, exit_status
-
-################################################################################
-
-def process_gpgv_output(status):
- # Process the status-fd output
- keywords = {}
- internal_error = ""
- for line in status.split('\n'):
- line = line.strip()
- if line == "":
- continue
- split = line.split()
- if len(split) < 2:
- internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
- continue
- (gnupg, keyword) = split[:2]
- if gnupg != "[GNUPG:]":
- internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
- continue
- args = split[2:]
- if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
- internal_error += "found duplicate status token ('%s').\n" % (keyword)
- continue
- else:
- keywords[keyword] = args
-
- return (keywords, internal_error)
-
-################################################################################
-
-def retrieve_key (filename, keyserver=None, keyring=None):
- """
- Retrieve the key that signed 'filename' from 'keyserver' and
- add it to 'keyring'. Returns nothing on success, or an error message
- on error.
- """
-
- # Defaults for keyserver and keyring
- if not keyserver:
- keyserver = Cnf["Dinstall::KeyServer"]
- if not keyring:
- keyring = get_primary_keyring_path()
-
- # Ensure the filename contains no shell meta-characters or other badness
- if not re_taint_free.match(filename):
- return "%s: tainted filename" % (filename)
-
- # Invoke gpgv on the file
- status_read, status_write = os.pipe()
- cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
- (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
-
- # Process the status-fd output
- (keywords, internal_error) = process_gpgv_output(status)
- if internal_error:
- return internal_error
-
- if not keywords.has_key("NO_PUBKEY"):
- return "didn't find expected NO_PUBKEY in gpgv status-fd output"
-
- fingerprint = keywords["NO_PUBKEY"][0]
- # XXX - gpg sucks. You can't use --secret-keyring=/dev/null as
- # it'll try to create a lockfile in /dev. A better solution might
- # be a tempfile or something.
- cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
- % (Cnf["Dinstall::SigningKeyring"])
- cmd += " --keyring %s --keyserver %s --recv-key %s" \
- % (keyring, keyserver, fingerprint)
- (result, output) = commands.getstatusoutput(cmd)
- if (result != 0):
- return "'%s' failed with exit code %s" % (cmd, result)
-
- return ""
-
-################################################################################
-
def gpg_keyring_args(keyrings=None):
if not keyrings:
keyrings = get_active_keyring_paths()
return " ".join(["--keyring %s" % x for x in keyrings])
-################################################################################
-@session_wrapper
-def check_signature (sig_filename, data_filename="", keyrings=None, autofetch=None, session=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().
- 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. 'autofetch' can either be None, True or
- False. If None, the default behaviour specified in the config will be
- used.
- """
-
- rejects = []
-
- # Ensure the filename contains no shell meta-characters or other badness
- if not re_taint_free.match(sig_filename):
- rejects.append("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
- return (None, rejects)
-
- if data_filename and not re_taint_free.match(data_filename):
- rejects.append("!!WARNING!! tainted data filename: '%s'." % (data_filename))
- return (None, rejects)
-
- if not keyrings:
- keyrings = [ x.keyring_name for x in session.query(Keyring).filter(Keyring.active == True).all() ]
-
- # Autofetch the signing key if that's enabled
- if autofetch == None:
- autofetch = Cnf.get("Dinstall::KeyAutoFetch")
- if autofetch:
- error_msg = retrieve_key(sig_filename)
- if error_msg:
- rejects.append(error_msg)
- return (None, rejects)
-
- # Build the command line
- status_read, status_write = os.pipe()
- cmd = "gpgv --status-fd %s %s %s %s" % (
- status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
-
- # Invoke gpgv on the file
- (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
-
- # Process the status-fd output
- (keywords, internal_error) = process_gpgv_output(status)
-
- # If we failed to parse the status-fd output, let's just whine and bail now
- if internal_error:
- rejects.append("internal error while performing signature check on %s." % (sig_filename))
- rejects.append(internal_error, "")
- rejects.append("Please report the above errors to the Archive maintainers by replying to this mail.", "")
- return (None, rejects)
-
- # Now check for obviously bad things in the processed output
- if keywords.has_key("KEYREVOKED"):
- rejects.append("The key used to sign %s has been revoked." % (sig_filename))
- if keywords.has_key("BADSIG"):
- rejects.append("bad signature on %s." % (sig_filename))
- if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
- rejects.append("failed to check signature on %s." % (sig_filename))
- if keywords.has_key("NO_PUBKEY"):
- args = keywords["NO_PUBKEY"]
- if len(args) >= 1:
- key = args[0]
- rejects.append("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
- if keywords.has_key("BADARMOR"):
- rejects.append("ASCII armour of signature was corrupt in %s." % (sig_filename))
- if keywords.has_key("NODATA"):
- rejects.append("no signature found in %s." % (sig_filename))
- if keywords.has_key("EXPKEYSIG"):
- args = keywords["EXPKEYSIG"]
- if len(args) >= 1:
- key = args[0]
- rejects.append("Signature made by expired key 0x%s" % (key))
- if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
- args = keywords["KEYEXPIRED"]
- expiredate=""
- if len(args) >= 1:
- timestamp = args[0]
- if timestamp.count("T") == 0:
- try:
- expiredate = time.strftime("%Y-%m-%d", time.gmtime(float(timestamp)))
- except ValueError:
- expiredate = "unknown (%s)" % (timestamp)
- else:
- expiredate = timestamp
- rejects.append("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
-
- if len(rejects) > 0:
- return (None, rejects)
-
- # Next check gpgv exited with a zero return code
- if exit_status:
- rejects.append("gpgv failed while checking %s." % (sig_filename))
- if status.strip():
- rejects.append(prefix_multi_line_string(status, " [GPG status-fd output:] "))
- else:
- rejects.append(prefix_multi_line_string(output, " [GPG output:] "))
- return (None, rejects)
-
- # Sanity check the good stuff we expect
- if not keywords.has_key("VALIDSIG"):
- rejects.append("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
- else:
- args = keywords["VALIDSIG"]
- if len(args) < 1:
- rejects.append("internal error while checking signature on %s." % (sig_filename))
- else:
- fingerprint = args[0]
- if not keywords.has_key("GOODSIG"):
- rejects.append("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
- if not keywords.has_key("SIG_ID"):
- rejects.append("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
-
- # Finally ensure there's not something we don't recognise
- known_keywords = dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
- SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
- NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="",POLICY_URL="")
-
- for keyword in keywords.keys():
- if not known_keywords.has_key(keyword):
- rejects.append("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
-
- if len(rejects) > 0:
- return (None, rejects)
- else:
- return (fingerprint, [])
-
################################################################################
def gpg_get_key_addresses(fingerprint):