diff options
author | Haibo Huang <hhb@google.com> | 2019-10-02 20:21:15 -0700 |
---|---|---|
committer | Haibo Huang <hhb@google.com> | 2019-10-02 20:21:15 -0700 |
commit | bbe5f36e271aa1697a66502e07920f6b7037b537 (patch) | |
tree | d4f1b19f018cbc3d55a4662bd96cb5de12364101 /asn1crypto | |
parent | 9d1650023fd202f8e544513a3868c754261cd533 (diff) | |
parent | fcbba299234325920f6cf042cbb062f4cfdb3ec0 (diff) | |
download | asn1crypto-bbe5f36e271aa1697a66502e07920f6b7037b537.tar.gz |
Upgrade python/asn1crypto to 1.0.0ndk-sysroot-r21
Test: None
Change-Id: Id976fdd7c170f3111e35c4a2cde69aba8715c243
Diffstat (limited to 'asn1crypto')
-rw-r--r-- | asn1crypto/_elliptic_curve.py | 314 | ||||
-rw-r--r-- | asn1crypto/_errors.py | 11 | ||||
-rw-r--r-- | asn1crypto/_ffi.py | 45 | ||||
-rw-r--r-- | asn1crypto/_int.py | 137 | ||||
-rw-r--r-- | asn1crypto/_iri.py | 11 | ||||
-rw-r--r-- | asn1crypto/_perf/__init__.py | 0 | ||||
-rw-r--r-- | asn1crypto/_perf/_big_num_ctypes.py | 69 | ||||
-rw-r--r-- | asn1crypto/algos.py | 38 | ||||
-rw-r--r-- | asn1crypto/cms.py | 31 | ||||
-rw-r--r-- | asn1crypto/core.py | 868 | ||||
-rw-r--r-- | asn1crypto/keys.py | 402 | ||||
-rw-r--r-- | asn1crypto/ocsp.py | 55 | ||||
-rw-r--r-- | asn1crypto/parser.py | 14 | ||||
-rw-r--r-- | asn1crypto/util.py | 746 | ||||
-rw-r--r-- | asn1crypto/version.py | 4 | ||||
-rw-r--r-- | asn1crypto/x509.py | 34 |
16 files changed, 1446 insertions, 1333 deletions
diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py deleted file mode 100644 index 8c0f12d..0000000 --- a/asn1crypto/_elliptic_curve.py +++ /dev/null @@ -1,314 +0,0 @@ -# coding: utf-8 - -""" -Classes and objects to represent prime-field elliptic curves and points on them. -Exports the following items: - - - PrimeCurve() - - PrimePoint() - - SECP192R1_CURVE - - SECP192R1_BASE_POINT - - SECP224R1_CURVE - - SECP224R1_BASE_POINT - - SECP256R1_CURVE - - SECP256R1_BASE_POINT - - SECP384R1_CURVE - - SECP384R1_BASE_POINT - - SECP521R1_CURVE - - SECP521R1_BASE_POINT - -The curve constants are all PrimeCurve() objects and the base point constants -are all PrimePoint() objects. - -Some of the following source code is derived from -http://webpages.charter.net/curryfans/peter/downloads.html, but has been heavily -modified to fit into this projects lint settings. The original project license -is listed below: - -Copyright (c) 2014 Peter Pearson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -from ._int import inverse_mod - - -class PrimeCurve(): - """ - Elliptic curve over a prime field. Characteristic two field curves are not - supported. - """ - - def __init__(self, p, a, b): - """ - The curve of points satisfying y^2 = x^3 + a*x + b (mod p) - - :param p: - The prime number as an integer - - :param a: - The component a as an integer - - :param b: - The component b as an integer - """ - - self.p = p - self.a = a - self.b = b - - def contains(self, point): - """ - :param point: - A Point object - - :return: - Boolean if the point is on this curve - """ - - y2 = point.y * point.y - x3 = point.x * point.x * point.x - return (y2 - (x3 + self.a * point.x + self.b)) % self.p == 0 - - -class PrimePoint(): - """ - A point on a prime-field elliptic curve - """ - - def __init__(self, curve, x, y, order=None): - """ - :param curve: - A PrimeCurve object - - :param x: - The x coordinate of the point as an integer - - :param y: - The y coordinate of the point as an integer - - :param order: - The order of the point, as an integer - optional - """ - - self.curve = curve - self.x = x - self.y = y - self.order = order - - # self.curve is allowed to be None only for INFINITY: - if self.curve: - if not self.curve.contains(self): - raise ValueError('Invalid EC point') - - if self.order: - if self * self.order != INFINITY: - raise ValueError('Invalid EC point') - - def __cmp__(self, other): - """ - :param other: - A PrimePoint object - - :return: - 0 if identical, 1 otherwise - """ - if self.curve == other.curve and self.x == other.x and self.y == other.y: - return 0 - else: - return 1 - - def __add__(self, other): - """ - :param other: - A PrimePoint object - - :return: - A PrimePoint object - """ - - # X9.62 B.3: - - if other == INFINITY: - return self - if self == INFINITY: - return other - assert self.curve == other.curve - if self.x == other.x: - if (self.y + other.y) % self.curve.p == 0: - return INFINITY - else: - return self.double() - - p = self.curve.p - - l_ = ((other.y - self.y) * inverse_mod(other.x - self.x, p)) % p - - x3 = (l_ * l_ - self.x - other.x) % p - y3 = (l_ * (self.x - x3) - self.y) % p - - return PrimePoint(self.curve, x3, y3) - - def __mul__(self, other): - """ - :param other: - An integer to multiple the Point by - - :return: - A PrimePoint object - """ - - def leftmost_bit(x): - assert x > 0 - result = 1 - while result <= x: - result = 2 * result - return result // 2 - - e = other - if self.order: - e = e % self.order - if e == 0: - return INFINITY - if self == INFINITY: - return INFINITY - assert e > 0 - - # From X9.62 D.3.2: - - e3 = 3 * e - negative_self = PrimePoint(self.curve, self.x, -self.y, self.order) - i = leftmost_bit(e3) // 2 - result = self - # print "Multiplying %s by %d (e3 = %d):" % ( self, other, e3 ) - while i > 1: - result = result.double() - if (e3 & i) != 0 and (e & i) == 0: - result = result + self - if (e3 & i) == 0 and (e & i) != 0: - result = result + negative_self - # print ". . . i = %d, result = %s" % ( i, result ) - i = i // 2 - - return result - - def __rmul__(self, other): - """ - :param other: - An integer to multiple the Point by - - :return: - A PrimePoint object - """ - - return self * other - - def double(self): - """ - :return: - A PrimePoint object that is twice this point - """ - - # X9.62 B.3: - - p = self.curve.p - a = self.curve.a - - l_ = ((3 * self.x * self.x + a) * inverse_mod(2 * self.y, p)) % p - - x3 = (l_ * l_ - 2 * self.x) % p - y3 = (l_ * (self.x - x3) - self.y) % p - - return PrimePoint(self.curve, x3, y3) - - -# This one point is the Point At Infinity for all purposes: -INFINITY = PrimePoint(None, None, None) - - -# NIST Curve P-192: -SECP192R1_CURVE = PrimeCurve( - 6277101735386680763835789423207666416083908700390324961279, - -3, - 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1 -) -SECP192R1_BASE_POINT = PrimePoint( - SECP192R1_CURVE, - 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012, - 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811, - 6277101735386680763835789423176059013767194773182842284081 -) - - -# NIST Curve P-224: -SECP224R1_CURVE = PrimeCurve( - 26959946667150639794667015087019630673557916260026308143510066298881, - -3, - 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4 -) -SECP224R1_BASE_POINT = PrimePoint( - SECP224R1_CURVE, - 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21, - 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34, - 26959946667150639794667015087019625940457807714424391721682722368061 -) - - -# NIST Curve P-256: -SECP256R1_CURVE = PrimeCurve( - 115792089210356248762697446949407573530086143415290314195533631308867097853951, - -3, - 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b -) -SECP256R1_BASE_POINT = PrimePoint( - SECP256R1_CURVE, - 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, - 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5, - 115792089210356248762697446949407573529996955224135760342422259061068512044369 -) - - -# NIST Curve P-384: -SECP384R1_CURVE = PrimeCurve( - 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, # noqa - -3, - 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef -) -SECP384R1_BASE_POINT = PrimePoint( - SECP384R1_CURVE, - 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7, - 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f, - 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643 -) - - -# NIST Curve P-521: -SECP521R1_CURVE = PrimeCurve( - 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, # noqa - -3, - 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 # noqa -) -SECP521R1_BASE_POINT = PrimePoint( - SECP521R1_CURVE, - 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, # noqa - 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, # noqa - 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 # noqa -) diff --git a/asn1crypto/_errors.py b/asn1crypto/_errors.py index cc785a5..d8797a2 100644 --- a/asn1crypto/_errors.py +++ b/asn1crypto/_errors.py @@ -1,9 +1,10 @@ # coding: utf-8 """ -Helper for formatting exception messages. Exports the following items: +Exports the following items: - unwrap() + - APIException() """ from __future__ import unicode_literals, division, absolute_import, print_function @@ -12,6 +13,14 @@ import re import textwrap +class APIException(Exception): + """ + An exception indicating an API has been removed from asn1crypto + """ + + pass + + def unwrap(string, *params): """ Takes a multi-line string and does the following: diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py deleted file mode 100644 index 2a4f5bf..0000000 --- a/asn1crypto/_ffi.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 - -""" -FFI helper compatibility functions. Exports the following items: - - - LibraryNotFoundError - - FFIEngineError - - bytes_from_buffer() - - buffer_from_bytes() - - null() -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -from ctypes import create_string_buffer - - -def buffer_from_bytes(initializer): - return create_string_buffer(initializer) - - -def bytes_from_buffer(buffer, maxlen=None): - return buffer.raw - - -def null(): - return None - - -class LibraryNotFoundError(Exception): - - """ - An exception when trying to find a shared library - """ - - pass - - -class FFIEngineError(Exception): - - """ - An exception when trying to instantiate ctypes or cffi - """ - - pass diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index d0c2319..094fc95 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -1,143 +1,6 @@ # coding: utf-8 - -""" -Function for calculating the modular inverse. Exports the following items: - - - inverse_mod() - -Source code is derived from -http://webpages.charter.net/curryfans/peter/downloads.html, but has been heavily -modified to fit into this projects lint settings. The original project license -is listed below: - -Copyright (c) 2014 Peter Pearson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - from __future__ import unicode_literals, division, absolute_import, print_function -import math -import platform - -from .util import int_to_bytes, int_from_bytes - -# First try to use ctypes with OpenSSL for better performance -try: - from ._ffi import ( - buffer_from_bytes, - bytes_from_buffer, - FFIEngineError, - LibraryNotFoundError, - null, - ) - - # Some versions of PyPy have segfault issues, so we just punt on PyPy - if platform.python_implementation() == 'PyPy': - raise EnvironmentError() - - try: - from ._perf._big_num_ctypes import libcrypto - - def inverse_mod(a, p): - """ - Compute the modular inverse of a (mod p) - - :param a: - An integer - - :param p: - An integer - - :return: - An integer - """ - - ctx = libcrypto.BN_CTX_new() - - a_bytes = int_to_bytes(abs(a)) - p_bytes = int_to_bytes(abs(p)) - - a_buf = buffer_from_bytes(a_bytes) - a_bn = libcrypto.BN_bin2bn(a_buf, len(a_bytes), null()) - if a < 0: - libcrypto.BN_set_negative(a_bn, 1) - - p_buf = buffer_from_bytes(p_bytes) - p_bn = libcrypto.BN_bin2bn(p_buf, len(p_bytes), null()) - if p < 0: - libcrypto.BN_set_negative(p_bn, 1) - - r_bn = libcrypto.BN_mod_inverse(null(), a_bn, p_bn, ctx) - r_len_bits = libcrypto.BN_num_bits(r_bn) - r_len = int(math.ceil(r_len_bits / 8)) - r_buf = buffer_from_bytes(r_len) - libcrypto.BN_bn2bin(r_bn, r_buf) - r_bytes = bytes_from_buffer(r_buf, r_len) - result = int_from_bytes(r_bytes) - - libcrypto.BN_free(a_bn) - libcrypto.BN_free(p_bn) - libcrypto.BN_free(r_bn) - libcrypto.BN_CTX_free(ctx) - - return result - except (LibraryNotFoundError, FFIEngineError): - raise EnvironmentError() - -# If there was an issue using ctypes or OpenSSL, we fall back to pure python -except (EnvironmentError, ImportError): - - def inverse_mod(a, p): - """ - Compute the modular inverse of a (mod p) - - :param a: - An integer - - :param p: - An integer - - :return: - An integer - """ - - if a < 0 or p <= a: - a = a % p - - # From Ferguson and Schneier, roughly: - - c, d = a, p - uc, vc, ud, vd = 1, 0, 0, 1 - while c != 0: - q, c, d = divmod(d, c) + (c,) - uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc - - # At this point, d is the GCD, and ud*a+vd*p = d. - # If d == 1, this means that ud is a inverse. - - assert d == 1 - if ud > 0: - return ud - else: - return ud + p - def fill_width(bytes_, width): """ diff --git a/asn1crypto/_iri.py b/asn1crypto/_iri.py index 57ddd40..7394b4d 100644 --- a/asn1crypto/_iri.py +++ b/asn1crypto/_iri.py @@ -34,13 +34,16 @@ else: ) -def iri_to_uri(value): +def iri_to_uri(value, normalize=False): """ - Normalizes and encodes a unicode IRI into an ASCII byte string URI + Encodes a unicode IRI into an ASCII byte string URI :param value: A unicode string of an IRI + :param normalize: + A bool that controls URI normalization + :return: A byte string of the ASCII-encoded URI """ @@ -91,7 +94,7 @@ def iri_to_uri(value): if port is not None: default_http = scheme == b'http' and port == b'80' default_https = scheme == b'https' and port == b'443' - if not default_http and not default_https: + if not normalize or (not default_http and not default_https): netloc += b':' + port # RFC 3986 allows a path to contain sub-delims, plus "@" and ":" @@ -101,7 +104,7 @@ def iri_to_uri(value): # RFC 3986 allows the fragment to contain sub-delims, plus "@", ":" , "/" and "?" fragment = _urlquote(parsed.fragment, safe='/?!$&\'()*+,;=@:') - if query is None and fragment is None and path == b'/': + if normalize and query is None and fragment is None and path == b'/': path = None # Python 2.7 compat diff --git a/asn1crypto/_perf/__init__.py b/asn1crypto/_perf/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/asn1crypto/_perf/__init__.py +++ /dev/null diff --git a/asn1crypto/_perf/_big_num_ctypes.py b/asn1crypto/_perf/_big_num_ctypes.py deleted file mode 100644 index 8e37e9b..0000000 --- a/asn1crypto/_perf/_big_num_ctypes.py +++ /dev/null @@ -1,69 +0,0 @@ -# coding: utf-8 - -""" -ctypes interface for BN_mod_inverse() function from OpenSSL. Exports the -following items: - - - libcrypto - - BN_bn2bin() - - BN_CTX_free() - - BN_CTX_new() - - BN_free() - - BN_mod_inverse() - - BN_new() - - BN_num_bits() - - BN_set_negative() - -Will raise asn1crypto._ffi.LibraryNotFoundError() if libcrypto can not be -found. Will raise asn1crypto._ffi.FFIEngineError() if there is an error -interfacing with libcrypto. -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -import sys - -from ctypes import CDLL, c_int, c_char_p, c_void_p -from ctypes.util import find_library - -from .._ffi import LibraryNotFoundError, FFIEngineError - - -try: - # On Python 2, the unicode string here may raise a UnicodeDecodeError as it - # tries to join a bytestring path to the unicode name "crypto" - libcrypto_path = find_library(b'crypto' if sys.version_info < (3,) else 'crypto') - if not libcrypto_path: - raise LibraryNotFoundError('The library libcrypto could not be found') - - libcrypto = CDLL(libcrypto_path) - - libcrypto.BN_new.argtypes = [] - libcrypto.BN_new.restype = c_void_p - - libcrypto.BN_bin2bn.argtypes = [c_char_p, c_int, c_void_p] - libcrypto.BN_bin2bn.restype = c_void_p - - libcrypto.BN_bn2bin.argtypes = [c_void_p, c_char_p] - libcrypto.BN_bn2bin.restype = c_int - - libcrypto.BN_set_negative.argtypes = [c_void_p, c_int] - libcrypto.BN_set_negative.restype = None - - libcrypto.BN_num_bits.argtypes = [c_void_p] - libcrypto.BN_num_bits.restype = c_int - - libcrypto.BN_free.argtypes = [c_void_p] - libcrypto.BN_free.restype = None - - libcrypto.BN_CTX_new.argtypes = [] - libcrypto.BN_CTX_new.restype = c_void_p - - libcrypto.BN_CTX_free.argtypes = [c_void_p] - libcrypto.BN_CTX_free.restype = None - - libcrypto.BN_mod_inverse.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] - libcrypto.BN_mod_inverse.restype = c_void_p - -except (AttributeError): - raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index c805433..d49be26 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -114,6 +114,10 @@ class HmacAlgorithmId(ObjectIdentifier): '1.2.840.113549.2.11': 'sha512', '1.2.840.113549.2.12': 'sha512_224', '1.2.840.113549.2.13': 'sha512_256', + '2.16.840.1.101.3.4.2.13': 'sha3_224', + '2.16.840.1.101.3.4.2.14': 'sha3_256', + '2.16.840.1.101.3.4.2.15': 'sha3_384', + '2.16.840.1.101.3.4.2.16': 'sha3_512', } @@ -135,6 +139,14 @@ class DigestAlgorithmId(ObjectIdentifier): '2.16.840.1.101.3.4.2.3': 'sha512', '2.16.840.1.101.3.4.2.5': 'sha512_224', '2.16.840.1.101.3.4.2.6': 'sha512_256', + '2.16.840.1.101.3.4.2.7': 'sha3_224', + '2.16.840.1.101.3.4.2.8': 'sha3_256', + '2.16.840.1.101.3.4.2.9': 'sha3_384', + '2.16.840.1.101.3.4.2.10': 'sha3_512', + '2.16.840.1.101.3.4.2.11': 'shake128', + '2.16.840.1.101.3.4.2.12': 'shake256', + '2.16.840.1.101.3.4.2.17': 'shake128_len', + '2.16.840.1.101.3.4.2.18': 'shake256_len', } @@ -240,6 +252,10 @@ class SignedDigestAlgorithmId(ObjectIdentifier): '1.2.840.10045.4.3.2': 'sha256_ecdsa', '1.2.840.10045.4.3.3': 'sha384_ecdsa', '1.2.840.10045.4.3.4': 'sha512_ecdsa', + '2.16.840.1.101.3.4.3.9': 'sha3_224_ecdsa', + '2.16.840.1.101.3.4.3.10': 'sha3_256_ecdsa', + '2.16.840.1.101.3.4.3.11': 'sha3_384_ecdsa', + '2.16.840.1.101.3.4.3.12': 'sha3_512_ecdsa', # For when the digest is specified elsewhere in a Sequence '1.2.840.113549.1.1.1': 'rsassa_pkcs1v15', '1.2.840.10040.4.1': 'dsa', @@ -266,6 +282,10 @@ class SignedDigestAlgorithmId(ObjectIdentifier): 'sha384_rsa': '1.2.840.113549.1.1.12', 'sha512_ecdsa': '1.2.840.10045.4.3.4', 'sha512_rsa': '1.2.840.113549.1.1.13', + 'sha3_224_ecdsa': '2.16.840.1.101.3.4.3.9', + 'sha3_256_ecdsa': '2.16.840.1.101.3.4.3.10', + 'sha3_384_ecdsa': '2.16.840.1.101.3.4.3.11', + 'sha3_512_ecdsa': '2.16.840.1.101.3.4.3.12', } @@ -309,6 +329,10 @@ class SignedDigestAlgorithm(_ForceNullParameters, Sequence): 'sha256_ecdsa': 'ecdsa', 'sha384_ecdsa': 'ecdsa', 'sha512_ecdsa': 'ecdsa', + 'sha3_224_ecdsa': 'ecdsa', + 'sha3_256_ecdsa': 'ecdsa', + 'sha3_384_ecdsa': 'ecdsa', + 'sha3_512_ecdsa': 'ecdsa', 'ecdsa': 'ecdsa', } if algorithm in algo_map: @@ -454,6 +478,15 @@ class Pbes1Params(Sequence): ] +class CcmParams(Sequence): + # https://tools.ietf.org/html/rfc5084 + # aes_ICVlen: 4 | 6 | 8 | 10 | 12 | 14 | 16 + _fields = [ + ('aes_nonce', OctetString), + ('aes_icvlen', Integer), + ] + + class PSourceAlgorithmId(ObjectIdentifier): _map = { '1.2.840.113549.1.1.9': 'p_specified', @@ -563,6 +596,7 @@ class EncryptionAlgorithmId(ObjectIdentifier): '1.3.14.3.2.7': 'des', '1.2.840.113549.3.7': 'tripledes_3key', '1.2.840.113549.3.2': 'rc2', + '1.2.840.113549.3.4': 'rc4', '1.2.840.113549.3.9': 'rc5', # From http://csrc.nist.gov/groups/ST/crypto_apps_infra/csor/algorithms.html#AES '2.16.840.1.101.3.4.1.1': 'aes128_ecb', @@ -628,6 +662,10 @@ class EncryptionAlgorithm(_ForceNullParameters, Sequence): 'aes128_ofb': OctetString, 'aes192_ofb': OctetString, 'aes256_ofb': OctetString, + # From RFC5084 + 'aes128_ccm': CcmParams, + 'aes192_ccm': CcmParams, + 'aes256_ccm': CcmParams, # From PKCS#5 'pbes1_md2_des': Pbes1Params, 'pbes1_md5_des': Pbes1Params, diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 9cad949..1fabc13 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -32,6 +32,7 @@ from .algos import ( EncryptionAlgorithm, HmacAlgorithm, KdfAlgorithm, + RSAESOAEPParams, SignedDigestAlgorithm, ) from .core import ( @@ -103,6 +104,14 @@ class CMSAttributeType(ObjectIdentifier): '1.2.840.113549.1.9.16.2.14': 'signature_time_stamp_token', # https://tools.ietf.org/html/rfc6211#page-5 '1.2.840.113549.1.9.52': 'cms_algorithm_protection', + # https://docs.microsoft.com/en-us/previous-versions/hh968145(v%3Dvs.85) + '1.3.6.1.4.1.311.2.4.1': 'microsoft_nested_signature', + # Some places refer to this as SPC_RFC3161_OBJID, others szOID_RFC3161_counterSign. + # https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/ns-wincrypt-crypt_algorithm_identifier + # refers to szOID_RFC3161_counterSign as "1.2.840.113549.1.9.16.1.4", + # but that OID is also called szOID_TIMESTAMP_TOKEN. Because of there being + # no canonical source for this OID, we give it our own name + '1.3.6.1.4.1.311.3.3.1': 'microsoft_time_stamp_token', } @@ -649,7 +658,8 @@ class RecipientIdentifier(Choice): class KeyEncryptionAlgorithmId(ObjectIdentifier): _map = { - '1.2.840.113549.1.1.1': 'rsa', + '1.2.840.113549.1.1.1': 'rsaes_pkcs1v15', + '1.2.840.113549.1.1.7': 'rsaes_oaep', '2.16.840.1.101.3.4.1.5': 'aes128_wrap', '2.16.840.1.101.3.4.1.8': 'aes128_wrap_pad', '2.16.840.1.101.3.4.1.25': 'aes192_wrap', @@ -658,6 +668,18 @@ class KeyEncryptionAlgorithmId(ObjectIdentifier): '2.16.840.1.101.3.4.1.48': 'aes256_wrap_pad', } + _reverse_map = { + 'rsa': '1.2.840.113549.1.1.1', + 'rsaes_pkcs1v15': '1.2.840.113549.1.1.1', + 'rsaes_oaep': '1.2.840.113549.1.1.7', + 'aes128_wrap': '2.16.840.1.101.3.4.1.5', + 'aes128_wrap_pad': '2.16.840.1.101.3.4.1.8', + 'aes192_wrap': '2.16.840.1.101.3.4.1.25', + 'aes192_wrap_pad': '2.16.840.1.101.3.4.1.28', + 'aes256_wrap': '2.16.840.1.101.3.4.1.45', + 'aes256_wrap_pad': '2.16.840.1.101.3.4.1.48', + } + class KeyEncryptionAlgorithm(_ForceNullParameters, Sequence): _fields = [ @@ -665,6 +687,11 @@ class KeyEncryptionAlgorithm(_ForceNullParameters, Sequence): ('parameters', Any, {'optional': True}), ] + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'rsaes_oaep': RSAESOAEPParams, + } + class KeyTransRecipientInfo(Sequence): _fields = [ @@ -929,4 +956,6 @@ CMSAttribute._oid_specs = { 'counter_signature': SignerInfos, 'signature_time_stamp_token': SetOfContentInfo, 'cms_algorithm_protection': SetOfCMSAlgorithmProtection, + 'microsoft_nested_signature': SetOfContentInfo, + 'microsoft_time_stamp_token': SetOfContentInfo, } diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 14a8203..933f8ca 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -49,6 +49,7 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function from datetime import datetime, timedelta +from fractions import Fraction import binascii import copy import math @@ -60,7 +61,7 @@ from ._errors import unwrap from ._ordereddict import OrderedDict from ._types import type_name, str_cls, byte_cls, int_types, chr_cls from .parser import _parse, _dump_header -from .util import int_to_bytes, int_from_bytes, timezone, extended_datetime +from .util import int_to_bytes, int_from_bytes, timezone, extended_datetime, create_timezone, utc_with_dst if sys.version_info <= (3,): from cStringIO import StringIO as BytesIO @@ -230,7 +231,7 @@ class Asn1Value(object): return value def __init__(self, explicit=None, implicit=None, no_explicit=False, tag_type=None, class_=None, tag=None, - optional=None, default=None, contents=None): + optional=None, default=None, contents=None, method=None): """ The optional parameter is not used, but rather included so we don't have to delete it from the parameter dictionary when passing as keyword @@ -275,6 +276,12 @@ class Asn1Value(object): :param contents: A byte string of the encoded contents of the value + :param method: + The method for the value - no default value since this is + normally set on a class. Valid values include: + - "primitive" or 0 + - "constructed" or 1 + :raises: ValueError - when implicit, explicit, tag_type, class_ or tag are invalid values """ @@ -384,7 +391,7 @@ class Asn1Value(object): self.implicit = True else: if class_ is not None: - if class_ not in CLASS_NUM_TO_NAME_MAP: + if class_ not in CLASS_NAME_TO_NUM_MAP: raise ValueError(unwrap( ''' class_ must be one of "universal", "application", @@ -394,9 +401,27 @@ class Asn1Value(object): )) self.class_ = CLASS_NAME_TO_NUM_MAP[class_] + if self.class_ is None: + self.class_ = 0 + if tag is not None: self.tag = tag + if method is not None: + if method not in set(["primitive", 0, "constructed", 1]): + raise ValueError(unwrap( + ''' + method must be one of "primitive" or "constructed", + not %s + ''', + repr(method) + )) + if method == "primitive": + method = 0 + elif method == "constructed": + method = 1 + self.method = method + if no_explicit: self.explicit = None @@ -603,6 +628,10 @@ class Asn1Value(object): contents = self.contents + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + if self._header is None or force: if isinstance(self, Constructable) and self._indefinite: self.method = 0 @@ -616,7 +645,7 @@ class Asn1Value(object): self._header = header self._trailer = b'' - return self._header + contents + return self._header + contents + self._trailer class ValueMap(): @@ -700,10 +729,6 @@ class Constructable(object): # length when parsed - affects parsing and dumping _indefinite = False - # Class attribute that indicates the offset into self.contents - # that contains the chunks of data to merge - _chunks_offset = 0 - def _merge_chunks(self): """ :return: @@ -713,7 +738,7 @@ class Constructable(object): if not self._indefinite: return self._as_chunk() - pointer = self._chunks_offset + pointer = 0 contents_len = len(self.contents) output = None @@ -740,9 +765,21 @@ class Constructable(object): byte strings, unicode strings or tuples. """ - if self._chunks_offset == 0: - return self.contents - return self.contents[self._chunks_offset:] + return self.contents + + def _setable_native(self): + """ + Returns a native value that can be round-tripped into .set(), to + result in a DER encoding. This differs from .native in that .native + is designed for the end use, and may account for the fact that the + merged value is further parsed as ASN.1, such as in the case of + ParsableOctetString() and ParsableOctetBitString(). + + :return: + A python value that is valid to pass to .set() + """ + + return self.native def _copy(self, other, copy_func): """ @@ -757,8 +794,10 @@ class Constructable(object): """ super(Constructable, self)._copy(other, copy_func) - self.method = other.method - self._indefinite = other._indefinite + # We really don't want to dump BER encodings, so if we see an + # indefinite encoding, let's re-encode it + if other._indefinite: + self.set(other._setable_native()) class Void(Asn1Value): @@ -792,7 +831,7 @@ class Void(Asn1Value): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: None @@ -860,7 +899,7 @@ class Any(Asn1Value): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: The .native value from the parsed value object @@ -982,6 +1021,10 @@ class Choice(Asn1Value): # The Asn1Value object for the chosen alternative _parsed = None + # Choice overrides .contents to be a property so that the code expecting + # the .contents attribute will get the .contents of the chosen alternative + _contents = None + # A list of tuples in one of the following forms. # # Option 1, a unicode string field name and a value class @@ -1045,8 +1088,8 @@ class Choice(Asn1Value): :param name: The name of the alternative to be set - used with value. Alternatively this may be a dict with a single key being the name - and the value being the value, or a two-element tuple of the the - name and the value. + and the value being the value, or a two-element tuple of the name + and the value. :param value: The alternative value to set - used with name @@ -1122,6 +1165,27 @@ class Choice(Asn1Value): raise e @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the chosen alternative + """ + + if self._parsed is not None: + return self._parsed.contents + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the chosen alternative + """ + + self._contents = value + + @property def name(self): """ :return: @@ -1139,16 +1203,15 @@ class Choice(Asn1Value): An Asn1Value object of the chosen alternative """ - if self._parsed is not None: - return self._parsed - - try: - _, spec, params = self._alternatives[self._choice] - self._parsed, _ = _parse_build(self.contents, spec=spec, spec_params=params) - except (ValueError, TypeError) as e: - args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args - raise e + if self._parsed is None: + try: + _, spec, params = self._alternatives[self._choice] + self._parsed, _ = _parse_build(self._contents, spec=spec, spec_params=params) + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e + return self._parsed @property def chosen(self): @@ -1162,7 +1225,7 @@ class Choice(Asn1Value): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: The .native value from the contained value object @@ -1271,13 +1334,17 @@ class Choice(Asn1Value): A byte string of the DER-encoded value """ - self.contents = self.chosen.dump(force=force) + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + + self._contents = self.chosen.dump(force=force) if self._header is None or force: self._header = b'' if self.explicit is not None: for class_, tag in self.explicit: - self._header = _dump_header(class_, 1, tag, self._header + self.contents) + self._header - return self._header + self.contents + self._header = _dump_header(class_, 1, tag, self._header + self._contents) + self._header + return self._header + self._contents class Concat(object): @@ -1644,6 +1711,10 @@ class Primitive(Asn1Value): A byte string of the DER-encoded value """ + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + if force: native = self.native self.contents = None @@ -1761,7 +1832,7 @@ class AbstractString(Constructable, Primitive): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A unicode string or None @@ -1812,7 +1883,7 @@ class Boolean(Primitive): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: True, False or None @@ -1891,7 +1962,7 @@ class Integer(Primitive, ValueMap): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: An integer or None @@ -1907,7 +1978,115 @@ class Integer(Primitive, ValueMap): return self._native -class BitString(Constructable, Castable, Primitive, ValueMap, object): +class _IntegerBitString(object): + """ + A mixin for IntegerBitString and BitString to parse the contents as an integer. + """ + + # Tuple of 1s and 0s; set through native + _unused_bits = () + + def _as_chunk(self): + """ + Parse the contents of a primitive BitString encoding as an integer value. + Allows reconstructing indefinite length values. + + :raises: + ValueError - when an invalid value is passed + + :return: + A list with one tuple (value, bits, unused_bits) where value is an integer + with the value of the BitString, bits is the bit count of value and + unused_bits is a tuple of 1s and 0s. + """ + + if self._indefinite: + # return an empty chunk, for cases like \x23\x80\x00\x00 + return [] + + unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0] + value = int_from_bytes(self.contents[1:]) + bits = (len(self.contents) - 1) * 8 + + if not unused_bits_len: + return [(value, bits, ())] + + if len(self.contents) == 1: + # Disallowed by X.690 §8.6.2.3 + raise ValueError('Empty bit string has {0} unused bits'.format(unused_bits_len)) + + if unused_bits_len > 7: + # Disallowed by X.690 §8.6.2.2 + raise ValueError('Bit string has {0} unused bits'.format(unused_bits_len)) + + unused_bits = _int_to_bit_tuple(value & ((1 << unused_bits_len) - 1), unused_bits_len) + value >>= unused_bits_len + bits -= unused_bits_len + + return [(value, bits, unused_bits)] + + def _chunks_to_int(self): + """ + Combines the chunks into a single value. + + :raises: + ValueError - when an invalid value is passed + + :return: + A tuple (value, bits, unused_bits) where value is an integer with the + value of the BitString, bits is the bit count of value and unused_bits + is a tuple of 1s and 0s. + """ + + if not self._indefinite: + # Fast path + return self._as_chunk()[0] + + value = 0 + total_bits = 0 + unused_bits = () + + # X.690 §8.6.3 allows empty indefinite encodings + for chunk, bits, unused_bits in self._merge_chunks(): + if total_bits & 7: + # Disallowed by X.690 §8.6.4 + raise ValueError('Only last chunk in a bit string may have unused bits') + total_bits += bits + value = (value << bits) | chunk + + return value, total_bits, unused_bits + + def _copy(self, other, copy_func): + """ + Copies the contents of another _IntegerBitString object to itself + + :param object: + Another instance of the same class + + :param copy_func: + An reference of copy.copy() or copy.deepcopy() to use when copying + lists, dicts and objects + """ + + super(_IntegerBitString, self)._copy(other, copy_func) + self._unused_bits = other._unused_bits + + @property + def unused_bits(self): + """ + The unused bits of the bit string encoding. + + :return: + A tuple of 1s and 0s + """ + + # call native to set _unused_bits + self.native + + return self._unused_bits + + +class BitString(_IntegerBitString, Constructable, Castable, Primitive, ValueMap): """ Represents a bit string from ASN.1 as a Python tuple of 1s and 0s """ @@ -1916,10 +2095,6 @@ class BitString(Constructable, Castable, Primitive, ValueMap, object): _size = None - # Used with _as_chunk() from Constructable - _chunk = None - _chunks_offset = 1 - def _setup(self): """ Generates _reverse_map from _map @@ -1983,8 +2158,6 @@ class BitString(Constructable, Castable, Primitive, ValueMap, object): type_name(value) )) - self._chunk = None - if self._map is not None: if len(value) > self._size: raise ValueError(unwrap( @@ -2024,6 +2197,7 @@ class BitString(Constructable, Castable, Primitive, ValueMap, object): value_bytes = (b'\x00' * (size_in_bytes - len(value_bytes))) + value_bytes self.contents = extra_bits_byte + value_bytes + self._unused_bits = (0,) * extra_bits self._header = None if self._indefinite: self._indefinite = False @@ -2135,40 +2309,10 @@ class BitString(Constructable, Castable, Primitive, ValueMap, object): self.set(self._native) - def _as_chunk(self): - """ - Allows reconstructing indefinite length values - - :return: - A tuple of integers - """ - - extra_bits = int_from_bytes(self.contents[0:1]) - bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) - byte_len = len(self.contents[1:]) - bit_len = len(bit_string) - - # Left-pad the bit string to a byte multiple to ensure we didn't - # lose any zero bits on the left - mod_bit_len = bit_len % 8 - if mod_bit_len != 0: - bit_string = ('0' * (8 - mod_bit_len)) + bit_string - bit_len = len(bit_string) - - if bit_len // 8 < byte_len: - missing_bytes = byte_len - (bit_len // 8) - bit_string = ('0' * (8 * missing_bytes)) + bit_string - - # Trim off the extra bits on the right used to fill the last byte - if extra_bits > 0: - bit_string = bit_string[0:0 - extra_bits] - - return tuple(map(int, tuple(bit_string))) - @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: If a _map is set, a set of names, or if no _map is set, a tuple of @@ -2183,7 +2327,9 @@ class BitString(Constructable, Castable, Primitive, ValueMap, object): self.set(set()) if self._native is None: - bits = self._merge_chunks() + int_value, bit_count, self._unused_bits = self._chunks_to_int() + bits = _int_to_bit_tuple(int_value, bit_count) + if self._map: self._native = set() for index, bit in enumerate(bits): @@ -2202,15 +2348,12 @@ class OctetBitString(Constructable, Castable, Primitive): tag = 3 - # Whenever dealing with octet-based bit strings, we really want the - # bytes, so we just ignore the unused bits portion since it isn't - # applicable to the current use case - # unused_bits = struct.unpack('>B', self.contents[0:1])[0] - _chunks_offset = 1 - # Instance attribute of (possibly-merged) byte string _bytes = None + # Tuple of 1s and 0s; set through native + _unused_bits = () + def set(self, value): """ Sets the value of the object @@ -2234,6 +2377,7 @@ class OctetBitString(Constructable, Castable, Primitive): self._bytes = value # Set the unused bits to 0 self.contents = b'\x00' + value + self._unused_bits = () self._header = None if self._indefinite: self._indefinite = False @@ -2250,7 +2394,18 @@ class OctetBitString(Constructable, Castable, Primitive): if self.contents is None: return b'' if self._bytes is None: - self._bytes = self._merge_chunks() + if not self._indefinite: + self._bytes, self._unused_bits = self._as_chunk()[0] + else: + chunks = self._merge_chunks() + self._unused_bits = () + for chunk in chunks: + if self._unused_bits: + # Disallowed by X.690 §8.6.4 + raise ValueError('Only last chunk in a bit string may have unused bits') + self._unused_bits = chunk[1] + self._bytes = b''.join(chunk[0] for chunk in chunks) + return self._bytes def _copy(self, other, copy_func): @@ -2267,11 +2422,46 @@ class OctetBitString(Constructable, Castable, Primitive): super(OctetBitString, self)._copy(other, copy_func) self._bytes = other._bytes + self._unused_bits = other._unused_bits + + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :raises: + ValueError - when an invalid value is passed + + :return: + List with one tuple, consisting of a byte string and an integer (unused bits) + """ + + unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0] + if not unused_bits_len: + return [(self.contents[1:], ())] + + if len(self.contents) == 1: + # Disallowed by X.690 §8.6.2.3 + raise ValueError('Empty bit string has {0} unused bits'.format(unused_bits_len)) + + if unused_bits_len > 7: + # Disallowed by X.690 §8.6.2.2 + raise ValueError('Bit string has {0} unused bits'.format(unused_bits_len)) + + mask = (1 << unused_bits_len) - 1 + last_byte = ord(self.contents[-1]) if _PY2 else self.contents[-1] + + # zero out the unused bits in the last byte. + zeroed_byte = last_byte & ~mask + value = self.contents[1:-1] + (chr(zeroed_byte) if _PY2 else bytes((zeroed_byte,))) + + unused_bits = _int_to_bit_tuple(last_byte & mask, unused_bits_len) + + return [(value, unused_bits)] @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A byte string or None @@ -2282,16 +2472,28 @@ class OctetBitString(Constructable, Castable, Primitive): return self.__bytes__() + @property + def unused_bits(self): + """ + The unused bits of the bit string encoding. + + :return: + A tuple of 1s and 0s + """ + + # call native to set _unused_bits + self.native + + return self._unused_bits + -class IntegerBitString(Constructable, Castable, Primitive): +class IntegerBitString(_IntegerBitString, Constructable, Castable, Primitive): """ Represents a bit string in ASN.1 as a Python integer """ tag = 3 - _chunks_offset = 1 - def set(self, value): """ Sets the value of the object @@ -2306,15 +2508,25 @@ class IntegerBitString(Constructable, Castable, Primitive): if not isinstance(value, int_types): raise TypeError(unwrap( ''' - %s value must be an integer, not %s + %s value must be a positive integer, not %s ''', type_name(self), type_name(value) )) + if value < 0: + raise ValueError(unwrap( + ''' + %s value must be a positive integer, not %d + ''', + type_name(self), + value + )) + self._native = value # Set the unused bits to 0 self.contents = b'\x00' + int_to_bytes(value, signed=True) + self._unused_bits = () self._header = None if self._indefinite: self._indefinite = False @@ -2322,31 +2534,10 @@ class IntegerBitString(Constructable, Castable, Primitive): if self._trailer != b'': self._trailer = b'' - def _as_chunk(self): - """ - Allows reconstructing indefinite length values - - :return: - A unicode string of bits - 1s and 0s - """ - - extra_bits = int_from_bytes(self.contents[0:1]) - bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) - - # Ensure we have leading zeros since these chunks may be concatenated together - mod_bit_len = len(bit_string) % 8 - if mod_bit_len != 0: - bit_string = ('0' * (8 - mod_bit_len)) + bit_string - - if extra_bits > 0: - return bit_string[0:0 - extra_bits] - - return bit_string - @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: An integer or None @@ -2356,14 +2547,8 @@ class IntegerBitString(Constructable, Castable, Primitive): return None if self._native is None: - extra_bits = int_from_bytes(self.contents[0:1]) - # Fast path - if not self._indefinite and extra_bits == 0: - self._native = int_from_bytes(self.contents[1:]) - else: - if self._indefinite and extra_bits > 0: - raise ValueError('Constructed bit string has extra bits on indefinite container') - self._native = int(self._merge_chunks(), 2) + self._native, __, self._unused_bits = self._chunks_to_int() + return self._native @@ -2433,7 +2618,7 @@ class OctetString(Constructable, Castable, Primitive): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A byte string or None @@ -2452,6 +2637,12 @@ class IntegerOctetString(Constructable, Castable, Primitive): tag = 4 + # An explicit length in bytes the integer should be encoded to. This should + # generally not be used since DER defines a canonical encoding, however some + # use of this, such as when storing elliptic curve private keys, requires an + # exact number of bytes, even if the leading bytes are null. + _encoded_width = None + def set(self, value): """ Sets the value of the object @@ -2466,14 +2657,23 @@ class IntegerOctetString(Constructable, Castable, Primitive): if not isinstance(value, int_types): raise TypeError(unwrap( ''' - %s value must be an integer, not %s + %s value must be a positive integer, not %s ''', type_name(self), type_name(value) )) + if value < 0: + raise ValueError(unwrap( + ''' + %s value must be a positive integer, not %d + ''', + type_name(self), + value + )) + self._native = value - self.contents = int_to_bytes(value, signed=False) + self.contents = int_to_bytes(value, signed=False, width=self._encoded_width) self._header = None if self._indefinite: self._indefinite = False @@ -2484,7 +2684,7 @@ class IntegerOctetString(Constructable, Castable, Primitive): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: An integer or None @@ -2497,6 +2697,19 @@ class IntegerOctetString(Constructable, Castable, Primitive): self._native = int_from_bytes(self._merge_chunks()) return self._native + def set_encoded_width(self, width): + """ + Set the explicit enoding width for the integer + + :param width: + An integer byte width to encode the integer to + """ + + self._encoded_width = width + # Make sure the encoded value is up-to-date with the proper width + if self.contents is not None and len(self.contents) != width: + self.set(self.native) + class ParsableOctetString(Constructable, Castable, Primitive): @@ -2592,6 +2805,16 @@ class ParsableOctetString(Constructable, Castable, Primitive): self._bytes = self._merge_chunks() return self._bytes + def _setable_native(self): + """ + Returns a byte string that can be passed into .set() + + :return: + A python value that is valid to pass to .set() + """ + + return self.__bytes__() + def _copy(self, other, copy_func): """ Copies the contents of another ParsableOctetString object to itself @@ -2611,7 +2834,7 @@ class ParsableOctetString(Constructable, Castable, Primitive): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A byte string or None @@ -2651,6 +2874,10 @@ class ParsableOctetString(Constructable, Castable, Primitive): A byte string of the DER-encoded value """ + # If the length is indefinite, force the re-encoding + if self._indefinite: + force = True + if force: if self._parsed is not None: native = self.parsed.dump(force=force) @@ -2666,12 +2893,6 @@ class ParsableOctetBitString(ParsableOctetString): tag = 3 - # Whenever dealing with octet-based bit strings, we really want the - # bytes, so we just ignore the unused bits portion since it isn't - # applicable to the current use case - # unused_bits = struct.unpack('>B', self.contents[0:1])[0] - _chunks_offset = 1 - def set(self, value): """ Sets the value of the object @@ -2702,6 +2923,23 @@ class ParsableOctetBitString(ParsableOctetString): if self._trailer != b'': self._trailer = b'' + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :raises: + ValueError - when an invalid value is passed + + :return: + A byte string + """ + + unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0] + if unused_bits_len: + raise ValueError('ParsableOctetBitString should have no unused bits') + + return self.contents[1:] + class Null(Primitive): """ @@ -2725,7 +2963,7 @@ class Null(Primitive): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: None @@ -2919,7 +3157,7 @@ class ObjectIdentifier(Primitive, ValueMap): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A unicode string or None. If _map is not defined, the unicode string @@ -2932,8 +3170,8 @@ class ObjectIdentifier(Primitive, ValueMap): if self._native is None: self._native = self.dotted - if self._map is not None and self._native in self._map: - self._native = self._map[self._native] + if self._map is not None and self._native in self._map: + self._native = self._map[self._native] return self._native @@ -3015,7 +3253,7 @@ class Enumerated(Integer): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A unicode string or None @@ -3312,8 +3550,6 @@ class Sequence(Asn1Value): invalid_value = False if isinstance(new_value, Any): invalid_value = new_value.parsed is None - elif isinstance(new_value, Choice): - invalid_value = new_value.chosen.contents is None else: invalid_value = new_value.contents is None @@ -3527,7 +3763,10 @@ class Sequence(Asn1Value): is_any = issubclass(field_spec, Any) if issubclass(value_spec, Choice): - if not isinstance(value, Asn1Value): + is_asn1value = isinstance(value, Asn1Value) + is_tuple = isinstance(value, tuple) and len(value) == 2 + is_dict = isinstance(value, dict) and len(value) == 1 + if not is_asn1value and not is_tuple and not is_dict: raise ValueError(unwrap( ''' Can not set a native python value to %s, which has the @@ -3536,6 +3775,8 @@ class Sequence(Asn1Value): field_name, type_name(value_spec) )) + if is_tuple or is_dict: + value = value_spec(value) if not isinstance(value, value_spec): wrapper = value_spec() wrapper.validate(value.class_, value.tag, value.contents) @@ -3550,12 +3791,30 @@ class Sequence(Asn1Value): new_value.parse(value_spec) elif (not specs_different or is_any) and not isinstance(value, value_spec): + if (not is_any or specs_different) and isinstance(value, Asn1Value): + raise TypeError(unwrap( + ''' + %s value must be %s, not %s + ''', + field_name, + type_name(value_spec), + type_name(value) + )) new_value = value_spec(value, **field_params) else: if isinstance(value, value_spec): new_value = value else: + if isinstance(value, Asn1Value): + raise TypeError(unwrap( + ''' + %s value must be %s, not %s + ''', + field_name, + type_name(value_spec), + type_name(value) + )) new_value = value_spec(value) # For when the field is OctetString or OctetBitString with embedded @@ -3701,6 +3960,7 @@ class Sequence(Asn1Value): index += 1 except (ValueError, TypeError) as e: + self.children = None args = e.args[1:] e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e @@ -3747,7 +4007,7 @@ class Sequence(Asn1Value): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: An OrderedDict or None. If an OrderedDict, all child values are @@ -3772,6 +4032,7 @@ class Sequence(Asn1Value): name = str_cls(index) self._native[name] = child.native except (ValueError, TypeError) as e: + self._native = None args = e.args[1:] e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e @@ -3826,6 +4087,10 @@ class Sequence(Asn1Value): A byte string of the DER-encoded value """ + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + if force: self._set_contents(force=force) @@ -4204,6 +4469,7 @@ class SequenceOf(Asn1Value): child._parse_children(recurse=True) self.children.append(child) except (ValueError, TypeError) as e: + self.children = None args = e.args[1:] e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e @@ -4222,7 +4488,7 @@ class SequenceOf(Asn1Value): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A list or None. If a list, all child values are recursively @@ -4289,6 +4555,10 @@ class SequenceOf(Asn1Value): A byte string of the DER-encoded value """ + # If the length is indefinite, force the re-encoding + if self._header is not None and self._header[-1:] == b'\x80': + force = True + if force: self._set_contents(force=force) @@ -4572,53 +4842,134 @@ class AbstractTime(AbstractString): """ @property + def _parsed_time(self): + """ + The parsed datetime string. + + :raises: + ValueError - when an invalid value is passed + + :return: + A dict with the parsed values + """ + + string = str_cls(self) + + m = self._TIMESTRING_RE.match(string) + if not m: + raise ValueError(unwrap( + ''' + Error parsing %s to a %s + ''', + string, + type_name(self), + )) + + groups = m.groupdict() + + tz = None + if groups['zulu']: + tz = timezone.utc + elif groups['dsign']: + sign = 1 if groups['dsign'] == '+' else -1 + tz = create_timezone(sign * timedelta( + hours=int(groups['dhour']), + minutes=int(groups['dminute'] or 0) + )) + + if groups['fraction']: + # Compute fraction in microseconds + fract = Fraction( + int(groups['fraction']), + 10 ** len(groups['fraction']) + ) * 1000000 + + if groups['minute'] is None: + fract *= 3600 + elif groups['second'] is None: + fract *= 60 + + fract_usec = int(fract.limit_denominator(1)) + + else: + fract_usec = 0 + + return { + 'year': int(groups['year']), + 'month': int(groups['month']), + 'day': int(groups['day']), + 'hour': int(groups['hour']), + 'minute': int(groups['minute'] or 0), + 'second': int(groups['second'] or 0), + 'tzinfo': tz, + 'fraction': fract_usec, + } + + @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: - A datetime.datetime object in the UTC timezone or None + A datetime.datetime object, asn1crypto.util.extended_datetime object or + None. The datetime object is usually timezone aware. If it's naive, then + it's in the sender's local time; see X.680 sect. 42.3 """ if self.contents is None: return None if self._native is None: - string = str_cls(self) - has_timezone = re.search('[-\\+]', string) + parsed = self._parsed_time - # We don't know what timezone it is in, or it is UTC because of a Z - # suffix, so we just assume UTC - if not has_timezone: - string = string.rstrip('Z') - date = self._date_by_len(string) - self._native = date.replace(tzinfo=timezone.utc) + fraction = parsed.pop('fraction', 0) - else: - # Python 2 doesn't support the %z format code, so we have to manually - # process the timezone offset. - date = self._date_by_len(string[0:-5]) - - hours = int(string[-4:-2]) - minutes = int(string[-2:]) - delta = timedelta(hours=abs(hours), minutes=minutes) - if hours < 0: - date -= delta - else: - date += delta + value = self._get_datetime(parsed) + + if fraction: + value += timedelta(microseconds=fraction) - self._native = date.replace(tzinfo=timezone.utc) + self._native = value return self._native class UTCTime(AbstractTime): """ - Represents a UTC time from ASN.1 as a Python datetime.datetime object in UTC + Represents a UTC time from ASN.1 as a timezone aware Python datetime.datetime object """ tag = 23 + # Regular expression for UTCTime as described in X.680 sect. 43 and ISO 8601 + _TIMESTRING_RE = re.compile(r''' + ^ + # YYMMDD + (?P<year>\d{2}) + (?P<month>\d{2}) + (?P<day>\d{2}) + + # hhmm or hhmmss + (?P<hour>\d{2}) + (?P<minute>\d{2}) + (?P<second>\d{2})? + + # Matches nothing, needed because GeneralizedTime uses this. + (?P<fraction>) + + # Z or [-+]hhmm + (?: + (?P<zulu>Z) + | + (?: + (?P<dsign>[-+]) + (?P<dhour>\d{2}) + (?P<dminute>\d{2}) + ) + ) + $ + ''', re.X) + def set(self, value): """ Sets the value of the object @@ -4631,6 +4982,15 @@ class UTCTime(AbstractTime): """ if isinstance(value, datetime): + if not value.tzinfo: + raise ValueError('Must be timezone aware') + + # Convert value to UTC. + value = value.astimezone(utc_with_dst) + + if not 1950 <= value.year <= 2049: + raise ValueError('Year of the UTCTime is not in range [1950, 2049], use GeneralizedTime instead') + value = value.strftime('%y%m%d%H%M%SZ') if _PY2: value = value.decode('ascii') @@ -4640,32 +5000,24 @@ class UTCTime(AbstractTime): # time that .native is called self._native = None - def _date_by_len(self, string): + def _get_datetime(self, parsed): """ - Parses a date from a string based on its length - - :param string: - A unicode string to parse + Create a datetime object from the parsed time. :return: - A datetime.datetime object or a unicode string + An aware datetime.datetime object """ - strlen = len(string) - - year_num = int(string[0:2]) - if year_num < 50: - prefix = '20' + # X.680 only specifies that UTCTime is not using a century. + # So "18" could as well mean 2118 or 1318. + # X.509 and CMS specify to use UTCTime for years earlier than 2050. + # Assume that UTCTime is only used for years [1950, 2049]. + if parsed['year'] < 50: + parsed['year'] += 2000 else: - prefix = '19' - - if strlen == 10: - return datetime.strptime(prefix + string, '%Y%m%d%H%M') + parsed['year'] += 1900 - if strlen == 12: - return datetime.strptime(prefix + string, '%Y%m%d%H%M%S') - - return string + return datetime(**parsed) class GeneralizedTime(AbstractTime): @@ -4676,6 +5028,44 @@ class GeneralizedTime(AbstractTime): tag = 24 + # Regular expression for GeneralizedTime as described in X.680 sect. 42 and ISO 8601 + _TIMESTRING_RE = re.compile(r''' + ^ + # YYYYMMDD + (?P<year>\d{4}) + (?P<month>\d{2}) + (?P<day>\d{2}) + + # hh or hhmm or hhmmss + (?P<hour>\d{2}) + (?: + (?P<minute>\d{2}) + (?P<second>\d{2})? + )? + + # Optional fraction; [.,]dddd (one or more decimals) + # If Seconds are given, it's fractions of Seconds. + # Else if Minutes are given, it's fractions of Minutes. + # Else it's fractions of Hours. + (?: + [,.] + (?P<fraction>\d+) + )? + + # Optional timezone. If left out, the time is in local time. + # Z or [-+]hh or [-+]hhmm + (?: + (?P<zulu>Z) + | + (?: + (?P<dsign>[-+]) + (?P<dhour>\d{2}) + (?P<dminute>\d{2})? + ) + )? + $ + ''', re.X) + def set(self, value): """ Sets the value of the object @@ -4689,7 +5079,18 @@ class GeneralizedTime(AbstractTime): """ if isinstance(value, (datetime, extended_datetime)): - value = value.strftime('%Y%m%d%H%M%SZ') + if not value.tzinfo: + raise ValueError('Must be timezone aware') + + # Convert value to UTC. + value = value.astimezone(utc_with_dst) + + if value.microsecond: + fraction = '.' + str(value.microsecond).zfill(6).rstrip('0') + else: + fraction = '' + + value = value.strftime('%Y%m%d%H%M%S') + fraction + 'Z' if _PY2: value = value.decode('ascii') @@ -4698,47 +5099,20 @@ class GeneralizedTime(AbstractTime): # time that .native is called self._native = None - def _date_by_len(self, string): + def _get_datetime(self, parsed): """ - Parses a date from a string based on its length - - :param string: - A unicode string to parse + Create a datetime object from the parsed time. :return: - A datetime.datetime object, asn1crypto.util.extended_datetime object or - a unicode string - """ - - strlen = len(string) - - date_format = None - if strlen == 10: - date_format = '%Y%m%d%H' - elif strlen == 12: - date_format = '%Y%m%d%H%M' - elif strlen == 14: - date_format = '%Y%m%d%H%M%S' - elif strlen == 18: - date_format = '%Y%m%d%H%M%S.%f' - - if date_format: - if len(string) >= 4 and string[0:4] == '0000': - # Year 2000 shares a calendar with year 0, and is supported natively - t = datetime.strptime('2000' + string[4:], date_format) - return extended_datetime( - 0, - t.month, - t.day, - t.hour, - t.minute, - t.second, - t.microsecond, - t.tzinfo - ) - return datetime.strptime(string, date_format) + A datetime.datetime object or asn1crypto.util.extended_datetime object. + It may or may not be aware. + """ - return string + if parsed['year'] == 0: + # datetime does not support year 0. Use extended_datetime instead. + return extended_datetime(**parsed) + else: + return datetime(**parsed) class GraphicString(AbstractString): @@ -4839,6 +5213,9 @@ def _basic_debug(prefix, self): elif has_header: print('%s %s %s tag %s' % (prefix, method_name, class_name, self.tag)) + if self._trailer: + print('%s Trailer: 0x%s' % (prefix, binascii.hexlify(self._trailer or b'').decode('utf-8'))) + print('%s Data: 0x%s' % (prefix, binascii.hexlify(self.contents or b'').decode('utf-8'))) @@ -4916,7 +5293,7 @@ def _build_id_tuple(params, spec): A 2-element integer tuple in the form (class_, tag) """ - # Handle situations where the the spec is not known at setup time + # Handle situations where the spec is not known at setup time if spec is None: return (None, None) @@ -4946,6 +5323,30 @@ def _build_id_tuple(params, spec): return (required_class, required_tag) +def _int_to_bit_tuple(value, bits): + """ + Format value as a tuple of 1s and 0s. + + :param value: + A non-negative integer to format + + :param bits: + Number of bits in the output + + :return: + A tuple of 1s and 0s with bits members. + """ + + if not value and not bits: + return () + + result = tuple(map(int, format(value, '0{0}b'.format(bits)))) + if len(result) != bits: + raise ValueError('Result too large: {0} > {1}'.format(len(result), bits)) + + return result + + _UNIVERSAL_SPECS = { 1: Boolean, 2: Integer, @@ -5078,8 +5479,10 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param )) info, _ = _parse(to_parse, len(to_parse)) parsed_class, parsed_method, parsed_tag, parsed_header, to_parse, parsed_trailer = info - explicit_header += parsed_header - explicit_trailer = parsed_trailer + explicit_trailer + + if not isinstance(value, Choice): + explicit_header += parsed_header + explicit_trailer = parsed_trailer + explicit_trailer value = _build(*info, spec=spec, spec_params={'no_explicit': True}) value._header = explicit_header @@ -5134,15 +5537,20 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param else: value.method = method value._indefinite = True - if tag != value.tag and tag != value._bad_tag: - raise ValueError(unwrap( - ''' - Error parsing %s - tag should have been %s, but %s was found - ''', - type_name(value), - value.tag, - tag - )) + if tag != value.tag: + if isinstance(value._bad_tag, tuple): + is_bad_tag = tag in value._bad_tag + else: + is_bad_tag = tag == value._bad_tag + if not is_bad_tag: + raise ValueError(unwrap( + ''' + Error parsing %s - tag should have been %s, but %s was found + ''', + type_name(value), + value.tag, + tag + )) # For explicitly tagged, un-speced parsings, we use a generic container # since we will be parsing the contents and discarding the outer object diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 9a09a31..3d447e3 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -19,17 +19,8 @@ from __future__ import unicode_literals, division, absolute_import, print_functi 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 ._errors import unwrap, APIException +from ._types import type_name, byte_cls from .algos import _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm, RSAESOAEPParams from .core import ( Any, @@ -49,6 +40,7 @@ from .core import ( SetOf, ) from .util import int_from_bytes, int_to_bytes +from asn1crypto.algos import RSASSAPSSParams class OtherPrimeInfo(Sequence): @@ -363,7 +355,9 @@ class NamedCurve(ObjectIdentifier): '1.2.840.10045.3.1.5': 'prime239v2', '1.2.840.10045.3.1.6': 'prime239v3', # https://tools.ietf.org/html/rfc5480#page-5 + # http://www.secg.org/sec2-v2.pdf '1.3.132.0.1': 'sect163k1', + '1.3.132.0.10': 'secp256k1', '1.3.132.0.15': 'sect163r2', '1.2.840.10045.3.1.1': 'secp192r1', '1.3.132.0.33': 'secp224r1', @@ -380,6 +374,76 @@ class NamedCurve(ObjectIdentifier): '1.3.132.0.39': 'sect571r1', } + _key_sizes = { + # Order values used to compute these sourced from + # http://cr.openjdk.java.net/~vinnie/7194075/webrev-3/src/share/classes/sun/security/ec/CurveDB.java.html + '1.2.840.10045.3.0.1': 21, + '1.2.840.10045.3.0.2': 21, + '1.2.840.10045.3.0.3': 21, + '1.2.840.10045.3.0.4': 21, + '1.2.840.10045.3.0.5': 24, + '1.2.840.10045.3.0.6': 24, + '1.2.840.10045.3.0.7': 24, + '1.2.840.10045.3.0.8': 24, + '1.2.840.10045.3.0.9': 24, + '1.2.840.10045.3.0.10': 25, + '1.2.840.10045.3.0.11': 30, + '1.2.840.10045.3.0.12': 30, + '1.2.840.10045.3.0.13': 30, + '1.2.840.10045.3.0.14': 30, + '1.2.840.10045.3.0.15': 30, + '1.2.840.10045.3.0.16': 33, + '1.2.840.10045.3.0.17': 37, + '1.2.840.10045.3.0.18': 45, + '1.2.840.10045.3.0.19': 45, + '1.2.840.10045.3.0.20': 53, + '1.2.840.10045.3.1.2': 24, + '1.2.840.10045.3.1.3': 24, + '1.2.840.10045.3.1.4': 30, + '1.2.840.10045.3.1.5': 30, + '1.2.840.10045.3.1.6': 30, + # Order values used to compute these sourced from + # http://www.secg.org/SEC2-Ver-1.0.pdf + '1.3.132.0.1': 21, + '1.3.132.0.10': 32, + '1.3.132.0.15': 21, + '1.2.840.10045.3.1.1': 24, + '1.3.132.0.33': 28, + '1.3.132.0.26': 29, + '1.2.840.10045.3.1.7': 32, + '1.3.132.0.27': 29, + '1.3.132.0.16': 36, + '1.3.132.0.17': 36, + '1.3.132.0.34': 48, + '1.3.132.0.36': 51, + '1.3.132.0.37': 51, + '1.3.132.0.35': 66, + '1.3.132.0.38': 72, + '1.3.132.0.39': 72, + } + + @classmethod + def register(cls, name, oid, key_size): + """ + Registers a new named elliptic curve that is not included in the + default list of named curves + + :param name: + A unicode string of the curve name + + :param oid: + A unicode string of the dotted format OID + + :param key_size: + An integer of the number of bytes the private key should be + encoded to + """ + + cls._map[oid] = name + if cls._reverse_map is not None: + cls._reverse_map[name] = oid + cls._key_sizes[oid] = key_size + class ECDomainParameters(Choice): """ @@ -392,6 +456,31 @@ class ECDomainParameters(Choice): ('implicit_ca', Null), ] + @property + def key_size(self): + if self.name == 'implicit_ca': + raise ValueError(unwrap( + ''' + Unable to calculate key_size from ECDomainParameters + that are implicitly defined by the CA key + ''' + )) + + if self.name == 'specified': + order = self.chosen['order'].native + return math.ceil(math.log(order, 2.0) / 8.0) + + oid = self.chosen.dotted + if oid not in NamedCurve._key_sizes: + raise ValueError(unwrap( + ''' + The asn1crypto.keys.NamedCurve %s does not have a registered key length, + please call asn1crypto.keys.NamedCurve.register() + ''', + repr(oid) + )) + return NamedCurve._key_sizes[oid] + class ECPrivateKeyVersion(Integer): """ @@ -416,6 +505,48 @@ class ECPrivateKey(Sequence): ('public_key', ECPointBitString, {'explicit': 1, 'optional': True}), ] + # Ensures the key is set to the correct length when encoding + _key_size = None + + # This is necessary to ensure the private_key IntegerOctetString is encoded properly + def __setitem__(self, key, value): + res = super(ECPrivateKey, self).__setitem__(key, value) + + if key == 'private_key': + if self._key_size is None: + # Infer the key_size from the existing private key if possible + pkey_contents = self['private_key'].contents + if isinstance(pkey_contents, byte_cls) and len(pkey_contents) > 1: + self.set_key_size(len(self['private_key'].contents)) + + elif self._key_size is not None: + self._update_key_size() + + elif key == 'parameters' and isinstance(self['parameters'], ECDomainParameters) and \ + self['parameters'].name != 'implicit_ca': + self.set_key_size(self['parameters'].key_size) + + return res + + def set_key_size(self, key_size): + """ + Sets the key_size to ensure the private key is encoded to the proper length + + :param key_size: + An integer byte length to encode the private_key to + """ + + self._key_size = key_size + self._update_key_size() + + def _update_key_size(self): + """ + Ensure the private_key explicit encoding width is set + """ + + if self._key_size is not None and isinstance(self['private_key'], IntegerOctetString): + self['private_key'].set_encoded_width(self._key_size) + class DSAParams(Sequence): """ @@ -463,6 +594,8 @@ class PrivateKeyAlgorithmId(ObjectIdentifier): _map = { # https://tools.ietf.org/html/rfc3279#page-19 '1.2.840.113549.1.1.1': 'rsa', + # https://tools.ietf.org/html/rfc4055#page-8 + '1.2.840.113549.1.1.10': 'rsassa_pss', # https://tools.ietf.org/html/rfc3279#page-18 '1.2.840.10040.4.1': 'dsa', # https://tools.ietf.org/html/rfc3279#page-13 @@ -485,6 +618,7 @@ class PrivateKeyAlgorithm(_ForceNullParameters, Sequence): _oid_specs = { 'dsa': DSAParams, 'ec': ECDomainParameters, + 'rsassa_pss': RSASSAPSSParams, } @@ -504,6 +638,7 @@ class PrivateKeyInfo(Sequence): algorithm = self['private_key_algorithm']['algorithm'].native return { 'rsa': RSAPrivateKey, + 'rsassa_pss': RSAPrivateKey, 'dsa': Integer, 'ec': ECPrivateKey, }[algorithm] @@ -585,78 +720,24 @@ class PrivateKeyInfo(Sequence): return container - def _compute_public_key(self): - """ - Computes the public key corresponding to the current private key. + # This is necessary to ensure any contained ECPrivateKey is the + # correct size + def __setitem__(self, key, value): + res = super(PrivateKeyInfo, self).__setitem__(key, value) - :return: - For RSA keys, an RSAPublicKey object. For DSA keys, an Integer - object. For EC keys, an ECPointBitString. - """ + algorithm = self['private_key_algorithm'] - if self.algorithm == 'dsa': - params = self['private_key_algorithm']['parameters'] - return Integer(pow( - params['g'].native, - self['private_key'].parsed.native, - params['p'].native - )) + # When possible, use the parameter info to make sure the private key encoding + # retains any necessary leading bytes, instead of them being dropped + if (key == 'private_key_algorithm' or key == 'private_key') and \ + algorithm['algorithm'].native == 'ec' and \ + isinstance(algorithm['parameters'], ECDomainParameters) and \ + algorithm['parameters'].name != 'implicit_ca' and \ + isinstance(self['private_key'], ParsableOctetString) and \ + isinstance(self['private_key'].parsed, ECPrivateKey): + self['private_key'].parsed.set_key_size(algorithm['parameters'].key_size) - 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) + return res def unwrap(self): """ @@ -667,25 +748,9 @@ class PrivateKeyInfo(Sequence): 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 + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().unwrap() has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().unwrap() instead') @property def curve(self): @@ -795,17 +860,9 @@ class PrivateKeyInfo(Sequence): 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 + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().public_key has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().public_key.unwrap() instead') @property def public_key_info(self): @@ -814,13 +871,9 @@ class PrivateKeyInfo(Sequence): 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 - }) + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().public_key_info has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().public_key.asn1 instead') @property def fingerprint(self): @@ -836,51 +889,9 @@ class PrivateKeyInfo(Sequence): 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 + raise APIException( + 'asn1crypto.keys.PrivateKeyInfo().fingerprint has been removed, ' + 'please use oscrypto.asymmetric.PrivateKey().fingerprint instead') class EncryptedPrivateKeyInfo(Sequence): @@ -932,6 +943,8 @@ class PublicKeyAlgorithmId(ObjectIdentifier): '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/rfc4055#page-8 + '1.2.840.113549.1.1.10': 'rsassa_pss', # https://tools.ietf.org/html/rfc3279#page-18 '1.2.840.10040.4.1': 'dsa', # https://tools.ietf.org/html/rfc3279#page-13 @@ -958,6 +971,7 @@ class PublicKeyAlgorithm(_ForceNullParameters, Sequence): 'ec': ECDomainParameters, 'dh': DomainParameters, 'rsaes_oaep': RSAESOAEPParams, + 'rsassa_pss': RSASSAPSSParams, } @@ -977,6 +991,7 @@ class PublicKeyInfo(Sequence): return { 'rsa': RSAPublicKey, 'rsaes_oaep': RSAPublicKey, + 'rsassa_pss': 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 @@ -1046,19 +1061,9 @@ class PublicKeyInfo(Sequence): 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 - )) + raise APIException( + 'asn1crypto.keys.PublicKeyInfo().unwrap() has been removed, ' + 'please use oscrypto.asymmetric.PublicKey().unwrap() instead') @property def curve(self): @@ -1203,47 +1208,6 @@ class PublicKeyInfo(Sequence): 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 + raise APIException( + 'asn1crypto.keys.PublicKeyInfo().fingerprint has been removed, ' + 'please use oscrypto.asymmetric.PublicKey().fingerprint instead') diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index f18d8e8..91c7fbf 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -12,6 +12,7 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function +from ._errors import unwrap from .algos import DigestAlgorithm, SignedDigestAlgorithm from .core import ( Boolean, @@ -319,6 +320,56 @@ class ResponderId(Choice): ] +# Custom class to return a meaningful .native attribute from CertStatus() +class StatusGood(Null): + def set(self, value): + """ + Sets the value of the object + + :param value: + None or 'good' + """ + + if value is not None and value != 'good' and not isinstance(value, Null): + raise ValueError(unwrap( + ''' + value must be one of None, "good", not %s + ''', + repr(value) + )) + + self.contents = b'' + + @property + def native(self): + return 'good' + + +# Custom class to return a meaningful .native attribute from CertStatus() +class StatusUnknown(Null): + def set(self, value): + """ + Sets the value of the object + + :param value: + None or 'unknown' + """ + + if value is not None and value != 'unknown' and not isinstance(value, Null): + raise ValueError(unwrap( + ''' + value must be one of None, "unknown", not %s + ''', + repr(value) + )) + + self.contents = b'' + + @property + def native(self): + return 'unknown' + + class RevokedInfo(Sequence): _fields = [ ('revocation_time', GeneralizedTime), @@ -328,9 +379,9 @@ class RevokedInfo(Sequence): class CertStatus(Choice): _alternatives = [ - ('good', Null, {'implicit': 0}), + ('good', StatusGood, {'implicit': 0}), ('revoked', RevokedInfo, {'implicit': 1}), - ('unknown', Null, {'implicit': 2}), + ('unknown', StatusUnknown, {'implicit': 2}), ] diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py index 07f53ab..c4f91f6 100644 --- a/asn1crypto/parser.py +++ b/asn1crypto/parser.py @@ -201,12 +201,6 @@ def _parse(encoded_data, data_len, pointer=0, lengths_only=False): # just scanned looking for \x00\x00, nested indefinite length values # would not work. contents_end = pointer - # Unfortunately we need to understand the contents of the data to - # properly scan forward, which bleeds some representation info into - # the parser. This condition handles the unused bits byte in - # constructed bit strings. - if tag == 3: - contents_end += 1 while contents_end < data_len: sub_header_end, contents_end = _parse(encoded_data, data_len, contents_end, lengths_only=True) if contents_end == sub_header_end and encoded_data[contents_end - 2:contents_end] == b'\x00\x00': @@ -270,11 +264,13 @@ def _dump_header(class_, method, tag, contents): id_num |= method << 5 if tag >= 31: - header += chr_cls(id_num | 31) + cont_bit = 0 while tag > 0: - continuation_bit = 0x80 if tag > 0x7F else 0 - header += chr_cls(continuation_bit | (tag & 0x7F)) + header = chr_cls(cont_bit | (tag & 0x7f)) + header + if not cont_bit: + cont_bit = 0x80 tag = tag >> 7 + header = chr_cls(id_num | 31) + header else: header += chr_cls(id_num | tag) diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 2e55ef8..4d743df 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -8,6 +8,8 @@ from bytes and UTC timezone. Exports the following items: - int_from_bytes() - int_to_bytes() - timezone.utc + - utc_with_dst + - create_timezone() - inet_ntop() - inet_pton() - uri_to_iri() @@ -18,7 +20,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math import sys -from datetime import datetime, date, time +from datetime import datetime, date, timedelta, tzinfo from ._errors import unwrap from ._iri import iri_to_uri, uri_to_iri # noqa @@ -34,10 +36,6 @@ else: # Python 2 if sys.version_info <= (3,): - from datetime import timedelta, tzinfo - - py2 = True - def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string @@ -49,13 +47,16 @@ if sys.version_info <= (3,): If the byte string should be encoded using two's complement :param width: - None == auto, otherwise an integer of the byte width for the return - value + If None, the minimal possible size (but at least 1), + otherwise an integer of the byte width for the return value :return: A byte string """ + if value == 0 and width == 0: + return b'' + # Handle negatives in two's complement is_neg = False if signed and value < 0: @@ -73,6 +74,8 @@ if sys.version_info <= (3,): output = b'\x00' + output if width is not None: + if len(output) > width: + raise OverflowError('int too big to convert') if is_neg: pad_char = b'\xFF' else: @@ -112,29 +115,92 @@ if sys.version_info <= (3,): return num - class utc(tzinfo): # noqa + class timezone(tzinfo): # noqa + """ + Implements datetime.timezone for py2. + Only full minute offsets are supported. + DST is not supported. + """ + + def __init__(self, offset, name=None): + """ + :param offset: + A timedelta with this timezone's offset from UTC - def tzname(self, _): - return b'UTC+00:00' + :param name: + Name of the timezone; if None, generate one. + """ - def utcoffset(self, _): - return timedelta(0) + if not timedelta(hours=-24) < offset < timedelta(hours=24): + raise ValueError('Offset must be in [-23:59, 23:59]') - def dst(self, _): - return timedelta(0) + if offset.seconds % 60 or offset.microseconds: + raise ValueError('Offset must be full minutes') + + self._offset = offset + + if name is not None: + self._name = name + elif not offset: + self._name = 'UTC' + else: + self._name = 'UTC' + _format_offset(offset) + + def __eq__(self, other): + """ + Compare two timezones + + :param other: + The other timezone to compare to + + :return: + A boolean + """ + + if type(other) != timezone: + return False + return self._offset == other._offset + + def tzname(self, dt): + """ + :param dt: + A datetime object; ignored. - class timezone(): # noqa + :return: + Name of this timezone + """ - utc = utc() + return self._name + def utcoffset(self, dt): + """ + :param dt: + A datetime object; ignored. + + :return: + A timedelta object with the offset from UTC + """ + + return self._offset + + def dst(self, dt): + """ + :param dt: + A datetime object; ignored. + + :return: + Zero timedelta + """ + + return timedelta(0) + + timezone.utc = timezone(timedelta(0)) # Python 3 else: from datetime import timezone # noqa - py2 = False - def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string @@ -146,8 +212,8 @@ else: If the byte string should be encoded using two's complement :param width: - None == auto, otherwise an integer of the byte width for the return - value + If None, the minimal possible size (but at least 1), + otherwise an integer of the byte width for the return value :return: A byte string @@ -183,31 +249,66 @@ else: return int.from_bytes(value, 'big', signed=signed) -_DAYS_PER_MONTH_YEAR_0 = { - 1: 31, - 2: 29, # Year 0 was a leap year - 3: 31, - 4: 30, - 5: 31, - 6: 30, - 7: 31, - 8: 31, - 9: 30, - 10: 31, - 11: 30, - 12: 31 -} +def _format_offset(off): + """ + Format a timedelta into "[+-]HH:MM" format or "" for None + """ + if off is None: + return '' + mins = off.days * 24 * 60 + off.seconds // 60 + sign = '-' if mins < 0 else '+' + return sign + '%02d:%02d' % divmod(abs(mins), 60) -class extended_date(object): + +class _UtcWithDst(tzinfo): """ - A datetime.date-like object that can represent the year 0. This is just - to handle 0000-01-01 found in some certificates. + Utc class where dst does not return None; required for astimezone """ - year = None - month = None - day = None + def tzname(self, dt): + return 'UTC' + + def utcoffset(self, dt): + return timedelta(0) + + def dst(self, dt): + return timedelta(0) + + +utc_with_dst = _UtcWithDst() + +_timezone_cache = {} + + +def create_timezone(offset): + """ + Returns a new datetime.timezone object with the given offset. + Uses cached objects if possible. + + :param offset: + A datetime.timedelta object; It needs to be in full minutes and between -23:59 and +23:59. + + :return: + A datetime.timezone object + """ + + try: + tz = _timezone_cache[offset] + except KeyError: + tz = _timezone_cache[offset] = timezone(offset) + return tz + + +class extended_date(object): + """ + A datetime.datetime-like object that represents the year 0. This is just + to handle 0000-01-01 found in some certificates. Python's datetime does + not support year 0. + + The proleptic gregorian calendar repeats itself every 400 years. Therefore, + the simplest way to format is to substitute year 2000. + """ def __init__(self, year, month, day): """ @@ -224,73 +325,63 @@ class extended_date(object): if year != 0: raise ValueError('year must be 0') - if month < 1 or month > 12: - raise ValueError('month is out of range') - - if day < 0 or day > _DAYS_PER_MONTH_YEAR_0[month]: - raise ValueError('day is out of range') - - self.year = year - self.month = month - self.day = day + self._y2k = date(2000, month, day) - def _format(self, format): + @property + def year(self): + """ + :return: + The integer 0 """ - Performs strftime(), always returning a unicode string - :param format: - A strftime() format string + return 0 + @property + def month(self): + """ :return: - A unicode string of the formatted date - """ - - format = format.replace('%Y', '0000') - # Year 0 is 1BC and a leap year. Leap years repeat themselves - # every 28 years. Because of adjustments and the proleptic gregorian - # calendar, the simplest way to format is to substitute year 2000. - temp = date(2000, self.month, self.day) - if '%c' in format: - c_out = temp.strftime('%c') - # Handle full years - c_out = c_out.replace('2000', '0000') - c_out = c_out.replace('%', '%%') - format = format.replace('%c', c_out) - if '%x' in format: - x_out = temp.strftime('%x') - # Handle formats such as 08/16/2000 or 16.08.2000 - x_out = x_out.replace('2000', '0000') - x_out = x_out.replace('%', '%%') - format = format.replace('%x', x_out) - return temp.strftime(format) - - def isoformat(self): + An integer from 1 to 12 """ - Formats the date as %Y-%m-%d + return self._y2k.month + + @property + def day(self): + """ :return: - The date formatted to %Y-%m-%d as a unicode string in Python 3 - and a byte string in Python 2 + An integer from 1 to 31 """ - return self.strftime('0000-%m-%d') + return self._y2k.day def strftime(self, format): """ Formats the date using strftime() :param format: - The strftime() format string + A strftime() format string :return: - The formatted date as a unicode string in Python 3 and a byte - string in Python 2 + A str, the formatted date as a unicode string + in Python 3 and a byte string in Python 2 """ - output = self._format(format) - if py2: - return output.encode('utf-8') - return output + # Format the date twice, once with year 2000, once with year 4000. + # The only differences in the result will be in the millennium. Find them and replace by zeros. + y2k = self._y2k.strftime(format) + y4k = self._y2k.replace(year=4000).strftime(format) + return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k)) + + def isoformat(self): + """ + Formats the date as %Y-%m-%d + + :return: + The date formatted to %Y-%m-%d as a unicode string in Python 3 + and a byte string in Python 2 + """ + + return self.strftime('0000-%m-%d') def replace(self, year=None, month=None, day=None): """ @@ -320,23 +411,40 @@ class extended_date(object): ) def __str__(self): - if py2: - return self.__bytes__() - else: - return self.__unicode__() - - def __bytes__(self): - return self.__unicode__().encode('utf-8') + """ + :return: + A str representing this extended_date, e.g. "0000-01-01" + """ - def __unicode__(self): - return self._format('%Y-%m-%d') + return self.strftime('%Y-%m-%d') def __eq__(self, other): + """ + Compare two extended_date objects + + :param other: + The other extended_date to compare to + + :return: + A boolean + """ + + # datetime.date object wouldn't compare equal because it can't be year 0 if not isinstance(other, self.__class__): return False return self.__cmp__(other) == 0 def __ne__(self, other): + """ + Compare two extended_date objects + + :param other: + The other extended_date to compare to + + :return: + A boolean + """ + return not self.__eq__(other) def _comparison_error(self, other): @@ -349,26 +457,26 @@ class extended_date(object): )) def __cmp__(self, other): + """ + Compare two extended_date or datetime.date objects + + :param other: + The other extended_date object to compare to + + :return: + An integer smaller than, equal to, or larger than 0 + """ + + # self is year 0, other is >= year 1 if isinstance(other, date): return -1 if not isinstance(other, self.__class__): self._comparison_error(other) - st = ( - self.year, - self.month, - self.day - ) - ot = ( - other.year, - other.month, - other.day - ) - - if st < ot: + if self._y2k < other._y2k: return -1 - if st > ot: + if self._y2k > other._y2k: return 1 return 0 @@ -387,158 +495,147 @@ class extended_date(object): class extended_datetime(object): """ - A datetime.datetime-like object that can represent the year 0. This is just - to handle 0000-01-01 found in some certificates. + A datetime.datetime-like object that represents the year 0. This is just + to handle 0000-01-01 found in some certificates. Python's datetime does + not support year 0. + + The proleptic gregorian calendar repeats itself every 400 years. Therefore, + the simplest way to format is to substitute year 2000. """ - year = None - month = None - day = None - hour = None - minute = None - second = None - microsecond = None - tzinfo = None + # There are 97 leap days during 400 years. + DAYS_IN_400_YEARS = 400 * 365 + 97 + DAYS_IN_2000_YEARS = 5 * DAYS_IN_400_YEARS - def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None): + def __init__(self, year, *args, **kwargs): """ :param year: The integer 0 - :param month: - An integer from 1 to 12 + :param args: + Other positional arguments; see datetime.datetime. - :param day: - An integer from 1 to 31 + :param kwargs: + Other keyword arguments; see datetime.datetime. + """ - :param hour: - An integer from 0 to 23 + if year != 0: + raise ValueError('year must be 0') - :param minute: - An integer from 0 to 59 + self._y2k = datetime(2000, *args, **kwargs) - :param second: - An integer from 0 to 59 + @property + def year(self): + """ + :return: + The integer 0 + """ - :param microsecond: - An integer from 0 to 999999 + return 0 + + @property + def month(self): + """ + :return: + An integer from 1 to 12 """ - if year != 0: - raise ValueError('year must be 0') + return self._y2k.month - if month < 1 or month > 12: - raise ValueError('month is out of range') + @property + def day(self): + """ + :return: + An integer from 1 to 31 + """ - if day < 0 or day > _DAYS_PER_MONTH_YEAR_0[month]: - raise ValueError('day is out of range') + return self._y2k.day - if hour < 0 or hour > 23: - raise ValueError('hour is out of range') + @property + def hour(self): + """ + :return: + An integer from 1 to 24 + """ - if minute < 0 or minute > 59: - raise ValueError('minute is out of range') + return self._y2k.hour - if second < 0 or second > 59: - raise ValueError('second is out of range') + @property + def minute(self): + """ + :return: + An integer from 1 to 60 + """ - if microsecond < 0 or microsecond > 999999: - raise ValueError('microsecond is out of range') + return self._y2k.minute - self.year = year - self.month = month - self.day = day - self.hour = hour - self.minute = minute - self.second = second - self.microsecond = microsecond - self.tzinfo = tzinfo + @property + def second(self): + """ + :return: + An integer from 1 to 60 + """ - def date(self): + return self._y2k.second + + @property + def microsecond(self): """ :return: - An asn1crypto.util.extended_date of the date + An integer from 0 to 999999 """ - return extended_date(self.year, self.month, self.day) + return self._y2k.microsecond - def time(self): + @property + def tzinfo(self): """ :return: - A datetime.time object of the time + If object is timezone aware, a datetime.tzinfo object, else None. """ - return time(self.hour, self.minute, self.second, self.microsecond, self.tzinfo) + return self._y2k.tzinfo def utcoffset(self): """ :return: - None or a datetime.timedelta() of the offset from UTC + If object is timezone aware, a datetime.timedelta object, else None. """ - if self.tzinfo is None: - return None - return self.tzinfo.utcoffset(self.replace(year=2000)) + return self._y2k.utcoffset() - def dst(self): + def time(self): """ :return: - None or a datetime.timedelta() of the daylight savings time offset + A datetime.time object """ - if self.tzinfo is None: - return None - return self.tzinfo.dst(self.replace(year=2000)) + return self._y2k.time() - def tzname(self): + def date(self): """ :return: - None or the name of the timezone as a unicode string in Python 3 - and a byte string in Python 2 + An asn1crypto.util.extended_date of the date """ - if self.tzinfo is None: - return None - return self.tzinfo.tzname(self.replace(year=2000)) + return extended_date(0, self.month, self.day) - def _format(self, format): + def strftime(self, format): """ - Performs strftime(), always returning a unicode string + Performs strftime(), always returning a str :param format: A strftime() format string :return: - A unicode string of the formatted datetime + A str of the formatted datetime """ - format = format.replace('%Y', '0000') - # Year 0 is 1BC and a leap year. Leap years repeat themselves - # every 28 years. Because of adjustments and the proleptic gregorian - # calendar, the simplest way to format is to substitute year 2000. - temp = datetime( - 2000, - self.month, - self.day, - self.hour, - self.minute, - self.second, - self.microsecond, - self.tzinfo - ) - if '%c' in format: - c_out = temp.strftime('%c') - # Handle full years - c_out = c_out.replace('2000', '0000') - c_out = c_out.replace('%', '%%') - format = format.replace('%c', c_out) - if '%x' in format: - x_out = temp.strftime('%x') - # Handle formats such as 08/16/2000 or 16.08.2000 - x_out = x_out.replace('2000', '0000') - x_out = x_out.replace('%', '%%') - format = format.replace('%x', x_out) - return temp.strftime(format) + # Format the datetime twice, once with year 2000, once with year 4000. + # The only differences in the result will be in the millennium. Find them and replace by zeros. + y2k = self._y2k.strftime(format) + y4k = self._y2k.replace(year=4000).strftime(format) + return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k)) def isoformat(self, sep='T'): """ @@ -554,91 +651,97 @@ class extended_datetime(object): string in Python 2 """ - if self.microsecond == 0: - return self.strftime('0000-%%m-%%d%s%%H:%%M:%%S' % sep) - return self.strftime('0000-%%m-%%d%s%%H:%%M:%%S.%%f' % sep) + s = '0000-%02d-%02d%c%02d:%02d:%02d' % (self.month, self.day, sep, self.hour, self.minute, self.second) + if self.microsecond: + s += '.%06d' % self.microsecond + return s + _format_offset(self.utcoffset()) - def strftime(self, format): + def replace(self, year=None, *args, **kwargs): """ - Formats the date using strftime() + Returns a new datetime.datetime or asn1crypto.util.extended_datetime + object with the specified components replaced - :param format: - The strftime() format string + :param year: + The new year to substitute. None to keep it. + + :param args: + Other positional arguments; see datetime.datetime.replace. + + :param kwargs: + Other keyword arguments; see datetime.datetime.replace. :return: - The formatted date as a unicode string in Python 3 and a byte - string in Python 2 + A datetime.datetime or asn1crypto.util.extended_datetime object """ - output = self._format(format) - if py2: - return output.encode('utf-8') - return output + if year: + return self._y2k.replace(year, *args, **kwargs) + + return extended_datetime.from_y2k(self._y2k.replace(2000, *args, **kwargs)) - def replace(self, year=None, month=None, day=None, hour=None, minute=None, - second=None, microsecond=None, tzinfo=None): + def astimezone(self, tz): """ - Returns a new datetime.datetime or asn1crypto.util.extended_datetime - object with the specified components replaced + Convert this extended_datetime to another timezone. + + :param tz: + A datetime.tzinfo object. :return: - A datetime.datetime or asn1crypto.util.extended_datetime object + A new extended_datetime or datetime.datetime object """ - if year is None: - year = self.year - if month is None: - month = self.month - if day is None: - day = self.day - if hour is None: - hour = self.hour - if minute is None: - minute = self.minute - if second is None: - second = self.second - if microsecond is None: - microsecond = self.microsecond - if tzinfo is None: - tzinfo = self.tzinfo + return extended_datetime.from_y2k(self._y2k.astimezone(tz)) - if year > 0: - cls = datetime - else: - cls = extended_datetime + def timestamp(self): + """ + Return POSIX timestamp. Only supported in python >= 3.3 - return cls( - year, - month, - day, - hour, - minute, - second, - microsecond, - tzinfo - ) + :return: + A float representing the seconds since 1970-01-01 UTC. This will be a negative value. + """ - def __str__(self): - if py2: - return self.__bytes__() - else: - return self.__unicode__() + return self._y2k.timestamp() - self.DAYS_IN_2000_YEARS * 86400 - def __bytes__(self): - return self.__unicode__().encode('utf-8') + def __str__(self): + """ + :return: + A str representing this extended_datetime, e.g. "0000-01-01 00:00:00.000001-10:00" + """ - def __unicode__(self): - format = '%Y-%m-%d %H:%M:%S' - if self.microsecond != 0: - format += '.%f' - return self._format(format) + return self.isoformat(sep=' ') def __eq__(self, other): - if not isinstance(other, self.__class__): + """ + Compare two extended_datetime objects + + :param other: + The other extended_datetime to compare to + + :return: + A boolean + """ + + # Only compare against other datetime or extended_datetime objects + if not isinstance(other, (self.__class__, datetime)): return False + + # Offset-naive and offset-aware datetimes are never the same + if (self.tzinfo is None) != (other.tzinfo is None): + return False + return self.__cmp__(other) == 0 def __ne__(self, other): + """ + Compare two extended_datetime objects + + :param other: + The other extended_datetime to compare to + + :return: + A boolean + """ + return not self.__eq__(other) def _comparison_error(self, other): @@ -660,42 +763,27 @@ class extended_datetime(object): )) def __cmp__(self, other): - so = self.utcoffset() - oo = other.utcoffset() + """ + Compare two extended_datetime or datetime.datetime objects - if (so is not None and oo is None) or (so is None and oo is not None): - raise TypeError("can't compare offset-naive and offset-aware datetimes") + :param other: + The other extended_datetime or datetime.datetime object to compare to - if isinstance(other, datetime): - return -1 + :return: + An integer smaller than, equal to, or larger than 0 + """ - if not isinstance(other, self.__class__): + if not isinstance(other, (self.__class__, datetime)): self._comparison_error(other) - st = ( - self.year, - self.month, - self.day, - self.hour, - self.minute, - self.second, - self.microsecond, - so - ) - ot = ( - other.year, - other.month, - other.day, - other.hour, - other.minute, - other.second, - other.microsecond, - oo - ) + if (self.tzinfo is None) != (other.tzinfo is None): + raise TypeError("can't compare offset-naive and offset-aware datetimes") - if st < ot: + diff = self - other + zero = timedelta(0) + if diff < zero: return -1 - if st > ot: + if diff > zero: return 1 return 0 @@ -710,3 +798,71 @@ class extended_datetime(object): def __ge__(self, other): return self.__cmp__(other) >= 0 + + def __add__(self, other): + """ + Adds a timedelta + + :param other: + A datetime.timedelta object to add. + + :return: + A new extended_datetime or datetime.datetime object. + """ + + return extended_datetime.from_y2k(self._y2k + other) + + def __sub__(self, other): + """ + Subtracts a timedelta or another datetime. + + :param other: + A datetime.timedelta or datetime.datetime or extended_datetime object to subtract. + + :return: + If a timedelta is passed, a new extended_datetime or datetime.datetime object. + Else a datetime.timedelta object. + """ + + if isinstance(other, timedelta): + return extended_datetime.from_y2k(self._y2k - other) + + if isinstance(other, extended_datetime): + return self._y2k - other._y2k + + if isinstance(other, datetime): + return self._y2k - other - timedelta(days=self.DAYS_IN_2000_YEARS) + + return NotImplemented + + def __rsub__(self, other): + return -(self - other) + + @classmethod + def from_y2k(cls, value): + """ + Revert substitution of year 2000. + + :param value: + A datetime.datetime object which is 2000 years in the future. + :return: + A new extended_datetime or datetime.datetime object. + """ + + year = value.year - 2000 + + if year > 0: + new_cls = datetime + else: + new_cls = cls + + return new_cls( + year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.microsecond, + value.tzinfo + ) diff --git a/asn1crypto/version.py b/asn1crypto/version.py index 2ce2408..0c08d01 100644 --- a/asn1crypto/version.py +++ b/asn1crypto/version.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.24.0' -__version_info__ = (0, 24, 0) +__version__ = '1.0.0' +__version_info__ = (1, 0, 0) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5a572a3..8341bb2 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -71,7 +71,7 @@ from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton class DNSName(IA5String): _encoding = 'idna' - _bad_tag = 19 + _bad_tag = (12, 19) def __ne__(self, other): return not self == other @@ -163,7 +163,7 @@ class URI(IA5String): if not isinstance(other, URI): return False - return iri_to_uri(self.native) == iri_to_uri(other.native) + return iri_to_uri(self.native, True) == iri_to_uri(other.native, True) def __unicode__(self): """ @@ -185,6 +185,9 @@ class EmailAddress(IA5String): # If the value has gone through the .set() method, thus normalizing it _normalized = False + # In the wild we've seen this encoded as a UTF8String and PrintableString + _bad_tag = (12, 19) + @property def contents(self): """ @@ -240,13 +243,15 @@ class EmailAddress(IA5String): A unicode string """ + # We've seen this in the wild as a PrintableString, and since ascii is a + # subset of cp1252, we use the later for decoding to be more user friendly if self._unicode is None: contents = self._merge_chunks() if contents.find(b'@') == -1: - self._unicode = contents.decode('ascii') + self._unicode = contents.decode('cp1252') else: mailbox, hostname = contents.rsplit(b'@', 1) - self._unicode = mailbox.decode('ascii') + '@' + hostname.decode('idna') + self._unicode = mailbox.decode('cp1252') + '@' + hostname.decode('idna') return self._unicode def __ne__(self, other): @@ -372,7 +377,7 @@ class IPAddress(OctetString): @property def native(self): """ - The a native Python datatype representation of this value + The native Python datatype representation of this value :return: A unicode string or None @@ -384,6 +389,7 @@ class IPAddress(OctetString): if self._native is None: byte_string = self.__bytes__() byte_len = len(byte_string) + value = None cidr_int = None if byte_len in set([32, 16]): value = inet_ntop(socket.AF_INET6, byte_string[0:16]) @@ -1692,6 +1698,8 @@ class KeyPurposeId(ObjectIdentifier): '1.3.6.1.5.5.7.3.29': 'cmc_archive', # https://tools.ietf.org/html/draft-ietf-sidr-bgpsec-pki-profiles-15#page-6 '1.3.6.1.5.5.7.3.30': 'bgpspec_router', + # https://www.ietf.org/proceedings/44/I-D/draft-ietf-ipsec-pki-req-01.txt + '1.3.6.1.5.5.8.2.2': 'ike_intermediate', # https://msdn.microsoft.com/en-us/library/windows/desktop/aa378132(v=vs.85).aspx # and https://support.microsoft.com/en-us/kb/287547 '1.3.6.1.4.1.311.10.3.1': 'microsoft_trust_list_signing', @@ -2573,6 +2581,22 @@ class Certificate(Sequence): return self._issuer_serial @property + def not_valid_after(self): + """ + :return: + A datetime of latest time when the certificate is still valid + """ + return self['tbs_certificate']['validity']['not_after'].native + + @property + def not_valid_before(self): + """ + :return: + A datetime of the earliest time when the certificate is valid + """ + return self['tbs_certificate']['validity']['not_before'].native + + @property def authority_key_identifier(self): """ :return: |