aboutsummaryrefslogtreecommitdiffstats
path: root/asn1crypto/keys.py
diff options
context:
space:
mode:
Diffstat (limited to 'asn1crypto/keys.py')
-rw-r--r--asn1crypto/keys.py1249
1 files changed, 1249 insertions, 0 deletions
diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py
new file mode 100644
index 0000000..9a09a31
--- /dev/null
+++ b/asn1crypto/keys.py
@@ -0,0 +1,1249 @@
+# coding: utf-8
+
+"""
+ASN.1 type classes for public and private keys. Exports the following items:
+
+ - DSAPrivateKey()
+ - ECPrivateKey()
+ - EncryptedPrivateKeyInfo()
+ - PrivateKeyInfo()
+ - PublicKeyInfo()
+ - RSAPrivateKey()
+ - RSAPublicKey()
+
+Other type classes are defined that help compose the types listed above.
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import hashlib
+import math
+
+from ._elliptic_curve import (
+ SECP192R1_BASE_POINT,
+ SECP224R1_BASE_POINT,
+ SECP256R1_BASE_POINT,
+ SECP384R1_BASE_POINT,
+ SECP521R1_BASE_POINT,
+ PrimeCurve,
+ PrimePoint,
+)
+from ._errors import unwrap
+from ._types import type_name, str_cls, byte_cls
+from .algos import _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm, RSAESOAEPParams
+from .core import (
+ Any,
+ Asn1Value,
+ BitString,
+ Choice,
+ Integer,
+ IntegerOctetString,
+ Null,
+ ObjectIdentifier,
+ OctetBitString,
+ OctetString,
+ ParsableOctetString,
+ ParsableOctetBitString,
+ Sequence,
+ SequenceOf,
+ SetOf,
+)
+from .util import int_from_bytes, int_to_bytes
+
+
+class OtherPrimeInfo(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-46
+ """
+
+ _fields = [
+ ('prime', Integer),
+ ('exponent', Integer),
+ ('coefficient', Integer),
+ ]
+
+
+class OtherPrimeInfos(SequenceOf):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-46
+ """
+
+ _child_spec = OtherPrimeInfo
+
+
+class RSAPrivateKeyVersion(Integer):
+ """
+ Original Name: Version
+ Source: https://tools.ietf.org/html/rfc3447#page-45
+ """
+
+ _map = {
+ 0: 'two-prime',
+ 1: 'multi',
+ }
+
+
+class RSAPrivateKey(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-45
+ """
+
+ _fields = [
+ ('version', RSAPrivateKeyVersion),
+ ('modulus', Integer),
+ ('public_exponent', Integer),
+ ('private_exponent', Integer),
+ ('prime1', Integer),
+ ('prime2', Integer),
+ ('exponent1', Integer),
+ ('exponent2', Integer),
+ ('coefficient', Integer),
+ ('other_prime_infos', OtherPrimeInfos, {'optional': True})
+ ]
+
+
+class RSAPublicKey(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-44
+ """
+
+ _fields = [
+ ('modulus', Integer),
+ ('public_exponent', Integer)
+ ]
+
+
+class DSAPrivateKey(Sequence):
+ """
+ The ASN.1 structure that OpenSSL uses to store a DSA private key that is
+ not part of a PKCS#8 structure. Reversed engineered from english-language
+ description on linked OpenSSL documentation page.
+
+ Original Name: None
+ Source: https://www.openssl.org/docs/apps/dsa.html
+ """
+
+ _fields = [
+ ('version', Integer),
+ ('p', Integer),
+ ('q', Integer),
+ ('g', Integer),
+ ('public_key', Integer),
+ ('private_key', Integer),
+ ]
+
+
+class _ECPoint():
+ """
+ In both PublicKeyInfo and PrivateKeyInfo, the EC public key is a byte
+ string that is encoded as a bit string. This class adds convenience
+ methods for converting to and from the byte string to a pair of integers
+ that are the X and Y coordinates.
+ """
+
+ @classmethod
+ def from_coords(cls, x, y):
+ """
+ Creates an ECPoint object from the X and Y integer coordinates of the
+ point
+
+ :param x:
+ The X coordinate, as an integer
+
+ :param y:
+ The Y coordinate, as an integer
+
+ :return:
+ An ECPoint object
+ """
+
+ x_bytes = int(math.ceil(math.log(x, 2) / 8.0))
+ y_bytes = int(math.ceil(math.log(y, 2) / 8.0))
+
+ num_bytes = max(x_bytes, y_bytes)
+
+ byte_string = b'\x04'
+ byte_string += int_to_bytes(x, width=num_bytes)
+ byte_string += int_to_bytes(y, width=num_bytes)
+
+ return cls(byte_string)
+
+ def to_coords(self):
+ """
+ Returns the X and Y coordinates for this EC point, as native Python
+ integers
+
+ :return:
+ A 2-element tuple containing integers (X, Y)
+ """
+
+ data = self.native
+ first_byte = data[0:1]
+
+ # Uncompressed
+ if first_byte == b'\x04':
+ remaining = data[1:]
+ field_len = len(remaining) // 2
+ x = int_from_bytes(remaining[0:field_len])
+ y = int_from_bytes(remaining[field_len:])
+ return (x, y)
+
+ if first_byte not in set([b'\x02', b'\x03']):
+ raise ValueError(unwrap(
+ '''
+ Invalid EC public key - first byte is incorrect
+ '''
+ ))
+
+ raise ValueError(unwrap(
+ '''
+ Compressed representations of EC public keys are not supported due
+ to patent US6252960
+ '''
+ ))
+
+
+class ECPoint(OctetString, _ECPoint):
+
+ pass
+
+
+class ECPointBitString(OctetBitString, _ECPoint):
+
+ pass
+
+
+class SpecifiedECDomainVersion(Integer):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 104
+ """
+ _map = {
+ 1: 'ecdpVer1',
+ 2: 'ecdpVer2',
+ 3: 'ecdpVer3',
+ }
+
+
+class FieldType(ObjectIdentifier):
+ """
+ Original Name: None
+ Source: http://www.secg.org/sec1-v2.pdf page 101
+ """
+
+ _map = {
+ '1.2.840.10045.1.1': 'prime_field',
+ '1.2.840.10045.1.2': 'characteristic_two_field',
+ }
+
+
+class CharacteristicTwoBasis(ObjectIdentifier):
+ """
+ Original Name: None
+ Source: http://www.secg.org/sec1-v2.pdf page 102
+ """
+
+ _map = {
+ '1.2.840.10045.1.2.1.1': 'gn_basis',
+ '1.2.840.10045.1.2.1.2': 'tp_basis',
+ '1.2.840.10045.1.2.1.3': 'pp_basis',
+ }
+
+
+class Pentanomial(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 102
+ """
+
+ _fields = [
+ ('k1', Integer),
+ ('k2', Integer),
+ ('k3', Integer),
+ ]
+
+
+class CharacteristicTwo(Sequence):
+ """
+ Original Name: Characteristic-two
+ Source: http://www.secg.org/sec1-v2.pdf page 101
+ """
+
+ _fields = [
+ ('m', Integer),
+ ('basis', CharacteristicTwoBasis),
+ ('parameters', Any),
+ ]
+
+ _oid_pair = ('basis', 'parameters')
+ _oid_specs = {
+ 'gn_basis': Null,
+ 'tp_basis': Integer,
+ 'pp_basis': Pentanomial,
+ }
+
+
+class FieldID(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 100
+ """
+
+ _fields = [
+ ('field_type', FieldType),
+ ('parameters', Any),
+ ]
+
+ _oid_pair = ('field_type', 'parameters')
+ _oid_specs = {
+ 'prime_field': Integer,
+ 'characteristic_two_field': CharacteristicTwo,
+ }
+
+
+class Curve(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 104
+ """
+
+ _fields = [
+ ('a', OctetString),
+ ('b', OctetString),
+ ('seed', OctetBitString, {'optional': True}),
+ ]
+
+
+class SpecifiedECDomain(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 103
+ """
+
+ _fields = [
+ ('version', SpecifiedECDomainVersion),
+ ('field_id', FieldID),
+ ('curve', Curve),
+ ('base', ECPoint),
+ ('order', Integer),
+ ('cofactor', Integer, {'optional': True}),
+ ('hash', DigestAlgorithm, {'optional': True}),
+ ]
+
+
+class NamedCurve(ObjectIdentifier):
+ """
+ Various named curves
+
+ Original Name: None
+ Source: https://tools.ietf.org/html/rfc3279#page-23,
+ https://tools.ietf.org/html/rfc5480#page-5
+ """
+
+ _map = {
+ # https://tools.ietf.org/html/rfc3279#page-23
+ '1.2.840.10045.3.0.1': 'c2pnb163v1',
+ '1.2.840.10045.3.0.2': 'c2pnb163v2',
+ '1.2.840.10045.3.0.3': 'c2pnb163v3',
+ '1.2.840.10045.3.0.4': 'c2pnb176w1',
+ '1.2.840.10045.3.0.5': 'c2tnb191v1',
+ '1.2.840.10045.3.0.6': 'c2tnb191v2',
+ '1.2.840.10045.3.0.7': 'c2tnb191v3',
+ '1.2.840.10045.3.0.8': 'c2onb191v4',
+ '1.2.840.10045.3.0.9': 'c2onb191v5',
+ '1.2.840.10045.3.0.10': 'c2pnb208w1',
+ '1.2.840.10045.3.0.11': 'c2tnb239v1',
+ '1.2.840.10045.3.0.12': 'c2tnb239v2',
+ '1.2.840.10045.3.0.13': 'c2tnb239v3',
+ '1.2.840.10045.3.0.14': 'c2onb239v4',
+ '1.2.840.10045.3.0.15': 'c2onb239v5',
+ '1.2.840.10045.3.0.16': 'c2pnb272w1',
+ '1.2.840.10045.3.0.17': 'c2pnb304w1',
+ '1.2.840.10045.3.0.18': 'c2tnb359v1',
+ '1.2.840.10045.3.0.19': 'c2pnb368w1',
+ '1.2.840.10045.3.0.20': 'c2tnb431r1',
+ '1.2.840.10045.3.1.2': 'prime192v2',
+ '1.2.840.10045.3.1.3': 'prime192v3',
+ '1.2.840.10045.3.1.4': 'prime239v1',
+ '1.2.840.10045.3.1.5': 'prime239v2',
+ '1.2.840.10045.3.1.6': 'prime239v3',
+ # https://tools.ietf.org/html/rfc5480#page-5
+ '1.3.132.0.1': 'sect163k1',
+ '1.3.132.0.15': 'sect163r2',
+ '1.2.840.10045.3.1.1': 'secp192r1',
+ '1.3.132.0.33': 'secp224r1',
+ '1.3.132.0.26': 'sect233k1',
+ '1.2.840.10045.3.1.7': 'secp256r1',
+ '1.3.132.0.27': 'sect233r1',
+ '1.3.132.0.16': 'sect283k1',
+ '1.3.132.0.17': 'sect283r1',
+ '1.3.132.0.34': 'secp384r1',
+ '1.3.132.0.36': 'sect409k1',
+ '1.3.132.0.37': 'sect409r1',
+ '1.3.132.0.35': 'secp521r1',
+ '1.3.132.0.38': 'sect571k1',
+ '1.3.132.0.39': 'sect571r1',
+ }
+
+
+class ECDomainParameters(Choice):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 102
+ """
+
+ _alternatives = [
+ ('specified', SpecifiedECDomain),
+ ('named', NamedCurve),
+ ('implicit_ca', Null),
+ ]
+
+
+class ECPrivateKeyVersion(Integer):
+ """
+ Original Name: None
+ Source: http://www.secg.org/sec1-v2.pdf page 108
+ """
+
+ _map = {
+ 1: 'ecPrivkeyVer1',
+ }
+
+
+class ECPrivateKey(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 108
+ """
+
+ _fields = [
+ ('version', ECPrivateKeyVersion),
+ ('private_key', IntegerOctetString),
+ ('parameters', ECDomainParameters, {'explicit': 0, 'optional': True}),
+ ('public_key', ECPointBitString, {'explicit': 1, 'optional': True}),
+ ]
+
+
+class DSAParams(Sequence):
+ """
+ Parameters for a DSA public or private key
+
+ Original Name: Dss-Parms
+ Source: https://tools.ietf.org/html/rfc3279#page-9
+ """
+
+ _fields = [
+ ('p', Integer),
+ ('q', Integer),
+ ('g', Integer),
+ ]
+
+
+class Attribute(Sequence):
+ """
+ Source: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-X.501-198811-S!!PDF-E&type=items page 8
+ """
+
+ _fields = [
+ ('type', ObjectIdentifier),
+ ('values', SetOf, {'spec': Any}),
+ ]
+
+
+class Attributes(SetOf):
+ """
+ Source: https://tools.ietf.org/html/rfc5208#page-3
+ """
+
+ _child_spec = Attribute
+
+
+class PrivateKeyAlgorithmId(ObjectIdentifier):
+ """
+ These OIDs for various public keys are reused when storing private keys
+ inside of a PKCS#8 structure
+
+ Original Name: None
+ Source: https://tools.ietf.org/html/rfc3279
+ """
+
+ _map = {
+ # https://tools.ietf.org/html/rfc3279#page-19
+ '1.2.840.113549.1.1.1': 'rsa',
+ # https://tools.ietf.org/html/rfc3279#page-18
+ '1.2.840.10040.4.1': 'dsa',
+ # https://tools.ietf.org/html/rfc3279#page-13
+ '1.2.840.10045.2.1': 'ec',
+ }
+
+
+class PrivateKeyAlgorithm(_ForceNullParameters, Sequence):
+ """
+ Original Name: PrivateKeyAlgorithmIdentifier
+ Source: https://tools.ietf.org/html/rfc5208#page-3
+ """
+
+ _fields = [
+ ('algorithm', PrivateKeyAlgorithmId),
+ ('parameters', Any, {'optional': True}),
+ ]
+
+ _oid_pair = ('algorithm', 'parameters')
+ _oid_specs = {
+ 'dsa': DSAParams,
+ 'ec': ECDomainParameters,
+ }
+
+
+class PrivateKeyInfo(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc5208#page-3
+ """
+
+ _fields = [
+ ('version', Integer),
+ ('private_key_algorithm', PrivateKeyAlgorithm),
+ ('private_key', ParsableOctetString),
+ ('attributes', Attributes, {'implicit': 0, 'optional': True}),
+ ]
+
+ def _private_key_spec(self):
+ algorithm = self['private_key_algorithm']['algorithm'].native
+ return {
+ 'rsa': RSAPrivateKey,
+ 'dsa': Integer,
+ 'ec': ECPrivateKey,
+ }[algorithm]
+
+ _spec_callbacks = {
+ 'private_key': _private_key_spec
+ }
+
+ _algorithm = None
+ _bit_size = None
+ _public_key = None
+ _fingerprint = None
+
+ @classmethod
+ def wrap(cls, private_key, algorithm):
+ """
+ Wraps a private key in a PrivateKeyInfo structure
+
+ :param private_key:
+ A byte string or Asn1Value object of the private key
+
+ :param algorithm:
+ A unicode string of "rsa", "dsa" or "ec"
+
+ :return:
+ A PrivateKeyInfo object
+ """
+
+ if not isinstance(private_key, byte_cls) and not isinstance(private_key, Asn1Value):
+ raise TypeError(unwrap(
+ '''
+ private_key must be a byte string or Asn1Value, not %s
+ ''',
+ type_name(private_key)
+ ))
+
+ if algorithm == 'rsa':
+ if not isinstance(private_key, RSAPrivateKey):
+ private_key = RSAPrivateKey.load(private_key)
+ params = Null()
+ elif algorithm == 'dsa':
+ if not isinstance(private_key, DSAPrivateKey):
+ private_key = DSAPrivateKey.load(private_key)
+ params = DSAParams()
+ params['p'] = private_key['p']
+ params['q'] = private_key['q']
+ params['g'] = private_key['g']
+ public_key = private_key['public_key']
+ private_key = private_key['private_key']
+ elif algorithm == 'ec':
+ if not isinstance(private_key, ECPrivateKey):
+ private_key = ECPrivateKey.load(private_key)
+ else:
+ private_key = private_key.copy()
+ params = private_key['parameters']
+ del private_key['parameters']
+ else:
+ raise ValueError(unwrap(
+ '''
+ algorithm must be one of "rsa", "dsa", "ec", not %s
+ ''',
+ repr(algorithm)
+ ))
+
+ private_key_algo = PrivateKeyAlgorithm()
+ private_key_algo['algorithm'] = PrivateKeyAlgorithmId(algorithm)
+ private_key_algo['parameters'] = params
+
+ container = cls()
+ container._algorithm = algorithm
+ container['version'] = Integer(0)
+ container['private_key_algorithm'] = private_key_algo
+ container['private_key'] = private_key
+
+ # Here we save the DSA public key if possible since it is not contained
+ # within the PKCS#8 structure for a DSA key
+ if algorithm == 'dsa':
+ container._public_key = public_key
+
+ return container
+
+ def _compute_public_key(self):
+ """
+ Computes the public key corresponding to the current private key.
+
+ :return:
+ For RSA keys, an RSAPublicKey object. For DSA keys, an Integer
+ object. For EC keys, an ECPointBitString.
+ """
+
+ if self.algorithm == 'dsa':
+ params = self['private_key_algorithm']['parameters']
+ return Integer(pow(
+ params['g'].native,
+ self['private_key'].parsed.native,
+ params['p'].native
+ ))
+
+ if self.algorithm == 'rsa':
+ key = self['private_key'].parsed
+ return RSAPublicKey({
+ 'modulus': key['modulus'],
+ 'public_exponent': key['public_exponent'],
+ })
+
+ if self.algorithm == 'ec':
+ curve_type, details = self.curve
+
+ if curve_type == 'implicit_ca':
+ raise ValueError(unwrap(
+ '''
+ Unable to compute public key for EC key using Implicit CA
+ parameters
+ '''
+ ))
+
+ if curve_type == 'specified':
+ if details['field_id']['field_type'] == 'characteristic_two_field':
+ raise ValueError(unwrap(
+ '''
+ Unable to compute public key for EC key over a
+ characteristic two field
+ '''
+ ))
+
+ curve = PrimeCurve(
+ details['field_id']['parameters'],
+ int_from_bytes(details['curve']['a']),
+ int_from_bytes(details['curve']['b'])
+ )
+ base_x, base_y = self['private_key_algorithm']['parameters'].chosen['base'].to_coords()
+ base_point = PrimePoint(curve, base_x, base_y)
+
+ elif curve_type == 'named':
+ if details not in ('secp192r1', 'secp224r1', 'secp256r1', 'secp384r1', 'secp521r1'):
+ raise ValueError(unwrap(
+ '''
+ Unable to compute public key for EC named curve %s,
+ parameters not currently included
+ ''',
+ details
+ ))
+
+ base_point = {
+ 'secp192r1': SECP192R1_BASE_POINT,
+ 'secp224r1': SECP224R1_BASE_POINT,
+ 'secp256r1': SECP256R1_BASE_POINT,
+ 'secp384r1': SECP384R1_BASE_POINT,
+ 'secp521r1': SECP521R1_BASE_POINT,
+ }[details]
+
+ public_point = base_point * self['private_key'].parsed['private_key'].native
+ return ECPointBitString.from_coords(public_point.x, public_point.y)
+
+ def unwrap(self):
+ """
+ Unwraps the private key into an RSAPrivateKey, DSAPrivateKey or
+ ECPrivateKey object
+
+ :return:
+ An RSAPrivateKey, DSAPrivateKey or ECPrivateKey object
+ """
+
+ if self.algorithm == 'rsa':
+ return self['private_key'].parsed
+
+ if self.algorithm == 'dsa':
+ params = self['private_key_algorithm']['parameters']
+ return DSAPrivateKey({
+ 'version': 0,
+ 'p': params['p'],
+ 'q': params['q'],
+ 'g': params['g'],
+ 'public_key': self.public_key,
+ 'private_key': self['private_key'].parsed,
+ })
+
+ if self.algorithm == 'ec':
+ output = self['private_key'].parsed
+ output['parameters'] = self['private_key_algorithm']['parameters']
+ output['public_key'] = self.public_key
+ return output
+
+ @property
+ def curve(self):
+ """
+ Returns information about the curve used for an EC key
+
+ :raises:
+ ValueError - when the key is not an EC key
+
+ :return:
+ A two-element tuple, with the first element being a unicode string
+ of "implicit_ca", "specified" or "named". If the first element is
+ "implicit_ca", the second is None. If "specified", the second is
+ an OrderedDict that is the native version of SpecifiedECDomain. If
+ "named", the second is a unicode string of the curve name.
+ """
+
+ if self.algorithm != 'ec':
+ raise ValueError(unwrap(
+ '''
+ Only EC keys have a curve, this key is %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ params = self['private_key_algorithm']['parameters']
+ chosen = params.chosen
+
+ if params.name == 'implicit_ca':
+ value = None
+ else:
+ value = chosen.native
+
+ return (params.name, value)
+
+ @property
+ def hash_algo(self):
+ """
+ Returns the name of the family of hash algorithms used to generate a
+ DSA key
+
+ :raises:
+ ValueError - when the key is not a DSA key
+
+ :return:
+ A unicode string of "sha1" or "sha2"
+ """
+
+ if self.algorithm != 'dsa':
+ raise ValueError(unwrap(
+ '''
+ Only DSA keys are generated using a hash algorithm, this key is
+ %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ byte_len = math.log(self['private_key_algorithm']['parameters']['q'].native, 2) / 8
+
+ return 'sha1' if byte_len <= 20 else 'sha2'
+
+ @property
+ def algorithm(self):
+ """
+ :return:
+ A unicode string of "rsa", "dsa" or "ec"
+ """
+
+ if self._algorithm is None:
+ self._algorithm = self['private_key_algorithm']['algorithm'].native
+ return self._algorithm
+
+ @property
+ def bit_size(self):
+ """
+ :return:
+ The bit size of the private key, as an integer
+ """
+
+ if self._bit_size is None:
+ if self.algorithm == 'rsa':
+ prime = self['private_key'].parsed['modulus'].native
+ elif self.algorithm == 'dsa':
+ prime = self['private_key_algorithm']['parameters']['p'].native
+ elif self.algorithm == 'ec':
+ prime = self['private_key'].parsed['private_key'].native
+ self._bit_size = int(math.ceil(math.log(prime, 2)))
+ modulus = self._bit_size % 8
+ if modulus != 0:
+ self._bit_size += 8 - modulus
+ return self._bit_size
+
+ @property
+ def byte_size(self):
+ """
+ :return:
+ The byte size of the private key, as an integer
+ """
+
+ return int(math.ceil(self.bit_size / 8))
+
+ @property
+ def public_key(self):
+ """
+ :return:
+ If an RSA key, an RSAPublicKey object. If a DSA key, an Integer
+ object. If an EC key, an ECPointBitString object.
+ """
+
+ if self._public_key is None:
+ if self.algorithm == 'ec':
+ key = self['private_key'].parsed
+ if key['public_key']:
+ self._public_key = key['public_key'].untag()
+ else:
+ self._public_key = self._compute_public_key()
+ else:
+ self._public_key = self._compute_public_key()
+
+ return self._public_key
+
+ @property
+ def public_key_info(self):
+ """
+ :return:
+ A PublicKeyInfo object derived from this private key.
+ """
+
+ return PublicKeyInfo({
+ 'algorithm': {
+ 'algorithm': self.algorithm,
+ 'parameters': self['private_key_algorithm']['parameters']
+ },
+ 'public_key': self.public_key
+ })
+
+ @property
+ def fingerprint(self):
+ """
+ Creates a fingerprint that can be compared with a public key to see if
+ the two form a pair.
+
+ This fingerprint is not compatible with fingerprints generated by any
+ other software.
+
+ :return:
+ A byte string that is a sha256 hash of selected components (based
+ on the key type)
+ """
+
+ if self._fingerprint is None:
+ params = self['private_key_algorithm']['parameters']
+ key = self['private_key'].parsed
+
+ if self.algorithm == 'rsa':
+ to_hash = '%d:%d' % (
+ key['modulus'].native,
+ key['public_exponent'].native,
+ )
+
+ elif self.algorithm == 'dsa':
+ public_key = self.public_key
+ to_hash = '%d:%d:%d:%d' % (
+ params['p'].native,
+ params['q'].native,
+ params['g'].native,
+ public_key.native,
+ )
+
+ elif self.algorithm == 'ec':
+ public_key = key['public_key'].native
+ if public_key is None:
+ public_key = self.public_key.native
+
+ if params.name == 'named':
+ to_hash = '%s:' % params.chosen.native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += public_key
+
+ elif params.name == 'implicit_ca':
+ to_hash = public_key
+
+ elif params.name == 'specified':
+ to_hash = '%s:' % params.chosen['field_id']['parameters'].native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += b':' + params.chosen['curve']['a'].native
+ to_hash += b':' + params.chosen['curve']['b'].native
+ to_hash += public_key
+
+ if isinstance(to_hash, str_cls):
+ to_hash = to_hash.encode('utf-8')
+
+ self._fingerprint = hashlib.sha256(to_hash).digest()
+
+ return self._fingerprint
+
+
+class EncryptedPrivateKeyInfo(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc5208#page-4
+ """
+
+ _fields = [
+ ('encryption_algorithm', EncryptionAlgorithm),
+ ('encrypted_data', OctetString),
+ ]
+
+
+# These structures are from https://tools.ietf.org/html/rfc3279
+
+class ValidationParms(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3279#page-10
+ """
+
+ _fields = [
+ ('seed', BitString),
+ ('pgen_counter', Integer),
+ ]
+
+
+class DomainParameters(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3279#page-10
+ """
+
+ _fields = [
+ ('p', Integer),
+ ('g', Integer),
+ ('q', Integer),
+ ('j', Integer, {'optional': True}),
+ ('validation_params', ValidationParms, {'optional': True}),
+ ]
+
+
+class PublicKeyAlgorithmId(ObjectIdentifier):
+ """
+ Original Name: None
+ Source: https://tools.ietf.org/html/rfc3279
+ """
+
+ _map = {
+ # https://tools.ietf.org/html/rfc3279#page-19
+ '1.2.840.113549.1.1.1': 'rsa',
+ # https://tools.ietf.org/html/rfc3447#page-47
+ '1.2.840.113549.1.1.7': 'rsaes_oaep',
+ # https://tools.ietf.org/html/rfc3279#page-18
+ '1.2.840.10040.4.1': 'dsa',
+ # https://tools.ietf.org/html/rfc3279#page-13
+ '1.2.840.10045.2.1': 'ec',
+ # https://tools.ietf.org/html/rfc3279#page-10
+ '1.2.840.10046.2.1': 'dh',
+ }
+
+
+class PublicKeyAlgorithm(_ForceNullParameters, Sequence):
+ """
+ Original Name: AlgorithmIdentifier
+ Source: https://tools.ietf.org/html/rfc5280#page-18
+ """
+
+ _fields = [
+ ('algorithm', PublicKeyAlgorithmId),
+ ('parameters', Any, {'optional': True}),
+ ]
+
+ _oid_pair = ('algorithm', 'parameters')
+ _oid_specs = {
+ 'dsa': DSAParams,
+ 'ec': ECDomainParameters,
+ 'dh': DomainParameters,
+ 'rsaes_oaep': RSAESOAEPParams,
+ }
+
+
+class PublicKeyInfo(Sequence):
+ """
+ Original Name: SubjectPublicKeyInfo
+ Source: https://tools.ietf.org/html/rfc5280#page-17
+ """
+
+ _fields = [
+ ('algorithm', PublicKeyAlgorithm),
+ ('public_key', ParsableOctetBitString),
+ ]
+
+ def _public_key_spec(self):
+ algorithm = self['algorithm']['algorithm'].native
+ return {
+ 'rsa': RSAPublicKey,
+ 'rsaes_oaep': RSAPublicKey,
+ 'dsa': Integer,
+ # We override the field spec with ECPoint so that users can easily
+ # decompose the byte string into the constituent X and Y coords
+ 'ec': (ECPointBitString, None),
+ 'dh': Integer,
+ }[algorithm]
+
+ _spec_callbacks = {
+ 'public_key': _public_key_spec
+ }
+
+ _algorithm = None
+ _bit_size = None
+ _fingerprint = None
+ _sha1 = None
+ _sha256 = None
+
+ @classmethod
+ def wrap(cls, public_key, algorithm):
+ """
+ Wraps a public key in a PublicKeyInfo structure
+
+ :param public_key:
+ A byte string or Asn1Value object of the public key
+
+ :param algorithm:
+ A unicode string of "rsa"
+
+ :return:
+ A PublicKeyInfo object
+ """
+
+ if not isinstance(public_key, byte_cls) and not isinstance(public_key, Asn1Value):
+ raise TypeError(unwrap(
+ '''
+ public_key must be a byte string or Asn1Value, not %s
+ ''',
+ type_name(public_key)
+ ))
+
+ if algorithm != 'rsa':
+ raise ValueError(unwrap(
+ '''
+ algorithm must "rsa", not %s
+ ''',
+ repr(algorithm)
+ ))
+
+ algo = PublicKeyAlgorithm()
+ algo['algorithm'] = PublicKeyAlgorithmId(algorithm)
+ algo['parameters'] = Null()
+
+ container = cls()
+ container['algorithm'] = algo
+ if isinstance(public_key, Asn1Value):
+ public_key = public_key.untag().dump()
+ container['public_key'] = ParsableOctetBitString(public_key)
+
+ return container
+
+ def unwrap(self):
+ """
+ Unwraps an RSA public key into an RSAPublicKey object. Does not support
+ DSA or EC public keys since they do not have an unwrapped form.
+
+ :return:
+ An RSAPublicKey object
+ """
+
+ if self.algorithm == 'rsa':
+ return self['public_key'].parsed
+
+ key_type = self.algorithm.upper()
+ a_an = 'an' if key_type == 'EC' else 'a'
+ raise ValueError(unwrap(
+ '''
+ Only RSA public keys may be unwrapped - this key is %s %s public
+ key
+ ''',
+ a_an,
+ key_type
+ ))
+
+ @property
+ def curve(self):
+ """
+ Returns information about the curve used for an EC key
+
+ :raises:
+ ValueError - when the key is not an EC key
+
+ :return:
+ A two-element tuple, with the first element being a unicode string
+ of "implicit_ca", "specified" or "named". If the first element is
+ "implicit_ca", the second is None. If "specified", the second is
+ an OrderedDict that is the native version of SpecifiedECDomain. If
+ "named", the second is a unicode string of the curve name.
+ """
+
+ if self.algorithm != 'ec':
+ raise ValueError(unwrap(
+ '''
+ Only EC keys have a curve, this key is %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ params = self['algorithm']['parameters']
+ chosen = params.chosen
+
+ if params.name == 'implicit_ca':
+ value = None
+ else:
+ value = chosen.native
+
+ return (params.name, value)
+
+ @property
+ def hash_algo(self):
+ """
+ Returns the name of the family of hash algorithms used to generate a
+ DSA key
+
+ :raises:
+ ValueError - when the key is not a DSA key
+
+ :return:
+ A unicode string of "sha1" or "sha2" or None if no parameters are
+ present
+ """
+
+ if self.algorithm != 'dsa':
+ raise ValueError(unwrap(
+ '''
+ Only DSA keys are generated using a hash algorithm, this key is
+ %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ parameters = self['algorithm']['parameters']
+ if parameters.native is None:
+ return None
+
+ byte_len = math.log(parameters['q'].native, 2) / 8
+
+ return 'sha1' if byte_len <= 20 else 'sha2'
+
+ @property
+ def algorithm(self):
+ """
+ :return:
+ A unicode string of "rsa", "dsa" or "ec"
+ """
+
+ if self._algorithm is None:
+ self._algorithm = self['algorithm']['algorithm'].native
+ return self._algorithm
+
+ @property
+ def bit_size(self):
+ """
+ :return:
+ The bit size of the public key, as an integer
+ """
+
+ if self._bit_size is None:
+ if self.algorithm == 'ec':
+ self._bit_size = ((len(self['public_key'].native) - 1) / 2) * 8
+ else:
+ if self.algorithm == 'rsa':
+ prime = self['public_key'].parsed['modulus'].native
+ elif self.algorithm == 'dsa':
+ prime = self['algorithm']['parameters']['p'].native
+ self._bit_size = int(math.ceil(math.log(prime, 2)))
+ modulus = self._bit_size % 8
+ if modulus != 0:
+ self._bit_size += 8 - modulus
+
+ return self._bit_size
+
+ @property
+ def byte_size(self):
+ """
+ :return:
+ The byte size of the public key, as an integer
+ """
+
+ return int(math.ceil(self.bit_size / 8))
+
+ @property
+ def sha1(self):
+ """
+ :return:
+ The SHA1 hash of the DER-encoded bytes of this public key info
+ """
+
+ if self._sha1 is None:
+ self._sha1 = hashlib.sha1(byte_cls(self['public_key'])).digest()
+ return self._sha1
+
+ @property
+ def sha256(self):
+ """
+ :return:
+ The SHA-256 hash of the DER-encoded bytes of this public key info
+ """
+
+ if self._sha256 is None:
+ self._sha256 = hashlib.sha256(byte_cls(self['public_key'])).digest()
+ return self._sha256
+
+ @property
+ def fingerprint(self):
+ """
+ Creates a fingerprint that can be compared with a private key to see if
+ the two form a pair.
+
+ This fingerprint is not compatible with fingerprints generated by any
+ other software.
+
+ :return:
+ A byte string that is a sha256 hash of selected components (based
+ on the key type)
+ """
+
+ if self._fingerprint is None:
+ key_type = self['algorithm']['algorithm'].native
+ params = self['algorithm']['parameters']
+
+ if key_type == 'rsa':
+ key = self['public_key'].parsed
+ to_hash = '%d:%d' % (
+ key['modulus'].native,
+ key['public_exponent'].native,
+ )
+
+ elif key_type == 'dsa':
+ key = self['public_key'].parsed
+ to_hash = '%d:%d:%d:%d' % (
+ params['p'].native,
+ params['q'].native,
+ params['g'].native,
+ key.native,
+ )
+
+ elif key_type == 'ec':
+ key = self['public_key']
+
+ if params.name == 'named':
+ to_hash = '%s:' % params.chosen.native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += key.native
+
+ elif params.name == 'implicit_ca':
+ to_hash = key.native
+
+ elif params.name == 'specified':
+ to_hash = '%s:' % params.chosen['field_id']['parameters'].native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += b':' + params.chosen['curve']['a'].native
+ to_hash += b':' + params.chosen['curve']['b'].native
+ to_hash += key.native
+
+ if isinstance(to_hash, str_cls):
+ to_hash = to_hash.encode('utf-8')
+
+ self._fingerprint = hashlib.sha256(to_hash).digest()
+
+ return self._fingerprint