python-tuf/tuf/rsa_key.py

425 lines
13 KiB
Python
Executable file

"""
<Program Name>
rsa_key.py
<Author>
Vladimir Diaz <vladimir.v.diaz@gmail.com>
<Started>
March 9, 2012. Based on a previous version of this module by Geremy Condra.
<Copyright>
See LICENSE for licensing information.
<Purpose>
The goal of this module is to support public-key cryptography using
the RSA algorithm. The RSA-related functions provided include
generate(), create_signature(), and verify_signature(). The 'evpy' package
used by 'rsa_key.py' generates the actual RSA keys and the functions listed
above can be viewed as an easy-to-use public interface. Additional functions
contained here include create_in_metadata_format() and
create_from_metadata_format(). These last two functions produce or use RSA
keys compatible with the key structures listed in TUF Metadata files.
The generate() function returns a dictionary containing all the information
needed of RSA keys, such as public and private keys, keyIDs, and an iden-
fier. create_signature() and verify_signature() are supplemental functions
used for generating RSA signatures and verifying them.
Key IDs are used as identifiers for keys (e.g., RSA key). They are the
hexadecimal representation of the hash of key object (specifically, the key
object containing only the public key). See 'rsa_key.py' and the
'_get_keyid()' function to see precisely how keyids are generated. One may
get the keyid of a key object by simply accessing the dictionary's 'keyid'
key (i.e., rsakey['keyid']).
"""
# Required for hexadecimal conversions.
import binascii
# Needed to generate, sign, and verify RSA keys.
import evpy.signature
import evpy.envelope
# Digest objects needed to generate hashes.
import tuf.hash
# Perform object format-checking.
import tuf.formats
_KEY_ID_HASH_ALGORITHM = 'sha256'
# Recommended RSA key sizes: http://www.rsa.com/rsalabs/node.asp?id=2004
# According to the document above, revised May 6, 2003, RSA keys of
# size 3072 provide security through 2031 and beyond.
_DEFAULT_RSA_KEY_BITS = 3072
def generate(bits=_DEFAULT_RSA_KEY_BITS):
"""
<Purpose>
Generate public and private RSA keys, with modulus length 'bits'.
In addition, a keyid used as an identifier for RSA keys is generated.
The object returned conforms to tuf.formats.RSAKEY_SCHEMA and as the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
<Arguments>
bits:
The key size, or key length, of the RSA key.
<Exceptions>
tuf.CryptoError, if an exception occurs after calling evpy.envelope.keygen().
tuf.FormatError, if 'bits' does not contain the correct format.
<Side Effects>
The RSA keys are generated by calling evpy.envelope.keygen().
<Returns>
A dictionary containing the RSA keys and other identifying information.
"""
# Does 'bits' have the correct format?
# This check will ensure 'bits' conforms to 'tuf.formats.RSAKEYBITS_SCHEMA'.
# 'bits' must be an integer object, with a minimum value of 2048.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEYBITS_SCHEMA.check_match(bits)
# Begin building the RSA key dictionary.
rsakey_dict = {}
keytype = 'rsa'
# Generate the public and private keys. 'public_key' and 'private_key'
# will both be strings containing RSA keys in PEM format.
# The evpy.envelope module performs the actual key generation. The
# evpy.envelope.keygen() function returns a (public, private) tuple.
try:
public_key, private_key = evpy.envelope.keygen(bits, pem=True)
except (EnvelopeError, KeygenError, MemoryError), e:
raise tuf.CryptoError(e)
# Generate the keyid for the RSA key. 'key_value' corresponds to the
# 'keyval' entry of the RSAKEY_SCHEMA dictionary.
key_value = {'public': public_key,
'private': ''}
keyid = _get_keyid(key_value)
# Build the 'rsakey_dict' dictionary.
# Update 'key_value' with the RSA private key prior to adding
# 'key_value' to 'rsakey_dict'.
key_value['private'] = private_key
rsakey_dict['keytype'] = keytype
rsakey_dict['keyid'] = keyid
rsakey_dict['keyval'] = key_value
return rsakey_dict
def create_in_metadata_format(key_value, private=False):
"""
<Purpose>
Return a dictionary conformant to tuf.formats.KEY_SCHEMA.
If 'private' is True, include the private key. The dictionary
returned has the form:
{'keytype': 'rsa',
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
or
{'keytype': 'rsa',
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': ''}} if 'private' is False.
The private and public keys are in PEM format.
RSA keys are stored in Metadata files (e.g., root.txt) in the format
returned by this function.
<Arguments>
key_value:
A dictionary containing a private and public RSA key.
'key_value' is of the form:
{'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}},
conformat to tuf.formats.KEYVAL_SCHEMA.
private:
Indicates if the private key should be included in the
returned dictionary.
<Exceptions>
tuf.FormatError, if 'key_value' does not conform to
tuf.formats.KEYVAL_SCHEMA.
<Side Effects>
None.
<Returns>
An KEY_SCHEMA dictionary.
"""
# Does 'key_value' have the correct format?
# This check will ensure 'key_value' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEYVAL_SCHEMA.check_match(key_value)
if private and key_value['private']:
return {'keytype': 'rsa', 'keyval': key_value}
else:
public_key_value = {'public': key_value['public'], 'private': ''}
return {'keytype': 'rsa', 'keyval': public_key_value}
def create_from_metadata_format(key_metadata):
"""
<Purpose>
Construct an RSA key dictionary (i.e., tuf.formats.RSAKEY_SCHEMA)
from 'key_metadata'. The dict returned by this function has the exact
format as the dict returned by generate(). It is of the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
RSA key dictionaries in RSAKEY_SCHEMA format should be used by
modules storing a collection of keys, such as a keydb and keystore.
RSA keys as stored in metadata files use a different format, so this
function should be called if an RSA key is extracted from one of these
metadata files and needs converting. Generate() creates an entirely
new key and returns it in the format appropriate for keydb and keystore.
<Arguments>
key_metadata:
The RSA key dictionary as stored in Metadata files, conforming to
tuf.formats.KEY_SCHEMA.
<Exceptions>
tuf.FormatError, if 'key_metadata' does not conform to
tuf.formats.KEY_SCHEMA.
<Side Effects>
None.
<Returns>
A dictionary containing the RSA keys and other identifying information.
"""
# Does 'key_metadata' have the correct format?
# This check will ensure 'key_metadata' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.KEY_SCHEMA.check_match(key_metadata)
# Construct the dictionary to be returned.
rsakey_dict = {}
keytype = 'rsa'
key_value = key_metadata['keyval']
keyid = _get_keyid(key_value)
# We now have all the required key values.
# Build 'rsakey_dict'.
rsakey_dict['keytype'] = keytype
rsakey_dict['keyid'] = keyid
rsakey_dict['keyval'] = key_value
return rsakey_dict
def _get_keyid(key_value):
"""Return the keyid for 'key_value'."""
# 'keyid' will be generated from an object conformant to KEY_SCHEMA,
# which is the format Metadata files (e.g., root.txt) store keys.
# 'create_in_metadata_format()' returns the object needed by _get_keyid().
rsakey_meta = create_in_metadata_format(key_value, private=False)
# Convert the RSA key to JSON Canonical format suitable for adding
# to digest objects.
rsakey_update_data = tuf.formats.encode_canonical(rsakey_meta)
# Create a digest object and call update(), using the JSON
# canonical format of 'rskey_meta' as the update data.
digest_object = tuf.hash.digest(_KEY_ID_HASH_ALGORITHM)
digest_object.update(rsakey_update_data)
# 'keyid' becomes the hexadecimal representation of the hash.
keyid = digest_object.hexdigest()
return keyid
def create_signature(rsakey_dict, data):
"""
<Purpose>
Return a signature dictionary of the form:
{'keyid': keyid, 'method': 'evp', 'sig': sig}.
The signing process will use the private key
rsakey_dict['keyval']['private'] and 'data' to generate the signature.
<Arguments>
rsakey_dict:
A dictionary containing the RSA keys and other identifying information.
'rsakey_dict' has the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
data:
Data object used by create_signature() to generate the signature.
<Exceptions>
TypeError, if a private key is not defined for 'rsakey_dict'.
tuf.FormatError, if an incorrect format is found for the
'rsakey_dict' object.
<Side Effects>
evpy.signature.sign() called to perform the actual signing.
<Returns>
A signature dictionary conformat to tuf.format.SIGNATURE_SCHEMA.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
# Signing the 'data' object requires a private key.
# The 'evp' (i.e., evpy) signing method is the only method
# currently supported.
signature = {}
private_key = rsakey_dict['keyval']['private']
keyid = rsakey_dict['keyid']
method = 'evp'
if private_key:
sig = evpy.signature.sign(data, key=private_key)
else:
raise TypeError('The required private key is not defined for rsakey_dict.')
# Build the signature dictionary to be returned.
# The hexadecimal representation of 'sig' is stored in the signature.
signature['keyid'] = keyid
signature['method'] = method
signature['sig'] = binascii.hexlify(sig)
return signature
def verify_signature(rsakey_dict, signature, data):
"""
<Purpose>
Determine whether the private key belonging to 'rsakey_dict' produced
'signature'. verify_signature() will use the public key found in
'rsakey_dict', the 'method' and 'sig' objects contained in 'signature',
and 'data' to complete the verification. Type-checking performed on both
'rsakey_dict' and 'signature'.
<Arguments>
rsakey_dict:
A dictionary containing the RSA keys and other identifying information.
'rsakey_dict' has the form:
{'keytype': 'rsa',
'keyid': keyid,
'keyval': {'public': '-----BEGIN RSA PUBLIC KEY----- ...',
'private': '-----BEGIN RSA PRIVATE KEY----- ...'}}
The public and private keys are in PEM format and stored as strings.
signature:
The signature dictionary produced by tuf.rsa_key.create_signature().
'signature' has the form:
{'keyid': keyid, 'method': 'method', 'sig': sig}. Conformant to
tuf.formats.SIGNATURE_SCHEMA.
data:
Data object used by tuf.rsa_key.create_signature() to generate
'signature'. 'data' is needed here to verify the signature.
<Exceptions>
tuf.UnknownMethodError. Raised if the signing method used by
'signature' is not one supported by tuf.rsa_key.create_signature().
tuf.FormatError. Raised if either 'rsakey_dict'
or 'signature' do not match their respective tuf.formats schema.
'rsakey_dict' must conform to tuf.formats.RSAKEY_SCHEMA.
'signature' must conform to tuf.formats.SIGNATURE_SCHEMA.
<Side Effects>
evpy.signature_verify() called to do the actual verification.
<Returns>
Boolean.
"""
# Does 'rsakey_dict' have the correct format?
# This check will ensure 'rsakey_dict' has the appropriate number
# of objects and object types, and that all dict keys are properly named.
# Raise 'tuf.FormatError' if the check fails.
tuf.formats.RSAKEY_SCHEMA.check_match(rsakey_dict)
# Does 'signature' have the correct format?
tuf.formats.SIGNATURE_SCHEMA.check_match(signature)
# Using the public key belonging to 'rsakey_dict'
# (i.e., rsakey_dict['keyval']['public']), verify whether 'signature'
# was produced by rsakey_dict's corresponding private key
# rsakey_dict['keyval']['private']. Before returning the Boolean result,
# ensure 'evp' was used as the signing method.
method = signature['method']
sig = signature['sig']
public_key = rsakey_dict['keyval']['public']
if method != 'evp':
raise tuf.UnknownMethodError(method)
return evpy.signature.verify(data, binascii.unhexlify(sig), key=public_key)