mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
These changes simplify logic, removing some try/except structures that were unnecessary and potentially confusing, and get us back to passing pylint's test. pylint 2.0.0 adds try-except-raise tests, to catch immediate re-raising after catching an exception, outside of some special cases. See this GitHub comment for more info: https://github.com/theupdateframework/tuf/pull/770#issuecomment-412993992 Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
399 lines
13 KiB
Python
Executable file
399 lines
13 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
# Copyright 2012 - 2017, New York University and the TUF contributors
|
|
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
|
|
"""
|
|
<Program Name>
|
|
sig.py
|
|
|
|
<Author>
|
|
Vladimir Diaz <vladimir.v.diaz@gmail.com>
|
|
|
|
<Started>
|
|
February 28, 2012. Based on a previous version by Geremy Condra.
|
|
|
|
<Copyright>
|
|
See LICENSE-MIT OR LICENSE for licensing information.
|
|
|
|
<Purpose>
|
|
Survivable key compromise is one feature of a secure update system
|
|
incorporated into TUF's design. Responsibility separation through
|
|
the use of multiple roles, multi-signature trust, and explicit and
|
|
implicit key revocation are some of the mechanisms employed towards
|
|
this goal of survivability. These mechanisms can all be seen in
|
|
play by the functions available in this module.
|
|
|
|
The signed metadata files utilized by TUF to download target files
|
|
securely are used and represented here as the 'signable' object.
|
|
More precisely, the signature structures contained within these metadata
|
|
files are packaged into 'signable' dictionaries. This module makes it
|
|
possible to capture the states of these signatures by organizing the
|
|
keys into different categories. As keys are added and removed, the
|
|
system must securely and efficiently verify the status of these signatures.
|
|
For instance, a bunch of keys have recently expired. How many valid keys
|
|
are now available to the Snapshot role? This question can be answered by
|
|
get_signature_status(), which will return a full 'status report' of these
|
|
'signable' dicts. This module also provides a convenient verify() function
|
|
that will determine if a role still has a sufficient number of valid keys.
|
|
If a caller needs to update the signatures of a 'signable' object, there
|
|
is also a function for that.
|
|
"""
|
|
|
|
# Help with Python 3 compatibility, where the print statement is a function, an
|
|
# implicit relative import is invalid, and the '/' operator performs true
|
|
# division. Example: print 'hello world' raises a 'SyntaxError' exception.
|
|
from __future__ import print_function
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import logging
|
|
|
|
import tuf
|
|
import tuf.keydb
|
|
import tuf.roledb
|
|
import tuf.formats
|
|
|
|
import securesystemslib
|
|
|
|
# See 'log.py' to learn how logging is handled in TUF.
|
|
logger = logging.getLogger('tuf.sig')
|
|
|
|
# Disable 'iso8601' logger messages to prevent 'iso8601' from clogging the
|
|
# log file.
|
|
iso8601_logger = logging.getLogger('iso8601')
|
|
iso8601_logger.disabled = True
|
|
|
|
|
|
def get_signature_status(signable, role=None, repository_name='default',
|
|
threshold=None, keyids=None):
|
|
"""
|
|
<Purpose>
|
|
Return a dictionary representing the status of the signatures listed in
|
|
'signable'. Given an object conformant to SIGNABLE_SCHEMA, a set of public
|
|
keys in 'tuf.keydb', a set of roles in 'tuf.roledb', and a role,
|
|
the status of these signatures can be determined. This method will iterate
|
|
the signatures in 'signable' and enumerate all the keys that are valid,
|
|
invalid, unrecognized, or unauthorized.
|
|
|
|
<Arguments>
|
|
signable:
|
|
A dictionary containing a list of signatures and a 'signed' identifier.
|
|
signable = {'signed': 'signer',
|
|
'signatures': [{'keyid': keyid,
|
|
'sig': sig}]}
|
|
|
|
Conformant to tuf.formats.SIGNABLE_SCHEMA.
|
|
|
|
role:
|
|
TUF role (e.g., 'root', 'targets', 'snapshot').
|
|
|
|
threshold:
|
|
Rather than reference the role's threshold as set in tuf.roledb.py, use
|
|
the given 'threshold' to calculate the signature status of 'signable'.
|
|
'threshold' is an integer value that sets the role's threshold value, or
|
|
the miminum number of signatures needed for metadata to be considered
|
|
fully signed.
|
|
|
|
keyids:
|
|
Similar to the 'threshold' argument, use the supplied list of 'keyids'
|
|
to calculate the signature status, instead of referencing the keyids
|
|
in tuf.roledb.py for 'role'.
|
|
|
|
<Exceptions>
|
|
securesystemslib.exceptions.FormatError, if 'signable' does not have the
|
|
correct format.
|
|
|
|
tuf.exceptions.UnknownRoleError, if 'role' is not recognized.
|
|
|
|
<Side Effects>
|
|
None.
|
|
|
|
<Returns>
|
|
A dictionary representing the status of the signatures in 'signable'.
|
|
Conformant to tuf.formats.SIGNATURESTATUS_SCHEMA.
|
|
"""
|
|
|
|
# Do the arguments have the correct format? This check will ensure that
|
|
# arguments have the appropriate number of objects and object types, and that
|
|
# all dict keys are properly named. Raise
|
|
# 'securesystemslib.exceptions.FormatError' if the check fails.
|
|
tuf.formats.SIGNABLE_SCHEMA.check_match(signable)
|
|
securesystemslib.formats.NAME_SCHEMA.check_match(repository_name)
|
|
|
|
if role is not None:
|
|
tuf.formats.ROLENAME_SCHEMA.check_match(role)
|
|
|
|
if threshold is not None:
|
|
securesystemslib.formats.THRESHOLD_SCHEMA.check_match(threshold)
|
|
|
|
if keyids is not None:
|
|
securesystemslib.formats.KEYIDS_SCHEMA.check_match(keyids)
|
|
|
|
# The signature status dictionary returned.
|
|
signature_status = {}
|
|
|
|
# The fields of the signature_status dict, where each field stores keyids. A
|
|
# description of each field:
|
|
#
|
|
# good_sigs = keys confirmed to have produced 'sig' using 'signed', which are
|
|
# associated with 'role';
|
|
#
|
|
# bad_sigs = negation of good_sigs;
|
|
#
|
|
# unknown_sigs = keys not found in the 'keydb' database;
|
|
#
|
|
# untrusted_sigs = keys that are not in the list of keyids associated with
|
|
# 'role';
|
|
#
|
|
# unknown_signing_scheme = signing schemes specified in keys that are
|
|
# unsupported;
|
|
good_sigs = []
|
|
bad_sigs = []
|
|
unknown_sigs = []
|
|
untrusted_sigs = []
|
|
unknown_signing_schemes = []
|
|
|
|
# Extract the relevant fields from 'signable' that will allow us to identify
|
|
# the different classes of keys (i.e., good_sigs, bad_sigs, etc.).
|
|
signed = signable['signed']
|
|
signatures = signable['signatures']
|
|
|
|
# Iterate the signatures and enumerate the signature_status fields.
|
|
# (i.e., good_sigs, bad_sigs, etc.).
|
|
for signature in signatures:
|
|
keyid = signature['keyid']
|
|
|
|
# Does the signature use an unrecognized key?
|
|
try:
|
|
key = tuf.keydb.get_key(keyid, repository_name)
|
|
|
|
except securesystemslib.exceptions.UnknownKeyError:
|
|
unknown_sigs.append(keyid)
|
|
continue
|
|
|
|
# Does the signature use an unknown/unsupported signing scheme?
|
|
try:
|
|
valid_sig = securesystemslib.keys.verify_signature(key, signature, signed)
|
|
|
|
except securesystemslib.exceptions.UnsupportedAlgorithmError:
|
|
unknown_signing_schemes.append(keyid)
|
|
continue
|
|
|
|
# We are now dealing with either a trusted or untrusted key...
|
|
if valid_sig:
|
|
if role is not None:
|
|
|
|
# Is this an unauthorized key? (a keyid associated with 'role')
|
|
# Note that if the role is not known, tuf.exceptions.UnknownRoleError
|
|
# is raised here.
|
|
if keyids is None:
|
|
keyids = tuf.roledb.get_role_keyids(role, repository_name)
|
|
|
|
if keyid not in keyids:
|
|
untrusted_sigs.append(keyid)
|
|
continue
|
|
|
|
# This is an unset role, thus an unknown signature.
|
|
else:
|
|
unknown_sigs.append(keyid)
|
|
continue
|
|
|
|
# Identify good/authorized key.
|
|
good_sigs.append(keyid)
|
|
|
|
else:
|
|
# This is a bad signature for a trusted key.
|
|
bad_sigs.append(keyid)
|
|
|
|
# Retrieve the threshold value for 'role'. Raise
|
|
# securesystemslib.exceptions.UnknownRoleError if we were given an invalid
|
|
# role.
|
|
if role is not None:
|
|
if threshold is None:
|
|
# Note that if the role is not known, tuf.exceptions.UnknownRoleError is
|
|
# raised here.
|
|
threshold = tuf.roledb.get_role_threshold(
|
|
role, repository_name=repository_name)
|
|
|
|
else:
|
|
logger.debug('Not using roledb.py\'s threshold for ' + repr(role))
|
|
|
|
else:
|
|
threshold = 0
|
|
|
|
# Build the signature_status dict.
|
|
signature_status['threshold'] = threshold
|
|
signature_status['good_sigs'] = good_sigs
|
|
signature_status['bad_sigs'] = bad_sigs
|
|
signature_status['unknown_sigs'] = unknown_sigs
|
|
signature_status['untrusted_sigs'] = untrusted_sigs
|
|
signature_status['unknown_signing_schemes'] = unknown_signing_schemes
|
|
|
|
return signature_status
|
|
|
|
|
|
|
|
|
|
|
|
def verify(signable, role, repository_name='default', threshold=None,
|
|
keyids=None):
|
|
"""
|
|
<Purpose>
|
|
Verify whether the authorized signatures of 'signable' meet the minimum
|
|
required by 'role'. Authorized signatures are those with valid keys
|
|
associated with 'role'. 'signable' must conform to SIGNABLE_SCHEMA
|
|
and 'role' must not equal 'None' or be less than zero.
|
|
|
|
<Arguments>
|
|
signable:
|
|
A dictionary containing a list of signatures and a 'signed' identifier.
|
|
signable = {'signed':, 'signatures': [{'keyid':, 'method':, 'sig':}]}
|
|
|
|
role:
|
|
TUF role (e.g., 'root', 'targets', 'snapshot').
|
|
|
|
threshold:
|
|
Rather than reference the role's threshold as set in tuf.roledb.py, use
|
|
the given 'threshold' to calculate the signature status of 'signable'.
|
|
'threshold' is an integer value that sets the role's threshold value, or
|
|
the miminum number of signatures needed for metadata to be considered
|
|
fully signed.
|
|
|
|
keyids:
|
|
Similar to the 'threshold' argument, use the supplied list of 'keyids'
|
|
to calculate the signature status, instead of referencing the keyids
|
|
in tuf.roledb.py for 'role'.
|
|
|
|
<Exceptions>
|
|
securesystemslib.exceptions.UnknownRoleError, if 'role' is not recognized.
|
|
|
|
securesystemslib.exceptions.FormatError, if 'signable' is not formatted
|
|
correctly.
|
|
|
|
securesystemslib.exceptions.Error, if an invalid threshold is encountered.
|
|
|
|
<Side Effects>
|
|
tuf.sig.get_signature_status() called. Any exceptions thrown by
|
|
get_signature_status() will be caught here and re-raised.
|
|
|
|
<Returns>
|
|
Boolean. True if the number of good signatures >= the role's threshold,
|
|
False otherwise.
|
|
"""
|
|
|
|
tuf.formats.SIGNABLE_SCHEMA.check_match(signable)
|
|
tuf.formats.ROLENAME_SCHEMA.check_match(role)
|
|
securesystemslib.formats.NAME_SCHEMA.check_match(repository_name)
|
|
|
|
# Retrieve the signature status. tuf.sig.get_signature_status() raises:
|
|
# securesystemslib.exceptions.UnknownRoleError
|
|
# securesystemslib.exceptions.FormatError. 'threshold' and 'keyids' are also
|
|
# validated.
|
|
status = get_signature_status(signable, role, repository_name, threshold, keyids)
|
|
|
|
# Retrieve the role's threshold and the authorized keys of 'status'
|
|
threshold = status['threshold']
|
|
good_sigs = status['good_sigs']
|
|
|
|
# Does 'status' have the required threshold of signatures?
|
|
# First check for invalid threshold values before returning result.
|
|
# Note: get_signature_status() is expected to verify that 'threshold' is
|
|
# not None or <= 0.
|
|
if threshold is None or threshold <= 0: #pragma: no cover
|
|
raise securesystemslib.exceptions.Error("Invalid threshold: " + repr(threshold))
|
|
|
|
return len(good_sigs) >= threshold
|
|
|
|
|
|
|
|
|
|
|
|
def may_need_new_keys(signature_status):
|
|
"""
|
|
<Purpose>
|
|
Return true iff downloading a new set of keys might tip this
|
|
signature status over to valid. This is determined by checking
|
|
if either the number of unknown or untrused keys is > 0.
|
|
|
|
<Arguments>
|
|
signature_status:
|
|
The dictionary returned by tuf.sig.get_signature_status().
|
|
|
|
<Exceptions>
|
|
securesystemslib.exceptions.FormatError, if 'signature_status does not have
|
|
the correct format.
|
|
|
|
<Side Effects>
|
|
None.
|
|
|
|
<Returns>
|
|
Boolean.
|
|
"""
|
|
|
|
# Does 'signature_status' have the correct format?
|
|
# This check will ensure 'signature_status' has the appropriate number
|
|
# of objects and object types, and that all dict keys are properly named.
|
|
# Raise 'securesystemslib.exceptions.FormatError' if the check fails.
|
|
securesystemslib.formats.SIGNATURESTATUS_SCHEMA.check_match(signature_status)
|
|
|
|
unknown = signature_status['unknown_sigs']
|
|
untrusted = signature_status['untrusted_sigs']
|
|
|
|
return len(unknown) or len(untrusted)
|
|
|
|
|
|
|
|
|
|
|
|
def generate_rsa_signature(signed, rsakey_dict):
|
|
"""
|
|
<Purpose>
|
|
Generate a new signature dict presumably to be added to the 'signatures'
|
|
field of 'signable'. The 'signable' dict is of the form:
|
|
|
|
{'signed': 'signer',
|
|
'signatures': [{'keyid': keyid,
|
|
'method': 'evp',
|
|
'sig': sig}]}
|
|
|
|
The 'signed' argument is needed here for the signing process.
|
|
The 'rsakey_dict' argument is used to generate 'keyid', 'method', and 'sig'.
|
|
|
|
The caller should ensure the returned signature is not already in
|
|
'signable'.
|
|
|
|
<Arguments>
|
|
signed:
|
|
The data used by 'securesystemslib.keys.create_signature()' to generate
|
|
signatures. It is stored in the 'signed' field of 'signable'.
|
|
|
|
rsakey_dict:
|
|
The RSA key, a 'securesystemslib.formats.RSAKEY_SCHEMA' dictionary.
|
|
Used here to produce 'keyid', 'method', and 'sig'.
|
|
|
|
<Exceptions>
|
|
securesystemslib.exceptions.FormatError, if 'rsakey_dict' does not have the
|
|
correct format.
|
|
|
|
TypeError, if a private key is not defined for 'rsakey_dict'.
|
|
|
|
<Side Effects>
|
|
None.
|
|
|
|
<Returns>
|
|
Signature dictionary conformant to securesystemslib.formats.SIGNATURE_SCHEMA.
|
|
Has the form:
|
|
{'keyid': keyid, 'method': 'evp', 'sig': sig}
|
|
"""
|
|
|
|
# We need 'signed' in canonical JSON format to generate
|
|
# the 'method' and 'sig' fields of the signature.
|
|
signed = securesystemslib.formats.encode_canonical(signed)
|
|
|
|
# Generate the RSA signature.
|
|
# Raises securesystemslib.exceptions.FormatError and TypeError.
|
|
signature = securesystemslib.keys.create_signature(rsakey_dict, signed)
|
|
|
|
return signature
|