aboutsummaryrefslogtreecommitdiff
path: root/asn1crypto
diff options
context:
space:
mode:
authorHaibo Huang <hhb@google.com>2019-10-02 20:21:15 -0700
committerHaibo Huang <hhb@google.com>2019-10-02 20:21:15 -0700
commitbbe5f36e271aa1697a66502e07920f6b7037b537 (patch)
treed4f1b19f018cbc3d55a4662bd96cb5de12364101 /asn1crypto
parent9d1650023fd202f8e544513a3868c754261cd533 (diff)
parentfcbba299234325920f6cf042cbb062f4cfdb3ec0 (diff)
downloadasn1crypto-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.py314
-rw-r--r--asn1crypto/_errors.py11
-rw-r--r--asn1crypto/_ffi.py45
-rw-r--r--asn1crypto/_int.py137
-rw-r--r--asn1crypto/_iri.py11
-rw-r--r--asn1crypto/_perf/__init__.py0
-rw-r--r--asn1crypto/_perf/_big_num_ctypes.py69
-rw-r--r--asn1crypto/algos.py38
-rw-r--r--asn1crypto/cms.py31
-rw-r--r--asn1crypto/core.py868
-rw-r--r--asn1crypto/keys.py402
-rw-r--r--asn1crypto/ocsp.py55
-rw-r--r--asn1crypto/parser.py14
-rw-r--r--asn1crypto/util.py746
-rw-r--r--asn1crypto/version.py4
-rw-r--r--asn1crypto/x509.py34
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: