From e91513efcd9714573e6bcefd3bb4cd9e680f04f7 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 3 Jun 2015 14:52:18 -0400 Subject: Initial version --- .gitignore | 8 + .pylintrc | 17 + asn1crypto/__init__.py | 0 asn1crypto/algos.py | 150 ++ asn1crypto/cms.py | 676 +++++++ asn1crypto/core.py | 2714 +++++++++++++++++++++++++++ asn1crypto/crl.py | 176 ++ asn1crypto/keys.py | 623 ++++++ asn1crypto/ocsp.py | 307 +++ asn1crypto/pdf.py | 62 + asn1crypto/pkcs12.py | 125 ++ asn1crypto/pkcs5.py | 429 +++++ asn1crypto/teletex_codec.py | 317 ++++ asn1crypto/tsa.py | 290 +++ asn1crypto/x509.py | 717 +++++++ lint.py | 33 + tests.py | 19 + tests/__init__.py | 0 tests/fixtures/cms-compressed.der | Bin 0 -> 109 bytes tests/fixtures/cms-compressed.pem | 5 + tests/fixtures/cms-digested.der | Bin 0 -> 117 bytes tests/fixtures/cms-digested.pem | 5 + tests/fixtures/cms-encrypted.der | Bin 0 -> 131 bytes tests/fixtures/cms-encrypted.pem | 5 + tests/fixtures/cms-enveloped.der | Bin 0 -> 579 bytes tests/fixtures/cms-enveloped.pem | 15 + tests/fixtures/cms-signed-digested.der | Bin 0 -> 1864 bytes tests/fixtures/cms-signed-digested.pem | 41 + tests/fixtures/cms-signed.der | Bin 0 -> 1919 bytes tests/fixtures/cms-signed.pem | 42 + tests/fixtures/eid2011.crl | Bin 0 -> 1181874 bytes tests/fixtures/keys/test-der.crt | Bin 0 -> 1227 bytes tests/fixtures/keys/test-der.key | Bin 0 -> 1192 bytes tests/fixtures/keys/test-dsa-der.crt | Bin 0 -> 1783 bytes tests/fixtures/keys/test-dsa-der.key | Bin 0 -> 1242 bytes tests/fixtures/keys/test-dsa.crt | 40 + tests/fixtures/keys/test-dsa.key | 28 + tests/fixtures/keys/test-dsa.param | 19 + tests/fixtures/keys/test-ec-der.crt | Bin 0 -> 892 bytes tests/fixtures/keys/test-ec-der.key | Bin 0 -> 364 bytes tests/fixtures/keys/test-ec.crt | 21 + tests/fixtures/keys/test-ec.key | 18 + tests/fixtures/keys/test-pkcs8-der.key | Bin 0 -> 1218 bytes tests/fixtures/keys/test-pkcs8.key | 28 + tests/fixtures/keys/test-public-der.key | Bin 0 -> 294 bytes tests/fixtures/keys/test-public-rsa-der.key | Bin 0 -> 270 bytes tests/fixtures/keys/test-public-rsa.key | 8 + tests/fixtures/keys/test-public.key | 9 + tests/fixtures/keys/test-rc2.p12 | Bin 0 -> 2910 bytes tests/fixtures/keys/test.crt | 28 + tests/fixtures/keys/test.key | 27 + tests/fixtures/message.der | 1 + tests/fixtures/message.pem | 4 + tests/fixtures/message.txt | 1 + tests/fixtures/ocsp_request | Bin 0 -> 120 bytes tests/fixtures/ocsp_response | Bin 0 -> 1425 bytes tests/fixtures/pkcs7-signed-digested.der | Bin 0 -> 1866 bytes tests/fixtures/pkcs7-signed-digested.pem | 41 + tests/fixtures/pkcs7-signed.der | Bin 0 -> 2046 bytes tests/fixtures/pkcs7-signed.pem | 45 + tests/fixtures/readme.md | 6 + tests/fixtures/tsa_request | Bin 0 -> 51 bytes tests/fixtures/tsa_response | Bin 0 -> 711 bytes tests/test_cms.py | 711 +++++++ tests/test_crl.py | 38 + tests/test_keys.py | 283 +++ tests/test_ocsp.py | 153 ++ tests/test_tsa.py | 242 +++ tests/test_x509.py | 476 +++++ 69 files changed, 9003 insertions(+) create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 asn1crypto/__init__.py create mode 100644 asn1crypto/algos.py create mode 100644 asn1crypto/cms.py create mode 100644 asn1crypto/core.py create mode 100644 asn1crypto/crl.py create mode 100644 asn1crypto/keys.py create mode 100644 asn1crypto/ocsp.py create mode 100644 asn1crypto/pdf.py create mode 100644 asn1crypto/pkcs12.py create mode 100644 asn1crypto/pkcs5.py create mode 100644 asn1crypto/teletex_codec.py create mode 100644 asn1crypto/tsa.py create mode 100644 asn1crypto/x509.py create mode 100644 lint.py create mode 100644 tests.py create mode 100644 tests/__init__.py create mode 100644 tests/fixtures/cms-compressed.der create mode 100644 tests/fixtures/cms-compressed.pem create mode 100644 tests/fixtures/cms-digested.der create mode 100644 tests/fixtures/cms-digested.pem create mode 100644 tests/fixtures/cms-encrypted.der create mode 100644 tests/fixtures/cms-encrypted.pem create mode 100644 tests/fixtures/cms-enveloped.der create mode 100644 tests/fixtures/cms-enveloped.pem create mode 100644 tests/fixtures/cms-signed-digested.der create mode 100644 tests/fixtures/cms-signed-digested.pem create mode 100644 tests/fixtures/cms-signed.der create mode 100644 tests/fixtures/cms-signed.pem create mode 100644 tests/fixtures/eid2011.crl create mode 100644 tests/fixtures/keys/test-der.crt create mode 100644 tests/fixtures/keys/test-der.key create mode 100644 tests/fixtures/keys/test-dsa-der.crt create mode 100644 tests/fixtures/keys/test-dsa-der.key create mode 100644 tests/fixtures/keys/test-dsa.crt create mode 100644 tests/fixtures/keys/test-dsa.key create mode 100644 tests/fixtures/keys/test-dsa.param create mode 100644 tests/fixtures/keys/test-ec-der.crt create mode 100644 tests/fixtures/keys/test-ec-der.key create mode 100644 tests/fixtures/keys/test-ec.crt create mode 100644 tests/fixtures/keys/test-ec.key create mode 100644 tests/fixtures/keys/test-pkcs8-der.key create mode 100644 tests/fixtures/keys/test-pkcs8.key create mode 100644 tests/fixtures/keys/test-public-der.key create mode 100644 tests/fixtures/keys/test-public-rsa-der.key create mode 100644 tests/fixtures/keys/test-public-rsa.key create mode 100644 tests/fixtures/keys/test-public.key create mode 100644 tests/fixtures/keys/test-rc2.p12 create mode 100644 tests/fixtures/keys/test.crt create mode 100644 tests/fixtures/keys/test.key create mode 100644 tests/fixtures/message.der create mode 100644 tests/fixtures/message.pem create mode 100644 tests/fixtures/message.txt create mode 100644 tests/fixtures/ocsp_request create mode 100644 tests/fixtures/ocsp_response create mode 100644 tests/fixtures/pkcs7-signed-digested.der create mode 100644 tests/fixtures/pkcs7-signed-digested.pem create mode 100644 tests/fixtures/pkcs7-signed.der create mode 100644 tests/fixtures/pkcs7-signed.pem create mode 100644 tests/fixtures/readme.md create mode 100644 tests/fixtures/tsa_request create mode 100644 tests/fixtures/tsa_response create mode 100644 tests/test_cms.py create mode 100644 tests/test_crl.py create mode 100644 tests/test_keys.py create mode 100644 tests/test_ocsp.py create mode 100644 tests/test_tsa.py create mode 100644 tests/test_x509.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f01e60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.pyc +.python-version +*.egg-info/ +build/ +dist/ +tests/output/ +.DS_Store diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..c59352d --- /dev/null +++ b/.pylintrc @@ -0,0 +1,17 @@ +[MASTER] + +ignore=.git + +[MESSAGES CONTROL] + +enable=W1619,R0401,W1602,W0211,E1604,W0622,W1605,C0204,W0123,E0236,E0701,W0142,W0234,E1306,W0222,E0202,W0612,C0202,W0199,W1628,W0611,W1614,C0328,F0401,C0303,W0122,W0104,W1623,W0640,W0108,E0602,E0105,E0211,W1601,W0120,E0235,E0108,W1300,W1608,F0202,E0222,W1302,W0403,E1601,E0239,W1621,W0101,E1302,C0203,E0104,W0410,W1616,W0109,E1305,E0238,E0603,C0327,C0326,W0621,E0109,E0101,W0404,W0102,W1612,W1607,W1304,W1613,E1606,E0221,W0150,W0613,E0611,E0100,E1304,C0103,E1608,E0213,F0220,W0223,W1622,C0121,W1617,W0231,W0105,W0106,W1501,W0711,E1605,W0604,W0602,W1627,W0212,W1503,E1301,W0301,W0311,W1606,W1305,W0233,W1306,W1618,W0402,W1626,E0106,E0102,W0632,W0401,W1615,W0633,E0601,W0702,W0704,W0614,W1604,E1602,W1624,W0406,E0203,W0623,C0102,E1300,E0111,E0711,C0321,W1603,E0237,W1401,W1632,E0604,W1303,E1310,W0232,W1631,C0330,E0107,W0601,W1307,W1611,E1607,W0603,W0107,W0221,W0312,W0710,W1629,W1402,C0112,W0703,W1630,E0103,W1610,W0631,E0710,W1502,W0110,E0712,C0304,E0703,E1303,E1603,W1301,W1609,E0110,W1620,W1625,W1633,E0702,E0001 + +disable=E1200,R0901,I0020,I0013,R0914,E1120,I0001,W0512,C1001,F0001,R0923,C0301,E1127,R0922,R0915,I0011,E1102,E1002,F0010,E1123,E1111,C0103,C0302,E1121,R0921,F0002,W0511,W1201,R0903,R0902,I0010,C0401,R0904,I0021,I0022,E1126,R0913,E1205,E1001,E1004,E0012,R0801,C0402,R0911,E1125,E1101,E0011,W0332,W1001,W1111,I0012,E1003,R0912,E1206,E1124,E1201,F0003,W1202,R0201,C0111,C0325,W0141,W0201 + +[FORMAT] + +expected-line-ending-format=LF + +[IMPORTS] + +deprecated-modules=optparse diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py new file mode 100644 index 0000000..78dbef3 --- /dev/null +++ b/asn1crypto/algos.py @@ -0,0 +1,150 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .core import Any, Integer, ObjectIdentifier, OctetString, Sequence + + +# OID in this file are pulled from https://tools.ietf.org/html/rfc3279, +# https://tools.ietf.org/html/rfc4055 and https://tools.ietf.org/html/rfc5758 + +class AlgorithmIdentifier(Sequence): + _fields = [ + ('algorithm', ObjectIdentifier), + ('parameters', Any, {'optional': True}), + ] + + +class HmacAlgorithmId(ObjectIdentifier): + _map = { + '1.3.14.3.2.10': 'des_mac', + '1.2.840.113549.2.7': 'sha1', + '1.2.840.113549.2.8': 'sha224', + '1.2.840.113549.2.9': 'sha256', + '1.2.840.113549.2.10': 'sha384', + '1.2.840.113549.2.11': 'sha512', + '1.2.840.113549.2.12': 'sha512_224', + '1.2.840.113549.2.13': 'sha512_256', + } + + +class HmacAlgorithm(Sequence): + _fields = [ + ('algorithm', HmacAlgorithmId, {'default': 'sha1'}), + ('parameters', Any, {'optional': True}), + ] + + +class DigestAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.2.2': 'md2', + '1.2.840.113549.2.5': 'md5', + '1.3.14.3.2.26': 'sha1', + '2.16.840.1.101.3.4.2.4': 'sha224', + '2.16.840.1.101.3.4.2.1': 'sha256', + '2.16.840.1.101.3.4.2.2': 'sha384', + '2.16.840.1.101.3.4.2.3': 'sha512', + } + + +class DigestAlgorithm(Sequence): + _fields = [ + ('algorithm', DigestAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + +# This structure is what is signed with a SignedDigestAlgorithm +class DigestInfo(Sequence): + _fields = [ + ('digest_algorithm', DigestAlgorithm), + ('digest', OctetString), + ] + + +class SignedDigestAlgorithmId(ObjectIdentifier): + _map = { + '1.3.14.3.2.3': 'md5_rsa', + '1.3.14.3.2.29': 'sha1_rsa', + '1.3.14.7.2.3.1': 'md2_rsa', + '1.2.840.113549.1.1.2': 'md2_rsa', + '1.2.840.113549.1.1.4': 'md5_rsa', + '1.2.840.113549.1.1.5': 'sha1_rsa', + '1.2.840.113549.1.1.14': 'sha224_rsa', + '1.2.840.113549.1.1.11': 'sha256_rsa', + '1.2.840.113549.1.1.12': 'sha384_rsa', + '1.2.840.113549.1.1.13': 'sha512_rsa', + '1.2.840.10040.4.3': 'sha1_dsa', + '1.3.14.3.2.13': 'sha1_dsa', + '1.3.14.3.2.27': 'sha1_dsa', + '2.16.840.1.101.3.4.3.1': 'sha224_dsa', + '2.16.840.1.101.3.4.3.2': 'sha256_dsa', + '1.2.840.10045.4.1': 'sha1_ecdsa', + '1.2.840.10045.4.3.1': 'sha224_ecdsa', + '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', + # For when the digest is specified elsewhere in a Sequence + '1.2.840.113549.1.1.1': 'rsa', + '1.2.840.10040.4.1': 'dsa', + '1.2.840.10045.4': 'ecdsa', + } + + +class SignedDigestAlgorithm(Sequence): + _fields = [ + ('algorithm', SignedDigestAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + +class Rc2Params(Sequence): + _fields = [ + ('rc2_parameter_version', Integer, {'optional': True}), + ('iv', OctetString), + ] + + +class Rc5ParamVersion(Integer): + _map = { + 16: 'v1-0' + } + + +class Rc5Params(Sequence): + _fields = [ + ('version', Rc5ParamVersion), + ('rounds', Integer), + ('block_size_in_bits', Integer), + ('iv', OctetString, {'optional': True}), + ] + + +class EncryptionAlgorithmId(ObjectIdentifier): + _map = { + '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.9': 'rc5', + '2.16.840.1.101.3.4.1.2': 'aes128', + '2.16.840.1.101.3.4.1.22': 'aes192', + '2.16.840.1.101.3.4.1.42': 'aes256', + } + + +class EncryptionAlgorithm(Sequence): + _fields = [ + ('algorithm', EncryptionAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'des': OctetString, + 'tripledes_3key': OctetString, + 'rc2': Rc2Params, + 'rc5': Rc5Params, + 'aes128': OctetString, + 'aes192': OctetString, + 'aes256': OctetString, + } diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py new file mode 100644 index 0000000..28380a6 --- /dev/null +++ b/asn1crypto/cms.py @@ -0,0 +1,676 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +try: + import zlib +except (ImportError): + zlib = None + +from .algos import ( + DigestAlgorithm, + EncryptionAlgorithm, + SignedDigestAlgorithm, + HmacAlgorithm, +) +from .core import ( + Any, + Choice, + Enumerated, + GeneralizedTime, + Integer, + ObjectIdentifier, + OctetBitString, + OctetString, + Sequence, + SequenceOf, + SetOf, + UTCTime, +) +from .crl import CertificateList +from .keys import PublicKeyInfo +from .ocsp import OCSPResponse +from .pkcs5 import KdfAlgorithm +from .x509 import Attributes, Certificate, Extensions, GeneralNames, Name + + + +# These structures are taken from +# ftp://ftp.rsasecurity.com/pub/pkcs/ascii/pkcs-6.asc + +class ExtendedCertificateInfo(Sequence): + _fields = [ + ('version', Integer), + ('certificate', Certificate), + ('attributes', Attributes), + ] + +class ExtendedCertificate(Sequence): + _fields = [ + ('extended_certificate_info', ExtendedCertificateInfo), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] + + +# These structures are taken from https://tools.ietf.org/html/rfc5652, +# https://tools.ietf.org/html/rfc5083, http://tools.ietf.org/html/rfc2315, +# https://tools.ietf.org/html/rfc5940, https://tools.ietf.org/html/rfc3274, +# https://tools.ietf.org/html/rfc3281 + + +class CMSVersion(Integer): + _map = { + 0: 'v0', + 1: 'v1', + 2: 'v2', + 3: 'v3', + 4: 'v4', + 5: 'v5', + } + + +class CMSAttributeType(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.3': 'content_type', + '1.2.840.113549.1.9.4': 'message_digest', + '1.2.840.113549.1.9.5': 'signing_time', + '1.2.840.113549.1.9.6': 'counter_signature', + } + + +class Time(Choice): + _alternatives = [ + ('utc_time', UTCTime), + ('generalized_time', GeneralizedTime), + ] + + +class ContentType(ObjectIdentifier): + _map = { + '1.2.840.113549.1.7.1': 'data', + '1.2.840.113549.1.7.2': 'signed_data', + '1.2.840.113549.1.7.3': 'enveloped_data', + '1.2.840.113549.1.7.4': 'signed_and_enveloped_data', + '1.2.840.113549.1.7.5': 'digested_data', + '1.2.840.113549.1.7.6': 'encrypted_data', + '1.2.840.113549.1.9.16.1.2': 'authenticated_data', + '1.2.840.113549.1.9.16.1.9': 'compressed_data', + '1.2.840.113549.1.9.16.1.23': 'authenticated_enveloped_data', + } + + +class SetOfContentType(SetOf): + _child_spec = ContentType + + +class SetOfOctetString(SetOf): + _child_spec = OctetString + + +class SetOfTime(SetOf): + _child_spec = Time + + +class CMSAttribute(Sequence): + _fields = [ + ('type', CMSAttributeType), + ('values', Any), + ] + + _oid_pair = ('type', 'values') + _oid_specs = {} + + +class CMSAttributes(SequenceOf): + _child_spec = CMSAttribute + + +class IssuerSerial(Sequence): + _fields = [ + ('issuer', GeneralNames), + ('serial', Integer), + ('issuer_uid', OctetBitString, {'optional': True}), + ] + + +class AttCertVersion(Integer): + _map = { + 0: 'v1', + 1: 'v2', + } + + +class AttCertSubject(Choice): + _alternatives = [ + ('base_certificate_id', IssuerSerial, {'tag_type': 'explicit', 'tag': 0}), + ('subject_name', GeneralNames, {'tag_type': 'explicit', 'tag': 1}), + ] + + +class AttCertValidityPeriod(Sequence): + _fields = [ + ('not_before_time', GeneralizedTime), + ('not_after_time', GeneralizedTime), + ] + + +class AttributeCertificateInfoV1(Sequence): + _fields = [ + ('version', AttCertVersion, {'default', 'v1'}), + ('subject', AttCertSubject), + ('issuer', GeneralNames), + ('signature', SignedDigestAlgorithm), + ('serial_number', Integer), + ('att_cert_validity_period', AttCertValidityPeriod), + ('attributes', Attributes), + ('issuer_unique_id', OctetBitString, {'optional': True}), + ('extensions', Extensions, {'optional': True}), + ] + + +class AttributeCertificateV1(Sequence): + _fields = [ + ('ac_info', AttributeCertificateInfoV1), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] + + +class DigestedObjectType(Enumerated): + _map = { + 0: 'public_key', + 1: 'public_key_cert', + 2: 'other_objy_types', + } + + +class ObjectDigestInfo(Sequence): + _fields = [ + ('digested_object_type', DigestedObjectType), + ('other_object_type_id', ObjectIdentifier, {'optional': True}), + ('digest_algorithm', DigestAlgorithm), + ('object_digest', OctetBitString), + ] + + +class Holder(Sequence): + _fields = [ + ('base_certificate_id', IssuerSerial, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('entity_name', GeneralNames, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('object_digest_info', ObjectDigestInfo, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ] + + +class V2Form(Sequence): + _fields = [ + ('issuer_name', GeneralNames, {'optional': True}), + ('base_certificate_id', IssuerSerial, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('object_digest_info', ObjectDigestInfo, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ] + + +class AttCertIssuer(Choice): + _alternatives = [ + ('v1_form', GeneralNames), + ('v2_form', V2Form, {'tag_type': 'explicit', 'tag': 0}), + ] + + +class AttributeCertificateInfoV2(Sequence): + _fields = [ + ('version', AttCertVersion), + ('holder', Holder), + ('issuer', AttCertIssuer), + ('signature', SignedDigestAlgorithm), + ('serial_number', Integer), + ('att_cert_validity_period', AttCertValidityPeriod), + ('attributes', Attributes), + ('issuer_unique_id', OctetBitString, {'optional': True}), + ('extensions', Extensions, {'optional': True}), + ] + + +class AttributeCertificateV2(Sequence): + _fields = [ + ('ac_info', AttributeCertificateInfoV2), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] + + +class OtherCertificateFormat(Sequence): + _fields = [ + ('other_cert_format', ObjectIdentifier), + ('other_cert', Any), + ] + + +class CertificateChoices(Choice): + _alternatives = [ + ('certificate', Certificate), + ('extended_certificate', ExtendedCertificate, {'tag_type': 'implicit', 'tag': 0}), + ('v1_attr_cert', AttributeCertificateV1, {'tag_type': 'implicit', 'tag': 1}), + ('v2_attr_cert', AttributeCertificateV2, {'tag_type': 'implicit', 'tag': 2}), + ('other', OtherCertificateFormat, {'tag_type': 'implicit', 'tag': 3}), + ] + + +class CertificateSet(SetOf): + _child_spec = CertificateChoices + + +class ContentInfo(Sequence): + _fields = [ + ('content_type', ContentType), + ('content', Any, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + _oid_pair = ('content_type', 'content') + _oid_specs = {} + + +class EncapsulatedContentInfo(Sequence): + _fields = [ + ('content_type', ContentType), + ('content', OctetString, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + _oid_pair = ('content_type', 'content') + _oid_specs = {} + + +class IssuerAndSerialNumber(Sequence): + _fields = [ + ('issuer', Name), + ('serial_number', Integer), + ] + + +class SignerIdentifier(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('subject_key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ] + + +class DigestAlgorithms(SetOf): + _child_spec = DigestAlgorithm + + +class CertificateRevocationLists(SetOf): + _child_spec = CertificateList + + +class SCVPReqRes(Sequence): + _fields = [ + ('request', ContentInfo, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('response', ContentInfo), + ] + + +class OtherRevInfoFormatId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.16.2': 'ocsp_response', + '1.3.6.1.5.5.7.16.4': 'scvp', + } + + +class OtherRevocationInfoFormat(Sequence): + _fields = [ + ('other_rev_info_format', OtherRevInfoFormatId), + ('other_rev_info', Any), + ] + + _oid_pair = ('other_rev_info_format', 'other_rev_info') + _oid_specs = { + 'ocsp_response': OCSPResponse, + 'scvp': SCVPReqRes, + } + + +class RevocationInfoChoice(Choice): + _alternatives = [ + ('crl', CertificateList), + ('other', OtherRevocationInfoFormat, {'tag_type': 'implciit', 'tag': 1}), + ] + + +class RevocationInfoChoices(SetOf): + _child_spec = RevocationInfoChoice + + +class SignerInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('sid', SignerIdentifier), + ('digest_algorithm', DigestAlgorithm), + ('signed_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetString), + ('unsigned_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class SignerInfos(SetOf): + _child_spec = SignerInfo + + +class SignedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('digest_algorithms', DigestAlgorithms), + ('encap_content_info', None), + ('certificates', CertificateSet, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('crls', RevocationInfoChoices, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('signer_infos', SignerInfos), + ] + + def _encap_content_info_spec(self): + # If the encap_content_info is version v1, then this could be a PKCS#7 + # structure, or a CMS structure. CMS wraps the encoded value in an + # Octet String tag. + + # If the version is greater than 1, it is definite CMS + if self['version'].native != 'v1': + return EncapsulatedContentInfo + + # Otherwise, the ContentInfo spec from PKCS#7 will be compatible with + # CMS v1 (which only allows Data, an Octet String) and PKCS#7, which + # allows Any + return ContentInfo + + _spec_callbacks = { + 'encap_content_info': _encap_content_info_spec + } + + +class OriginatorInfo(Sequence): + _fields = [ + ('certs', CertificateSet, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('crls', RevocationInfoChoices, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class RecipientIdentifier(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('subject_key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ] + + +class KeyEncryptionAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.1.1': 'rsa' + } + + +class KeyEncryptionAlgorithm(Sequence): + _fields = [ + ('algorithm', KeyEncryptionAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + +class KeyTransRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('rid', RecipientIdentifier), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('encrypted_key', OctetString), + ] + + +class OriginatorIdentifierOrKey(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('subject_key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ('originator_key', PublicKeyInfo, {'tag_type': 'implicit', 'tag': 1}), + ] + + +class OtherKeyAttribute(Sequence): + _fields = [ + ('key_attr_id', ObjectIdentifier), + ('key_attr', Any), + ] + + +class RecipientKeyIdentifier(Sequence): + _fields = [ + ('subject_key_identifier', OctetString), + ('date', GeneralizedTime, {'optional': True}), + ('other', OtherKeyAttribute, {'optional': True}), + ] + + +class KeyAgreementRecipientIdentifier(Choice): + _alternatives = [ + ('issuer_and_serial_number', IssuerAndSerialNumber), + ('r_key_id', RecipientKeyIdentifier, {'tag_type': 'implicit', 'tag': 0}), + ] + + +class RecipientEncryptedKey(Sequence): + _fields = [ + ('rid', KeyAgreementRecipientIdentifier), + ('encrypted_key', OctetString), + ] + + +class RecipientEncryptedKeys(SequenceOf): + _child_spec = RecipientEncryptedKey + + +class KeyAgreeRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator', OriginatorIdentifierOrKey, {'tag_type': 'explicit', 'tag': 0}), + ('ukm', OctetString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('recipient_encrypted_keys', RecipientEncryptedKeys), + ] + + +class KEKIdentifier(Sequence): + _fields = [ + ('key_identifier', OctetString), + ('date', GeneralizedTime, {'optional': True}), + ('other', OtherKeyAttribute, {'optional': True}), + ] + + +class KEKRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('kekid', KEKIdentifier), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('encrypted_key', OctetString), + ] + + +class PasswordRecipientInfo(Sequence): + _fields = [ + ('version', CMSVersion), + ('key_derivation_algorithm', KdfAlgorithm, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('key_encryption_algorithm', KeyEncryptionAlgorithm), + ('encrypted_key', OctetString), + ] + + +class OtherRecipientInfo(Sequence): + _fields = [ + ('ori_type', ObjectIdentifier), + ('ori_value', Any), + ] + + +class RecipientInfo(Choice): + _alternatives = [ + ('ktri', KeyTransRecipientInfo), + ('kari', KeyAgreeRecipientInfo, {'tag_type': 'implicit', 'tag': 1}), + ('kekri', KEKRecipientInfo, {'tag_type': 'implicit', 'tag': 2}), + ('pwri', PasswordRecipientInfo, {'tag_type': 'implicit', 'tag': 3}), + ('ori', OtherRecipientInfo, {'tag_type': 'implicit', 'tag': 4}), + ] + + +class RecipientInfos(SetOf): + _child_spec = RecipientInfo + + +class EncryptedContentInfo(Sequence): + _fields = [ + ('content_type', ContentType), + ('content_encryption_algorithm', EncryptionAlgorithm), + ('encrypted_content', OctetString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ] + + +class EnvelopedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator_info', OriginatorInfo, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('recipient_infos', RecipientInfos), + ('encrypted_content_info', EncryptedContentInfo), + ('unprotected_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class SignedAndEnvelopedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('recipient_infos', RecipientInfos), + ('digest_algorithms', DigestAlgorithms), + ('encrypted_content_info', EncryptedContentInfo), + ('certificates', CertificateSet, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('crls', CertificateRevocationLists, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('signer_infos', SignerInfos), + ] + + +class DigestedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('digest_algorithm', DigestAlgorithm), + ('encap_content_info', None), + ('digest', OctetString), + ] + + def _encap_content_info_spec(self): + # If the encap_content_info is version v1, then this could be a PKCS#7 + # structure, or a CMS structure. CMS wraps the encoded value in an + # Octet String tag. + + # If the version is greater than 1, it is definite CMS + if self['version'].native != 'v1': + return EncapsulatedContentInfo + + # Otherwise, the ContentInfo spec from PKCS#7 will be compatible with + # CMS v1 (which only allows Data, an Octet String) and PKCS#7, which + # allows Any + return ContentInfo + + _spec_callbacks = { + 'encap_content_info': _encap_content_info_spec + } + + +class EncryptedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('encrypted_content_info', EncryptedContentInfo), + ('unprotected_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class AuthenticatedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator_info', OriginatorInfo, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('recipient_infos', RecipientInfos), + ('mac_algorithm', HmacAlgorithm), + ('digest_algorithm', DigestAlgorithm, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + # This does not require the _spec_callbacks approach of SignedData and + # DigestedData since AuthenticatedData was not part of PKCS#7 + ('encap_content_info', EncapsulatedContentInfo), + ('auth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('mac', OctetString), + ('unauth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ] + + +class AuthEnvelopedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('originator_info', OriginatorInfo, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('recipient_infos', RecipientInfos), + ('auth_encrypted_content_info', EncryptedContentInfo), + ('auth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('mac', OctetString), + ('unauth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ] + + +class CompressionAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.16.3.8': 'zlib', + } + + +class CompressionAlgorithm(Sequence): + _fields = [ + ('algorithm', CompressionAlgorithmId), + ('parameters', Any), + ] + + +class CompressedData(Sequence): + _fields = [ + ('version', CMSVersion), + ('compression_algorithm', CompressionAlgorithm), + ('encap_content_info', EncapsulatedContentInfo), + ] + + _decompressed = None + + @property + def decompressed(self): + if self._decompressed is None: + if zlib is None: + raise SystemError('The zlib module is not available') + self._decompressed = zlib.decompress(self['encap_content_info']['content'].native) + return self._decompressed + + +ContentInfo._oid_specs = { #pylint: disable=W0212 + 'data': OctetString, + 'signed_data': SignedData, + 'enveloped_data': EnvelopedData, + 'signed_and_enveloped_data': SignedAndEnvelopedData, + 'digested_data': DigestedData, + 'encrypted_data': EncryptedData, + 'authenticated_data': AuthenticatedData, + 'compressed_data': CompressedData, + 'authenticated_enveloped_data': AuthEnvelopedData, +} + + +EncapsulatedContentInfo._oid_specs = { #pylint: disable=W0212 + 'signed_data': SignedData, + 'enveloped_data': EnvelopedData, + 'signed_and_enveloped_data': SignedAndEnvelopedData, + 'digested_data': DigestedData, + 'encrypted_data': EncryptedData, + 'authenticated_data': AuthenticatedData, + 'compressed_data': CompressedData, + 'authenticated_enveloped_data': AuthEnvelopedData, +} + + +CMSAttribute._oid_specs = { #pylint: disable=W0212 + 'content_type': SetOfContentType, + 'message_digest': SetOfOctetString, + 'signing_time': SetOfTime, + 'counter_signature': SignerInfos, +} diff --git a/asn1crypto/core.py b/asn1crypto/core.py new file mode 100644 index 0000000..ce3edeb --- /dev/null +++ b/asn1crypto/core.py @@ -0,0 +1,2714 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +import re +from collections import OrderedDict +from datetime import datetime, timedelta, tzinfo + +from . import teletex_codec + +try: + str_cls = unicode #pylint: disable=E0602 + byte_cls = str + py2 = True + chr_cls = chr + range = xrange #pylint: disable=E0602,W0622 + + def int_to_bytes(value, signed=False): + # Handle negatives in two's complement + if signed and value < 0: + value = (~value) + 1 + + hex_str = '%x' % value + if len(hex_str) & 1: + hex_str = '0' + hex_str + return hex_str.decode('hex') + + def int_from_bytes(value, signed=False): + num = long(value.encode("hex"), 16) #pylint: disable=E0602 + + if not signed: + return num + + # Check for sign bit and handle two's complement + if ord(value[0:1]) & 0x80: + bit_len = len(value) * 8 + return num - (1 << bit_len) + + return num + + class utc(tzinfo): + + def tzname(self, _): + return 'UTC+00:00' + + def utcoffset(self, _): + return timedelta(0) + + def dst(self, _): + return None + + class timezone(): + + utc = utc() + +except (NameError): + str_cls = str + byte_cls = bytes + py2 = False + + def chr_cls(num): + return bytes([num]) + + def int_to_bytes(value, signed=False): + return value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) + + def int_from_bytes(value, signed=False): + return int.from_bytes(value, 'big', signed=signed) + + from datetime import timezone + + + +teletex_codec.register() + + +CLASS_NUM_TO_NAME_MAP = { + 0: 'universal', + 1: 'application', + 2: 'context', + 3: 'private', +} + +CLASS_NAME_TO_NUM_MAP = { + 'universal': 0, + 'application': 1, + 'context': 2, + 'private': 3, + 0: 0, + 1: 1, + 2: 2, + 3: 3, +} + +METHOD_NUM_TO_NAME_MAP = { + 0: 'primitive', + 1: 'constructed', +} + + +# A global tracker to ensure that _setup() is called for every class, even +# if is has been called for a parent class. This allows different _fields +# definitions for child classes. Without such a construct, the child classes +# would just see the parent class attributes and would use them. +_SETUP_CLASSES = {} + + +class Asn1Value(): + """ + The basis of all ASN.1 values + """ + + # The integer 0 for primitive, 1 for constructed + method = None + + # An integer 0 through 3 - see CLASS_NUM_TO_NAME_MAP for value + class_ = None + + # An integer 1 or greater indicating the tag number + tag = None + + # A unicode string or None - "explicit" or "implicit" for + # tagged values, None for normal + tag_type = None + + # If "explicit"ly tagged, the class and tag for the wrapped header + explicit_class = None + explicit_tag = None + + # The BER/DER header bytes + header = None + + # Raw encoded value bytes not including class, method, tag, length header + contents = None + + # The BER/DER trailer bytes + trailer = b'' + + # The native python representation of the value + _native = None + + @classmethod + def load(cls, encoded_data, **kwargs): + """ + Loads a BER/DER-encoded byte string using the current class as the spec + + :param encoded_data: + A byte string of BER or DER encoded data + + :return: + A instance of the current class + """ + + spec = None + if cls.tag is not None: + spec = cls + + value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs) + return value + + #pylint: disable=W0613 + def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=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 + args + + :param tag_type: + None for normal values, or one of "implicit", "explicit" for tagged + values + + :param class_: + The class for the value - defaults to "universal" if tag_type is + None, otherwise defaults to "context". Valid values include: + - "universal" + - "application" + - "context" + - "private" + + :param tag: + The integer tag to override - usually this is used with tag_type or + class_ + + :param optional: + Dummy parameter that allows "optional" key in spec param dicts + + :param default: + The default value to use if the value is currently None + + :raises: + ValueError - when tag_type, class_ or tag are invalid values + """ + + if self.__class__ not in _SETUP_CLASSES: + cls = self.__class__ + if hasattr(cls, '_setup'): + self._setup() + _SETUP_CLASSES[cls] = True + + if tag_type is not None: + if tag_type not in ('implicit', 'explicit'): + raise ValueError('tag_type is not one of "implicit", "explicit"') + self.tag_type = tag_type + + if class_ is None: + class_ = 'context' + if class_ not in CLASS_NAME_TO_NUM_MAP: + raise ValueError('class_ is not one of "universal", "application", "context", "private"') + class_ = CLASS_NAME_TO_NUM_MAP[class_] + + if tag is not None: + if not isinstance(tag, int): + raise ValueError('tag is not an integer') + + if tag_type == 'implicit': + self.class_ = class_ + self.tag = tag + else: + self.explicit_class = class_ + self.explicit_tag = tag + else: + if class_ is not None: + if class_ not in CLASS_NUM_TO_NAME_MAP: + raise ValueError('class_ is not one of "universal", "application", "context", "private"') + self.class_ = CLASS_NAME_TO_NUM_MAP[class_] + + if tag is not None: + self.tag = tag + + if default is not None: + self.set(default) + + def __str__(self): + """ + Since str is differnt in Python 2 and 3, this calls the appropriate + method, __unicode__() or __bytes__() + + :return: + A unicode string + """ + + if py2: + return self.__bytes__() + else: + return self.__unicode__() + + def __repr__(self): + """ + :return: + A unicode string + """ + return '<%s %s>' % (self.__class__.__name__, repr(self.contents)) + + def dump(self): + """ + Encodes the value using DER + + :return: + A byte string of the DER-encoded value + """ + + if self.header is None: + header = b'' + + id_num = 0 + id_num |= self.class_ << 6 + id_num |= self.method << 5 + + if self.tag >= 31: + header += chr_cls(id_num | 31) + tag = self.tag + while tag > 0: + continuation_bit = 0x80 if tag > 0x7F else 0 + header += chr_cls(continuation_bit | (tag & 0x7F)) + tag = tag >> 7 + else: + header += chr_cls(id_num | self.tag) + + length = len(self.contents) + if length <= 127: + header += chr_cls(length) + else: + length_bytes = int_to_bytes(length) + header += chr_cls(0x80 | len(length_bytes)) + header += length_bytes + + self.header = header + + if self.tag_type == 'explicit': + container = Asn1Value() + container.method = 1 + container.class_ = self.explicit_class + container.tag = self.explicit_tag + container.contents = self.header + self.contents + self.trailer + # Force the container to generate the header and footer + container.dump() + self.header = container.header + self.header + self.trailer += container.trailer + + return self.header + self.contents + self.trailer + + +class ValueMap(): + """ + Basic functionality that allows for mapping values from ints or OIDs to + python unicode strings + """ + + # A dict from primitive value (int or OID) to unicode string. This needs + # to be defined in the source code + _map = None + + # A dict from unicode string to int/OID. This is automatically generated + # from _map the first time it is needed + _reverse_map = None + + #pylint: disable=W0212 + def _setup(self): + """ + Generates _reverse_map from _map + """ + + cls = self.__class__ + if cls._map is None: + return + cls._reverse_map = {} + for key, value in cls._map.items(): + cls._reverse_map[value] = key + + +class NoValue(Asn1Value): + """ + A representation of an optional value that is not present. Has .native + property and .dump() method to be compatible with other value classes. + """ + + def __len__(self): + return 0 + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + None + """ + + return None + + def dump(self): + """ + Encodes the value using DER + + :return: + A byte string of the DER-encoded value + """ + + return b'' + + + +class Any(Asn1Value): + """ + A value class that can contain any value, and allows for easy parsing of + the underlying encoded value using a spec. This is normally contained in + a Structure that has an ObjectIdentifier field and _oid_pair and _oid_specs + defined. + """ + + # The parsed value object + _parsed = None + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + The .native value from the parsed value object + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0].native + + @property + def parsed(self): + """ + Returns the parsed object from .parse() + + :return: + The object returned by .parse() + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0] + + def parse(self, spec=None, spec_params=None): + """ + Parses the contents generically, or using a spec with optional params + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :return: + An object of the type spec, or if not present, a child of Asn1Value + """ + + if self._parsed is None or self._parsed[1:3] != (spec, spec_params): + parsed_value, _ = _parse_build(self.header + self.contents + self.trailer, spec=spec, spec_params=spec_params) + self._parsed = (parsed_value, spec, spec_params) + return self._parsed[0] + + def dump(self): + """ + Encodes the value using DER + + :return: + A byte string of the DER-encoded value + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0].dump() + + +class Choice(Asn1Value): + """ + A class to handle when a value may be one of several options + """ + + # The index in _alternatives of the validated alternative + _choice = None + + # The name of the chosen alternative + _name = None + + # The Asn1Value object for the chosen alternative + _parsed = None + + # A list of tuples in one of the following forms. + # + # Option 1, a unicode string field name and a value class + # + # ("name", Asn1ValueClass) + # + # Option 2, same as Option 1, but with a dict of class params + # + # ("name", Asn1ValueClass, {'tag_type': 'explicit', 'tag': 5}) + _alternatives = None + + # A dict that maps tuples of (class_, tag) to an index in _alternatives + _id_map = None + + # A dict that maps alternative names to an index in _alternatives + _name_map = None + + #pylint: disable=W0212 + def _setup(self): + """ + Generates _id_map from _alternatives to allow validating contents + """ + + cls = self.__class__ + cls._id_map = {} + cls._name_map = {} + for index, info in enumerate(cls._alternatives): + params = info[2] if len(info) > 2 else {} + id_ = _build_id_tuple(params, info[1]) + cls._id_map[id_] = index + cls._name_map[info[0]] = index + + def __init__(self, name=None, value=None, tag_type=None, **kwargs): + """ + Checks to ensure implicit tagging is not being used since it is + incompatible with Choice, then forwards on to Asn1Value.__init__() + + :param name: + The name of the alternative to be set - used with value + + :param value: + The alternative value to set - used with name + + :param tag_type: + The tag_type of the value - None, "implicit" or "explicit" + + :raises: + ValueError - when tag_type is "implicit" + """ + + if tag_type == 'implicit': + raise ValueError('The Choice type can not be implicitly tagged even if in an implicit module - due to its nature any tagging must be explicit') + kwargs['tag_type'] = tag_type + Asn1Value.__init__(self, **kwargs) + + if name is not None: + if name not in self._name_map: + raise ValueError('The name specified, "%s", is not a valid alternative for %s' % (name, self.__class__.__name__)) + + self._choice = self._name_map[name] + info = self._alternatives[self._choice] + spec = info[1] + params = {} if len(info) < 3 else info[2] + + if not isinstance(value, spec): + value = spec(value, **params) + self._parsed = value + + @property + def name(self): + """ + :return: + A unicode string of the field name of the chosen alternative + """ + if not self._name: + self._name = self._alternatives[self._choice][0] + return self._name + + def parse(self): + """ + Parses the detected alternative + + :return: + An Asn1Value object of the chosen alternative + """ + + if self._parsed is not None: + return self._parsed + + try: + info = self._alternatives[self._choice] + params = info[2] if len(info) > 2 else {} + self._parsed, _ = _parse_build(self.header + self.contents + self.trailer, spec=info[1], spec_params=params) + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + raise e + + @property + def chosen(self): + """ + :return: + An Asn1Value object of the chosen alternative + """ + + return self.parse() + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + The .native value from the contained value object + """ + + return self.chosen.native + + def validate(self, class_, tag): + """ + Ensures that the class and tag specified exist as an alternative + + :param class_: + The integer class_ from the encoded value header + + :param tag: + The interger tag from the encoded value header + + :raises: + ValueError - when value is not a valid alternative + """ + + id_ = (class_, tag) + + if id_ in self._id_map: + self._choice = self._id_map[id_] + return + + # This means the Choice was implicitly tagged + if self.class_ is not None and self.tag is not None: + if len(self._alternatives) > 1: + raise ValueError('%s was implicitly tagged, but more than one alternative exists' % self.__class__.__name__) + if id_ == (self.class_, self.tag): + self._choice = 0 + return + + asn1 = self._format_class_tag(class_, tag) + asn1s = [self._format_class_tag(id_[0], id_[1]) for id_ in self._id_map] + + raise ValueError('Value %s did not match the class and tag of any of the alternatives in %s: %s' % (asn1, self.__class__.__name__, '. '.join(asn1s))) + + def _format_class_tag(self, class_, tag): + """ + :return: + A unicode string of a human-friendly representation of the class and tag + """ + + return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag) + + def dump(self): + """ + Encodes the value using DER + + :return: + A byte string of the DER-encoded value + """ + + return self.chosen.dump() + + +class Primitive(Asn1Value): + """ + Sets the class_ and method attributes for primitive, universal values + """ + + class_ = 0 + + method = 0 + + def __init__(self, value=None, default=None, **kwargs): + """ + Sets the value of the object before passing to Asn1Value.__init__() + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified + """ + + Asn1Value.__init__(self, **kwargs) + + if value is not None: + self.set(value) + + elif default is not None: + self.set(default) + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + """ + + if not isinstance(value, byte_cls): + raise ValueError('%s value must be a byte string' % self.__class__.__name__) + + self._native = value + self.contents = value + self.header = None + if self.trailer != b'': + self.trailer = b'' + + +class AbstractString(Primitive): + """ + A base class for all strings that have a known encoding. In general, we do + not worry ourselves with confirming that the decoded values match a specific + set of characters, only that they are decoded into a Python unicode string + """ + + # The Python encoding name to use when decoding or encoded the contents + _encoding = 'latin1' + + def set(self, value): + """ + Sets the value of the string + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise ValueError('%s value must be a unicode string' % self.__class__.__name__) + + self._native = value + self.contents = value.encode(self._encoding) + self.header = None + if self.trailer != b'': + self.trailer = b'' + + def __unicode__(self): + """ + :return: + A unicode string + """ + return self.contents.decode(self._encoding) + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A unicode string or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self.__unicode__() + return self._native + + +class Boolean(Primitive): + """ + Represents a boolean in both ASN.1 and Python + """ + + tag = 1 + + def set(self, value): + """ + Sets the value of the object + + :param value: + True, False or another value that works with bool() + """ + + self._native = bool(value) + self.contents = b'\x00' if not value else b'\xff' + self.header = None + if self.trailer != b'': + self.trailer = b'' + + # Python 2 + def __nonzero__(self): + """ + :return: + True or False + """ + return self.__bool__() + + def __bool__(self): + """ + :return: + True or False + """ + return self.contents != b'\x00' + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + True, False or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self.__bool__() + return self._native + + +class Integer(Primitive, ValueMap): + """ + Represents an integer in both ASN.1 and Python + """ + + tag = 2 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer, or a unicode string if _map is set + + :raises: + ValueError - when an invalid value is passed + """ + + if isinstance(value, str_cls): + if self._map is None: + raise ValueError('%s value is a unicode string, but no _map provided' % self.__class__.__name__) + + if value not in self._reverse_map: + raise ValueError('%s value, %s, is not present in the _map' % (self.__class__.__name__, value)) + + value = self._reverse_map[value] + + elif not isinstance(value, int): + raise ValueError('%s value must be an integer or unicode string when a name_map is provided' % self.__class__.__name__) + + self._native = self._map[value] if self._map and value in self._map else value + + self.contents = int_to_bytes(value, signed=True) + self.header = None + if self.trailer != b'': + self.trailer = b'' + + def __int__(self): + """ + :return: + An integer + """ + return int_from_bytes(self.contents, signed=True) + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + An integer or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self.__int__() + if self._map is not None and self._native in self._map: + self._native = self._map[self._native] + return self._native + + +class BitString(Primitive, ValueMap, object): + """ + Represents a bit string from ASN.1 as a Python tuple of 1s and 0s + """ + + tag = 3 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer or a tuple of integers 0 and 1 + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int) and not isinstance(value, tuple): + raise ValueError('%s value must be an integer or a tuple of ones and zeros' % self.__class__.__name__) + + if isinstance(value, tuple): + self._native = value + value = ''.join(map(str_cls, value)) + + elif isinstance(value, int): + self._native = tuple(map(int, tuple(value))) + value = '{0:b}'.format(value) + + size = max(self._map.keys()) + 1 + if len(value) != size: + raise ValueError('%s value must be %s bits long, specified was only %s long' % (self.__class__.__name__, size, len(value))) + + extra_bits = (size % 8) + if extra_bits != 0: + value += '0' * extra_bits + + self.contents = int_to_bytes(extra_bits) + int_to_bytes(int(value, 2)) + self.header = None + if self.trailer != b'': + self.trailer = b'' + + def __getattr__(self, key): + """ + Retrieves one of the bits based on a name from the _map + + :param key: + The unicode string of one of the bit names + + :raises: + ValueError - when _map is not set or the key name is invalid + + :return: + A 1 or a 0 + """ + + if not isinstance(self._map, dict): + raise ValueError('%s bit map has not been defined' % self.__class__.__name__) + + if key not in self._map: + raise ValueError('%s map does not contain an entry for "%s"' % (self.__class__.__name__, key)) + + if self._native is None: + _ = self.native + + return self._native[self._reverse_map[key]] + + def __setattr__(self, key, value): + """ + Sets one of the bits based on a name from the _map + + :param key: + The unicode string of one of the bit names + + :param value: + A 1 or a 0 + + :raises: + ValueError - when _map is not set or the key name is invalid + """ + + if not isinstance(self._map, dict) or key not in self._map: + return super(BitString, self).__setattr__(key, value) + + if self._native is None: + _ = self.native + + self._native[self._reverse_map[key]] = 1 if value else 0 + self.set(self._native) + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + The tuple of integers 1 and 0, or None + """ + + # For BitString we default the value to be all zeros + if self.contents is None: + size = max(self._map.keys()) + 1 + self.set((0,) * size) + + if self._native is None: + extra_bits = int_from_bytes(self.contents[0:1]) + bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) + if extra_bits > 0: + bit_string = bit_string[0:0-extra_bits] + self._native = tuple(map(int, tuple(bit_string))) + return self._native + + +class OctetBitString(Primitive): + """ + Represents a bit string in ASN.1 as a Python byte string + """ + + tag = 3 + + _parsed = None + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, byte_cls): + raise ValueError('%s value must be a byte string' % self.__class__.__name__) + + self._native = value + # Set the unused bits to 0 + self.contents = b'\x00' + value + self.header = None + if self.trailer != b'': + self.trailer = b'' + + def parse(self, spec=None, spec_params=None): + """ + Parses the contents generically, or using a spec with optional params + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :return: + An object of the type spec, or if not present, a child of Asn1Value + """ + + if self._parsed is None or self._parsed[1:3] != (spec, spec_params): + parsed_value, _ = _parse_build(self.__bytes__(), spec=spec, spec_params=spec_params) + self._parsed = (parsed_value, spec, spec_params) + return self._parsed[0] + + def __bytes__(self): + """ + :return: + A byte string + """ + + # 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] + return self.contents[1:] + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A byte string or None + """ + + if self.contents is None: + return None + + if self._native is None: + if self._parsed is not None: + self._native = self._parsed[0].native + else: + self._native = self.__bytes__() + return self._native + + @property + def parsed(self): + """ + Returns the parsed object from .parse() + + :return: + The object returned by .parse() + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0] + + +class IntegerBitString(Primitive): + """ + Represents a bit string in ASN.1 as a Python integer + """ + + tag = 3 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int): + raise ValueError('%s value must be an integer' % self.__class__.__name__) + + self._native = value + # Set the unused bits to 0 + self.contents = b'\x00' + int_to_bytes(value) + self.header = None + if self.trailer != b'': + self.trailer = b'' + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + An integer or None + """ + + if self.contents is None: + return None + + if self._native is None: + extra_bits = int_from_bytes(self.contents[0:1]) + if extra_bits > 0: + bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) + bit_string = bit_string[0:0-extra_bits] + self._native = int(bit_string, 2) + else: + self._native = int_from_bytes(self.contents[1:]) + return self._native + + +class OctetString(Primitive): + """ + Represents a byte string in both ASN.1 and Python + """ + + tag = 4 + + _parsed = None + + def parse(self, spec=None, spec_params=None): + """ + Parses the contents generically, or using a spec with optional params + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :return: + An object of the type spec, or if not present, a child of Asn1Value + """ + + if self._parsed is None or self._parsed[1:3] != (spec, spec_params): + parsed_value, _ = _parse_build(byte_cls(self), spec=spec, spec_params=spec_params) + self._parsed = (parsed_value, spec, spec_params) + return self._parsed[0] + + def __bytes__(self): + """ + :return: + A byte string + """ + return self.contents + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A byte string or None + """ + + if self.contents is None: + return None + + if self._native is None: + if self._parsed is not None: + self._native = self._parsed[0].native + else: + self._native = self.__bytes__() + return self._native + + @property + def parsed(self): + """ + Returns the parsed object from .parse() + + :return: + The object returned by .parse() + """ + + if self._parsed is None: + self.parse() + + return self._parsed[0] + + +class IntegerOctetString(OctetString): + """ + Represents a byte string in ASN.1 as a Python integer + """ + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int): + raise ValueError('%s value must be an integer' % self.__class__.__name__) + + self._native = value + # Set the unused bits to 0 + self.contents = int_to_bytes(value) + self.header = None + if self.trailer != b'': + self.trailer = b'' + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + An integer or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = int_from_bytes(self.contents) + return self._native + + +class Null(Primitive): + """ + Represents a null value in ASN.1 as None in Python + """ + + tag = 5 + + contents = b'' + + def set(self, value): + """ + Sets the value of the object + + :param value: + None + """ + + pass + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + None + """ + + return None + + +class ObjectIdentifier(Primitive, ValueMap): + """ + Represents an object identifier in ASN.1 as a Python unicode dotted + integer string + """ + + tag = 6 + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string. May be a dotted integer string, or if _map is + provided, one of the mapped values. + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, str_cls): + raise ValueError('%s value must be a unicode string' % self.__class__.__name__) + + self._native = value + + if self._map is not None: + if value is self._reverse_map: + value = self._reverse_map[value] + + self.contents = b'' + for part in value.split('.'): + encoded_part = byte_cls(0x7F & part) + part = part >> 7 + while part > 0: + encoded_part = byte_cls(0x80 | (0x7F & part)) + encoded_part + part = part >> 7 + self.contents += encoded_part + + self.header = None + if self.trailer != b'': + self.trailer = b'' + + def __unicode__(self): + """ + :return: + A unicode string + """ + output = [] + + part = 0 + for byte in self.contents: + if py2: + byte = ord(byte) + part = part * 128 + part += byte & 127 + # Last byte in subidentifier has the eighth bit set to 0 + if byte & 0x80 == 0: + if len(output) == 0: + output.append(str_cls(part // 40)) + output.append(str_cls(part % 40)) + else: + output.append(str_cls(part)) + part = 0 + + return '.'.join(output) + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A unicode string or None. If _map is not defined, the unicode string + is a string of dotted integers. If _map is defined and the dotted + string is present in the _map, the mapped value is returned. + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self.__unicode__() + if self._map is not None and self._native in self._map: + self._native = self._map[self._native] + return self._native + + +class ObjectDescriptor(Primitive): + """ + Represents an object descriptor from ASN.1 - no Python implementation + """ + + tag = 7 + + +class InstanceOf(Primitive): + """ + Represents an instance from ASN.1 - no Python implementation + """ + + tag = 8 + + +class Real(Primitive): + """ + Represents a real number from ASN.1 - no Python implementation + """ + + tag = 9 + + +class Enumerated(Integer): + """ + Represents a enumerated list of integers from ASN.1 as a Python + unicode string + """ + + tag = 10 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer or a unicode string from _map + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int) and not isinstance(value, str_cls): + raise ValueError('%s value must be an integer or a unicode string' % self.__class__.__name__) + + if isinstance(value, str_cls): + if value not in self._reverse_map: + raise ValueError('%s value "%s" is not a valid value' % (self.__class__.__name__, value)) + + value = self._reverse_map[value] + + elif value not in self._map: + raise ValueError('%s value %s is not a valid value' % (self.__class__.__name__, value)) + + Integer.set(self, value) + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A unicode string or None + """ + + if self.contents is None: + return None + + if self._native is None: + self._native = self._map[self.__int__()] + return self._native + + +class UTF8String(AbstractString): + """ + Represents a UTF-8 string from ASN.1 as a Python unicode string + """ + + tag = 12 + _encoding = 'utf-8' + + +class RelativeOid(ObjectIdentifier): + """ + Represents an object identifier in ASN.1 as a Python unicode dotted + integer string + """ + + tag = 13 + + +class Sequence(Asn1Value): + """ + Represents a sequence of fields from ASN.1 as a Python object with a + dict-like interface + """ + + tag = 16 + + class_ = 0 + method = 1 + + # A list of child objects, in order of _fields + children = None + + # A list of tuples in one of the following forms. + # + # Option 1, a unicode string field name and a value class + # + # ("name", Asn1ValueClass) + # + # Option 2, same as Option 1, but with a dict of class params + # + # ("name", Asn1ValueClass, {'tag_type': 'explicit', 'tag': 5}) + _fields = [] + + # A dict with keys being the name of a field and the value being a unicode + # string of the method name on self to call to get the spec for that field + _spec_callbacks = None + + # A dict that maps unicode string field names to an index in _fields + _field_map = None + + # A list in the same order as _fields that has tuples in the form (class_, tag) + _field_ids = None + + # An optional 2-element tuple that defines the field names of an OID field + # and the field that the OID should be used to help decode. Works with the + # _oid_specs attribute. + _oid_pair = None + + # A dict with keys that are unicode string OID values and values that are + # Asn1Value classes to use for decoding a variable-type field. + _oid_specs = None + + # A 2-element tuple of the indexes in _fields of the OID and value fields + _oid_nums = None + + def _lazy_child(self, index): + """ + Builds a child object if the child has only been parsed into a tuple so far + """ + + child = self.children[index] + if isinstance(child, tuple): + child = _build(*child) + self.children[index] = child + return child + + def __len__(self): + """ + :return: + Integer + """ + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + return len(self.children) + + def __getitem__(self, key): + """ + Allows accessing fields by name or index + + :param key: + A unicode string of the field name, or an integer of the field index + + :raises: + ValueError - when a field name or index is invalid + + :return: + The Asn1Value object of the field specified + """ + + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + if not isinstance(key, int): + if key not in self._field_map: + raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) + key = self._field_map[key] + + if key >= len(self.children): + raise KeyError('No field numbered %s is present in this %s' % (key, self.__class__.__name__)) + + return self._lazy_child(key) + + def __setitem__(self, key, value): + """ + Allows settings fields by name or index + + :param key: + A unicode string of the field name, or an integer of the field index + + :param value: + A native Python datatype to set the field value to. This method will + construct the appropriate Asn1Value object from _fields. + + :raises: + ValueError - when a field name or index is invalid + """ + + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + if not isinstance(key, int): + if key not in self._field_map: + raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) + key = self._field_map[key] + + field_info = self._fields[key] + field_spec = field_info[1] + value_spec = field_spec + + if self._spec_callbacks is not None and field_info[0] in self._spec_callbacks: + callback = self._spec_callbacks[field_info[0]] + spec_override = callback(self) + # Allow a spec callback to specify both the base spec and + # the override, for situations such as OctetString and parse_as + if isinstance(spec_override, tuple) and len(spec_override) == 2: + field_spec, value_spec = spec_override + else: + value_spec = spec_override + + elif self._oid_nums is not None and key == self._oid_nums[1]: + oid = self._lazy_child(self._oid_nums[0]).native + if oid in self._oid_specs: + value_spec = self._oid_specs[oid] + + if issubclass(value_spec, Choice): + if not isinstance(value, Asn1Value): + raise ValueError('Can not set a native python value to %s, which has the choice type of %s – value must be an instance of Asn1Value' % (field_info[0], value_spec.__name__)) + if not isinstance(value, value_spec): + wrapper = value_spec() + wrapper.validate(value.class_, value.tag) + wrapper._parsed = value #pylint: disable=W0212 + new_value = wrapper + else: + new_value = value + + elif isinstance(value, value_spec): + new_value = value + + else: + new_value = value_spec(value, **(field_info[2] if len(field_info) > 2 else {})) + + # For when the field is OctetString or OctetBitString with embedded + # values we need to wrap the value in the field spec to get the + # appropriate encoded value. + if field_spec != value_spec and not issubclass(field_spec, Any): + wrapper = field_spec(value=new_value.dump()) + wrapper._parsed = new_value #pylint: disable=W0212 + new_value = wrapper + + if new_value.contents is None: + raise ValueError('Value for field "%s" of %s is not set' % (field_info[0], self.__class__.__name__)) + + self.children[key] = new_value + + if self._native is not None: + self._native[self._fields[key][0]] = self.children[key].native + + def __delitem__(self, key): + """ + Allows deleting optional or default fields by name or index + + :param key: + A unicode string of the field name, or an integer of the field index + + :raises: + ValueError - when a field name or index is invalid, or the field is not optional or defaulted + """ + + # We inline this check to prevent method invocation each time + if self.children is None: + self._parse_children() + + if not isinstance(key, int): + if key not in self._field_map: + raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) + key = self._field_map[key] + + info = self._fields[key] + if len(info) < 3 or ('default' not in info[2] and 'optional' not in info[2]): + raise ValueError('Can not delete the value for the field "%s" of %s since it is not optional or defaulted' % (info[0], self.__class__.__name__)) + + if 'optional' in info[2]: + self.children[key] = NoValue() + if self._native is not None: + self._native[info[0]] = None + else: + self.__setitem__(key, None) + + def __iter__(self): #pylint: disable=W0234 + """ + :return: + An iterator of field key names + """ + + for info in self._fields: + yield info[0] + + def _set_contents(self): + """ + Updates the .contents attribute of the value with the encoded value of + all of the child objects + """ + + self.contents = b'' + for index, info in enumerate(self._fields): + child = self.children[index] + if isinstance(child, tuple): + child_dump = child[3] + child[4] + child[5] + else: + child_dump = child.dump() + # Skip values that are the same as the default + if len(info) > 2 and 'default' in info[2]: + default_value = info[1](**info[2]) + if default_value.dump() == child_dump: + continue + self.contents += child_dump + self.header = None + if self.trailer != b'': + self.trailer = b'' + + #pylint: disable=W0212 + def _setup(self): + """ + Generates _field_map, _field_ids and _oid_nums for use in parsing + """ + + cls = self.__class__ + cls._field_map = {} + cls._field_ids = [] + for index, field in enumerate(cls._fields): + cls._field_map[field[0]] = index + params = field[2] if len(field) > 2 else {} + cls._field_ids.append(_build_id_tuple(params, field[1])) + + if cls._oid_pair is not None: + cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) + + def _parse_children(self, recurse=False): + """ + Parses the contents and generates Asn1Value objects based on the + definitions from _fields. + + :param recurse: + If child objects that are Sequence or SequenceOf objects should + be recursively parsed + + :raises: + ValueError - when an error occurs parsing child objects + """ + + try: + self.children = [] + contents_length = len(self.contents) + child_pointer = 0 + field = 0 + while child_pointer < contents_length: + parts, num_bytes = _parse(self.contents, pointer=child_pointer) + + if field < len(self._fields): + field_info = self._fields[field] + field_params = field_info[2] if len(field_info) > 2 else {} + + field_spec = field_info[1] + value_spec = field_spec + spec_override = None + + if self._spec_callbacks is not None and field_info[0] in self._spec_callbacks: + callback = self._spec_callbacks[field_info[0]] + spec_override = callback(self) + if spec_override: + # Allow a spec callback to specify both the base spec and + # the override, for situations such as OctetString and parse_as + if isinstance(spec_override, tuple) and len(spec_override) == 2: + field_spec, value_spec = spec_override #pylint: disable=W0633 + else: + value_spec = spec_override + + elif self._oid_nums is not None and self._oid_nums[1] == field: + oid = self._lazy_child(self._oid_nums[0]).native + if oid in self._oid_specs: + spec_override = self._oid_specs[oid] + value_spec = spec_override + + # If the next value is optional or default, allow it to not be present + if 'optional' in field_params or 'default' in field_params: + id_ = (parts[0], parts[2]) + + if self._field_ids[field] != id_ and field_spec != Any: + if 'optional' in field_params: + self.children.append(NoValue()) + else: + self.children.append(field_spec(**field_params)) + field += 1 + continue + + if field_spec is None or (issubclass(field_spec, Any) and spec_override): + field_spec = value_spec + spec_override = None + + if spec_override: + child = parts + (field_spec, field_params, value_spec) + else: + child = parts + (field_spec, field_params) + + else: + child = parts + + if recurse: + child = _build(*child) + if isinstance(child, (Sequence, SequenceOf)): + child._parse_children(recurse=True) #pylint: disable=W0212 + + self.children.append(child) + child_pointer += num_bytes + field += 1 + + total_fields = len(self._fields) + while len(self.children) < total_fields: + self.children.append(NoValue()) + + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + raise e + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + An OrderedDict or None. If an OrderedDict, all child values are + recursively converted to native representation also. + """ + + if self.contents is None: + return None + + if self._native is None: + if self.children is None: + self._parse_children(recurse=True) + self._native = OrderedDict() + for index, child in enumerate(self.children): + if isinstance(child, tuple): + child = _build(*child) + self.children[index] = child + self._native[self._fields[index][0]] = child.native + return self._native + + +class SequenceOf(Asn1Value): + """ + Represents a sequence (ordered) of a single type of values from ASN.1 as a + Python object with a list-like interface + """ + + tag = 16 + + class_ = 0 + method = 1 + + # A list of child objects + children = None + + # An Asn1Value class to use when parsing children + _child_spec = None + + def __init__(self, spec=None, **kwargs): + """ + Allows setting the _child_spec via the spec parameter before + passing everything else along to Asn1Value.__init__() + + :param spec: + A class derived from Asn1Value to use to parse children + """ + + if spec: + self._child_spec = spec + + Asn1Value.__init__(self, **kwargs) + + def _lazy_child(self, index): + """ + Builds a child object if the child has only been parsed into a tuple so far + """ + + child = self.children[index] + if isinstance(child, tuple): + child = _build(*child) + self.children[index] = child + return child + + def __len__(self): + """ + :return: + An integer + """ + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + return len(self.children) + + def __getitem__(self, key): + """ + Allows accessing children via index + + :param key: + Integer index of child + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + return self._lazy_child(key) + + def __setitem__(self, key, value): + """ + Allows overriding a child via index + + :param key: + Integer index of child + + :param value: + Native python datatype that will be passed to _child_spec to create + new child object + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + if issubclass(self._child_spec, Any): + if isinstance(value, Asn1Value): + self.chilren[key] = value + else: + raise ValueError('Can not set a native python value to %s where the _child_spec is Any – value must be an instance of Asn1Value' % self.__class__.__name__) + + elif issubclass(self._child_spec, Choice): + if not isinstance(value, Asn1Value): + raise ValueError('Can not set a native python value to %s where the _child_spec is the choice type %s – value must be an instance of Asn1Value' % (self.__class__.__name__, self._child_spec.__name__)) + if not isinstance(value, self._child_spec): + wrapper = self._child_spec() + wrapper.validate(value.class_, value.tag) + wrapper._parsed = value #pylint: disable=W0212 + value = wrapper + self.children[key] = value + + elif isinstance(value, self._child_spec): + self.children[key] = value + + else: + self.children[key] = self._child_spec(value=value) + + if self._native is not None: + self._native[key] = self.children[key].native + self._set_contents() + + def __delitem__(self, key): + """ + Allows removing a child via index + + :param key: + Integer index of child + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + self.children.remove(key) + if self._native is not None: + self._native.remove(key) + self._set_contents() + + def __iter__(self): #pylint: disable=W0234 + """ + :return: + An iter() of child objects + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + for index in range(0, len(self.children)): + yield self._lazy_child(index) + + def _set_contents(self): + """ + Encodes all child objects into the contents for this object + """ + + self.contents = b'' + for child in self: + self.contents += child.dump() + self.header = None + if self.trailer != b'': + self.trailer = b'' + + def _parse_children(self, recurse=False): + """ + Parses the contents and generates Asn1Value objects based on the + definitions from _child_spec. + + :param recurse: + If child objects that are Sequence or SequenceOf objects should + be recursively parsed + + :raises: + ValueError - when an error occurs parsing child objects + """ + + try: + self.children = [] + contents_length = len(self.contents) + child_pointer = 0 + while child_pointer < contents_length: + parts, num_bytes = _parse(self.contents, pointer=child_pointer) + if self._child_spec: + child = parts + (self._child_spec,) + else: + child = parts + if recurse: + child = _build(*child) + if isinstance(child, (Sequence, SequenceOf)): + child._parse_children(recurse=True) #pylint: disable=W0212 + self.children.append(child) + child_pointer += num_bytes + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + raise e + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A list or None. If a list, all child values are recursively + converted to native representation also. + """ + + if self.contents is None: + return None + + if self._native is None: + if self.children is None: + self._parse_children(recurse=True) + self._native = [child.native for child in self] + return self._native + + +class Set(Sequence): + """ + Represents a set of fields (unordered) from ASN.1 as a Python object with a + dict-like interface + """ + + method = 1 + class_ = 0 + tag = 17 + + # A dict of 2-element tuples in the form (class_, tag) as keys and integers + # as values that are the index of the field in _fields + _field_ids = None + + #pylint: disable=W0212 + def _setup(self): + """ + Generates _field_map, _field_ids and _oid_nums for use in parsing + """ + + cls = self.__class__ + cls._field_map = {} + cls._field_ids = {} + for index, field in enumerate(cls._fields): + cls._field_map[field[0]] = index + params = field[2] if len(field) > 2 else {} + cls._field_ids[_build_id_tuple(params, field[1])] = index + + if cls._oid_pair is not None: + cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) + + def _parse_children(self, recurse=False): + """ + Parses the contents and generates Asn1Value objects based on the + definitions from _fields. + + :param recurse: + If child objects that are Sequence or SequenceOf objects should + be recursively parsed + + :raises: + ValueError - when an error occurs parsing child objects + """ + + try: + child_map = {} + contents_length = len(self.contents) + child_pointer = 0 + while child_pointer < contents_length: + parts, num_bytes = _parse(self.contents, pointer=child_pointer) + + id_ = (parts[0], parts[2]) + + field = self._field_ids[id_] + field_info = self._fields[field] + field_params = field_info[2] if len(field_info) > 2 else {} + + spec = field_info[1] + parse_as = None + + if self._oid_nums is not None and self._oid_nums[1] == field: + oid = self.children[self._oid_nums[0]].native + if isinstance(spec, Any): + spec = self._oid_specs[oid] + else: + parse_as = self._oid_specs[oid] + + if parse_as: + child = parts + (spec, field_params, parse_as) + else: + child = parts + (spec, field_params) + + if recurse: + child = _build(*child) + if isinstance(child, (Sequence, SequenceOf)): + child._parse_children(recurse=True) #pylint: disable=W0212 + + child_map[field] = child + child_pointer += num_bytes + + total_fields = len(self._fields) + + for index in range(0, total_fields): + if index in child_map: + continue + field_info = self._fields[index] + + missing = False + + if len(field_info) < 3: + missing = True + elif 'optional' not in field_info[2] and 'default' not in field_info[2]: + missing = True + elif 'optional' in field_info[2]: + child_map[index] = NoValue() + elif 'default' in field_info[2]: + child_map[index] = field_info[1](**field_info[2]) + + if missing: + raise ValueError('Missing required field "%s" from %s' % (field_info[0], self.__class__.__name__)) + + self.children = [] + for index in range(0, total_fields): + self.children.append(child_map[index]) + + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + raise e + + +class SetOf(SequenceOf): + """ + Represents a set (unordered) of a single type of values from ASN.1 as a + Python object with a list-like interface + """ + + tag = 17 + + +class EmbeddedPdv(Sequence): + """ + A sequence structure + """ + + tag = 11 + + +class NumericString(AbstractString): + """ + Represents a numeric string from ASN.1 as a Python unicode string + """ + + tag = 18 + _encoding = 'latin1' + + +class PrintableString(AbstractString): + """ + Represents a printable string from ASN.1 as a Python unicode string + """ + + tag = 19 + _encoding = 'latin1' + + +class TeletexString(AbstractString): + """ + Represents a teletex string from ASN.1 as a Python unicode string + """ + + tag = 20 + _encoding = 'teletex' + + +class VideotexString(OctetString): + """ + Represents a videotex string from ASN.1 as a Python byte string + """ + + tag = 21 + + +class IA5String(AbstractString): + """ + Represents an IA5 string from ASN.1 as a Python unicode string + """ + + tag = 22 + _encoding = 'latin1' + + +class AbstractTime(AbstractString): + """ + Represents a time from ASN.1 as a Python datetime.datetime object + """ + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A datetime.datetime object in the UTC timezone or None + """ + + if self.contents is None: + return None + + if self._native is None: + string = str_cls(self) + has_timezone = re.search('[-\\+]', string) + + # 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) + + 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 + + self._native = date.replace(tzinfo=timezone.utc) + + return self._native + + +class UTCTime(AbstractTime): + """ + Represents a UTC time from ASN.1 as a Python datetime.datetime object in UTC + """ + + tag = 23 + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string or a datetime.datetime object + + :raises: + ValueError - when an invalid value is passed + """ + + if isinstance(value, datetime): + value = value.strftime('%y%m%d%H%M%SZ') + + AbstractString.set(self, value) + # Set it to None and let the class take care of converting the next + # time that .native is called + self._native = None + + def _date_by_len(self, string): + """ + Parses a date from a string based on its length + + :param string: + A unicode string to parse + + :return: + A datetime.datetime object or a unicode string + """ + + strlen = len(string) + + if strlen == 8: + return datetime.strptime(string, '%y%m%d%H') + + if strlen == 10: + return datetime.strptime(string, '%y%m%d%H%M') + + if strlen == 12: + return datetime.strptime(string, '%y%m%d%H%M%S') + + if strlen == 16: + return datetime.strptime(string, '%y%m%d%H%M%S.%f') + + return string + +class GeneralizedTime(AbstractTime): + """ + Represents a generalized time from ASN.1 as a Python datetime.datetime + object in UTC + """ + + tag = 24 + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string or a datetime.datetime object + + :raises: + ValueError - when an invalid value is passed + """ + + if isinstance(value, datetime): + value = value.strftime('%Y%m%d%H%M%SZ') + + AbstractString.set(self, value) + # Set it to None and let the class take care of converting the next + # time that .native is called + self._native = None + + def _date_by_len(self, string): + """ + Parses a date from a string based on its length + + :param string: + A unicode string to parse + + :return: + A datetime.datetime object or a unicode string + """ + + strlen = len(string) + + if strlen == 10: + return datetime.strptime(string, '%Y%m%d%H') + + if strlen == 12: + return datetime.strptime(string, '%Y%m%d%H%M') + + if strlen == 14: + return datetime.strptime(string, '%Y%m%d%H%M%S') + + if strlen == 18: + return datetime.strptime(string, '%Y%m%d%H%M%S.%f') + + return string + + +class GraphicString(AbstractString): + """ + Represents a graphic string from ASN.1 as a Python unicode string + """ + + tag = 25 + # This is technically not correct since this type can contain any charset + _encoding = 'latin1' + + +class VisibleString(AbstractString): + """ + Represents a visible string from ASN.1 as a Python unicode string + """ + + tag = 26 + _encoding = 'latin1' + + +class GeneralString(AbstractString): + """ + Represents a general string from ASN.1 as a Python unicode string + """ + + tag = 27 + # This is technically not correct since this type can contain any charset + _encoding = 'latin1' + + +class UniversalString(AbstractString): + """ + Represents a universal string from ASN.1 as a Python unicode string + """ + + tag = 28 + _encoding = 'utf-32-be' + + +class CharacterString(AbstractString): + """ + Represents a character string from ASN.1 as a Python unicode string + """ + + tag = 29 + # This is technically not correct since this type can contain any charset + _encoding = 'latin1' + + +class BMPString(AbstractString): + """ + Represents a BMP string from ASN.1 as a Python unicode string + """ + + tag = 30 + _encoding = 'utf-16-be' + + +def _build_id_tuple(params, spec): + """ + Builds a 2-element tuple used to identify fields by grabbing the class_ + and tag from an Asn1Value class and the params dict being passed to it + + :param params: + A dict of params to pass to spec + + :param spec: + An Asn1Value class + + :return: + A 2-element integer tuple in the form (class_, tag) + """ + + # Handle situations where the the spec is not known at setup time + if spec is None: + return (None, None) + + required_class = spec.class_ + required_tag = spec.tag + + tag_type = params.get('tag_type', spec.tag_type) + if tag_type is not None: + required_class = 2 + + required_class = params.get('class_', required_class) + required_tag = params.get('tag', required_tag) + + return (required_class, required_tag) + + +def _parse_id(encoded_data, pointer): + """ + Peeks ahead into a byte string and parses the ASN.1 header + + :param encoded_data: + A byte string + + :param pointer: + The index in the byte string to parse the header from + + :return: + A 4-element tuple of (class_, method, tag, number_of_bytes_consumed) + """ + + original_pointer = pointer + + first_octet = ord(encoded_data[pointer:pointer+1]) + pointer += 1 + + class_ = first_octet >> 6 + method = (first_octet >> 5) & 1 + + tag = first_octet & 31 + # Base 128 length using 8th bit as continuation indicator + if tag == 31: + tag = 0 + while True: + num = ord(encoded_data[pointer:pointer+1]) + pointer += 1 + tag *= 128 + tag += num & 127 + if num >> 7 == 0: + break + + num_bytes = pointer - original_pointer + + return (class_, method, tag, num_bytes) + + +def _build(class_, method, tag, header, contents, trailer, spec=None, spec_params=None, nested_spec=None): + """ + Builds an Asn1Value object generically, or using a spec with optional params + + :param class_: + An integer representing the ASN1 class + + :param method: + An integer representing the ASN1 method + + :param tag: + An integer representing the ASN1 tag + + :param header: + A byte string of the ASN1 header (class, method, tag, length) + + :param contents: + A byte string of the ASN1 value + + :param trailer: + A byte string of any ASN1 trailer (only used by indefinite length encodings) + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :param nested_spec: + For certain Asn1Value classes (such as OctetString and BitString), the + contents can be further parsed and interpreted as another Asn1Value. + This parameter controls the spec for that sub-parsing. + + :return: + An object of the type spec, or if not specified, a child of Asn1Value + """ + + if header is None: + return NoValue() + + # If an explicit specification was passed in, make sure it matches + if spec is not None: + if spec_params: + value = spec(**spec_params) + else: + value = spec() + + if isinstance(value, Any): + pass + + elif value.tag_type == 'explicit': + if class_ != value.explicit_class: + raise ValueError( + 'Error parsing %s - explicitly-tagged class should have been %s, but %s was found' % + ( + value.__class__.__name__, + CLASS_NUM_TO_NAME_MAP.get(value.explicit_class), + CLASS_NUM_TO_NAME_MAP.get(class_, class_) + ) + ) + if method != 1: + raise ValueError( + 'Error parsing %s - explicitly-tagged method should have been %s, but %s was found' % + ( + value.__class__.__name__, + METHOD_NUM_TO_NAME_MAP.get(1), + METHOD_NUM_TO_NAME_MAP.get(method, method) + ) + ) + if tag != value.explicit_tag: + raise ValueError( + 'Error parsing %s - explicitly-tagged tag should have been %s, but %s was found' % + ( + value.__class__.__name__, + value.explicit_tag, + tag + ) + ) + + elif isinstance(value, Choice): + value.validate(class_, tag) + + else: + if class_ != value.class_: + raise ValueError( + 'Error parsing %s - class should have been %s, but %s was found' % + ( + value.__class__.__name__, + CLASS_NUM_TO_NAME_MAP.get(value.class_), + CLASS_NUM_TO_NAME_MAP.get(class_, class_) + ) + ) + if method != value.method: + raise ValueError( + 'Error parsing %s - method should have been %s, but %s was found' % + ( + value.__class__.__name__, + METHOD_NUM_TO_NAME_MAP.get(value.method), + METHOD_NUM_TO_NAME_MAP.get(method, method) + ) + ) + if tag != value.tag: + raise ValueError( + 'Error parsing %s - tag should have been %s, but %s was found' % + ( + value.__class__.__name__, + value.tag, + tag + ) + ) + + # If no spec was specified, allow anything and just process what + # is in the input data + else: + spec = { + 1: Boolean, + 2: Integer, + 3: BitString, + 4: OctetString, + 5: Null, + 6: ObjectIdentifier, + 7: ObjectDescriptor, + 8: InstanceOf, + 9: Real, + 10: Enumerated, + 11: EmbeddedPdv, + 12: UTF8String, + 13: RelativeOid, + 16: Sequence, + 17: Set, + 18: NumericString, + 19: PrintableString, + 20: TeletexString, + 21: VideotexString, + 22: IA5String, + 23: UTCTime, + 24: GeneralizedTime, + 25: GraphicString, + 26: VisibleString, + 27: GeneralString, + 28: UniversalString, + 29: CharacterString, + 30: BMPString + }[tag] + + value = spec(class_=class_) + + value.header = header + value.contents = contents + if trailer is not None and trailer != b'': + value.trailer = trailer + + # Destroy any default value that our contents have overwritten + value._native = None #pylint: disable=W0212 + + # For explicitly tagged values, parse the inner value and pull it out + if value.tag_type == 'explicit': + original_value = value + (class_, method, tag, header, contents, trailer), _ = _parse(value.contents) + value = _build(class_, method, tag, header, contents, trailer, spec=spec) + value.header = original_value.header + header + value.trailer += original_value.trailer + value.tag_type = 'explicit' + value.explicit_class = original_value.explicit_class + value.explicit_tag = original_value.explicit_tag + + # Force parsing the Choice now + if isinstance(value, Choice): + value.parse() + + if nested_spec: + value.parse(nested_spec) + + return value + + +def _parse(encoded_data, pointer=0): + """ + Parses a byte string into component parts + + :param encoded_data: + A byte string that contains BER-encoded data + + :param pointer: + The index in the byte string to parse from + + :return: + A 2-element tuple: + - 0: A tuple of (class_, method, tag, header, content, trailer) + - 1: An integer indicating how many bytes were consumed + """ + + if len(encoded_data) == 0: + return ((None, None, None, None, None, None), 0) + + start = pointer + + class_, method, tag, num_bytes = _parse_id(encoded_data, pointer) + pointer += num_bytes + + length_octet = ord(encoded_data[pointer:pointer+1]) + pointer += 1 + length_type = length_octet >> 7 + if length_type == 1: + length = 0 + remaining_length_octets = length_octet & 127 + while remaining_length_octets > 0: + length *= 256 + length += ord(encoded_data[pointer:pointer+1]) + pointer += 1 + remaining_length_octets -= 1 + else: + length = length_octet & 127 + + header = encoded_data[start:pointer] + + # Indefinite length + if length_type == 1 and length == 0: + end_token = encoded_data.find(b'\x00\x00', pointer) + contents = encoded_data[pointer:end_token] + pointer = end_token + 2 + trailer = b'\x00\x00' + else: + contents = encoded_data[pointer:pointer+length] + pointer += length + trailer = b'' + + num_bytes = pointer - start + + return ((class_, method, tag, header, contents, trailer), num_bytes) + + +def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None): + """ + Parses a byte string generically, or using a spec with optional params + + :param encoded_data: + A byte string that contains BER-encoded data + + :param pointer: + The index in the byte string to parse from + + :param spec: + A class derived from Asn1Value that defines what class_ and tag the + value should have, and the semantics of the encoded value. The + return value will be of this type. If omitted, the encoded value + will be decoded using the standard universal tag based on the + encoded tag number. + + :param spec_params: + A dict of params to pass to the spec object + + :return: + A 2-element tuple: + - 0: An object of the type spec, or if not specified, a child of Asn1Value + - 1: An integer indicating how many bytes were consumed + """ + + (class_, method, tag, header, contents, trailer), num_bytes = _parse(encoded_data, pointer) + value = _build(class_, method, tag, header, contents, trailer, spec=spec, spec_params=spec_params) + return (value, num_bytes) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py new file mode 100644 index 0000000..e7d80c7 --- /dev/null +++ b/asn1crypto/crl.py @@ -0,0 +1,176 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .algos import SignedDigestAlgorithm +from .core import ( + Boolean, + Enumerated, + GeneralizedTime, + Integer, + ObjectIdentifier, + OctetBitString, + OctetString, + Sequence, + SequenceOf, +) +from .x509 import ( + AuthorityKeyIdentifier, + CRLDistributionPoints, + DistributionPointName, + GeneralName, + GeneralNames, + Name, + ReasonFlags, + Time, +) + + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc5280 + + +class Version(Integer): + _map = { + 0: 'v1', + 1: 'v2', + 2: 'v3', + } + + +class IssuingDistributionPoint(Sequence): + _fields = [ + ('distribution_point', DistributionPointName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('only_contains_user_certs', Boolean, {'tag_type': 'implicit', 'tag': 1, 'default': False}), + ('only_contains_ca_certs', Boolean, {'tag_type': 'implicit', 'tag': 2, 'default': False}), + ('only_some_reasons', ReasonFlags, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ('indirect_crl', Boolean, {'tag_type': 'implicit', 'tag': 4, 'default': False}), + ('only_contains_attribute_certs', Boolean, {'tag_type': 'implicit', 'tag': 5, 'default': False}), + ] + + +class AccessMethod(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1': 'ocsp', + '1.3.6.1.5.5.7.48.2': 'ca_issuers', + } + + +class AccessDescription(Sequence): + _fields = [ + ('access_method', AccessMethod), + ('access_location', GeneralName), + ] + + +class AuthorityInfoAccessSyntax(SequenceOf): + _child_spec = AccessDescription + + +class TBSCertListExtensionId(ObjectIdentifier): + _map = { + '2.5.29.18': 'issuer_alt_name', + '2.5.29.20': 'crl_number', + '2.5.29.27': 'delta_crl_indicator', + '2.5.29.28': 'issuing_distribution_point', + '2.5.29.35': 'authority_key_identifier', + '2.5.29.46': 'freshest_crl', + '1.3.6.1.5.5.7.1.1': 'authority_information_access', + } + + +class TBSCertListExtension(Sequence): + _fields = [ + ('extn_id', TBSCertListExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'issuer_alt_name': GeneralNames, + 'crl_number': Integer, + 'delta_crl_indicator': Integer, + 'issuing_distribution_point': IssuingDistributionPoint, + 'authority_key_identifier': AuthorityKeyIdentifier, + 'freshest_crl': CRLDistributionPoints, + 'authority_information_access': AuthorityInfoAccessSyntax, + } + + +class TBSCertListExtensions(SequenceOf): + _child_spec = TBSCertListExtension + + +class CRLReason(Enumerated): + _map = { + 0: 'unspecified', + 1: 'key_compromise', + 2: 'ca_compromise', + 3: 'affiliation_changed', + 4: 'superseded', + 5: 'cessation_of_operation', + 6: 'certificate_hold', + 8: 'remove_from_crl', + 9: 'privilege_withdrawn', + 10: 'aa_compromise', + } + + +class CRLEntryExtensionId(ObjectIdentifier): + _map = { + '2.5.29.21': 'crl_reason', + '2.5.29.24': 'invalidity_date', + '2.5.29.29': 'certificate_issuer', + } + + +class CRLEntryExtension(Sequence): + _fields = [ + ('extn_id', CRLEntryExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'crl_reason': CRLReason, + 'invalidity_date': GeneralizedTime, + 'certificate_issuer': GeneralNames, + } + + +class CRLEntryExtensions(SequenceOf): + _child_spec = CRLEntryExtension + + +class RevokedCertificate(Sequence): + _fields = [ + ('user_certificate', Integer), + ('revocation_date', Time), + ('crl_entry_extensions', CRLEntryExtensions, {'optional': True}), + ] + + +class RevokedCertificates(SequenceOf): + _child_spec = RevokedCertificate + + +class TbsCertList(Sequence): + _fields = [ + ('version', Version, {'optional': True}), + ('signature', SignedDigestAlgorithm), + ('issuer', Name), + ('this_update', Time), + ('next_update', Time), + ('revoked_certificates', RevokedCertificates, {'optional': True}), + ('crl_extensions', TBSCertListExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + +class CertificateList(Sequence): + _fields = [ + ('tbs_cert_list', TbsCertList), + ('signature_algorith', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py new file mode 100644 index 0000000..76ba948 --- /dev/null +++ b/asn1crypto/keys.py @@ -0,0 +1,623 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +import hashlib +from decimal import localcontext + +from .algos import DigestAlgorithm +from .core import ( + Any, + Choice, + Integer, + IntegerBitString, + IntegerOctetString, + Null, + ObjectIdentifier, + OctetBitString, + OctetString, + Sequence, + SequenceOf, + SetOf, +) +from .pkcs5 import Pkcs5EncryptionAlgorithm + +try: + # Python 2 + str_cls = unicode #pylint: disable=E0602 + byte_cls = str + import cPickle as pickle #pylint: disable=F0401 + +except NameError: + # Python 3 + str_cls = str + byte_cls = bytes + import pickle + + + +class OtherPrimeInfo(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3447#page-46 + """ + + _fields = [ + ('prime', Integer), + ('exponent', Integer), + ('coefficient', Integer), + ] + + +class OtherPrimeInfos(SequenceOf): + """ + Source: https://tools.ietf.org/html/rfc3447#page-46 + """ + + _child_spec = OtherPrimeInfo + + +class RSAPrivateKeyVersion(Integer): + """ + Original Name: Version + Source: https://tools.ietf.org/html/rfc3447#page-45 + """ + + _map = { + 0: 'two-prime', + 1: 'multi', + } + + +class RSAPrivateKey(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3447#page-45 + """ + + _fields = [ + ('version', RSAPrivateKeyVersion), + ('modulus', Integer), + ('public_exponent', Integer), + ('private_exponent', Integer), + ('prime1', Integer), + ('prime2', Integer), + ('exponent1', Integer), + ('exponent2', Integer), + ('coefficient', Integer), + ('other_prime_infos', OtherPrimeInfos, {'optional': True}) + ] + + +class RSAPublicKey(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3447#page-44 + """ + + _fields = [ + ('modulus', Integer), + ('public_exponent', Integer) + ] + + +class DSAPrivateKey(Sequence): + """ + The ASN1 structure that OpenSSL uses to store a DSA private key that is + not part of a PKCS#8 structure. Reversed engineered from english-language + description on linked OpenSSL documentation page. + + Original Name: None + Source: https://www.openssl.org/docs/apps/dsa.html + """ + + _fields = [ + ('version', Integer), + ('p', Integer), + ('q', Integer), + ('g', Integer), + ('public_key', Integer), + ('private_key', Integer), + ] + + +class SpecifiedECDomainVersion(Integer): + """ + Source: http://www.secg.org/sec1-v2.pdf page 104 + """ + _map = { + 1: 'ecdpVer1', + 2: 'ecdpVer2', + 3: 'ecdpVer3', + } + + +class FieldType(ObjectIdentifier): + """ + Original Name: None + Source: http://www.secg.org/sec1-v2.pdf page 101 + """ + + _map = { + '1.2.840.10045.1.1': 'prime_field', + '1.2.840.10045.1.2': 'characteristic_two_field', + } + + +class CharacteristicTwoBasis(ObjectIdentifier): + """ + Original Name: None + Source: http://www.secg.org/sec1-v2.pdf page 102 + """ + + _map = { + '1.2.840.10045.1.2.1.1': 'gn_basis', + '1.2.840.10045.1.2.1.2': 'tp_basis', + '1.2.840.10045.1.2.1.3': 'pp_basis', + } + + +class Pentanomial(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 102 + """ + + _fields = [ + ('k1', Integer), + ('k2', Integer), + ('k3', Integer), + ] + + +class CharacteristicTwo(Sequence): + """ + Original Name: Characteristic-two + Source: http://www.secg.org/sec1-v2.pdf page 101 + """ + + _fields = [ + ('m', Integer), + ('basis', CharacteristicTwoBasis), + ('parameters', Any), + ] + + _oid_pair = ('basis', 'parameters') + _oid_specs = { + 'gn_basis': Null, + 'tp_basis': Integer, + 'pp_basis': Pentanomial, + } + + +class FieldID(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 100 + """ + + _fields = [ + ('field_type', FieldType), + ('parameters', Any), + ] + + _oid_pair = ('field_type', 'parameters') + _oid_specs = { + 'prime_field': Integer, + 'characteristic_two_field': CharacteristicTwo, + } + + +class Curve(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 104 + """ + + _fields = [ + ('a', OctetString), + ('b', OctetString), + ('seed', OctetBitString, {'optional': True}), + ] + + +class SpecifiedECDomain(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 103 + """ + + _fields = [ + ('version', SpecifiedECDomainVersion), + ('field_id', FieldID), + ('curve', Curve), + ('base', OctetString), + ('order', Integer), + ('cofactor', Integer, {'optional': True}), + ('hash', DigestAlgorithm, {'optional': True}), + ] + + +class NamedCurve(ObjectIdentifier): + """ + Various named curves + + Original Name: None + Source: https://tools.ietf.org/html/rfc3279#page-23, + https://tools.ietf.org/html/rfc5480#page-5 + """ + + _map = { + # https://tools.ietf.org/html/rfc3279#page-23 + '1.2.840.10045.3.0.1': 'c2pnb163v1', + '1.2.840.10045.3.0.2': 'c2pnb163v2', + '1.2.840.10045.3.0.3': 'c2pnb163v3', + '1.2.840.10045.3.0.4': 'c2pnb176w1', + '1.2.840.10045.3.0.5': 'c2tnb191v1', + '1.2.840.10045.3.0.6': 'c2tnb191v2', + '1.2.840.10045.3.0.7': 'c2tnb191v3', + '1.2.840.10045.3.0.8': 'c2onb191v4', + '1.2.840.10045.3.0.9': 'c2onb191v5', + '1.2.840.10045.3.0.10': 'c2pnb208w1', + '1.2.840.10045.3.0.11': 'c2tnb239v1', + '1.2.840.10045.3.0.12': 'c2tnb239v2', + '1.2.840.10045.3.0.13': 'c2tnb239v3', + '1.2.840.10045.3.0.14': 'c2onb239v4', + '1.2.840.10045.3.0.15': 'c2onb239v5', + '1.2.840.10045.3.0.16': 'c2pnb272w1', + '1.2.840.10045.3.0.17': 'c2pnb304w1', + '1.2.840.10045.3.0.18': 'c2tnb359v1', + '1.2.840.10045.3.0.19': 'c2pnb368w1', + '1.2.840.10045.3.0.20': 'c2tnb431r1', + '1.2.840.10045.3.1.1': 'prime192v1', + '1.2.840.10045.3.1.2': 'prime192v2', + '1.2.840.10045.3.1.3': 'prime192v3', + '1.2.840.10045.3.1.4': 'prime239v1', + '1.2.840.10045.3.1.5': 'prime239v2', + '1.2.840.10045.3.1.6': 'prime239v3', + '1.2.840.10045.3.1.7': 'prime256v1', + # https://tools.ietf.org/html/rfc5480#page-5 + '1.3.132.0.1': 'sect163k1', + '1.3.132.0.15': 'sect163r2', + '1.3.132.0.33': 'secp224r1', + '1.3.132.0.26': 'sect233k1', + '1.3.132.0.27': 'sect233r1', + '1.3.132.0.16': 'sect283k1', + '1.3.132.0.17': 'sect283r1', + '1.3.132.0.34': 'secp384r1', + '1.3.132.0.36': 'sect409k1', + '1.3.132.0.37': 'sect409r1', + '1.3.132.0.35': 'secp521r1', + '1.3.132.0.38': 'sect571k1', + '1.3.132.0.39': 'sect571r1', + } + + +class ECDomainParameters(Choice): + """ + Source: http://www.secg.org/sec1-v2.pdf page 102 + """ + + _alternatives = [ + ('specified', SpecifiedECDomain), + ('named', NamedCurve), + ('implicit_ca', Null), + ] + + +class ECPrivateKeyVersion(Integer): + """ + Original Name: None + Source: http://www.secg.org/sec1-v2.pdf page 108 + """ + + _map = { + 1: 'ecPrivkeyVer1', + } + + +class ECPrivateKey(Sequence): + """ + Source: http://www.secg.org/sec1-v2.pdf page 108 + """ + + _fields = [ + ('version', ECPrivateKeyVersion), + ('private_key', IntegerOctetString), + ('parameters', ECDomainParameters, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('public_key', IntegerBitString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ] + + + +class DSAParams(Sequence): + """ + Parameters for a DSA public or private key + + Original Name: Dss-Parms + Source: https://tools.ietf.org/html/rfc3279#page-9 + """ + + _fields = [ + ('p', Integer), + ('q', Integer), + ('g', Integer), + ] + + +class Attribute(Sequence): + """ + Source: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-X.501-198811-S!!PDF-E&type=items page 8 + """ + + _fields = [ + ('type', ObjectIdentifier), + ('values', SetOf, {'spec': Any}), + ] + + +class Attributes(SetOf): + """ + Source: https://tools.ietf.org/html/rfc5208#page-3 + """ + + _child_spec = Attribute + + +class PrivateKeyAlgorithmId(ObjectIdentifier): + """ + These OIDs for various public keys are reused when storing private keys + inside of a PKCS#8 structure + + Original Name: None + Source: https://tools.ietf.org/html/rfc3279 + """ + + _map = { + # https://tools.ietf.org/html/rfc3279#page-19 + '1.2.840.113549.1.1.1': 'rsa', + # https://tools.ietf.org/html/rfc3279#page-18 + '1.2.840.10040.4.1': 'dsa', + # https://tools.ietf.org/html/rfc3279#page-13 + '1.2.840.10045.2.1': 'ecdsa', + } + + +class PrivateKeyAlgorithm(Sequence): + """ + Original Name: PrivateKeyAlgorithmIdentifier + Source: https://tools.ietf.org/html/rfc5208#page-3 + """ + + _fields = [ + ('algorithm', PrivateKeyAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'rsa': Null, + 'dsa': DSAParams, + 'ecdsa': ECDomainParameters, + } + + +class PrivateKeyInfo(Sequence): + """ + Source: https://tools.ietf.org/html/rfc5208#page-3 + """ + + _fields = [ + ('version', Integer), + ('private_key_algorithm', PrivateKeyAlgorithm), + ('private_key', OctetString), + ('attributes', Attributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ] + + def _private_key_spec(self): + algorithm = self['private_key_algorithm']['algorithm'].native + return { + 'rsa': RSAPrivateKey, + 'dsa': Integer, + 'ecdsa': ECPrivateKey, + }[algorithm] + + _spec_callbacks = { + 'private_key': _private_key_spec + } + + _fingerprint = None + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a public key to see if + the two form a pair. + + This fingerprint is not compatiable with fingerprints generated by any + other software. + + :raises: + ValueError - when the private key is ECDSA, but the public_key field is empty + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + if self._fingerprint is None: + key_type = self['private_key_algorithm']['algorithm'].native + params = self['private_key_algorithm']['parameters'].native + key = self['private_key'].parsed + + if key_type == 'rsa': + to_hash = '%d:%d' % ( + key['modulus'].native, + key['public_exponent'].native, + ) + + elif key_type == 'dsa': + # The private key structure for PKCS#8 does not include the + # public key, so we must calculate it here + with localcontext() as ctx: + ctx.prec = 200 + public_key = ctx.power(params['g'].native, key.native, params['p'].native) + + to_hash = '%d:%d:%d:%d' % ( + params['p'].native, + params['q'].native, + params['g'].native, + int(public_key), + ) + + elif key_type == 'ecdsa': + public_key = key['public_key'].native + if public_key is None: + raise ValueError('Unable to compute fingerprint of ecdsa private key since the public_key field is empty') + + if params.name == 'named': + to_hash = '%s:%d' % ( + params.chosen.native, + public_key, + ) + + elif params.named == 'implicit_ca': + to_hash = str_cls(public_key) + + elif params.named == 'specified': + to_hash = b'%s:%s' % ( + pickle.dumps(params.native), + str_cls(public_key).encode('utf-8'), + ) + + if isinstance(to_hash, str_cls): + to_hash = to_hash.encode('utf-8') + + self._fingerprint = hashlib.sha256(to_hash).digest() + + return self._fingerprint + + +class EncryptedPrivateKeyInfo(Sequence): + """ + Source: https://tools.ietf.org/html/rfc5208#page-4 + """ + + _fields = [ + ('encryption_algorithm', Pkcs5EncryptionAlgorithm), + ('encrypted_data', OctetString), + ] + + +# These structures are from https://tools.ietf.org/html/rfc3279 + +class PublicKeyAlgorithmId(ObjectIdentifier): + """ + Original Name: None + Source: https://tools.ietf.org/html/rfc3279 + """ + + _map = { + # https://tools.ietf.org/html/rfc3279#page-19 + '1.2.840.113549.1.1.1': 'rsa', + # https://tools.ietf.org/html/rfc3279#page-18 + '1.2.840.10040.4.1': 'dsa', + # https://tools.ietf.org/html/rfc3279#page-13 + '1.2.840.10045.2.1': 'ecdsa', + } + + +class PublicKeyAlgorithm(Sequence): + """ + Original Name: AlgorithmIdentifier + Source: https://tools.ietf.org/html/rfc5280#page-18 + """ + + _fields = [ + ('algorithm', PublicKeyAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'rsa': Null, + 'dsa': DSAParams, + 'ecdsa': ECDomainParameters, + } + + +class PublicKeyInfo(Sequence): + """ + Original Name: SubjectPublicKeyInfo + Source: https://tools.ietf.org/html/rfc5280#page-17 + """ + + _fields = [ + ('algorithm', PublicKeyAlgorithm), + ('public_key', OctetBitString), + ] + + def _public_key_spec(self): + algorithm = self['algorithm']['algorithm'].native + return { + 'rsa': RSAPublicKey, + 'dsa': Integer, + # ECSDA's public key is an ECPoint, which is an OctetString. Since + # we are using OctetBitString here, we don't need further parsing. + 'ecdsa': OctetString, + }[algorithm] + + _spec_callbacks = { + 'public_key': _public_key_spec + } + + _fingerprint = None + + @property + def fingerprint(self): + """ + Creates a fingerprint that can be compared with a private key to see if + the two form a pair. + + This fingerprint is not compatiable with fingerprints generated by any + other software. + + :return: + A byte string that is a sha256 hash of selected components (based + on the key type) + """ + + if self._fingerprint is None: + key_type = self['algorithm']['algorithm'].native + params = self['algorithm']['parameters'].native + key = self['public_key'].parsed + + if key_type == 'rsa': + to_hash = '%d:%d' % ( + key['modulus'].native, + key['public_exponent'].native, + ) + + elif key_type == 'dsa': + to_hash = '%d:%d:%d:%d' % ( + params['p'].native, + params['q'].native, + params['g'].native, + key.native, + ) + + elif key_type == 'ecdsa': + if params.name == 'named': + to_hash = '%s:%d' % ( + params.chosen.native, + key.native, + ) + + elif params.named == 'implicit_ca': + to_hash = str_cls(key.native) + + elif params.named == 'specified': + to_hash = b'%s:%s' % ( + pickle.dumps(params.native), + str_cls(key.native).encode('utf-8'), + ) + + if isinstance(to_hash, str_cls): + to_hash = to_hash.encode('utf-8') + + self._fingerprint = hashlib.sha256(to_hash).digest() + + return self._fingerprint diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py new file mode 100644 index 0000000..69f9abf --- /dev/null +++ b/asn1crypto/ocsp.py @@ -0,0 +1,307 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .algos import DigestAlgorithm, SignedDigestAlgorithm +from .core import ( + Boolean, + Choice, + Enumerated, + GeneralizedTime, + IA5String, + Integer, + Null, + ObjectIdentifier, + OctetBitString, + OctetString, + Sequence, + SequenceOf, +) +from .crl import AuthorityInfoAccessSyntax, CRLReason +from .keys import PublicKeyAlgorithm +from .x509 import Certificate, GeneralName, GeneralNames, Name + + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc6960 + + +class ResponseType(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.1': 'basic_ocsp_response', + } + + +class AcceptableResponses(SequenceOf): + _child_spec = ResponseType + + +class ServiceLocator(Sequence): + _fields = [ + ('issuer', Name), + ('locator', AuthorityInfoAccessSyntax), + ] + + +class PreferredSignatureAlgorithm(Sequence): + _fields = [ + ('sig_identifier', SignedDigestAlgorithm), + ('cert_identifier', PublicKeyAlgorithm, {'optional': True}), + ] + + +class PreferredSignatureAlgorithms(SequenceOf): + _child_spec = PreferredSignatureAlgorithm + + +class RequestExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.7': 'ocsp_service_locator', + } + + +class RequestExtension(Sequence): + _fields = [ + ('extn_id', RequestExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'ocsp_service_locator': ServiceLocator, + } + + +class RequestExtensions(SequenceOf): + _child_spec = RequestExtension + + +class TBSRequestExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.2': 'ocsp_noonce', + '1.3.6.1.5.5.7.48.1.4': 'ocsp_response', + '1.3.6.1.5.5.7.48.1.8': 'ocsp_preferred_signature_algorithms', + } + + +class TBSRequestExtension(Sequence): + _fields = [ + ('extn_id', TBSRequestExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'ocsp_noonce': OctetString, + 'ocsp_response': AcceptableResponses, + 'ocsp_preferred_signature_algorithms': PreferredSignatureAlgorithms, + } + + +class TBSRequestExtensions(SequenceOf): + _child_spec = TBSRequestExtension + + +class ResponseDataExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.2': 'ocsp_noonce', + '1.3.6.1.5.5.7.48.1.9': 'ocsp_extended_revoke', + } + + +class ResponseDataExtension(Sequence): + _fields = [ + ('extn_id', ResponseDataExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'ocsp_noonce': OctetString, + 'ocsp_extended_revoke': Null, + } + + +class ResponseDataExtensions(SequenceOf): + _child_spec = ResponseDataExtension + + +class CrlId(Sequence): + _fields = [ + ('crl_url', IA5String, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('crl_num', Integer, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('crl_time', GeneralizedTime, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ] + + +class SingleResponseExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.3': 'ocsp_crl', + '1.3.6.1.5.5.7.48.1.6': 'ocsp_archive_cutoff', + # These are CRLEntryExtension values from https://tools.ietf.org/html/rfc5280 + '2.5.29.21': 'crl_reason', + '2.5.29.24': 'invalidity_date', + '2.5.29.29': 'certificate_issuer', + } + + +class SingleResponseExtension(Sequence): + _fields = [ + ('extn_id', SingleResponseExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'ocsp_crl': CrlId, + 'ocsp_archive_cutoff': GeneralizedTime, + 'crl_reason': CRLReason, + 'invalidity_date': GeneralizedTime, + 'certificate_issuer': GeneralNames, + } + + +class SingleResponseExtensions(SequenceOf): + _child_spec = SingleResponseExtension + + +class Version(Integer): + _map = { + 0: 'v1' + } + +class CertId(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm), + ('issuer_name_hash', OctetString), + ('issuer_key_hash', OctetString), + ('serial_number', Integer), + ] + + +class Request(Sequence): + _fields = [ + ('req_cert', CertId), + ('single_request_extensions', RequestExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + +class Requests(SequenceOf): + _child_spec = Request + + +class TBSRequest(Sequence): + _fields = [ + ('version', Version, {'tag_type': 'explicit', 'tag': 0, 'default': 'v1'}), + ('requestor_name', GeneralName, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('request_list', Requests), + ('request_extensions', TBSRequestExtensions, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ] + + +class Certificates(SequenceOf): + _child_spec = Certificate + + +class Signature(Sequence): + _fields = [ + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ('certs', Certificates, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + +class OCSPRequest(Sequence): + _fields = [ + ('tbs_request', TBSRequest), + ('optional_signature', Signature, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + +class OCSPResponseStatus(Enumerated): + _map = { + 0: 'successful', + 1: 'malformed_request', + 2: 'internal_error', + 3: 'try_later', + 5: 'sign_required', + 6: 'unauthoried', + } + + +class ResponderId(Choice): + _alternatives = [ + ('by_name', Name, {'tag_type': 'explicit', 'tag': 1}), + ('by_key', OctetString, {'tag_type': 'explicit', 'tag': 2}), + ] + + +class RevokedInfo(Sequence): + _fields = [ + ('revocation_time', GeneralizedTime), + ('revocation_reason', CRLReason, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + +class CertStatus(Choice): + _alternatives = [ + ('good', Null, {'tag_type': 'implicit', 'tag': 0}), + ('revoked', RevokedInfo, {'tag_type': 'implicit', 'tag': 1}), + ('unknown', Null, {'tag_type': 'implicit', 'tag': 2}), + ] + + +class SingleResponse(Sequence): + _fields = [ + ('cert_id', CertId), + ('cert_status', CertStatus), + ('this_update', GeneralizedTime), + ('next_update', GeneralizedTime, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('single_extensions', SingleResponseExtensions, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ] + + +class Responses(SequenceOf): + _child_spec = SingleResponse + + +class ResponseData(Sequence): + _fields = [ + ('version', Version, {'tag_type': 'explicit', 'tag': 0, 'default': 'v1'}), + ('responder_id', ResponderId), + ('produced_at', GeneralizedTime), + ('responses', Responses), + ('response_extensions', ResponseDataExtensions, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ] + + +class BasicOCSPResponse(Sequence): + _fields = [ + ('tbs_response_data', ResponseData), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ('certs', Certificates, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] + + +class ResponseBytes(Sequence): + _fields = [ + ('response_type', ResponseType), + ('response', OctetString), + ] + + _oid_pair = ('response_type', 'response') + _oid_specs = { + 'basic_ocsp_response': BasicOCSPResponse, + } + + +class OCSPResponse(Sequence): + _fields = [ + ('response_status', OCSPResponseStatus), + ('response_bytes', ResponseBytes, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py new file mode 100644 index 0000000..f707682 --- /dev/null +++ b/asn1crypto/pdf.py @@ -0,0 +1,62 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .cms import CMSAttributeType, CMSAttribute +from .core import ( + Boolean, + Integer, + Null, + ObjectIdentifier, + OctetString, + Sequence, + SetOf, +) +from .crl import CertificateList +from .ocsp import OCSPResponse +from .x509 import ExtensionId, Extension, GeneralName, KeyPurposeId + + + +class AdobeArchiveRevInfo(Sequence): + _fields = [ + ('version', Integer) + ] + + +class AdobeTimestamp(Sequence): + _fields = [ + ('version', Integer), + ('location', GeneralName), + ('requires_auth', Boolean), + ] + + +class OtherRevInfo(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('value', OctetString), + ] + + +class RevocationInfoArchival(Sequence): + _fields = [ + ('crl', CertificateList, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('ocsp', OCSPResponse, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('other_rev_info', OtherRevInfo, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ] + + +class SetOfRevocationInfoArchival(SetOf): + _child_spec = RevocationInfoArchival + + +ExtensionId._map['1.2.840.113583.1.1.9.2'] = 'adobe_archive_rev_info' #pylint: disable=W0212 +ExtensionId._map['1.2.840.113583.1.1.9.1'] = 'adobe_timestamp' #pylint: disable=W0212 +ExtensionId._map['1.2.840.113583.1.1.10'] = 'adobe_ppklite_credential' #pylint: disable=W0212 +Extension._oid_specs['adobe_archive_rev_info'] = AdobeArchiveRevInfo #pylint: disable=W0212 +Extension._oid_specs['adobe_timestamp'] = AdobeTimestamp #pylint: disable=W0212 +Extension._oid_specs['adobe_ppklite_credential'] = Null #pylint: disable=W0212 +KeyPurposeId._map['1.2.840.113583.1.1.5'] = 'pdf_signing' #pylint: disable=W0212 +CMSAttributeType._map['1.2.840.113583.1.1.8'] = 'adobe_revocation_info_archival' #pylint: disable=W0212 +CMSAttribute._oid_specs['adobe_revocation_info_archival'] = SetOfRevocationInfoArchival #pylint: disable=W0212 diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py new file mode 100644 index 0000000..22430c1 --- /dev/null +++ b/asn1crypto/pkcs12.py @@ -0,0 +1,125 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .algos import DigestInfo +from .core import ( + Any, + Integer, + ObjectIdentifier, + OctetString, + Sequence, + SequenceOf, +) +from .cms import ContentInfo, SignedData +from .keys import PrivateKeyInfo, EncryptedPrivateKeyInfo +from .x509 import Attributes, Certificate + + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc7292 + +class MacData(Sequence): + _fields = [ + ('mac', DigestInfo), + ('mac_salt', OctetString), + ('iterations', Integer, {'default': 1}), + ] + + +class Version(Integer): + _map = { + 3: 'v3' + } + + +class Pfx(Sequence): + _fields = [ + ('version', Version), + ('auth_safe', ContentInfo), + ('mac_data', MacData, {'optional': True}) + ] + + _authenticated_safe = None + + @property + def authenticated_safe(self): + if self._authenticated_safe is None: + content = self['auth_safe']['content'] + if isinstance(content, SignedData): + content = content['content_info']['content'] + self._authenticated_safe = AuthenticatedSafe.load(content.native) + return self._authenticated_safe + + +class AuthenticatedSafe(SequenceOf): + _child_spec = ContentInfo + + +class BagId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.12.10.1.1': 'key_bag', + '1.2.840.113549.1.12.10.1.2': 'pkcs8_shrouded_key_bag', + '1.2.840.113549.1.12.10.1.3': 'cert_bag', + '1.2.840.113549.1.12.10.1.4': 'crl_bag', + '1.2.840.113549.1.12.10.1.5': 'secret_bag', + '1.2.840.113549.1.12.10.1.6': 'safe_contents', + } + + +class CertId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.22.1': 'x509', + '1.2.840.113549.1.9.22.2': 'sdsi', + } + + +class CertBag(Sequence): + _fields = [ + ('cert_id', CertId), + ('cert_value', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ] + + _oid_pair = ('cert_id', 'cert_value') + _oid_specs = { + 'x509': Certificate, + } + + +class CrlBag(Sequence): + _fields = [ + ('crl_id', ObjectIdentifier), + ('crl_value', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ] + + +class SecretBag(Sequence): + _fields = [ + ('secret_type_id', ObjectIdentifier), + ('secret_value', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ] + + +class SafeContents(SequenceOf): + pass + + +class SafeBag(Sequence): + _fields = [ + ('bag_id', BagId), + ('bag_value', Any, {'tag_type': 'explicit', 'tag': 0}), + ('bag_attributes', Attributes, {'optional': True}), + ] + + _oid_pair = ('bag_id', 'bag_value') + _oid_specs = { + 'key_bag': PrivateKeyInfo, + 'pkcs8_shrouded_key_bag': EncryptedPrivateKeyInfo, + 'cert_bag': CertBag, + 'crl_bag': CrlBag, + 'secret_bag': SecretBag, + 'safe_contents': SafeContents + } + + +SafeContents._child_spec = SafeBag #pylint: disable=W0212 diff --git a/asn1crypto/pkcs5.py b/asn1crypto/pkcs5.py new file mode 100644 index 0000000..17d1874 --- /dev/null +++ b/asn1crypto/pkcs5.py @@ -0,0 +1,429 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .algos import AlgorithmIdentifier, EncryptionAlgorithm, HmacAlgorithm +from .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence + + +# The structures in this file are taken from +# http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf +# with extra OIDs from https://tools.ietf.org/html/rfc7292 (PKCS#12) + + +class Pbes1Params(Sequence): + _fields = [ + ('salt', OctetString), + ('iterations', Integer), + ] + + +class Pbkdf2Salt(Choice): + _fields = [ + ('specified', OctetString), + ('other_source', AlgorithmIdentifier), + ] + + +class Pbkdf2Params(Sequence): + _fields = [ + ('salt', Pbkdf2Salt), + ('iteration_count', Integer), + ('key_length', Integer, {'optional': True}), + ('prf', HmacAlgorithm, {'optional': True}), + ] + + +class KdfAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.5.12': 'pbkdf2' + } + + +class KdfAlgorithm(Sequence): + _fields = [ + ('algorithm', KdfAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'pbkdf2': Pbkdf2Params + } + + +class Pbes2Params(Sequence): + _fields = [ + ('key_derivation_func', KdfAlgorithm), + ('encryption_scheme', EncryptionAlgorithm), + ] + + +class Pkcs5EncryptionId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.5.13': 'pbes2', + '1.2.840.113549.1.5.1': 'pbes1_md2_des', + '1.2.840.113549.1.5.3': 'pbes1_md5_des', + '1.2.840.113549.1.5.4': 'pbes1_md2_rc2', + '1.2.840.113549.1.5.6': 'pbes1_md5_rc2', + '1.2.840.113549.1.5.10': 'pbes1_sha1_des', + '1.2.840.113549.1.5.11': 'pbes1_sha1_rc2', + '1.2.840.113549.1.12.1.1': 'pkcs12_sha1_rc4_128', + '1.2.840.113549.1.12.1.2': 'pkcs12_sha1_rc4_40', + '1.2.840.113549.1.12.1.3': 'pkcs12_sha1_tripledes_3key', + '1.2.840.113549.1.12.1.4': 'pkcs12_sha1_tripledes_2key', + '1.2.840.113549.1.12.1.5': 'pkcs12_sha1_rc2_128', + '1.2.840.113549.1.12.1.6': 'pkcs12_sha1_rc2_40', + } + + +class Pkcs5EncryptionAlgorithm(Sequence): + _fields = [ + ('algorithm', Pkcs5EncryptionId), + ('parameters', Any), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'pbes2': Pbes2Params, + 'pbes1_md2_des': Pbes1Params, + 'pbes1_md5_des': Pbes1Params, + 'pbes1_md2_rc2': Pbes1Params, + 'pbes1_md5_rc2': Pbes1Params, + 'pbes1_sha1_des': Pbes1Params, + 'pbes1_sha1_rc2': Pbes1Params, + 'pkcs12_sha1_rc4_128': Pbes1Params, + 'pkcs12_sha1_rc4_40': Pbes1Params, + 'pkcs12_sha1_tripledes_3key': Pbes1Params, + 'pkcs12_sha1_tripledes_2key': Pbes1Params, + 'pkcs12_sha1_rc2_128': Pbes1Params, + 'pkcs12_sha1_rc2_40': Pbes1Params, + } + + @property + def kdf(self): + """ + Returns the name of the key derivation function to use. + + :return: + A unicode from of one of the following: "pbkdf1", "pbkdf2", "pkcs12_kdf" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + return self['parameters'].parsed['key_derivation_func']['algorithm'].native + + if encryption_algo.find('.') == -1: + encryption_algo, _ = self['algorithm'].native.split('_', 2) + + if encryption_algo == 'pbes1': + return 'pbkdf1' + + if encryption_algo == 'pkcs12': + return 'pkcs12_kdf' + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation function' % encryption_algo) + + @property + def kdf_hmac(self): + """ + Returns the HMAC algorithm to use with the KDF. + + :return: + A unicode string of one of the following: "md2", "md5", "sha1", "sha224", "sha256", "sha384", "sha512" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + return self['parameters'].parsed['key_derivation_func']['parameters']['prf']['algorithm'].native + + if encryption_algo.find('.') == -1: + _, hmac_algo, _ = self['algorithm'].native.split('_', 3) + return hmac_algo + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation hmac algorithm' % encryption_algo) + + @property + def kdf_salt(self): + """ + Returns the byte string to use as the salt for the KDF. + + :return: + A byte string + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + salt = self['parameters'].parsed['key_derivation_func']['algorithm']['salt'] + + if salt.name == 'other_source': + raise ValueError('Can not determine key derivation salt - the reversed-for-future-use other source salt choice was specified in the PBKDF2 params structure') + + return salt.native + + if encryption_algo.find('.') == -1: + return self['parameters'].parsed['salt'].native + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation salt' % encryption_algo) + + @property + def kdf_iterations(self): + """ + Returns the number of iterations that should be run via the KDF. + + :return: + An integer + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + return self['parameters'].parsed['key_derivation_func']['algorithm']['iteration_count'].native + + if encryption_algo.find('.') == -1: + return self['parameters'].parsed['iterations'].native + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation iterations' % encryption_algo) + + @property + def key_length(self): + """ + Returns the key length to pass to the KDF/cipher. The PKCS#5 spec does + not specify a way to store the RC5 key length, however this tends not + to be a problem since OpenSSL does not support RC5 in PKCS#8 and OS X + does not provide an RC5 cipher for use in the Security Transforms + library. + + :raises: + ValueError - when the key length can not be determined + + :return: + An integer representing the length in bytes + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + key_length = self['parameters'].parsed['key_derivation_func']['algorithm']['key_length'].native + if key_length is not None: + return key_length + + # If the KDF params don't specify the key size, we can infer it from + # the encryption scheme for all schemes except for RC5. However, in + # practical terms, neither OpenSSL or OS X support RC5 for PKCS#8 + # so it is unlikely to be an issue that is run into. + + cipher = self['parameters'].parsed['encryption_scheme']['algorithm'].native + + cipher_lengths = { + 'des': 8, + 'tripledes_3key': 24, + 'aes128': 16, + 'aes192': 24, + 'aes256': 32, + } + + if cipher in cipher_lengths: + return cipher_lengths[cipher] + + if cipher == 'rc2': + rc2_params = self['parameters'].parsed['encryption_scheme']['parameters'].parsed + rc2_parameter_version = rc2_params['rc2_parameter_version'].native + + # See page 24 of http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf + encoded_key_bits_map = { + 160: 5, # 40-bit + 120: 8, # 64-bit + 58: 16, # 128-bit + } + + if rc2_parameter_version in encoded_key_bits_map: + return encoded_key_bits_map[rc2_parameter_version] + + if rc2_parameter_version >= 256: + return rc2_parameter_version + + if rc2_parameter_version is None: + return 4 # 32-bit default + + raise ValueError('Invalid RC2 parameter version found in PBES2 encryption scheme parameters') + + # There + raise ValueError('Unable to determine the key length for the encryption scheme "%s"' % cipher) + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 8, + 'pbes1_md5_des': 8, + 'pbes1_md2_rc2': 8, + 'pbes1_md5_rc2': 8, + 'pbes1_sha1_des': 8, + 'pbes1_sha1_rc2': 8, + 'pkcs12_sha1_rc4_128': 16, + 'pkcs12_sha1_rc4_40': 5, + 'pkcs12_sha1_tripledes_3key': 24, + 'pkcs12_sha1_tripledes_2key': 16, + 'pkcs12_sha1_rc2_128': 16, + 'pkcs12_sha1_rc2_40': 5, + }[encryption_algo] + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation function key length' % encryption_algo) + + @property + def encryption_cipher(self): + """ + Returns the name of the symmetric encryption cipher to use. The key + length can be retrieved via the .key_length property to disabiguate + between different variations of TripleDES, AES, and the RC* ciphers. + + :return: + A unicode string from one of the following: "rc2", "rc4", "rc5", "des", "tripledes", "aes" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + cipher = self['parameters'].parsed['encryption_scheme']['algorithm'].native + cipher_map = { + 'des': 'des', + 'tripledes_3key': 'tripledes', + 'aes128': 'aes', + 'aes192': 'aes', + 'aes256': 'aes', + 'rc2': 'rc2', + 'rc5': 'rc5', + } + if cipher in cipher_map: + return cipher_map[cipher] + + raise ValueError('Unrecognized encryption cipher "%s"' % cipher) + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 'des', + 'pbes1_md5_des': 'des', + 'pbes1_md2_rc2': 'rc2', + 'pbes1_md5_rc2': 'rc2', + 'pbes1_sha1_des': 'des', + 'pbes1_sha1_rc2': 'rc2', + 'pkcs12_sha1_rc4_128': 'rc4', + 'pkcs12_sha1_rc4_40': 'rc4', + 'pkcs12_sha1_tripledes_3key': 'tripledes', + 'pkcs12_sha1_tripledes_2key': 'tripledes', + 'pkcs12_sha1_rc2_128': 'rc2', + 'pkcs12_sha1_rc2_40': 'rc2', + }[encryption_algo] + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine encryption cipher' % encryption_algo) + + @property + def encryption_block_size(self): + """ + Returns the block size of the encryption cipher, in bytes. For RC4, a + stream cipher, 0 is returned. + + :return: + An integer that is the block size in bytes + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + scheme = self['parameters'].parsed['encryption_scheme'] + cipher = scheme['algorithm'].native + cipher_map = { + 'des': 8, + 'tripledes_3key': 8, + 'aes128': 16, + 'aes192': 16, + 'aes256': 16, + 'rc2': 8, + } + if cipher in cipher_map: + return cipher_map[cipher] + + if cipher == 'rc5': + return scheme['parameters'].parsed['block_size_in_bits'].native / 8 + + raise ValueError('Unrecognized encryption cipher "%s", can not determine block size' % cipher) + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 8, + 'pbes1_md5_des': 8, + 'pbes1_md2_rc2': 8, + 'pbes1_md5_rc2': 8, + 'pbes1_sha1_des': 8, + 'pbes1_sha1_rc2': 8, + 'pkcs12_sha1_rc4_128': 0, + 'pkcs12_sha1_rc4_40': 0, + 'pkcs12_sha1_tripledes_3key': 8, + 'pkcs12_sha1_tripledes_2key': 8, + 'pkcs12_sha1_rc2_128': 8, + 'pkcs12_sha1_rc2_40': 8, + }[encryption_algo] + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine encryption block size' % encryption_algo) + + @property + def encryption_iv(self): + """ + Returns the byte string of the initialization vector for the encryption + scheme. Only the PBES2 stores the IV in the params. For PBES1, the IV + is derived from the KDF and this property will return None. + + :return: + A byte string or None + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + scheme = self['parameters'].parsed['encryption_scheme'] + cipher = scheme['algorithm'].native + + if cipher == 'rc2' or cipher == 'rc5': + return scheme['parameters'].parsed['iv'].native + + # For DES/Triple DES and AES the IV is the entirety of the parameters + if cipher.find('.') == -1: + return scheme['parameters'].native + + raise ValueError('Unrecognized encryption cipher "%s", can not determine initialization vector' % cipher) + + # All of the PBES1 algos use their KDF to create the IV. For the pbkdf1, + # the KDF is told to generate a key that is an extra 8 bytes long, and + # that is used for the IV. For the PKCS#12 KDF, it is called with an id + # of 2 to generate the IV. In either case, we can't return the IV + # without knowing the user's password. + if encryption_algo.find('.') == -1: + return None + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine initialization vector' % encryption_algo) + + +class Pbmac1Params(Sequence): + _fields = [ + ('key_derivation_func', KdfAlgorithm), + ('message_auth_scheme', HmacAlgorithm), + ] + + +class Pkcs5MacId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.5.14': 'pbmac1', + } + + +class Pkcs5MacAlgorithm(Sequence): + _fields = [ + ('algorithm', Pkcs5MacId), + ('parameters', Any), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'pbmac1': Pbmac1Params, + } diff --git a/asn1crypto/teletex_codec.py b/asn1crypto/teletex_codec.py new file mode 100644 index 0000000..473ba9c --- /dev/null +++ b/asn1crypto/teletex_codec.py @@ -0,0 +1,317 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +import codecs + + +class TeletexCodec(codecs.Codec): + + def encode(self, input_, errors='strict'): + return codecs.charmap_encode(input_, errors, ENCODING_TABLE) + + def decode(self, input_, errors='strict'): + return codecs.charmap_decode(input_, errors, DECODING_TABLE) + + +class TeletexIncrementalEncoder(codecs.IncrementalEncoder): + + def encode(self, input_, final=False): + return codecs.charmap_encode(input_, self.errors, ENCODING_TABLE)[0] + + +class TeletexIncrementalDecoder(codecs.IncrementalDecoder): + + def decode(self, input_, final=False): + return codecs.charmap_decode(input_, self.errors, DECODING_TABLE)[0] + + +class TeletexStreamWriter(TeletexCodec, codecs.StreamWriter): + + pass + + +class TeletexStreamReader(TeletexCodec, codecs.StreamReader): + + pass + + +def teletex_search_function(name): + if name != 'teletex': + return None + + return codecs.CodecInfo( + name='teletex', + encode=TeletexCodec().encode, + decode=TeletexCodec().decode, + incrementalencoder=TeletexIncrementalEncoder, + incrementaldecoder=TeletexIncrementalDecoder, + streamreader=TeletexStreamReader, + streamwriter=TeletexStreamWriter, + ) + + +def register(): + codecs.register(teletex_search_function) + + +# http://en.wikipedia.org/wiki/ITU_T.61 +DECODING_TABLE = ( + '\u0000' + '\u0001' + '\u0002' + '\u0003' + '\u0004' + '\u0005' + '\u0006' + '\u0007' + '\u0008' + '\u0009' + '\u000A' + '\u000B' + '\u000C' + '\u000D' + '\u000E' + '\u000F' + '\u0010' + '\u0011' + '\u0012' + '\u0013' + '\u0014' + '\u0015' + '\u0016' + '\u0017' + '\u0018' + '\u0019' + '\u001A' + '\u001B' + '\u001C' + '\u001D' + '\u001E' + '\u001F' + '\u0020' + '\u0021' + '\u0022' + '\ufffe' + '\ufffe' + '\u0025' + '\u0026' + '\u0027' + '\u0028' + '\u0029' + '\u002A' + '\u002B' + '\u002C' + '\u002D' + '\u002E' + '\u002F' + '\u0030' + '\u0031' + '\u0032' + '\u0033' + '\u0034' + '\u0035' + '\u0036' + '\u0037' + '\u0038' + '\u0039' + '\u003A' + '\u003B' + '\u003C' + '\u003D' + '\u003E' + '\u003F' + '\u0040' + '\u0041' + '\u0042' + '\u0043' + '\u0044' + '\u0045' + '\u0046' + '\u0047' + '\u0048' + '\u0049' + '\u004A' + '\u004B' + '\u004C' + '\u004D' + '\u004E' + '\u004F' + '\u0050' + '\u0051' + '\u0052' + '\u0053' + '\u0054' + '\u0055' + '\u0056' + '\u0057' + '\u0058' + '\u0059' + '\u005A' + '\u005B' + '\ufffe' + '\u005D' + '\ufffe' + '\u005F' + '\ufffe' + '\u0061' + '\u0062' + '\u0063' + '\u0064' + '\u0065' + '\u0066' + '\u0067' + '\u0068' + '\u0069' + '\u006A' + '\u006B' + '\u006C' + '\u006D' + '\u006E' + '\u006F' + '\u0070' + '\u0071' + '\u0072' + '\u0073' + '\u0074' + '\u0075' + '\u0076' + '\u0077' + '\u0078' + '\u0079' + '\u007A' + '\ufffe' + '\u007C' + '\ufffe' + '\ufffe' + '\u007F' + '\u0080' + '\u0081' + '\u0082' + '\u0083' + '\u0084' + '\u0085' + '\u0086' + '\u0087' + '\u0088' + '\u0089' + '\u008A' + '\u008B' + '\u008C' + '\u008D' + '\u008E' + '\u008F' + '\u0090' + '\u0091' + '\u0092' + '\u0093' + '\u0094' + '\u0095' + '\u0096' + '\u0097' + '\u0098' + '\u0099' + '\u009A' + '\u009B' + '\u009C' + '\u009D' + '\u009E' + '\u009F' + '\u00A0' + '\u00A1' + '\u00A2' + '\u00A3' + '\u0024' + '\u00A5' + '\u0023' + '\u00A7' + '\u00A4' + '\ufffe' + '\ufffe' + '\u00AB' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\u00B0' + '\u00B1' + '\u00B2' + '\u00B3' + '\u00D7' + '\u00B5' + '\u00B6' + '\u00B7' + '\u00F7' + '\ufffe' + '\ufffe' + '\u00BB' + '\u00BC' + '\u00BD' + '\u00BE' + '\u00BF' + '\ufffe' + '\u0300' + '\u0301' + '\u0302' + '\u0303' + '\u0304' + '\u0306' + '\u0307' + '\u0308' + '\ufffe' + '\u030A' + '\u0327' + '\u0332' + '\u030B' + '\u0328' + '\u030C' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\u2126' + '\u00C6' + '\u00D0' + '\u00AA' + '\u0126' + '\ufffe' + '\u0132' + '\u013F' + '\u0141' + '\u00D8' + '\u0152' + '\u00BA' + '\u00DE' + '\u0166' + '\u014A' + '\u0149' + '\u0138' + '\u00E6' + '\u0111' + '\u00F0' + '\u0127' + '\u0131' + '\u0133' + '\u0140' + '\u0142' + '\u00F8' + '\u0153' + '\u00DF' + '\u00FE' + '\u0167' + '\u014B' + '\ufffe' +) +ENCODING_TABLE = codecs.charmap_build(DECODING_TABLE) diff --git a/asn1crypto/tsa.py b/asn1crypto/tsa.py new file mode 100644 index 0000000..61fa7ca --- /dev/null +++ b/asn1crypto/tsa.py @@ -0,0 +1,290 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .algos import DigestAlgorithm +from .core import ( + Any, + BitString, + Boolean, + Choice, + GeneralizedTime, + IA5String, + Integer, + ObjectIdentifier, + OctetString, + Sequence, + SequenceOf, + SetOf, + UTF8String, +) +from .cms import ( + CMSAttribute, + CMSAttributeType, + ContentInfo, + ContentType, + EncapsulatedContentInfo, +) +from .crl import CertificateList +from .x509 import Attributes, CertificatePolicies, GeneralName, GeneralNames + + +# The structures in this file are based on https://tools.ietf.org/html/rfc3161, +# https://tools.ietf.org/html/rfc4998, https://tools.ietf.org/html/rfc5544, +# https://tools.ietf.org/html/rfc5035, https://tools.ietf.org/html/rfc2634 + +class Version(Integer): + _map = { + 0: 'v0', + 1: 'v1', + 2: 'v2', + 3: 'v3', + 4: 'v4', + 5: 'v5', + } + + +class MessageImprint(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm), + ('hashed_message', OctetString), + ] + + +class Accuracy(Sequence): + _fields = [ + ('seconds', Integer, {'optional': True}), + ('millis', Integer, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('micros', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class Extension(Sequence): + _fields = [ + ('extn_id', ObjectIdentifier), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + +class Extensions(SequenceOf): + _child_spec = Extension + + +class TSTInfo(Sequence): + _fields = [ + ('version', Version), + ('policy', ObjectIdentifier), + ('message_imprint', MessageImprint), + ('serial_number', Integer), + ('gen_time', GeneralizedTime), + ('accuracy', Accuracy, {'optional': True}), + ('ordering', Boolean, {'default': False}), + ('nonce', Integer, {'optional': True}), + ('tsa', GeneralName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class TimeStampReq(Sequence): + _fields = [ + ('version', Version), + ('message_imprint', MessageImprint), + ('req_policy', ObjectIdentifier, {'optional': True}), + ('nonce', Integer, {'optional': True}), + ('cert_req', Boolean, {'default': False}), + ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ] + + +class PKIStatus(Integer): + _map = { + 0: 'granted', + 1: 'granted_with_mods', + 2: 'rejection', + 3: 'waiting', + 4: 'revocation_warning', + 5: 'revocation_notification', + } + + +class PKIFreeText(SequenceOf): + _child_spec = UTF8String + + +class PKIFailureInfo(BitString): + _map = { + 0: 'bad_alg', + 2: 'bad_request', + 5: 'bad_data_format', + 14: 'time_not_available', + 15: 'unaccepted_policy', + 16: 'unaccepted_extensions', + 17: 'add_info_not_available', + 25: 'system_failure', + } + + +class PKIStatusInfo(Sequence): + _fields = [ + ('status', PKIStatus), + ('status_string', PKIFreeText, {'optional': True}), + ('fail_info', PKIFailureInfo, {'optional': True}), + ] + + +class TimeStampResp(Sequence): + _fields = [ + ('status', PKIStatusInfo), + ('time_stamp_token', ContentInfo), + ] + + +class MetaData(Sequence): + _fields = [ + ('hash_protected', Boolean), + ('file_name', UTF8String, {'optional': True}), + ('media_type', IA5String, {'optional': True}), + ('other_meta_data', Attributes, {'optional': True}), + ] + + +class TimeStampAndCRL(SequenceOf): + _fields = [ + ('time_stamp', EncapsulatedContentInfo), + ('crl', CertificateList, {'optional': True}), + ] + + +class TimeStampTokenEvidence(SequenceOf): + _child_spec = TimeStampAndCRL + + +class DigestAlgorithms(SequenceOf): + _child_spec = DigestAlgorithm + + +class EncryptionInfo(Sequence): + _fields = [ + ('encryption_info_type', ObjectIdentifier), + ('encryption_info_value', Any), + ] + + +class PartialHashtree(SequenceOf): + _child_spec = OctetString + + +class PartialHashtrees(SequenceOf): + _child_spec = PartialHashtree + + +class ArchiveTimeStamp(Sequence): + _fields = [ + ('digest_algorithm', DigestAlgorithm, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('attributes', Attributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('reduced_hashtree', PartialHashtrees, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('time_stamp', ContentInfo), + ] + + +class ArchiveTimeStampSequence(SequenceOf): + _child_spec = ArchiveTimeStamp + + +class EvidenceRecord(Sequence): + _fields = [ + ('version', Version), + ('digest_algorithms', DigestAlgorithms), + ('crypto_infos', Attributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('encryption_info', EncryptionInfo, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('archive_time_stamp_sequence', ArchiveTimeStampSequence), + ] + + +class OtherEvidence(Sequence): + _fields = [ + ('oe_type', ObjectIdentifier), + ('oe_value', Any), + ] + + +class Evidence(Choice): + _alternatives = [ + ('tst_evidence', TimeStampTokenEvidence, {'tag_type': 'implicit', 'tag': 0}), + ('ers_evidence', EvidenceRecord, {'tag_type': 'implicit', 'tag': 1}), + ('other_evidence', OtherEvidence, {'tag_type': 'implicit', 'tag': 2}), + ] + + +class TimeStampedData(Sequence): + _fields = [ + ('version', Version), + ('data_uri', IA5String, {'optional': True}), + ('meta_data', MetaData, {'optional': True}), + ('content', OctetString, {'optional': True}), + ('temporal_evidence', Evidence), + ] + + +class IssuerSerial(Sequence): + _fields = [ + ('issuer', GeneralNames), + ('serial_number', Integer), + ] + + +class ESSCertID(Sequence): + _fields = [ + ('cert_hash', OctetString), + ('issuer_serial', IssuerSerial, {'optional': True}), + ] + + +class ESSCertIDs(SequenceOf): + _child_spec = ESSCertID + + +class SigningCertificate(Sequence): + _fields = [ + ('certs', ESSCertIDs), + ('policies', CertificatePolicies, {'optional': True}), + ] + + +class SetOfSigningCertificates(SetOf): + _child_spec = SigningCertificate + + +class ESSCertIDv2(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm, {'default': 'sha256'}), + ('cert_hash', OctetString), + ('issuer_serial', IssuerSerial, {'optional': True}), + ] + + +class ESSCertIDv2s(SequenceOf): + _child_spec = ESSCertIDv2 + + +class SigningCertificateV2(Sequence): + _fields = [ + ('certs', ESSCertIDv2s), + ('policies', CertificatePolicies, {'optional': True}), + ] + + +class SetOfSigningCertificatesV2(SetOf): + _child_spec = SigningCertificateV2 + + +EncapsulatedContentInfo._oid_specs['tst_info'] = TSTInfo #pylint: disable=W0212 +EncapsulatedContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 +ContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 +ContentType._map['1.2.840.113549.1.9.16.1.4'] = 'tst_info' #pylint: disable=W0212 +ContentType._map['1.2.840.113549.1.9.16.1.31'] = 'timestamped_data' #pylint: disable=W0212 +CMSAttributeType._map['1.2.840.113549.1.9.16.2.12'] = 'signing_certificate' #pylint: disable=W0212 +CMSAttribute._oid_specs['signing_certificate'] = SetOfSigningCertificates #pylint: disable=W0212 +CMSAttributeType._map['1.2.840.113549.1.9.16.2.47'] = 'signing_certificate_v2' #pylint: disable=W0212 +CMSAttribute._oid_specs['signing_certificate_v2'] = SetOfSigningCertificatesV2 #pylint: disable=W0212 diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py new file mode 100644 index 0000000..bfdf473 --- /dev/null +++ b/asn1crypto/x509.py @@ -0,0 +1,717 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from collections import OrderedDict + +from .core import ( + Any, + BitString, + BMPString, + Boolean, + Choice, + GeneralizedTime, + GeneralString, + IA5String, + Integer, + Null, + NumericString, + ObjectIdentifier, + OctetBitString, + OctetString, + PrintableString, + Sequence, + SequenceOf, + Set, + SetOf, + TeletexString, + UniversalString, + UTCTime, + UTF8String, + VisibleString, +) +from .algos import SignedDigestAlgorithm +from .keys import PublicKeyInfo + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc5280 +# and a few other supplementary sources, mostly due to extra supported +# extension and name OIDs + +class Attribute(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('values', SetOf, {'spec': Any}), + ] + + +class Attributes(SequenceOf): + _child_spec = Attribute + + +class KeyUsage(BitString): + _map = { + 0: 'digital_signature', + 1: 'non_repudiation', + 2: 'key_encipherment', + 3: 'data_encipherment', + 4: 'key_agreement', + 5: 'key_cert_sign', + 6: 'crl_sign', + 7: 'encipher_only', + 8: 'decipher_only', + } + + +class PrivateKeyUsagePeriod(Sequence): + _fields = [ + ('not_before', GeneralizedTime, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('not_after', GeneralizedTime, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class DirectoryString(Choice): + _alternatives = [ + ('teletex_string', TeletexString), + ('printable_string', PrintableString), + ('universal_string', UniversalString), + ('utf8_string', UTF8String), + ('bmp_string', BMPString), + ] + + +class NameType(ObjectIdentifier): + _map = { + '2.5.4.3': 'common_name', + '2.5.4.4': 'surname', + '2.5.4.5': 'serial_number', + '2.5.4.6': 'country_name', + '2.5.4.7': 'locality_name', + '2.5.4.8': 'state_or_province_name', + '2.5.4.10': 'organization_name', + '2.5.4.11': 'organizational_unit_name', + '2.5.4.12': 'title', + '2.5.4.41': 'name', + '2.5.4.42': 'given_name', + '2.5.4.43': 'initials', + '2.5.4.44': 'generation_qualifier', + '2.5.4.46': 'dn_qualifier', + # https://tools.ietf.org/html/rfc2985#page-26 + '1.2.840.113549.1.9.1': 'email_address', + # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf + '1.3.6.1.4.1.311.60.2.1.1': 'incorporation_locality', + '1.3.6.1.4.1.311.60.2.1.2': 'incorporation_state_or_province', + '1.3.6.1.4.1.311.60.2.1.3': 'incorporation_country', + } + + +class NameTypeAndValue(Sequence): + _fields = [ + ('type', NameType), + ('value', Any), + ] + + _oid_pair = ('type', 'value') + _oid_specs = { + 'common_name': DirectoryString, + 'surname': DirectoryString, + 'serial_number': PrintableString, + 'country_name': PrintableString, + 'locality_name': DirectoryString, + 'state_or_province_name': DirectoryString, + 'organization_name': DirectoryString, + 'organizational_unit_name': DirectoryString, + 'title': DirectoryString, + 'name': DirectoryString, + 'given_name': DirectoryString, + 'initials': DirectoryString, + 'generation_qualifier': DirectoryString, + 'dn_qualifier': PrintableString, + # https://tools.ietf.org/html/rfc2985#page-26 + 'email_address': IA5String, + # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf + 'incorporation_locality': DirectoryString, + 'incorporation_state_or_province': DirectoryString, + 'incorporation_country': PrintableString, + } + + +class RelativeDistinguishedName(SetOf): + _child_spec = NameTypeAndValue + + +class RDNSequence(SequenceOf): + _child_spec = RelativeDistinguishedName + + +class Name(Choice): + _alternatives = [ + ('', RDNSequence), + ] + + @property + def native(self): + if self.contents is None: + return None + if self._native is None: + self._native = OrderedDict() + for rdn in self.chosen.native: + for type_val in rdn: + type_val_type = type_val['type'] + if type_val_type in self._native: + self._native[type_val_type] = [self._native[type_val_type]] + self._native[type_val_type].append(type_val['value']) + else: + self._native[type_val_type] = type_val['value'] + return self._native + + +class AnotherName(Sequence): + _fields = [ + ('type_id', ObjectIdentifier), + ('value', Any, {'tag_type': 'explicit', 'tag': 0}), + ] + + +class CountryName(Choice): + class_ = 1 + tag = 1 + + _alternatives = [ + ('x121_dcc_code', NumericString), + ('iso_3166_alpha2_code', PrintableString), + ] + + +class AdministrationDomainName(Choice): + class_ = 1 + tag = 2 + + _alternatives = [ + ('numeric', NumericString), + ('printable', PrintableString), + ] + + +class PrivateDomainName(Choice): + _alternatives = [ + ('numeric', NumericString), + ('printable', PrintableString), + ] + + +class PersonalName(Set): + _fields = [ + ('surname', PrintableString, {'tag_type': 'implicit', 'tag': 0}), + ('given_name', PrintableString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('initials', PrintableString, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('generation_qualifier', PrintableString, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ] + + +class TeletexPersonalName(Set): + _fields = [ + ('surname', TeletexString, {'tag_type': 'implicit', 'tag': 0}), + ('given_name', TeletexString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('initials', TeletexString, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('generation_qualifier', TeletexString, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ] + + +class OrganizationalUnitNames(SequenceOf): + _child_spec = PrintableString + + +class TeletexOrganizationalUnitNames(SequenceOf): + _child_spec = TeletexString + + +class BuiltInStandardAttributes(Sequence): + _fields = [ + ('country_name', CountryName, {'optional': True}), + ('administration_domain_name', AdministrationDomainName, {'optional': True}), + ('network_address', NumericString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('terminal_identifier', PrintableString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('private_domain_name', PrivateDomainName, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ('organization_name', PrintableString, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ('numeric_user_identifier', NumericString, {'tag_type': 'implicit', 'tag': 4, 'optional': True}), + ('personal_name', PersonalName, {'tag_type': 'implicit', 'tag': 5, 'optional': True}), + ('organizational_unit_names', OrganizationalUnitNames, {'tag_type': 'implicit', 'tag': 6, 'optional': True}), + ] + + +class BuiltInDomainDefinedAttribute(Sequence): + _fields = [ + ('type', PrintableString), + ('value', PrintableString), + ] + + +class BuiltInDomainDefinedAttributes(SequenceOf): + _child_spec = BuiltInDomainDefinedAttribute + + +class TeletexDomainDefinedAttribute(Sequence): + _fields = [ + ('type', TeletexString), + ('value', TeletexString), + ] + + +class TeletexDomainDefinedAttributes(SequenceOf): + _child_spec = TeletexDomainDefinedAttribute + + +class PhysicalDeliveryCountryName(Choice): + _alternatives = [ + ('x121_dcc_code', NumericString), + ('iso_3166_alpha2_code', PrintableString), + ] + + +class PostalCode(Choice): + _alternatives = [ + ('numeric_code', NumericString), + ('printable_code', PrintableString), + ] + + +class PDSParameter(Set): + _fields = [ + ('printable_string', PrintableString, {'optional': True}), + ('teletex_string', TeletexString, {'optional': True}), + ] + + +class PrintableAddress(SequenceOf): + _child_spec = PrintableString + + +class UnformattedPostalAddress(Set): + _fields = [ + ('printable_address', PrintableAddress, {'optional': True}), + ('teletex_string', TeletexString, {'optional': True}), + ] + + +class E1634Address(Sequence): + _fields = [ + ('number', NumericString, {'tag_type': 'implicit', 'tag': 0}), + ('sub_address', NumericString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class NAddresses(SetOf): + _child_spec = OctetString + + +class PresentationAddress(Sequence): + _fields = [ + ('p_selector', OctetString, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('s_selector', OctetString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('t_selector', OctetString, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ('n_addresses', NAddresses, {'tag_type': 'explicit', 'tag': 3}), + ] + + +class ExtendedNetworkAddress(Choice): + _alternatives = [ + ('e163_4_address', E1634Address), + ('psap_address', PresentationAddress, {'tag_type': 'implicit', 'tag': 0}) + ] + + +class TerminalType(Integer): + _map = { + 3: 'telex', + 4: 'teletex', + 5: 'g3_facsimile', + 6: 'g4_facsimile', + 7: 'ia5_terminal', + 8: 'videotex', + } + + +class ExtensionAttributeType(Integer): + _map = { + 1: 'common_name', + 2: 'teletex_common_name', + 3: 'teletex_organization_name', + 4: 'teletex_personal_name', + 5: 'teletex_organization_unit_names', + 6: 'teletex_domain_defined_attributes', + 7: 'pds_name', + 8: 'physical_delivery_country_name', + 9: 'postal_code', + 10: 'physical_delivery_office_name', + 11: 'physical_delivery_office_number', + 12: 'extension_of_address_components', + 13: 'physical_delivery_personal_name', + 14: 'physical_delivery_organization_name', + 15: 'extension_physical_delivery_address_components', + 16: 'unformatted_postal_address', + 17: 'street_address', + 18: 'post_office_box_address', + 19: 'poste_restante_address', + 20: 'unique_postal_name', + 21: 'local_postal_attributes', + 22: 'extended_network_address', + 23: 'terminal_type', + } + + +class ExtensionAttribute(Sequence): + _fields = [ + ('extension_attribute_type', ExtensionAttributeType, {'tag_type': 'implicit', 'tag': 0}), + ('extension_attribute_value', Any, {'tag_type': 'explicit', 'tag': 1}), + ] + + _oid_pair = ('extension_attribute_type', 'extension_attribute_value') + _oid_specs = { + 'common_name': PrintableString, + 'teletex_common_name': TeletexString, + 'teletex_organization_name': TeletexString, + 'teletex_personal_name': TeletexPersonalName, + 'teletex_organization_unit_names': TeletexOrganizationalUnitNames, + 'teletex_domain_defined_attributes': TeletexDomainDefinedAttributes, + 'pds_name': PrintableString, + 'physical_delivery_country_name': PhysicalDeliveryCountryName, + 'postal_code': PostalCode, + 'physical_delivery_office_name': PDSParameter, + 'physical_delivery_office_number': PDSParameter, + 'extension_of_address_components': PDSParameter, + 'physical_delivery_personal_name': PDSParameter, + 'physical_delivery_organization_name': PDSParameter, + 'extension_physical_delivery_address_components': PDSParameter, + 'unformatted_postal_address': UnformattedPostalAddress, + 'street_address': PDSParameter, + 'post_office_box_address': PDSParameter, + 'poste_restante_address': PDSParameter, + 'unique_postal_name': PDSParameter, + 'local_postal_attributes': PDSParameter, + 'extended_network_address': ExtendedNetworkAddress, + 'terminal_type': TerminalType, + } + + +class ExtensionAttributes(SequenceOf): + _child_spec = ExtensionAttribute + + +class ORAddress(Sequence): + _fields = [ + ('built_in_standard_attributes', BuiltInStandardAttributes), + ('built_in_domain_defined_attributes', BuiltInDomainDefinedAttributes, {'optional': True}), + ('extension_attributes', ExtensionAttributes, {'optional': True}), + ] + + +class EDIPartyName(Sequence): + _fields = [ + ('name_assigner', DirectoryString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('party_name', DirectoryString, {'tag_type': 'implicit', 'tag': 1}), + ] + + +class GeneralName(Choice): + _alternatives = [ + ('other_name', AnotherName, {'tag_type': 'implicit', 'tag': 0}), + ('rfc822_name', IA5String, {'tag_type': 'implicit', 'tag': 1}), + ('dns_name', IA5String, {'tag_type': 'implicit', 'tag': 2}), + ('x400_address', ORAddress, {'tag_type': 'implicit', 'tag': 3}), + ('directory_name', Name, {'tag_type': 'explicit', 'tag': 4}), + ('edi_party_name', EDIPartyName, {'tag_type': 'implicit', 'tag': 5}), + ('uniform_resource_identifier', IA5String, {'tag_type': 'implicit', 'tag': 6}), + ('ip_address', OctetString, {'tag_type': 'implicit', 'tag': 7}), + ('registered_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 8}), + ] + + +class GeneralNames(SequenceOf): + _child_spec = GeneralName + + +class Time(Choice): + _alternatives = [ + ('utc_time', UTCTime), + ('general_time', GeneralizedTime), + ] + + +class Validity(Sequence): + _fields = [ + ('not_before', Time), + ('not_after', Time), + ] + + +class BasicConstraints(Sequence): + _fields = [ + ('ca', Boolean, {'default': False}), + ('path_len_constraint', Integer, {'optional': True}), + ] + + +class AuthorityKeyIdentifier(Sequence): + _fields = [ + ('key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('authority_cert_issuer', GeneralNames, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('authority_cert_serial_number', Integer, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ] + + +class DistributionPointName(Choice): + _alternatives = [ + ('full_name', GeneralNames, {'tag_type': 'implicit', 'tag': 0}), + ('name_relative_to_crl_issuer', RelativeDistinguishedName, {'tag_type': 'implicit', 'tag': 1}), + ] + + +class ReasonFlags(BitString): + _map = { + 0: 'unused', + 1: 'key_compromise', + 2: 'ca_compromise', + 3: 'affiliation_changed', + 4: 'superseded', + 5: 'cessation_of_operation', + 6: 'certificate_hold', + 7: 'privilege_withdrawn', + 8: 'aa_compromise', + } + + +class GeneralSubtree(Sequence): + _fields = [ + ('base', GeneralName), + ('minimum', Integer, {'tag_type': 'implicit', 'tag': 0, 'default': 0}), + ('maximum', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class GeneralSubtrees(SequenceOf): + _child_spec = GeneralSubtree + + +class NameConstraints(Sequence): + _fields = [ + ('permitted_subtrees', GeneralSubtrees, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('excluded_subtrees', GeneralSubtrees, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class DistributionPoint(Sequence): + _fields = [ + ('distribution_point', DistributionPointName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('reasons', ReasonFlags, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('crl_issuer', GeneralNames, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ] + + +class CRLDistributionPoints(SequenceOf): + _child_spec = DistributionPoint + + +class DisplayText(Choice): + _alternatives = [ + ('ia5_string', IA5String), + ('visible_string', VisibleString), + ('bmp_string', BMPString), + ('utf8_string', UTF8String), + ] + + +class NoticeNumbers(SequenceOf): + _child_spec = Integer + + +class NoticeReference(Sequence): + _fields = [ + ('organization', DisplayText), + ('notice_numbers', NoticeNumbers), + ] + + +class UserNotice(Sequence): + _fields = [ + ('notice_ref', NoticeReference, {'optional': True}), + ('explicit_text', DisplayText, {'optional': True}), + ] + + +class PolicyQualifierId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.2.1': 'certification_practice_statement', + '1.3.6.1.5.5.7.2.2': 'user_notice', + } + + +class PolicyQualifierInfo(Sequence): + _fields = [ + ('policy_qualifier_id', PolicyQualifierId), + ('qualifier', Any), + ] + + _oid_pair = ('policy_qualifier_id', 'qualifier') + _oid_specs = { + 'certification_practice_statement': IA5String, + 'user_notice': UserNotice, + } + + +class PolicyQualifierInfos(SequenceOf): + _child_spec = PolicyQualifierInfo + + +class PolicyInformation(Sequence): + _fields = [ + ('policy_identifier', ObjectIdentifier), + ('policy_qualifiers', PolicyQualifierInfos, {'optional': True}) + ] + + +class CertificatePolicies(SequenceOf): + _child_spec = PolicyInformation + + +class PolicyMapping(Sequence): + _fields = [ + ('issuer_domain_policy', ObjectIdentifier), + ('subject_domain_policy', ObjectIdentifier), + ] + + +class PolicyMappings(SequenceOf): + _child_spec = PolicyMapping + + +class PolicyConstraints(Sequence): + _fields = [ + ('require_explicit_policy', Integer, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('inhibit_policy_mapping', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class KeyPurposeId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.3.1': 'server_auth', + '1.3.6.1.5.5.7.3.2': 'client_auth', + '1.3.6.1.5.5.7.3.3': 'code_signing', + '1.3.6.1.5.5.7.3.4': 'email_protection', + '1.3.6.1.5.5.7.3.5': 'ipsec_end_system', + '1.3.6.1.5.5.7.3.6': 'ipsec_tunnel', + '1.3.6.1.5.5.7.3.7': 'ipsec_user', + '1.3.6.1.5.5.7.3.8': 'time_stamping', + '1.3.6.1.5.5.7.3.9': 'ocsp_signing', + '1.3.6.1.5.5.7.3.19': 'wireless_access_points', + } + + +class ExtKeyUsageSyntax(SequenceOf): + _child_spec = KeyPurposeId + + +class EntrustVersionInfo(Sequence): + _fields = [ + ('entrust_vers', GeneralString), + ('entrust_info_flags', BitString) + ] + + +class NetscapeCertificateType(BitString): + _map = { + 0: 'ssl_client', + 1: 'ssl_server', + 2: 'email', + 3: 'object_signing', + 4: 'reserved', + 5: 'ssl_ca', + 6: 'email_ca', + 7: 'object_signing_ca', + } + + +class ExtensionId(ObjectIdentifier): + _map = { + '2.5.29.9': 'subject_directory_attributes', + '2.5.29.14': 'key_identifier', + '2.5.29.15': 'key_usage', + '2.5.29.16': 'private_key_usage_period', + '2.5.29.17': 'subject_alt_name', + '2.5.29.19': 'basic_constraints', + '2.5.29.23': 'hold_instruction_code', + '2.5.29.30': 'name_constraints', + '2.5.29.31': 'crl_distribution_points', + '2.5.29.32': 'certificate_policies', + '2.5.29.33': 'policy_mappings', + '2.5.29.35': 'authority_key_identifier', + '2.5.29.36': 'policy_constraints', + '2.5.29.37': 'extended_key_usage', + '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', + '1.2.840.113533.7.65.0': 'entrust_version_extension', + '2.16.840.1.113730.1.1': 'netscape_certificate_type', + } + + +class Extension(Sequence): + _fields = [ + ('extn_id', ExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'subject_directory_attributes': Attributes, + 'key_identifier': OctetString, + 'key_usage': KeyUsage, + 'private_key_usage_period': PrivateKeyUsagePeriod, + 'subject_alt_name': GeneralNames, + 'basic_constraints': BasicConstraints, + 'hold_instruction_code': ObjectIdentifier, + 'name_constraints': NameConstraints, + 'crl_distribution_points': CRLDistributionPoints, + 'certificate_policies': CertificatePolicies, + 'policy_mappings': PolicyMappings, + 'authority_key_identifier': AuthorityKeyIdentifier, + 'policy_constraints': PolicyConstraints, + 'extended_key_usage': ExtKeyUsageSyntax, + 'ocsp_no_check': Null, + 'entrust_version_extension': EntrustVersionInfo, + 'netscape_certificate_type': NetscapeCertificateType, + } + + +class Extensions(SequenceOf): + _child_spec = Extension + + +class Version(Integer): + _map = { + 0: 'v1', + 1: 'v2', + 2: 'v3', + } + + +class TbsCertificate(Sequence): + _fields = [ + ('version', Version, {'tag_type': 'explicit', 'tag': 0, 'default': 'v1'}), + ('serial_number', Integer), + ('signature', SignedDigestAlgorithm), + ('issuer', Name), + ('validity', Validity), + ('subject', Name), + ('subject_public_key_info', PublicKeyInfo), + ('issuer_unique_id', OctetBitString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('subject_unique_id', OctetBitString, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('extensions', Extensions, {'tag_type': 'explicit', 'tag': 3, 'optional': True}), + ] + + +class Certificate(Sequence): + _fields = [ + ('tbs_certificate', TbsCertificate), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature_value', OctetBitString), + ] diff --git a/lint.py b/lint.py new file mode 100644 index 0000000..bead1e8 --- /dev/null +++ b/lint.py @@ -0,0 +1,33 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import os + +from pylint.lint import Run + + +cur_dir = os.path.dirname(__file__) +rc_path = os.path.join(cur_dir, './.pylintrc') + +print('Running pylint...') + +files = [ + '__init__.py', + 'algos.py', + 'cms.py', + 'core.py', + 'crl.py', + 'keys.py', + 'ocsp.py', + 'pdf.py', + 'pkcs12.py', + 'pkcs5.py', + 'teletex_codec.py', + 'tsa.py', + 'x509.py', +] + +args = ['--rcfile=%s' % rc_path] +args += ['asn1crypto/' + f for f in files] + +Run(args) diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..0339b5c --- /dev/null +++ b/tests.py @@ -0,0 +1,19 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest + +from tests.test_cms import CMSTests #pylint: disable=E0611,W0611 +from tests.test_crl import CRLTests #pylint: disable=E0611,W0611 +from tests.test_keys import KeysTests #pylint: disable=E0611,W0611 +from tests.test_ocsp import OCSPTests #pylint: disable=E0611,W0611 +from tests.test_tsa import TSATests #pylint: disable=E0611,W0611 +from tests.test_x509 import X509Tests #pylint: disable=E0611,W0611 + + +if __name__ == '__main__': + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for test_class in [CMSTests, CRLTests, KeysTests, OCSPTests, TSATests, X509Tests]: + suite.addTest(loader.loadTestsFromTestCase(test_class)) + unittest.TextTestRunner().run(suite) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/cms-compressed.der b/tests/fixtures/cms-compressed.der new file mode 100644 index 0000000..e5096c6 Binary files /dev/null and b/tests/fixtures/cms-compressed.der differ diff --git a/tests/fixtures/cms-compressed.pem b/tests/fixtures/cms-compressed.pem new file mode 100644 index 0000000..950be90 --- /dev/null +++ b/tests/fixtures/cms-compressed.pem @@ -0,0 +1,5 @@ +-----BEGIN CMS----- +MGsGCyqGSIb3DQEJEAEJoFwwWgIBADANBgsqhkiG9w0BCRADCDBGBgkqhkiG9w0B +BwGgOQQ3eJwLycgsVgCikoxUhdzU4uLE9FSFknyF1LzkxILi0pzEklSFzDyFAG/n +YGVzfWffYC4Atc8QcQ== +-----END CMS----- diff --git a/tests/fixtures/cms-digested.der b/tests/fixtures/cms-digested.der new file mode 100644 index 0000000..a4eb8a2 Binary files /dev/null and b/tests/fixtures/cms-digested.der differ diff --git a/tests/fixtures/cms-digested.pem b/tests/fixtures/cms-digested.pem new file mode 100644 index 0000000..1d7b13b --- /dev/null +++ b/tests/fixtures/cms-digested.pem @@ -0,0 +1,5 @@ +-----BEGIN CMS----- +MHMGCSqGSIb3DQEHBaBmMGQCAQAwBwYFKw4DAhowQAYJKoZIhvcNAQcBoDMEMVRo +aXMgaXMgdGhlIG1lc3NhZ2UgdG8gZW5jYXBzdWxhdGUgaW4gUEtDUyM3L0NNUwoE +FFPJ28Ft2zQ7KE7vpgMOAmR5Ma/7 +-----END CMS----- diff --git a/tests/fixtures/cms-encrypted.der b/tests/fixtures/cms-encrypted.der new file mode 100644 index 0000000..e1a5348 Binary files /dev/null and b/tests/fixtures/cms-encrypted.der differ diff --git a/tests/fixtures/cms-encrypted.pem b/tests/fixtures/cms-encrypted.pem new file mode 100644 index 0000000..68c79a4 --- /dev/null +++ b/tests/fixtures/cms-encrypted.pem @@ -0,0 +1,5 @@ +-----BEGIN CMS----- +MIGABgkqhkiG9w0BBwagczBxAgEAMGwGCSqGSIb3DQEHATAdBglghkgBZQMEAQIE +EBqIsppjB6o+f3Sg7LyvJryAQL68WX9DaEsZR5WzD5pQpj1sUCQ8lCHXfiISF/p+ +Jw1F8o40U42rIwxsdB8ogmfUv5JBh9tg9N8m19p/Sxzaq/A= +-----END CMS----- diff --git a/tests/fixtures/cms-enveloped.der b/tests/fixtures/cms-enveloped.der new file mode 100644 index 0000000..93d0edf Binary files /dev/null and b/tests/fixtures/cms-enveloped.der differ diff --git a/tests/fixtures/cms-enveloped.pem b/tests/fixtures/cms-enveloped.pem new file mode 100644 index 0000000..3953e74 --- /dev/null +++ b/tests/fixtures/cms-enveloped.pem @@ -0,0 +1,15 @@ +-----BEGIN CMS----- +MIICPwYJKoZIhvcNAQcDoIICMDCCAiwCAQAxggHIMIIBxAIBADCBqzCBnTELMAkG +A1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMxEDAOBgNVBAcTB05ld2J1 +cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZmaWNpdCBMQzEQMA4GA1UECxMHVGVz +dGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4wHAYJKoZIhvcNAQkBFg93aWxsQGNv +ZGV4bnMuaW8CCQC95dmkEDFcgjANBgkqhkiG9w0BAQEFAASCAQAymb244S2S7UzU +TkgCLEWMJUUAOtK9ohuHdVMuu4bhnrg9Eic/sjyX97mFTEH8DSR6spYoTAjDMc8s +1SUUI75XDtds8OuBh49nG1FZDyqEC1KIjstUPlh1uDZOlW1O6KWkyEwO1/h3W/eh +XYKQ8GWJ0il+qJqDx503Uaa9Y9B15XykkLsMVkXlJ6xhRIIJPJDlzeL8tgQXgt/u +VluLmpTLh4WjMSPlya6VsZj/1+u1N1uA5uVAF2EfZpa/sC9XMHDv5erK5eMy4zmP +o/f1wgyU5kb90kJ2bbYIcoRguWHU+LGZkIgodVT6siGlAVkqts7wk9Pa0DVhOynA +M54ogatiMFsGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQI3WDWf/x1nJKAOBmYUh+T +dQihPsfvvp/htl1WNztPcVoeb+yuuaQRKFtLYIEST5KvGVps3gJGWfmgr52jn3nM +LiFS +-----END CMS----- diff --git a/tests/fixtures/cms-signed-digested.der b/tests/fixtures/cms-signed-digested.der new file mode 100644 index 0000000..4f8fae9 Binary files /dev/null and b/tests/fixtures/cms-signed-digested.der differ diff --git a/tests/fixtures/cms-signed-digested.pem b/tests/fixtures/cms-signed-digested.pem new file mode 100644 index 0000000..10c8118 --- /dev/null +++ b/tests/fixtures/cms-signed-digested.pem @@ -0,0 +1,41 @@ +-----BEGIN CMS----- +MIIHRAYJKoZIhvcNAQcCoIIHNTCCBzECAQIxDTALBglghkgBZQMEAgEwdQYJKoZI +hvcNAQcFoGgEZjBkAgEAMAcGBSsOAwIaMEAGCSqGSIb3DQEHAaAzBDFUaGlzIGlz +IHRoZSBtZXNzYWdlIHRvIGVuY2Fwc3VsYXRlIGluIFBLQ1MjNy9DTVMKBBRTydvB +bds0OyhO76YDDgJkeTGv+6CCBMswggTHMIIDr6ADAgECAgkAveXZpBAxXIIwDQYJ +KoZIhvcNAQELBQAwgZ0xCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNl +dHRzMRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmlj +aXQgTEMxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCVdpbGwgQm9uZDEeMBwG +CSqGSIb3DQEJARYPd2lsbEBjb2RleG5zLmlvMB4XDTE1MDUwNjE0MzcxNloXDTI1 +MDUwMzE0MzcxNlowgZ0xCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNl +dHRzMRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmlj +aXQgTEMxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCVdpbGwgQm9uZDEeMBwG +CSqGSIb3DQEJARYPd2lsbEBjb2RleG5zLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAvVsoB3Ti5kosAiq9UN54F6rsUDZ+lLnGRZyodtqvO8PX/8Qb ++QJCKsmvfTae6yMkXF2OLdpUNEY/HT5ZbAZqPL6Ta9ibS3uZI/9uZUYIzTqh+8Nl +QxZXUt3hLIiceu5LVCPjEeUHv9n6YBZikK52YAUgkSC2UcPN7Oq7qQsRU0HWVssf +6U83K6fBcL0VJhaF6SMDIFp+UUGShBX3SNd+5Mbs+HSbgMB9mfmfmv9im+YoQOQ+ +RpbWYC3yp6XhvxGSUCHy3y9NJ+9C5N7LDcYVwp7srKYochoMPHDCcAt8ZY1re3ti +hVk/19WuCGRHvcMEKccjHba4MdROTAGYh1QvXwIDAQABo4IBBjCCAQIwHQYDVR0O +BBYEFL5ChT3M/+P5KAKPflhWtP0DXOpLMIHSBgNVHSMEgcowgceAFL5ChT3M/+P5 +KAKPflhWtP0DXOpLoYGjpIGgMIGdMQswCQYDVQQGEwJVUzEWMBQGA1UECBMNTWFz +c2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVyeTEeMBwGA1UEChMVQ29kZXggTm9u +IFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlXaWxsIEJv +bmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5pb4IJAL3l2aQQMVyCMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAD/l+a9ZIxFaoPolqWs2vdJq +Kjn5z8vPFtDuRllfUQfkGj6+EPo9+zqWuHMTk5VLzqw4i09O1PYpAgTTBPE19f3B +DON6QfhDaxlGlZoq8rpwf5jOpQqfcOIgKm7y9q7jAcOuV9X8LZeEp/m7mogH5LI6 +EYOj5eFjRyOCH8VLb//X+obGt/FN44fGhE3lgfjIo0Y1qkvdcX662nBUV12ZJFRY +IGvsWh/1EmXM8JxU/+y9MBcOL/J2Ed688SvbWhipfQqcklrQm8nH3YrxgoVNWPbH +k88Kf2rN7kL9F78kVsIi7CruRxfdZBF35L3Mdu0b/iDcOcaJt1zNLD3nb19lnDcx +ggHVMIIB0QIBATCBqzCBnTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1 +c2V0dHMxEDAOBgNVBAcTB05ld2J1cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZm +aWNpdCBMQzEQMA4GA1UECxMHVGVzdGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4w +HAYJKoZIhvcNAQkBFg93aWxsQGNvZGV4bnMuaW8CCQC95dmkEDFcgjALBglghkgB +ZQMEAgEwDQYJKoZIhvcNAQEBBQAEggEAcLwYgkHW2Odc3EInpaiqixYVYTrlR1P9 +j0WjguJyRAfRy7+0hUoqFhne3FMVz5juXA7f3sh5zis4YTawocuU1k/Ng+8MySOg +e4tlQFw9qD7MDR8XI/N0n36I+PO+ThmVD+uVVWm0qsMqNgOTHNzlZT9OXgPIVthX +j+gthTLa/XnU3YjKoxRB5DsDiA4rdtxEPU3/ssjDg7EzN1NRM0vKGq1+arxhi4Tb +f89hsh0hg8+4P8aY7dhmBs8DMJadtHoW326nMOt390AT+/KsQXmd3MDtS4sZ7gU9 +YSA5foAdOiNpSENgiz5jrQF63m8BulHzSxS/a3caMsIMk8w1vGbGaQ== +-----END CMS----- diff --git a/tests/fixtures/cms-signed.der b/tests/fixtures/cms-signed.der new file mode 100644 index 0000000..22db9a0 Binary files /dev/null and b/tests/fixtures/cms-signed.der differ diff --git a/tests/fixtures/cms-signed.pem b/tests/fixtures/cms-signed.pem new file mode 100644 index 0000000..dda8a0c --- /dev/null +++ b/tests/fixtures/cms-signed.pem @@ -0,0 +1,42 @@ +-----BEGIN CMS----- +MIIHewYJKoZIhvcNAQcCoIIHbDCCB2gCAQExDTALBglghkgBZQMEAgEwQQYJKoZI +hvcNAQcBoDQEMlRoaXMgaXMgdGhlIG1lc3NhZ2UgdG8gZW5jYXBzdWxhdGUgaW4g +UEtDUyM3L0NNUw0KoIIEyzCCBMcwggOvoAMCAQICCQC95dmkEDFcgjANBgkqhkiG +9w0BAQsFADCBnTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMx +EDAOBgNVBAcTB05ld2J1cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZmaWNpdCBM +QzEQMA4GA1UECxMHVGVzdGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4wHAYJKoZI +hvcNAQkBFg93aWxsQGNvZGV4bnMuaW8wHhcNMTUwNTA2MTQzNzE2WhcNMjUwNTAz +MTQzNzE2WjCBnTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMx +EDAOBgNVBAcTB05ld2J1cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZmaWNpdCBM +QzEQMA4GA1UECxMHVGVzdGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4wHAYJKoZI +hvcNAQkBFg93aWxsQGNvZGV4bnMuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC9WygHdOLmSiwCKr1Q3ngXquxQNn6UucZFnKh22q87w9f/xBv5AkIq +ya99Np7rIyRcXY4t2lQ0Rj8dPllsBmo8vpNr2JtLe5kj/25lRgjNOqH7w2VDFldS +3eEsiJx67ktUI+MR5Qe/2fpgFmKQrnZgBSCRILZRw83s6rupCxFTQdZWyx/pTzcr +p8FwvRUmFoXpIwMgWn5RQZKEFfdI137kxuz4dJuAwH2Z+Z+a/2Kb5ihA5D5GltZg +LfKnpeG/EZJQIfLfL00n70Lk3ssNxhXCnuyspihyGgw8cMJwC3xljWt7e2KFWT/X +1a4IZEe9wwQpxyMdtrgx1E5MAZiHVC9fAgMBAAGjggEGMIIBAjAdBgNVHQ4EFgQU +vkKFPcz/4/koAo9+WFa0/QNc6kswgdIGA1UdIwSByjCBx4AUvkKFPcz/4/koAo9+ +WFa0/QNc6kuhgaOkgaAwgZ0xCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNo +dXNldHRzMRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3Vm +ZmljaXQgTEMxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCVdpbGwgQm9uZDEe +MBwGCSqGSIb3DQEJARYPd2lsbEBjb2RleG5zLmlvggkAveXZpBAxXIIwDAYDVR0T +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAP+X5r1kjEVqg+iWpaza90moqOfnP +y88W0O5GWV9RB+QaPr4Q+j37Opa4cxOTlUvOrDiLT07U9ikCBNME8TX1/cEM43pB ++ENrGUaVmiryunB/mM6lCp9w4iAqbvL2ruMBw65X1fwtl4Sn+buaiAfksjoRg6Pl +4WNHI4IfxUtv/9f6hsa38U3jh8aETeWB+MijRjWqS91xfrracFRXXZkkVFgga+xa +H/USZczwnFT/7L0wFw4v8nYR3rzxK9taGKl9CpySWtCbycfdivGChU1Y9seTzwp/ +as3uQv0XvyRWwiLsKu5HF91kEXfkvcx27Rv+INw5xom3XM0sPedvX2WcNzGCAkAw +ggI8AgEBMIGrMIGdMQswCQYDVQQGEwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0 +czEQMA4GA1UEBxMHTmV3YnVyeTEeMBwGA1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0 +IExDMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkq +hkiG9w0BCQEWD3dpbGxAY29kZXhucy5pbwIJAL3l2aQQMVyCMAsGCWCGSAFlAwQC +AaBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE1 +MDUzMDEzMTIyOVowLwYJKoZIhvcNAQkEMSIEIKEw4oeQWlgVekRUerm8rtMA8+w+ +l/8DIHk0nWKqIKUdMA0GCSqGSIb3DQEBAQUABIIBAAq+nsctzfEj50rHoCybqWR9 +ztPUTDiyluwCBckipKeADQV3UiWe5ZmQrH0u7ZmOGRGgEHmQLULuvU7pjWE82n8M +i575Gq3jiwEZyjK6M1lNcsqwMGxozazxQiXWptC8AT1rAeGh00UulGMAOE1Bepdy +0ermLsgLcxDDLxj/k5OgfIiaj8f6gUHmL1VwoAM0uPvW4b89M7vhBm9DzIOombVH +zst7AFXuQDpwYd+sNxT7+L9Fx0G40S1tnePw9z7mRBDYgXl9lhDJmm/E2YMD6rTa +933g/T2wb2l8NSl73syc7S6JPU4plUKO7orSBdnDsUqZMx+IyiV/bMVs4d1yocw= +-----END CMS----- diff --git a/tests/fixtures/eid2011.crl b/tests/fixtures/eid2011.crl new file mode 100644 index 0000000..4905182 Binary files /dev/null and b/tests/fixtures/eid2011.crl differ diff --git a/tests/fixtures/keys/test-der.crt b/tests/fixtures/keys/test-der.crt new file mode 100644 index 0000000..1fcb3e8 Binary files /dev/null and b/tests/fixtures/keys/test-der.crt differ diff --git a/tests/fixtures/keys/test-der.key b/tests/fixtures/keys/test-der.key new file mode 100644 index 0000000..a41e064 Binary files /dev/null and b/tests/fixtures/keys/test-der.key differ diff --git a/tests/fixtures/keys/test-dsa-der.crt b/tests/fixtures/keys/test-dsa-der.crt new file mode 100644 index 0000000..cac08b4 Binary files /dev/null and b/tests/fixtures/keys/test-dsa-der.crt differ diff --git a/tests/fixtures/keys/test-dsa-der.key b/tests/fixtures/keys/test-dsa-der.key new file mode 100644 index 0000000..15ae627 Binary files /dev/null and b/tests/fixtures/keys/test-dsa-der.key differ diff --git a/tests/fixtures/keys/test-dsa.crt b/tests/fixtures/keys/test-dsa.crt new file mode 100644 index 0000000..ab09906 --- /dev/null +++ b/tests/fixtures/keys/test-dsa.crt @@ -0,0 +1,40 @@ +-----BEGIN CERTIFICATE----- +MIIG8zCCBpmgAwIBAgIJAMaQ/YDLvjIbMAsGCWCGSAFlAwQDAjCBnTELMAkGA1UE +BhMCVVMxFjAUBgNVBAgMDU1hc3NhY2h1c2V0dHMxEDAOBgNVBAcMB05ld2J1cnkx +HjAcBgNVBAoMFUNvZGV4IE5vbiBTdWZmaWNpdCBMQzEQMA4GA1UECwwHVGVzdGlu +ZzESMBAGA1UEAwwJV2lsbCBCb25kMR4wHAYJKoZIhvcNAQkBFg93aWxsQGNvZGV4 +bnMuaW8wHhcNMTUwNTIwMTMwOTAyWhcNMjUwNTE3MTMwOTAyWjCBnTELMAkGA1UE +BhMCVVMxFjAUBgNVBAgMDU1hc3NhY2h1c2V0dHMxEDAOBgNVBAcMB05ld2J1cnkx +HjAcBgNVBAoMFUNvZGV4IE5vbiBTdWZmaWNpdCBMQzEQMA4GA1UECwwHVGVzdGlu +ZzESMBAGA1UEAwwJV2lsbCBCb25kMR4wHAYJKoZIhvcNAQkBFg93aWxsQGNvZGV4 +bnMuaW8wggTGMIIDOQYHKoZIzjgEATCCAywCggGBAMbPTSSUs/wLKhWTbXrOT6V1 +hqmh8oL8x3G/lK9VAqqmvtQSEI8QKf+TAsaOgq1vLyb+zcMnZjX/n0uJmFHxFk8V +QY7veWfG+C4xFpjWUoxbVndiVyfbBjKMD7i1IjFbg2u3XYlC8OGViic+M1iqNbJq +wcj9orfJIyVWVvWDDW3g0J+KwLQkh2DNZdznz04zuxYjO3MQUaz4QSMJXQE5WLxZ +9EyNpG6j2HD4S/ktsxPLusQrgErmE9wx0XBM7VIUzHEmilSRp+X88rXPI1jLWtis +NYOKio+FwNC3/VT6d+8ZSgq8GIQd8fVPjru/lQxSLMuCUngRYJ5gdT/u1CZ6ncUJ +/xu7JJljOYy+B4rxqcUqyO6Qa3FDBye8I8P9stObh69s0ogrfSMhFwAgIP45cnIC +xTbzjL3Yw7g5bprR3Nqba8arD3OamM1mWK2+e8Jy5LDu3bza3L+mgmpHEtCNuCyi +qmPDBg4J6aBhKUI0ntdDT+RSsdM+tsYKXxi/HQsKSQIhAJ5FSY1/86FSKaFCuEGB ++qgfKZVwH1HEWvgnCldQaV27AoIBgCGNfRiw5SxUJpG5IyskFx2vw166Ehmac8gY +9YyutfG5QD9dstgbaZ4eni4vte6bHRqEAobXJz1kzXZRb5xmbUaZ/5qBkdwlQAyr +XH/Dp/NorL7uotTjK61V7S4ET1nnmaX1iJa4o39cr0gDof7srgLmC0IoUIf13XC6 +JMsRZs4H495hvQ39RsN+qi7j3L9iJYQbaUC7BgZDNisoRuEWpAkTPD5xsxtfpuDz +m14AKNozobUI++S0EgmTCMzqvG4bMY3zAW6hOT/AIFDy3dbfdchL6iRMQeRtjoTP +jrJg0Mf4i0lquW1lpYD8jTemUyqPCUAqlStdxqmT7AqfI/exOtKaJC89ZhrSveIt ++Djx9Tg079UpL9YNgIi4v51XBwxEXIjeLgbNXBCqYYvMwWpUArDUbEDCfSX+Vn0a +RigDLaDUOZ8Y1cvxXsghGq53P85W++hdt+X6VF+lpklQn3N4Ogtr9fgkApMmXcrD +RoKPRP+sY5V34HHSQWs4j1+IujRdagOCAYUAAoIBgCkqtuFV45W0iPrr+yMROcoZ +JdgpcKB5Gfm5lCdAeR8TOeLB0x5C57WMXInwOn+aGYDdmRF/Ul33JiUGxWMaYhKp +ux8HCsDVV5GQFNblisQOWvX7W5lgDtkVF+36GpeRMF+AFqV29vUDyokFVhK/hIPL +rkuRQD7TaH05zmQWfv9eaAmmu/JBEMD1z/1EISyryIjXKm6kSNexz5vB2gKr+Qa5 ++5vLDCoi9G8DPlbF/MbqrpAdiQ1VQ2nXmtaDaBOeulFFmWrIvbNN3u3THjPVJu0C +wEVsHkr8Ka+chfxSaY0+FoUmHkofruNKlbKvKJTNRKQzdW7q16D2A1x9H5Geo0Ae +2FHeJhOKl67dV1biHsaEyrLdymO9BYLBYpJJK1JBtDNhltf+U6Mzz7cl2R92gKct +p2gDPjPHBi1TB8RLQ10obClW2KfdTNYCUxyIO2w1kx/3lGh9vYTWTbbnDtGYvSO9 +b7GmDTmtio62035sXw2/i/rrIkW2ujzyAxN8y2+ZRKNQME4wHQYDVR0OBBYEFIGj +N4b5mSjydHBgh/LTfo0ZYai+MB8GA1UdIwQYMBaAFIGjN4b5mSjydHBgh/LTfo0Z +Yai+MAwGA1UdEwQFMAMBAf8wCwYJYIZIAWUDBAMCA0cAMEQCIHk4IW8QkENNVUol +sHGgbCKDIVwkBb5n9GRAOEkjVDYZAiBbMoPqjvFLDhYIBNj0z3gYDZWAZz/jiIkh +jtP1bbo4zQ== +-----END CERTIFICATE----- diff --git a/tests/fixtures/keys/test-dsa.key b/tests/fixtures/keys/test-dsa.key new file mode 100644 index 0000000..66e0f63 --- /dev/null +++ b/tests/fixtures/keys/test-dsa.key @@ -0,0 +1,28 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIE1gIBAAKCAYEAxs9NJJSz/AsqFZNtes5PpXWGqaHygvzHcb+Ur1UCqqa+1BIQ +jxAp/5MCxo6CrW8vJv7NwydmNf+fS4mYUfEWTxVBju95Z8b4LjEWmNZSjFtWd2JX +J9sGMowPuLUiMVuDa7ddiULw4ZWKJz4zWKo1smrByP2it8kjJVZW9YMNbeDQn4rA +tCSHYM1l3OfPTjO7FiM7cxBRrPhBIwldATlYvFn0TI2kbqPYcPhL+S2zE8u6xCuA +SuYT3DHRcEztUhTMcSaKVJGn5fzytc8jWMta2Kw1g4qKj4XA0Lf9VPp37xlKCrwY +hB3x9U+Ou7+VDFIsy4JSeBFgnmB1P+7UJnqdxQn/G7skmWM5jL4HivGpxSrI7pBr +cUMHJ7wjw/2y05uHr2zSiCt9IyEXACAg/jlycgLFNvOMvdjDuDlumtHc2ptrxqsP +c5qYzWZYrb57wnLksO7dvNrcv6aCakcS0I24LKKqY8MGDgnpoGEpQjSe10NP5FKx +0z62xgpfGL8dCwpJAiEAnkVJjX/zoVIpoUK4QYH6qB8plXAfUcRa+CcKV1BpXbsC +ggGAIY19GLDlLFQmkbkjKyQXHa/DXroSGZpzyBj1jK618blAP12y2Btpnh6eLi+1 +7psdGoQChtcnPWTNdlFvnGZtRpn/moGR3CVADKtcf8On82isvu6i1OMrrVXtLgRP +WeeZpfWIlrijf1yvSAOh/uyuAuYLQihQh/XdcLokyxFmzgfj3mG9Df1Gw36qLuPc +v2IlhBtpQLsGBkM2KyhG4RakCRM8PnGzG1+m4PObXgAo2jOhtQj75LQSCZMIzOq8 +bhsxjfMBbqE5P8AgUPLd1t91yEvqJExB5G2OhM+OsmDQx/iLSWq5bWWlgPyNN6ZT +Ko8JQCqVK13GqZPsCp8j97E60pokLz1mGtK94i34OPH1ODTv1Skv1g2AiLi/nVcH +DERciN4uBs1cEKphi8zBalQCsNRsQMJ9Jf5WfRpGKAMtoNQ5nxjVy/FeyCEarnc/ +zlb76F235fpUX6WmSVCfc3g6C2v1+CQCkyZdysNGgo9E/6xjlXfgcdJBaziPX4i6 +NF1qAoIBgCkqtuFV45W0iPrr+yMROcoZJdgpcKB5Gfm5lCdAeR8TOeLB0x5C57WM +XInwOn+aGYDdmRF/Ul33JiUGxWMaYhKpux8HCsDVV5GQFNblisQOWvX7W5lgDtkV +F+36GpeRMF+AFqV29vUDyokFVhK/hIPLrkuRQD7TaH05zmQWfv9eaAmmu/JBEMD1 +z/1EISyryIjXKm6kSNexz5vB2gKr+Qa5+5vLDCoi9G8DPlbF/MbqrpAdiQ1VQ2nX +mtaDaBOeulFFmWrIvbNN3u3THjPVJu0CwEVsHkr8Ka+chfxSaY0+FoUmHkofruNK +lbKvKJTNRKQzdW7q16D2A1x9H5Geo0Ae2FHeJhOKl67dV1biHsaEyrLdymO9BYLB +YpJJK1JBtDNhltf+U6Mzz7cl2R92gKctp2gDPjPHBi1TB8RLQ10obClW2KfdTNYC +UxyIO2w1kx/3lGh9vYTWTbbnDtGYvSO9b7GmDTmtio62035sXw2/i/rrIkW2ujzy +AxN8y2+ZRAIhAJUN+c1g4U0Po+7Wx2LUiiZhEHwwn4c/ItFl1L631UoM +-----END DSA PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-dsa.param b/tests/fixtures/keys/test-dsa.param new file mode 100644 index 0000000..9c83152 --- /dev/null +++ b/tests/fixtures/keys/test-dsa.param @@ -0,0 +1,19 @@ +-----BEGIN DSA PARAMETERS----- +MIIDLAKCAYEAxs9NJJSz/AsqFZNtes5PpXWGqaHygvzHcb+Ur1UCqqa+1BIQjxAp +/5MCxo6CrW8vJv7NwydmNf+fS4mYUfEWTxVBju95Z8b4LjEWmNZSjFtWd2JXJ9sG +MowPuLUiMVuDa7ddiULw4ZWKJz4zWKo1smrByP2it8kjJVZW9YMNbeDQn4rAtCSH +YM1l3OfPTjO7FiM7cxBRrPhBIwldATlYvFn0TI2kbqPYcPhL+S2zE8u6xCuASuYT +3DHRcEztUhTMcSaKVJGn5fzytc8jWMta2Kw1g4qKj4XA0Lf9VPp37xlKCrwYhB3x +9U+Ou7+VDFIsy4JSeBFgnmB1P+7UJnqdxQn/G7skmWM5jL4HivGpxSrI7pBrcUMH +J7wjw/2y05uHr2zSiCt9IyEXACAg/jlycgLFNvOMvdjDuDlumtHc2ptrxqsPc5qY +zWZYrb57wnLksO7dvNrcv6aCakcS0I24LKKqY8MGDgnpoGEpQjSe10NP5FKx0z62 +xgpfGL8dCwpJAiEAnkVJjX/zoVIpoUK4QYH6qB8plXAfUcRa+CcKV1BpXbsCggGA +IY19GLDlLFQmkbkjKyQXHa/DXroSGZpzyBj1jK618blAP12y2Btpnh6eLi+17psd +GoQChtcnPWTNdlFvnGZtRpn/moGR3CVADKtcf8On82isvu6i1OMrrVXtLgRPWeeZ +pfWIlrijf1yvSAOh/uyuAuYLQihQh/XdcLokyxFmzgfj3mG9Df1Gw36qLuPcv2Il +hBtpQLsGBkM2KyhG4RakCRM8PnGzG1+m4PObXgAo2jOhtQj75LQSCZMIzOq8bhsx +jfMBbqE5P8AgUPLd1t91yEvqJExB5G2OhM+OsmDQx/iLSWq5bWWlgPyNN6ZTKo8J +QCqVK13GqZPsCp8j97E60pokLz1mGtK94i34OPH1ODTv1Skv1g2AiLi/nVcHDERc +iN4uBs1cEKphi8zBalQCsNRsQMJ9Jf5WfRpGKAMtoNQ5nxjVy/FeyCEarnc/zlb7 +6F235fpUX6WmSVCfc3g6C2v1+CQCkyZdysNGgo9E/6xjlXfgcdJBaziPX4i6NF1q +-----END DSA PARAMETERS----- diff --git a/tests/fixtures/keys/test-ec-der.crt b/tests/fixtures/keys/test-ec-der.crt new file mode 100644 index 0000000..8acb2fb Binary files /dev/null and b/tests/fixtures/keys/test-ec-der.crt differ diff --git a/tests/fixtures/keys/test-ec-der.key b/tests/fixtures/keys/test-ec-der.key new file mode 100644 index 0000000..47e5c55 Binary files /dev/null and b/tests/fixtures/keys/test-ec-der.key differ diff --git a/tests/fixtures/keys/test-ec.crt b/tests/fixtures/keys/test-ec.crt new file mode 100644 index 0000000..db32939 --- /dev/null +++ b/tests/fixtures/keys/test-ec.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDeDCCAx2gAwIBAgIJANwFLdPfoenEMAoGCCqGSM49BAMCMIGdMQswCQYDVQQG +EwJVUzEWMBQGA1UECAwNTWFzc2FjaHVzZXR0czEQMA4GA1UEBwwHTmV3YnVyeTEe +MBwGA1UECgwVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLDAdUZXN0aW5n +MRIwEAYDVQQDDAlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhu +cy5pbzAeFw0xNTA1MjAxMjU2NDZaFw0yNTA1MTcxMjU2NDZaMIGdMQswCQYDVQQG +EwJVUzEWMBQGA1UECAwNTWFzc2FjaHVzZXR0czEQMA4GA1UEBwwHTmV3YnVyeTEe +MBwGA1UECgwVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLDAdUZXN0aW5n +MRIwEAYDVQQDDAlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhu +cy5pbzCCAUswggEDBgcqhkjOPQIBMIH3AgEBMCwGByqGSM49AQECIQD/////AAAA +AQAAAAAAAAAAAAAAAP///////////////zBbBCD/////AAAAAQAAAAAAAAAAAAAA +AP///////////////AQgWsY12Ko6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsD +FQDEnTYIhucEk2pmeOETnSa3gZ9+kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLesz +oPShOUXYmMKWT+NC4v4af5uO5+tKfA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD///// +AAAAAP//////////vOb6racXnoTzucrC/GMlUQIBAQNCAASLXUxx99bGo0ljQlxH +n8tzJB3J3dEt8TqftwTeINBYAJNU9onHL4cr9/k9OzTtnnsOPVdC33gDC8wxxgPX +n2ABo1AwTjAdBgNVHQ4EFgQUVKpUcGw0Gm3rXZfXHvzVJDyKDtcwHwYDVR0jBBgw +FoAUVKpUcGw0Gm3rXZfXHvzVJDyKDtcwDAYDVR0TBAUwAwEB/zAKBggqhkjOPQQD +AgNJADBGAiEA9D5hQQo7QqQNufAlMB4Dq0RWytRShGgp2gu5C6BCSJ0CIQDE6YAV +vKH2xayaPtml3X1TP+BmNORDjILENbGqBPaJ9Q== +-----END CERTIFICATE----- diff --git a/tests/fixtures/keys/test-ec.key b/tests/fixtures/keys/test-ec.key new file mode 100644 index 0000000..6543574 --- /dev/null +++ b/tests/fixtures/keys/test-ec.key @@ -0,0 +1,18 @@ +-----BEGIN EC PARAMETERS----- +MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP////////// +/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6 +k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+ +kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK +fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz +ucrC/GMlUQIBAQ== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIIBaAIBAQQg6OWPI5AB6xIvibu6NwIv2Myye+QfQp4+U/mJ0GmFQ12ggfowgfcC +AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// +MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr +vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE +axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W +K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 +YyVRAgEBoUQDQgAEi11McffWxqNJY0JcR5/LcyQdyd3RLfE6n7cE3iDQWACTVPaJ +xy+HK/f5PTs07Z57Dj1XQt94AwvMMcYD159gAQ== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-pkcs8-der.key b/tests/fixtures/keys/test-pkcs8-der.key new file mode 100644 index 0000000..a857947 Binary files /dev/null and b/tests/fixtures/keys/test-pkcs8-der.key differ diff --git a/tests/fixtures/keys/test-pkcs8.key b/tests/fixtures/keys/test-pkcs8.key new file mode 100644 index 0000000..298c6b3 --- /dev/null +++ b/tests/fixtures/keys/test-pkcs8.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9WygHdOLmSiwC +Kr1Q3ngXquxQNn6UucZFnKh22q87w9f/xBv5AkIqya99Np7rIyRcXY4t2lQ0Rj8d +PllsBmo8vpNr2JtLe5kj/25lRgjNOqH7w2VDFldS3eEsiJx67ktUI+MR5Qe/2fpg +FmKQrnZgBSCRILZRw83s6rupCxFTQdZWyx/pTzcrp8FwvRUmFoXpIwMgWn5RQZKE +FfdI137kxuz4dJuAwH2Z+Z+a/2Kb5ihA5D5GltZgLfKnpeG/EZJQIfLfL00n70Lk +3ssNxhXCnuyspihyGgw8cMJwC3xljWt7e2KFWT/X1a4IZEe9wwQpxyMdtrgx1E5M +AZiHVC9fAgMBAAECggEATw0fSP2jPED63my5XGmD+V2CCnq1naFxBN7B9dyWC31X +T4+vneUzeml4ue1zqvag1263TK05OhmZf7vn2RFUiMeHBB8JthmDdWPN0rnKMuOn +fFO2kqthCVdYCh9+NFQHXrkcsvvKoG1/+V1fCMfM44lAb1YYx0nXTnEwpwHX2cmK +OBjX0SD/2y81CrrDB8MAIoBhe6yjPwCkajNPpvz6jacg1AdoKCZ/mo9uDjgl9ff6 +TZ6vtsr6aTty6g4hY4Hki1J/KrgZ1ssz5db8YgaRyVdQ5ZEOaK252gsq2/la29iu +xZ7WLOcciuUjLuAjWL1n5X2YRhv5pk8O2MHgq7L+AQKBgQDtUFKi/g6mHvzKIgjC +YnpP7j611slDaXFXsa6xJWOPvq+ZkBI0nsSClHnCgfUA8TIeCdSShErx8ZQen4vG +azautiK9yNdngNO+ln+pBUkE0nilXJaf6KruAuujbCi2IsMHN4b2ijjoLOfRAI6T +u2h/FJtwSSGQ75S7BzdlKUQ59wKBgQDMRB8dA0XMEeqW89Vt/L34jGcb67fdYQpI +yv7V6T+Fp7jf4Pi+d0FfUM+6FpZRqmxT0aADlghc6EIiufHgxsy9wGfnkJYh/sn5 +0R09pQ4Y35i1htXb1TjmdfR1LhBfEE5VnCepQFEPCm1IVVoscNSs4Li+eRJPqJEe +H84nkC4b2QKBgQCbz5ICLBZIIa5NtJzVq7yswDryPuxz00Y0kpek/WxqE4PNqlcZ +r2hMZ9mtyI+pJ7OFH2UvMabXRYq/tHccNoZ3nWQgAT7UWTQtPTjiK3MutFW8FJdc +tHGNxeMasEfmldpA4cc+FbCZV+p4QgpamsBYN5p61bkxJOwdA/bt93MxLwKBgDgN +eXw8qaqWQAmsX6UO9hJ+dMz0oj/doTTYf5Wzq/rBS7ojwh6CGy5MvrQR/q3qVk+p +9n8FbMYR9hQRco57/zMS2XBx/MDXahVjjOKdqICq2vz1QzpQCI01UR/WxCdSEizr +7PZE7/lwowx2X4hSbgoCoK+kCaJSX4Akui2hIwYBAoGBAJxbCxNsycUmsHEdV7Tk +Lxj8N5TUKPtOUoq5hKJ0BK6zQqEm/tuRIgQ3fHdEkUNWuQRbfQAf9v6gwodKOKV5 +Y/s0sJPhj939nJuFq+E7OKiXjTwaxk17phVPOT9RK39rhkErUrlGUeZOIVg1e95x +erMeeTvs7MIywm215b+CvUAF +-----END PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-public-der.key b/tests/fixtures/keys/test-public-der.key new file mode 100644 index 0000000..c9d2f8d Binary files /dev/null and b/tests/fixtures/keys/test-public-der.key differ diff --git a/tests/fixtures/keys/test-public-rsa-der.key b/tests/fixtures/keys/test-public-rsa-der.key new file mode 100644 index 0000000..6622597 Binary files /dev/null and b/tests/fixtures/keys/test-public-rsa-der.key differ diff --git a/tests/fixtures/keys/test-public-rsa.key b/tests/fixtures/keys/test-public-rsa.key new file mode 100644 index 0000000..59082b6 --- /dev/null +++ b/tests/fixtures/keys/test-public-rsa.key @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAvVsoB3Ti5kosAiq9UN54F6rsUDZ+lLnGRZyodtqvO8PX/8Qb+QJC +KsmvfTae6yMkXF2OLdpUNEY/HT5ZbAZqPL6Ta9ibS3uZI/9uZUYIzTqh+8NlQxZX +Ut3hLIiceu5LVCPjEeUHv9n6YBZikK52YAUgkSC2UcPN7Oq7qQsRU0HWVssf6U83 +K6fBcL0VJhaF6SMDIFp+UUGShBX3SNd+5Mbs+HSbgMB9mfmfmv9im+YoQOQ+RpbW +YC3yp6XhvxGSUCHy3y9NJ+9C5N7LDcYVwp7srKYochoMPHDCcAt8ZY1re3tihVk/ +19WuCGRHvcMEKccjHba4MdROTAGYh1QvXwIDAQAB +-----END RSA PUBLIC KEY----- diff --git a/tests/fixtures/keys/test-public.key b/tests/fixtures/keys/test-public.key new file mode 100644 index 0000000..733b4f4 --- /dev/null +++ b/tests/fixtures/keys/test-public.key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvVsoB3Ti5kosAiq9UN54 +F6rsUDZ+lLnGRZyodtqvO8PX/8Qb+QJCKsmvfTae6yMkXF2OLdpUNEY/HT5ZbAZq +PL6Ta9ibS3uZI/9uZUYIzTqh+8NlQxZXUt3hLIiceu5LVCPjEeUHv9n6YBZikK52 +YAUgkSC2UcPN7Oq7qQsRU0HWVssf6U83K6fBcL0VJhaF6SMDIFp+UUGShBX3SNd+ +5Mbs+HSbgMB9mfmfmv9im+YoQOQ+RpbWYC3yp6XhvxGSUCHy3y9NJ+9C5N7LDcYV +wp7srKYochoMPHDCcAt8ZY1re3tihVk/19WuCGRHvcMEKccjHba4MdROTAGYh1Qv +XwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/fixtures/keys/test-rc2.p12 b/tests/fixtures/keys/test-rc2.p12 new file mode 100644 index 0000000..40a8b81 Binary files /dev/null and b/tests/fixtures/keys/test-rc2.p12 differ diff --git a/tests/fixtures/keys/test.crt b/tests/fixtures/keys/test.crt new file mode 100644 index 0000000..4f390e6 --- /dev/null +++ b/tests/fixtures/keys/test.crt @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExzCCA6+gAwIBAgIJAL3l2aQQMVyCMA0GCSqGSIb3DQEBCwUAMIGdMQswCQYD +VQQGEwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVy +eTEeMBwGA1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0 +aW5nMRIwEAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29k +ZXhucy5pbzAeFw0xNTA1MDYxNDM3MTZaFw0yNTA1MDMxNDM3MTZaMIGdMQswCQYD +VQQGEwJVUzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVy +eTEeMBwGA1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0 +aW5nMRIwEAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29k +ZXhucy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL1bKAd04uZK +LAIqvVDeeBeq7FA2fpS5xkWcqHbarzvD1//EG/kCQirJr302nusjJFxdji3aVDRG +Px0+WWwGajy+k2vYm0t7mSP/bmVGCM06ofvDZUMWV1Ld4SyInHruS1Qj4xHlB7/Z ++mAWYpCudmAFIJEgtlHDzezqu6kLEVNB1lbLH+lPNyunwXC9FSYWhekjAyBaflFB +koQV90jXfuTG7Ph0m4DAfZn5n5r/YpvmKEDkPkaW1mAt8qel4b8RklAh8t8vTSfv +QuTeyw3GFcKe7KymKHIaDDxwwnALfGWNa3t7YoVZP9fVrghkR73DBCnHIx22uDHU +TkwBmIdUL18CAwEAAaOCAQYwggECMB0GA1UdDgQWBBS+QoU9zP/j+SgCj35YVrT9 +A1zqSzCB0gYDVR0jBIHKMIHHgBS+QoU9zP/j+SgCj35YVrT9A1zqS6GBo6SBoDCB +nTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMxEDAOBgNVBAcT +B05ld2J1cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZmaWNpdCBMQzEQMA4GA1UE +CxMHVGVzdGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4wHAYJKoZIhvcNAQkBFg93 +aWxsQGNvZGV4bnMuaW+CCQC95dmkEDFcgjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQA/5fmvWSMRWqD6JalrNr3Saio5+c/LzxbQ7kZZX1EH5Bo+vhD6 +Pfs6lrhzE5OVS86sOItPTtT2KQIE0wTxNfX9wQzjekH4Q2sZRpWaKvK6cH+YzqUK +n3DiICpu8vau4wHDrlfV/C2XhKf5u5qIB+SyOhGDo+XhY0cjgh/FS2//1/qGxrfx +TeOHxoRN5YH4yKNGNapL3XF+utpwVFddmSRUWCBr7Fof9RJlzPCcVP/svTAXDi/y +dhHevPEr21oYqX0KnJJa0JvJx92K8YKFTVj2x5PPCn9qze5C/Re/JFbCIuwq7kcX +3WQRd+S9zHbtG/4g3DnGibdczSw9529fZZw3 +-----END CERTIFICATE----- diff --git a/tests/fixtures/keys/test.key b/tests/fixtures/keys/test.key new file mode 100644 index 0000000..f768269 --- /dev/null +++ b/tests/fixtures/keys/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAvVsoB3Ti5kosAiq9UN54F6rsUDZ+lLnGRZyodtqvO8PX/8Qb ++QJCKsmvfTae6yMkXF2OLdpUNEY/HT5ZbAZqPL6Ta9ibS3uZI/9uZUYIzTqh+8Nl +QxZXUt3hLIiceu5LVCPjEeUHv9n6YBZikK52YAUgkSC2UcPN7Oq7qQsRU0HWVssf +6U83K6fBcL0VJhaF6SMDIFp+UUGShBX3SNd+5Mbs+HSbgMB9mfmfmv9im+YoQOQ+ +RpbWYC3yp6XhvxGSUCHy3y9NJ+9C5N7LDcYVwp7srKYochoMPHDCcAt8ZY1re3ti +hVk/19WuCGRHvcMEKccjHba4MdROTAGYh1QvXwIDAQABAoIBAE8NH0j9ozxA+t5s +uVxpg/ldggp6tZ2hcQTewfXclgt9V0+Pr53lM3ppeLntc6r2oNdut0ytOToZmX+7 +59kRVIjHhwQfCbYZg3VjzdK5yjLjp3xTtpKrYQlXWAoffjRUB165HLL7yqBtf/ld +XwjHzOOJQG9WGMdJ105xMKcB19nJijgY19Eg/9svNQq6wwfDACKAYXusoz8ApGoz +T6b8+o2nINQHaCgmf5qPbg44JfX3+k2er7bK+mk7cuoOIWOB5ItSfyq4GdbLM+XW +/GIGkclXUOWRDmitudoLKtv5WtvYrsWe1iznHIrlIy7gI1i9Z+V9mEYb+aZPDtjB +4Kuy/gECgYEA7VBSov4Oph78yiIIwmJ6T+4+tdbJQ2lxV7GusSVjj76vmZASNJ7E +gpR5woH1APEyHgnUkoRK8fGUHp+Lxms2rrYivcjXZ4DTvpZ/qQVJBNJ4pVyWn+iq +7gLro2wotiLDBzeG9oo46Czn0QCOk7tofxSbcEkhkO+Uuwc3ZSlEOfcCgYEAzEQf +HQNFzBHqlvPVbfy9+IxnG+u33WEKSMr+1ek/hae43+D4vndBX1DPuhaWUapsU9Gg +A5YIXOhCIrnx4MbMvcBn55CWIf7J+dEdPaUOGN+YtYbV29U45nX0dS4QXxBOVZwn +qUBRDwptSFVaLHDUrOC4vnkST6iRHh/OJ5AuG9kCgYEAm8+SAiwWSCGuTbSc1au8 +rMA68j7sc9NGNJKXpP1sahODzapXGa9oTGfZrciPqSezhR9lLzGm10WKv7R3HDaG +d51kIAE+1Fk0LT044itzLrRVvBSXXLRxjcXjGrBH5pXaQOHHPhWwmVfqeEIKWprA +WDeaetW5MSTsHQP27fdzMS8CgYA4DXl8PKmqlkAJrF+lDvYSfnTM9KI/3aE02H+V +s6v6wUu6I8IeghsuTL60Ef6t6lZPqfZ/BWzGEfYUEXKOe/8zEtlwcfzA12oVY4zi +naiAqtr89UM6UAiNNVEf1sQnUhIs6+z2RO/5cKMMdl+IUm4KAqCvpAmiUl+AJLot +oSMGAQKBgQCcWwsTbMnFJrBxHVe05C8Y/DeU1Cj7TlKKuYSidASus0KhJv7bkSIE +N3x3RJFDVrkEW30AH/b+oMKHSjileWP7NLCT4Y/d/ZybhavhOziol408GsZNe6YV +Tzk/USt/a4ZBK1K5RlHmTiFYNXvecXqzHnk77OzCMsJtteW/gr1ABQ== +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/message.der b/tests/fixtures/message.der new file mode 100644 index 0000000..9d0c94a --- /dev/null +++ b/tests/fixtures/message.der @@ -0,0 +1 @@ +0A *H 42This is the message to encapsulate in PKCS#7/CMS diff --git a/tests/fixtures/message.pem b/tests/fixtures/message.pem new file mode 100644 index 0000000..a235e61 --- /dev/null +++ b/tests/fixtures/message.pem @@ -0,0 +1,4 @@ +-----BEGIN CMS----- +MEEGCSqGSIb3DQEHAaA0BDJUaGlzIGlzIHRoZSBtZXNzYWdlIHRvIGVuY2Fwc3Vs +YXRlIGluIFBLQ1MjNy9DTVMNCg== +-----END CMS----- diff --git a/tests/fixtures/message.txt b/tests/fixtures/message.txt new file mode 100644 index 0000000..7992225 --- /dev/null +++ b/tests/fixtures/message.txt @@ -0,0 +1 @@ +This is the message to encapsulate in PKCS#7/CMS diff --git a/tests/fixtures/ocsp_request b/tests/fixtures/ocsp_request new file mode 100644 index 0000000..bf07bf9 Binary files /dev/null and b/tests/fixtures/ocsp_request differ diff --git a/tests/fixtures/ocsp_response b/tests/fixtures/ocsp_response new file mode 100644 index 0000000..9b94de4 Binary files /dev/null and b/tests/fixtures/ocsp_response differ diff --git a/tests/fixtures/pkcs7-signed-digested.der b/tests/fixtures/pkcs7-signed-digested.der new file mode 100644 index 0000000..13e7505 Binary files /dev/null and b/tests/fixtures/pkcs7-signed-digested.der differ diff --git a/tests/fixtures/pkcs7-signed-digested.pem b/tests/fixtures/pkcs7-signed-digested.pem new file mode 100644 index 0000000..2f1964f --- /dev/null +++ b/tests/fixtures/pkcs7-signed-digested.pem @@ -0,0 +1,41 @@ +-----BEGIN PKCS7----- +MIIHRgYJKoZIhvcNAQcCoIIHNzCCBzMCAQExDzANBglghkgBZQMEAgEFADBzBgkq +hkiG9w0BBwWgZjBkAgEAMAcGBSsOAwIaMEAGCSqGSIb3DQEHAaAzBDFUaGlzIGlz +IHRoZSBtZXNzYWdlIHRvIGVuY2Fwc3VsYXRlIGluIFBLQ1MjNy9DTVMKBBRTydvB +bds0OyhO76YDDgJkeTGv+6CCBMswggTHMIIDr6ADAgECAgkAveXZpBAxXIIwDQYJ +KoZIhvcNAQELBQAwgZ0xCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNl +dHRzMRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmlj +aXQgTEMxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCVdpbGwgQm9uZDEeMBwG +CSqGSIb3DQEJARYPd2lsbEBjb2RleG5zLmlvMB4XDTE1MDUwNjE0MzcxNloXDTI1 +MDUwMzE0MzcxNlowgZ0xCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNl +dHRzMRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmlj +aXQgTEMxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCVdpbGwgQm9uZDEeMBwG +CSqGSIb3DQEJARYPd2lsbEBjb2RleG5zLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAvVsoB3Ti5kosAiq9UN54F6rsUDZ+lLnGRZyodtqvO8PX/8Qb ++QJCKsmvfTae6yMkXF2OLdpUNEY/HT5ZbAZqPL6Ta9ibS3uZI/9uZUYIzTqh+8Nl +QxZXUt3hLIiceu5LVCPjEeUHv9n6YBZikK52YAUgkSC2UcPN7Oq7qQsRU0HWVssf +6U83K6fBcL0VJhaF6SMDIFp+UUGShBX3SNd+5Mbs+HSbgMB9mfmfmv9im+YoQOQ+ +RpbWYC3yp6XhvxGSUCHy3y9NJ+9C5N7LDcYVwp7srKYochoMPHDCcAt8ZY1re3ti +hVk/19WuCGRHvcMEKccjHba4MdROTAGYh1QvXwIDAQABo4IBBjCCAQIwHQYDVR0O +BBYEFL5ChT3M/+P5KAKPflhWtP0DXOpLMIHSBgNVHSMEgcowgceAFL5ChT3M/+P5 +KAKPflhWtP0DXOpLoYGjpIGgMIGdMQswCQYDVQQGEwJVUzEWMBQGA1UECBMNTWFz +c2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVyeTEeMBwGA1UEChMVQ29kZXggTm9u +IFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlXaWxsIEJv +bmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5pb4IJAL3l2aQQMVyCMAwG +A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAD/l+a9ZIxFaoPolqWs2vdJq +Kjn5z8vPFtDuRllfUQfkGj6+EPo9+zqWuHMTk5VLzqw4i09O1PYpAgTTBPE19f3B +DON6QfhDaxlGlZoq8rpwf5jOpQqfcOIgKm7y9q7jAcOuV9X8LZeEp/m7mogH5LI6 +EYOj5eFjRyOCH8VLb//X+obGt/FN44fGhE3lgfjIo0Y1qkvdcX662nBUV12ZJFRY +IGvsWh/1EmXM8JxU/+y9MBcOL/J2Ed688SvbWhipfQqcklrQm8nH3YrxgoVNWPbH +k88Kf2rN7kL9F78kVsIi7CruRxfdZBF35L3Mdu0b/iDcOcaJt1zNLD3nb19lnDcx +ggHXMIIB0wIBATCBqzCBnTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1 +c2V0dHMxEDAOBgNVBAcTB05ld2J1cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZm +aWNpdCBMQzEQMA4GA1UECxMHVGVzdGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4w +HAYJKoZIhvcNAQkBFg93aWxsQGNvZGV4bnMuaW8CCQC95dmkEDFcgjANBglghkgB +ZQMEAgEFADANBgkqhkiG9w0BAQEFAASCAQBwvBiCQdbY51zcQielqKqLFhVhOuVH +U/2PRaOC4nJEB9HLv7SFSioWGd7cUxXPmO5cDt/eyHnOKzhhNrChy5TWT82D7wzJ +I6B7i2VAXD2oPswNHxcj83Sffoj4875OGZUP65VVabSqwyo2A5Mc3OVlP05eA8hW +2FeP6C2FMtr9edTdiMqjFEHkOwOIDit23EQ9Tf+yyMODsTM3U1EzS8oarX5qvGGL +hNt/z2GyHSGDz7g/xpjt2GYGzwMwlp20ehbfbqcw63f3QBP78qxBeZ3cwO1Lixnu +BT1hIDl+gB06I2lIQ2CLPmOtAXrebwG6UfNLFL9rdxoywgyTzDW8ZsZp +-----END PKCS7----- diff --git a/tests/fixtures/pkcs7-signed.der b/tests/fixtures/pkcs7-signed.der new file mode 100644 index 0000000..cfd02be Binary files /dev/null and b/tests/fixtures/pkcs7-signed.der differ diff --git a/tests/fixtures/pkcs7-signed.pem b/tests/fixtures/pkcs7-signed.pem new file mode 100644 index 0000000..51e9ac7 --- /dev/null +++ b/tests/fixtures/pkcs7-signed.pem @@ -0,0 +1,45 @@ +-----BEGIN PKCS7----- +MIIH+gYJKoZIhvcNAQcCoIIH6zCCB+cCAQExDzANBglghkgBZQMEAgEFADBABgkq +hkiG9w0BBwGgMwQxVGhpcyBpcyB0aGUgbWVzc2FnZSB0byBlbmNhcHN1bGF0ZSBp +biBQS0NTIzcvQ01TCqCCBMswggTHMIIDr6ADAgECAgkAveXZpBAxXIIwDQYJKoZI +hvcNAQELBQAwgZ0xCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNldHRz +MRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmljaXQg +TEMxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCVdpbGwgQm9uZDEeMBwGCSqG +SIb3DQEJARYPd2lsbEBjb2RleG5zLmlvMB4XDTE1MDUwNjE0MzcxNloXDTI1MDUw +MzE0MzcxNlowgZ0xCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNldHRz +MRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmljaXQg +TEMxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCVdpbGwgQm9uZDEeMBwGCSqG +SIb3DQEJARYPd2lsbEBjb2RleG5zLmlvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAvVsoB3Ti5kosAiq9UN54F6rsUDZ+lLnGRZyodtqvO8PX/8Qb+QJC +KsmvfTae6yMkXF2OLdpUNEY/HT5ZbAZqPL6Ta9ibS3uZI/9uZUYIzTqh+8NlQxZX +Ut3hLIiceu5LVCPjEeUHv9n6YBZikK52YAUgkSC2UcPN7Oq7qQsRU0HWVssf6U83 +K6fBcL0VJhaF6SMDIFp+UUGShBX3SNd+5Mbs+HSbgMB9mfmfmv9im+YoQOQ+RpbW +YC3yp6XhvxGSUCHy3y9NJ+9C5N7LDcYVwp7srKYochoMPHDCcAt8ZY1re3tihVk/ +19WuCGRHvcMEKccjHba4MdROTAGYh1QvXwIDAQABo4IBBjCCAQIwHQYDVR0OBBYE +FL5ChT3M/+P5KAKPflhWtP0DXOpLMIHSBgNVHSMEgcowgceAFL5ChT3M/+P5KAKP +flhWtP0DXOpLoYGjpIGgMIGdMQswCQYDVQQGEwJVUzEWMBQGA1UECBMNTWFzc2Fj +aHVzZXR0czEQMA4GA1UEBxMHTmV3YnVyeTEeMBwGA1UEChMVQ29kZXggTm9uIFN1 +ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlXaWxsIEJvbmQx +HjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5pb4IJAL3l2aQQMVyCMAwGA1Ud +EwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAD/l+a9ZIxFaoPolqWs2vdJqKjn5 +z8vPFtDuRllfUQfkGj6+EPo9+zqWuHMTk5VLzqw4i09O1PYpAgTTBPE19f3BDON6 +QfhDaxlGlZoq8rpwf5jOpQqfcOIgKm7y9q7jAcOuV9X8LZeEp/m7mogH5LI6EYOj +5eFjRyOCH8VLb//X+obGt/FN44fGhE3lgfjIo0Y1qkvdcX662nBUV12ZJFRYIGvs +Wh/1EmXM8JxU/+y9MBcOL/J2Ed688SvbWhipfQqcklrQm8nH3YrxgoVNWPbHk88K +f2rN7kL9F78kVsIi7CruRxfdZBF35L3Mdu0b/iDcOcaJt1zNLD3nb19lnDcxggK+ +MIICugIBATCBqzCBnTELMAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0 +dHMxEDAOBgNVBAcTB05ld2J1cnkxHjAcBgNVBAoTFUNvZGV4IE5vbiBTdWZmaWNp +dCBMQzEQMA4GA1UECxMHVGVzdGluZzESMBAGA1UEAxMJV2lsbCBCb25kMR4wHAYJ +KoZIhvcNAQkBFg93aWxsQGNvZGV4bnMuaW8CCQC95dmkEDFcgjANBglghkgBZQME +AgEFAKCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEP +Fw0xNTA2MDMwNTU1MjZaMC8GCSqGSIb3DQEJBDEiBCBSiCVHFVstUERoBSTIcVrM +Yig2F7do7qESkJZPlK7beTB5BgkqhkiG9w0BCQ8xbDBqMAsGCWCGSAFlAwQBKjAL +BglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqGSIb3DQMC +AgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIBKDANBgkq +hkiG9w0BAQEFAASCAQBC8B6Ad4REtfK6ziHWA4xbEeb/pgHXenwJODI5Gmc8K5Vh +Gibq8oPK2Ve4OJq09FrPYtWB3u7mrLroChmF/oROC7/G8UfzXCGqdjbjs6T5BQN1 +mNVEilm8mVCa0cjH65dMbR+iFg54zxYb7DXpV7E0Uuy2CJtNHjuFq6lpETzl/WWM +fe4IJnGtxEOtRfoP+vJQ647krV1AbpRcKeThbTm14vVv5/HeCg6R4m5GDy4tyIHy +5w/pJA6uhqmr6D7w02kxBaEZvvD2P9YvlblkhtSNSvnxFOjSWQgKaWiBkcOO899Q +d6QhQDwCD6XTiwiyOzMXierRMPLpuduDE+QZbOHk +-----END PKCS7----- diff --git a/tests/fixtures/readme.md b/tests/fixtures/readme.md new file mode 100644 index 0000000..1d444e7 --- /dev/null +++ b/tests/fixtures/readme.md @@ -0,0 +1,6 @@ +# Fixtures + +Item of note: the files `cms-signed-digested.pem`, `cms-signed-digested.der`, +`pkcs7-signed-digested.pem` and `pkcs7-signed-digested.der` all have invalid +signatures since the structures were hand edited to highlight the one +incompatibility between CMS and PKCS#7. diff --git a/tests/fixtures/tsa_request b/tests/fixtures/tsa_request new file mode 100644 index 0000000..214d28b Binary files /dev/null and b/tests/fixtures/tsa_request differ diff --git a/tests/fixtures/tsa_response b/tests/fixtures/tsa_response new file mode 100644 index 0000000..3921362 Binary files /dev/null and b/tests/fixtures/tsa_response differ diff --git a/tests/test_cms.py b/tests/test_cms.py new file mode 100644 index 0000000..fde2214 --- /dev/null +++ b/tests/test_cms.py @@ -0,0 +1,711 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import os +from datetime import datetime +from collections import OrderedDict + +from asn1crypto import cms, core + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + + +class CMSTests(unittest.TestCase): + + def test_parse_content_info_data(self): + with open(os.path.join(fixtures_dir, 'message.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + self.assertEqual( + 'data', + info['content_type'].native + ) + self.assertEqual( + b'This is the message to encapsulate in PKCS#7/CMS\r\n', + info['content'].native + ) + + def test_parse_content_info_compressed_data(self): + with open(os.path.join(fixtures_dir, 'cms-compressed.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + compressed_data = info['content'] + + self.assertEqual( + 'compressed_data', + info['content_type'].native + ) + self.assertEqual( + 'v0', + compressed_data['version'].native + ) + self.assertEqual( + 'zlib', + compressed_data['compression_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + compressed_data['compression_algorithm']['parameters'].native + ) + self.assertEqual( + 'data', + compressed_data['encap_content_info']['content_type'].native + ) + self.assertEqual( + b'\x78\x9C\x0B\xC9\xC8\x2C\x56\x00\xA2\x92\x8C\x54\x85\xDC\xD4\xE2\xE2\xC4\xF4\x54\x85\x92\x7C\x85\xD4\xBC\xE4\xC4\x82\xE2\xD2\x9C\xC4\x92\x54\x85\xCC\x3C\x85\x00\x6F\xE7\x60\x65\x73\x7D\x67\xDF\x60\x2E\x00\xB5\xCF\x10\x71', + compressed_data['encap_content_info']['content'].native + ) + self.assertEqual( + b'This is the message to encapsulate in PKCS#7/CMS\n', + compressed_data.decompressed + ) + + def test_parse_content_info_digested_data(self): + with open(os.path.join(fixtures_dir, 'cms-digested.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + digested_data = info['content'] + + self.assertEqual( + 'digested_data', + info['content_type'].native + ) + self.assertEqual( + 'v0', + digested_data['version'].native + ) + self.assertEqual( + 'sha1', + digested_data['digest_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + digested_data['digest_algorithm']['parameters'].native + ) + self.assertEqual( + 'data', + digested_data['encap_content_info']['content_type'].native + ) + self.assertEqual( + b'This is the message to encapsulate in PKCS#7/CMS\n', + digested_data['encap_content_info']['content'].native + ) + self.assertEqual( + b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB', + digested_data['digest'].native + ) + + def test_parse_content_info_encrypted_data(self): + with open(os.path.join(fixtures_dir, 'cms-encrypted.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + encrypted_data = info['content'] + encrypted_content_info = encrypted_data['encrypted_content_info'] + + self.assertEqual( + 'encrypted_data', + info['content_type'].native + ) + self.assertEqual( + 'v0', + encrypted_data['version'].native + ) + self.assertEqual( + 'data', + encrypted_content_info['content_type'].native + ) + self.assertEqual( + 'aes128', + encrypted_content_info['content_encryption_algorithm']['algorithm'].native + ) + self.assertEqual( + b'\x1F\x34\x54\x9F\x7F\xB7\x06\xBD\x81\x57\x68\x84\x79\xB5\x2F\x6F', + encrypted_content_info['content_encryption_algorithm']['parameters'].native + ) + self.assertEqual( + b'\x80\xEE\x34\x8B\xFC\x04\x69\x4F\xBE\x15\x1C\x0C\x39\x2E\xF3\xEA\x8E\xEE\x17\x0D\x39\xC7\x4B\x6C\x4B\x13\xEF\x17\x82\x0D\xED\xBA\x6D\x2F\x3B\xAB\x4E\xEB\xF0\xDB\xD9\x6E\x1C\xC2\x3C\x1C\x4C\xFA\xF3\x98\x9B\x89\xBD\x48\x77\x07\xE2\x6B\x71\xCF\xB7\xFF\xCE\xA5', + encrypted_content_info['encrypted_content'].native + ) + + def test_parse_content_info_enveloped_data(self): + with open(os.path.join(fixtures_dir, 'cms-enveloped.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + enveloped_data = info['content'] + encrypted_content_info = enveloped_data['encrypted_content_info'] + recipient = enveloped_data['recipient_infos'][0].chosen + + self.assertEqual( + 'enveloped_data', + info['content_type'].native + ) + self.assertEqual( + 'v0', + enveloped_data['version'].native + ) + self.assertEqual( + None, + enveloped_data['originator_info'].native + ) + self.assertEqual( + 1, + len(enveloped_data['recipient_infos']) + ) + self.assertEqual( + 'v0', + recipient['version'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'issuer', + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]) + ), + ( + 'serial_number', + 13683582341504654466 + ) + ]), + recipient['rid'].native + ) + self.assertEqual( + 'rsa', + recipient['key_encryption_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + recipient['key_encryption_algorithm']['parameters'].native + ) + self.assertEqual( + b'\x97\x0A\xFD\x3B\x5C\x27\x45\x69\xCC\xDD\x45\x9E\xA7\x3C\x07\x27\x35\x16\x20\x21\xE4\x6E\x1D\xF8\x5B\xE8\x7F\xD8\x40\x41\xE9\xF2\x92\xCD\xC8\xC5\x03\x95\xEC\x6C\x0B\x97\x71\x87\x86\x3C\xEB\x68\x84\x06\x4E\xE6\xD0\xC4\x7D\x32\xFE\xA6\x06\xC9\xD5\xE1\x8B\xDA\xBF\x96\x5C\x20\x15\x49\x64\x7A\xA2\x4C\xFF\x8B\x0D\xEA\x76\x35\x9B\x7C\x43\xF7\x21\x95\x26\xE7\x70\x30\x98\x5F\x0D\x5E\x4A\xCB\xAD\x47\xDF\x46\xDA\x1F\x0E\xE2\xFE\x3A\x40\xD9\xF2\xDC\x0C\x97\xD9\x91\xED\x34\x8D\xF3\x73\xB0\x90\xF9\xDD\x31\x4D\x37\x93\x81\xD3\x92\xCB\x72\x4A\xD6\x9D\x01\x82\x85\xD5\x1F\xE2\xAA\x32\x12\x82\x4E\x17\xF6\xAA\x58\xDE\xBD\x1B\x80\xAF\x61\xF1\x8A\xD1\x7F\x9D\x41\x6A\xC0\xE4\xC7\x7E\x17\xDC\x94\x33\xE9\x74\x7E\xE9\xF8\x5C\x30\x87\x9B\xD6\xF0\xE3\x4A\xB7\xE3\xCC\x51\x8A\xD4\x37\xF1\xF9\x33\xB5\xD6\x1F\x36\xC1\x6F\x91\xA8\x5F\xE2\x6B\x08\xC7\x9D\xE8\xFD\xDC\xE8\x78\xE0\xC0\xC7\xCF\xC5\xEE\x60\xEC\x54\xFF\x1A\x9C\xF7\x4E\x2C\xD0\x88\xDC\xC2\x1F\xDC\x8A\x37\x9B\x71\x20\xFF\xFD\x6C\xE5\xBA\x8C\xDF\x0E\x3F\x20\xC6\xCB\x08\xA7\x07\xDB\x83', + recipient['encrypted_key'].native + ) + self.assertEqual( + 'data', + encrypted_content_info['content_type'].native + ) + self.assertEqual( + 'tripledes_3key', + encrypted_content_info['content_encryption_algorithm']['algorithm'].native + ) + self.assertEqual( + b'\x52\x50\x98\xFA\x33\x88\xC7\x3C', + encrypted_content_info['content_encryption_algorithm']['parameters'].native + ) + self.assertEqual( + b'\xDC\x88\x55\x08\xE5\x67\x70\x49\x99\x54\xFD\xF8\x40\x7C\x38\xD5\x78\x1D\x6A\x95\x6D\x1E\xC4\x12\x39\xFE\xC0\x76\xDC\xF5\x79\x1A\x69\xA1\xB9\x40\x1E\xCF\xC8\x79\x3E\xF3\x38\xB4\x90\x00\x27\xD1\xB5\x64\xAB\x99\x51\x13\xF1\x0A', + encrypted_content_info['encrypted_content'].native + ) + self.assertEqual( + None, + enveloped_data['unprotected_attrs'].native + ) + + def test_parse_content_info_cms_signed_data(self): + with open(os.path.join(fixtures_dir, 'cms-signed.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + signed_data = info['content'] + encap_content_info = signed_data['encap_content_info'] + + self.assertEqual( + 'signed_data', + info['content_type'].native + ) + self.assertEqual( + 'v1', + signed_data['version'].native + ) + self.assertEqual( + [ + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]) + ], + signed_data['digest_algorithms'].native + ) + self.assertEqual( + 'data', + encap_content_info['content_type'].native + ) + self.assertEqual( + b'This is the message to encapsulate in PKCS#7/CMS\r\n', + encap_content_info['content'].native + ) + + self.assertEqual( + 1, + len(signed_data['certificates']) + ) + certificate = signed_data['certificates'][0] + with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: + self.assertEqual( + f.read(), + certificate.dump() + ) + + self.assertEqual( + 1, + len(signed_data['signer_infos']) + ) + signer = signed_data['signer_infos'][0] + + self.assertEqual( + 'v1', + signer['version'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'issuer', + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]) + ), + ( + 'serial_number', + 13683582341504654466 + ) + ]), + signer['sid'].native + ) + self.assertEqual( + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]), + signer['digest_algorithm'].native + ) + + signed_attrs = signer['signed_attrs'] + + self.assertEqual( + 3, + len(signed_attrs) + ) + self.assertEqual( + 'content_type', + signed_attrs[0]['type'].native + ) + self.assertEqual( + 'data', + signed_attrs[0]['values'][0].native + ) + self.assertEqual( + 'signing_time', + signed_attrs[1]['type'].native + ) + self.assertEqual( + datetime(2015, 5, 30, 13, 12, 38, tzinfo=core.timezone.utc), + signed_attrs[1]['values'][0].native + ) + self.assertEqual( + 'message_digest', + signed_attrs[2]['type'].native + ) + self.assertEqual( + b'\xA1\x30\xE2\x87\x90\x5A\x58\x15\x7A\x44\x54\x7A\xB9\xBC\xAE\xD3\x00\xF3\xEC\x3E\x97\xFF\x03\x20\x79\x34\x9D\x62\xAA\x20\xA5\x1D', + signed_attrs[2]['values'][0].native + ) + + self.assertEqual( + OrderedDict([ + ('algorithm', 'rsa'), + ('parameters', None), + ]), + signer['signature_algorithm'].native + ) + self.assertEqual( + b'\xAC\x2F\xE3\x25\x39\x8F\xD3\xDF\x80\x4F\x0D\xBA\xB1\xEE\x99\x09\xA9\x21\xBB\xDF\x3C\x1E\x70\xDA\xDF\xC4\x0F\x1D\x10\x29\xBC\x94\xBE\xF8\xA8\xC2\x2D\x2A\x1F\x14\xBC\x4A\x5B\x66\x7F\x6F\xE4\xDF\x82\x4D\xD9\x3F\xEB\x89\xAA\x05\x1A\xE5\x58\xCE\xC4\x33\x53\x6E\xE4\x66\xF9\x21\xCF\x80\x35\x46\x88\xB5\x6A\xEA\x5C\x54\x49\x40\x31\xD6\xDC\x20\xD8\xA0\x63\x8C\xC1\xC3\xA1\x72\x5D\x0D\xCE\x43\xB1\x5C\xD8\x32\x3F\xA9\xE7\xBB\xD9\x56\xAE\xE7\xFB\x7C\x37\x32\x8B\x93\xC2\xC4\x47\xDD\x00\xFB\x1C\xEF\xC3\x68\x32\xDC\x06\x26\x17\x45\xF5\xB3\xDC\xD8\x5C\x2B\xC1\x8B\x97\x93\xB8\xF1\x85\xE2\x92\x3B\xC4\x6A\x6A\x89\xC5\x14\x51\x4A\x06\x11\x54\xB0\x29\x07\x75\xD8\xDF\x6B\xFB\x21\xE4\xA4\x09\x17\xAF\xAC\xA0\xF5\xC0\xFE\x7B\x03\x04\x40\x41\x57\xC4\xFD\x58\x1D\x10\x5E\xAC\x23\xAB\xAA\x80\x95\x96\x02\x71\x84\x9C\x0A\xBD\x54\xC4\xA2\x47\xAA\xE7\xC3\x09\x13\x6E\x26\x7D\x72\xAA\xA9\x0B\xF3\xCC\xC4\x48\xB4\x97\x14\x00\x47\x2A\x6B\xD3\x93\x3F\xD8\xFD\xAA\xB9\xFB\xFB\xD5\x09\x8D\x82\x8B\xDE\x0F\xED\x39\x6D\x7B\xDC\x76\x8B\xA6\x4E\x9B\x7A\xBA', + signer['signature'].native + ) + + def test_parse_content_info_pkcs7_signed_data(self): + with open(os.path.join(fixtures_dir, 'pkcs7-signed.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + signed_data = info['content'] + encap_content_info = signed_data['encap_content_info'] + + self.assertEqual( + 'signed_data', + info['content_type'].native + ) + self.assertEqual( + 'v1', + signed_data['version'].native + ) + self.assertEqual( + [ + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]) + ], + signed_data['digest_algorithms'].native + ) + self.assertEqual( + 'data', + encap_content_info['content_type'].native + ) + self.assertEqual( + b'This is the message to encapsulate in PKCS#7/CMS\n', + encap_content_info['content'].native + ) + + self.assertEqual( + 1, + len(signed_data['certificates']) + ) + certificate = signed_data['certificates'][0] + with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: + self.assertEqual( + f.read(), + certificate.dump() + ) + + self.assertEqual( + 1, + len(signed_data['signer_infos']) + ) + signer = signed_data['signer_infos'][0] + + self.assertEqual( + 'v1', + signer['version'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'issuer', + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]) + ), + ( + 'serial_number', + 13683582341504654466 + ) + ]), + signer['sid'].native + ) + self.assertEqual( + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]), + signer['digest_algorithm'].native + ) + + signed_attrs = signer['signed_attrs'] + + self.assertEqual( + 4, + len(signed_attrs) + ) + self.assertEqual( + 'content_type', + signed_attrs[0]['type'].native + ) + self.assertEqual( + 'data', + signed_attrs[0]['values'][0].native + ) + self.assertEqual( + 'signing_time', + signed_attrs[1]['type'].native + ) + self.assertEqual( + datetime(2015, 6, 3, 5, 55, 12, tzinfo=core.timezone.utc), + signed_attrs[1]['values'][0].native + ) + self.assertEqual( + 'message_digest', + signed_attrs[2]['type'].native + ) + self.assertEqual( + b'\x52\x88\x25\x47\x15\x5B\x2D\x50\x44\x68\x05\x24\xC8\x71\x5A\xCC\x62\x28\x36\x17\xB7\x68\xEE\xA1\x12\x90\x96\x4F\x94\xAE\xDB\x79', + signed_attrs[2]['values'][0].native + ) + + self.assertEqual( + OrderedDict([ + ('algorithm', 'rsa'), + ('parameters', None), + ]), + signer['signature_algorithm'].native + ) + self.assertEqual( + b'\x43\x66\xEE\xF4\x6A\x02\x6F\xFE\x0D\xAE\xE6\xF3\x7A\x8F\x2C\x8E\x26\xB6\x25\x68\xEF\x5B\x4B\x4F\x9C\xE4\xE6\x71\x42\x22\xEC\x97\xFC\x53\xD9\xD6\x36\x1F\xA1\x32\x35\xFF\xA9\x95\x45\x50\x36\x36\x0C\x9A\x10\x6F\x06\xB6\x9D\x25\x10\x08\xF5\xF4\xE1\x68\x62\x60\xE5\xBF\xBD\xE2\x9F\xBD\x8A\x10\x29\x3B\xAF\xE7\xD6\x55\x7C\xEE\x3B\xFB\x93\x42\xE0\xB4\x4F\x89\xD0\x7B\x18\x51\x85\x90\x47\xF0\x5E\xE1\x15\x2C\xC1\x9A\xF1\x49\xE8\x11\x29\x17\x2E\x77\xD3\x35\x10\xAA\xCD\x32\x07\x32\x74\xCF\x2D\x89\xBD\xEF\xC7\xC9\xE7\xEC\x90\x44\xCE\x0B\xC5\x97\x00\x26\x67\x8A\x89\x5B\xFA\x46\xB2\x92\xD5\xCB\xA3\x52\x16\xDC\xF0\xF0\x79\xCB\x90\x93\x8E\x26\xB3\xEB\x8F\xBD\x54\x06\xD6\xB0\xA0\x04\x47\x7C\x63\xFC\x88\x5A\xE3\x81\xDF\x1E\x4D\x39\xFD\xF5\xA0\xE2\xD3\xAB\x13\xC1\xCF\x50\xB2\x0B\xC9\x36\xD6\xCB\xEA\x55\x39\x97\x8E\x34\x47\xE3\x6B\x44\x4A\x0E\x03\xAF\x41\xB2\x47\x2E\x26\xA3\x6B\x5F\xA1\x5C\x86\xA1\x96\x37\x02\xD3\x7C\x5F\xC1\xAF\x81\xE4\x1A\xD9\x87\x44\xB5\xB3\x5C\x45\x6C\xFF\x97\x4C\x3A\xB4\x2F\x5C\x2F\x86\x15\x51\x71\xA6\x27\x68', + signer['signature'].native + ) + + def test_parse_content_info_cms_signed_digested_data(self): + with open(os.path.join(fixtures_dir, 'cms-signed-digested.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + signed_data = info['content'] + encap_content_info = signed_data['encap_content_info'] + + self.assertEqual( + 'signed_data', + info['content_type'].native + ) + self.assertEqual( + 'v2', + signed_data['version'].native + ) + self.assertEqual( + [ + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]) + ], + signed_data['digest_algorithms'].native + ) + self.assertEqual( + 'digested_data', + encap_content_info['content_type'].native + ) + self.assertEqual( + OrderedDict([ + ('version', 'v0'), + ( + 'digest_algorithm', + OrderedDict([ + ('algorithm', 'sha1'), + ('parameters', None), + ]) + ), + ( + 'encap_content_info', + OrderedDict([ + ('content_type', 'data'), + ('content', b'This is the message to encapsulate in PKCS#7/CMS\n'), + ]) + ), + ( + 'digest', + b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB' + ) + ]), + encap_content_info['content'].native + ) + + self.assertEqual( + 1, + len(signed_data['certificates']) + ) + certificate = signed_data['certificates'][0] + with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: + self.assertEqual( + f.read(), + certificate.dump() + ) + + self.assertEqual( + 1, + len(signed_data['signer_infos']) + ) + signer = signed_data['signer_infos'][0] + + self.assertEqual( + 'v1', + signer['version'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'issuer', + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]) + ), + ( + 'serial_number', + 13683582341504654466 + ) + ]), + signer['sid'].native + ) + self.assertEqual( + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]), + signer['digest_algorithm'].native + ) + + signed_attrs = signer['signed_attrs'] + + self.assertEqual( + 0, + len(signed_attrs) + ) + + self.assertEqual( + OrderedDict([ + ('algorithm', 'rsa'), + ('parameters', None), + ]), + signer['signature_algorithm'].native + ) + self.assertEqual( + b'\x70\xBC\x18\x82\x41\xD6\xD8\xE7\x5C\xDC\x42\x27\xA5\xA8\xAA\x8B\x16\x15\x61\x3A\xE5\x47\x53\xFD\x8F\x45\xA3\x82\xE2\x72\x44\x07\xD1\xCB\xBF\xB4\x85\x4A\x2A\x16\x19\xDE\xDC\x53\x15\xCF\x98\xEE\x5C\x0E\xDF\xDE\xC8\x79\xCE\x2B\x38\x61\x36\xB0\xA1\xCB\x94\xD6\x4F\xCD\x83\xEF\x0C\xC9\x23\xA0\x7B\x8B\x65\x40\x5C\x3D\xA8\x3E\xCC\x0D\x1F\x17\x23\xF3\x74\x9F\x7E\x88\xF8\xF3\xBE\x4E\x19\x95\x0F\xEB\x95\x55\x69\xB4\xAA\xC3\x2A\x36\x03\x93\x1C\xDC\xE5\x65\x3F\x4E\x5E\x03\xC8\x56\xD8\x57\x8F\xE8\x2D\x85\x32\xDA\xFD\x79\xD4\xDD\x88\xCA\xA3\x14\x41\xE4\x3B\x03\x88\x0E\x2B\x76\xDC\x44\x3D\x4D\xFF\xB2\xC8\xC3\x83\xB1\x33\x37\x53\x51\x33\x4B\xCA\x1A\xAD\x7E\x6A\xBC\x61\x8B\x84\xDB\x7F\xCF\x61\xB2\x1D\x21\x83\xCF\xB8\x3F\xC6\x98\xED\xD8\x66\x06\xCF\x03\x30\x96\x9D\xB4\x7A\x16\xDF\x6E\xA7\x30\xEB\x77\xF7\x40\x13\xFB\xF2\xAC\x41\x79\x9D\xDC\xC0\xED\x4B\x8B\x19\xEE\x05\x3D\x61\x20\x39\x7E\x80\x1D\x3A\x23\x69\x48\x43\x60\x8B\x3E\x63\xAD\x01\x7A\xDE\x6F\x01\xBA\x51\xF3\x4B\x14\xBF\x6B\x77\x1A\x32\xC2\x0C\x93\xCC\x35\xBC\x66\xC6\x69', + signer['signature'].native + ) + + + def test_parse_content_info_pkcs7_signed_digested_data(self): + with open(os.path.join(fixtures_dir, 'pkcs7-signed-digested.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + signed_data = info['content'] + encap_content_info = signed_data['encap_content_info'] + + self.assertEqual( + 'signed_data', + info['content_type'].native + ) + self.assertEqual( + 'v1', + signed_data['version'].native + ) + self.assertEqual( + [ + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]) + ], + signed_data['digest_algorithms'].native + ) + self.assertEqual( + 'digested_data', + encap_content_info['content_type'].native + ) + self.assertEqual( + OrderedDict([ + ('version', 'v0'), + ( + 'digest_algorithm', + OrderedDict([ + ('algorithm', 'sha1'), + ('parameters', None), + ]) + ), + ( + 'encap_content_info', + OrderedDict([ + ('content_type', 'data'), + ('content', b'This is the message to encapsulate in PKCS#7/CMS\n'), + ]) + ), + ( + 'digest', + b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB' + ) + ]), + encap_content_info['content'].native + ) + + self.assertEqual( + 1, + len(signed_data['certificates']) + ) + certificate = signed_data['certificates'][0] + with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: + self.assertEqual( + f.read(), + certificate.dump() + ) + + self.assertEqual( + 1, + len(signed_data['signer_infos']) + ) + signer = signed_data['signer_infos'][0] + + self.assertEqual( + 'v1', + signer['version'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'issuer', + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]) + ), + ( + 'serial_number', + 13683582341504654466 + ) + ]), + signer['sid'].native + ) + self.assertEqual( + OrderedDict([ + ('algorithm', 'sha256'), + ('parameters', None), + ]), + signer['digest_algorithm'].native + ) + + signed_attrs = signer['signed_attrs'] + + self.assertEqual( + 0, + len(signed_attrs) + ) + + self.assertEqual( + OrderedDict([ + ('algorithm', 'rsa'), + ('parameters', None), + ]), + signer['signature_algorithm'].native + ) + self.assertEqual( + b'\x70\xBC\x18\x82\x41\xD6\xD8\xE7\x5C\xDC\x42\x27\xA5\xA8\xAA\x8B\x16\x15\x61\x3A\xE5\x47\x53\xFD\x8F\x45\xA3\x82\xE2\x72\x44\x07\xD1\xCB\xBF\xB4\x85\x4A\x2A\x16\x19\xDE\xDC\x53\x15\xCF\x98\xEE\x5C\x0E\xDF\xDE\xC8\x79\xCE\x2B\x38\x61\x36\xB0\xA1\xCB\x94\xD6\x4F\xCD\x83\xEF\x0C\xC9\x23\xA0\x7B\x8B\x65\x40\x5C\x3D\xA8\x3E\xCC\x0D\x1F\x17\x23\xF3\x74\x9F\x7E\x88\xF8\xF3\xBE\x4E\x19\x95\x0F\xEB\x95\x55\x69\xB4\xAA\xC3\x2A\x36\x03\x93\x1C\xDC\xE5\x65\x3F\x4E\x5E\x03\xC8\x56\xD8\x57\x8F\xE8\x2D\x85\x32\xDA\xFD\x79\xD4\xDD\x88\xCA\xA3\x14\x41\xE4\x3B\x03\x88\x0E\x2B\x76\xDC\x44\x3D\x4D\xFF\xB2\xC8\xC3\x83\xB1\x33\x37\x53\x51\x33\x4B\xCA\x1A\xAD\x7E\x6A\xBC\x61\x8B\x84\xDB\x7F\xCF\x61\xB2\x1D\x21\x83\xCF\xB8\x3F\xC6\x98\xED\xD8\x66\x06\xCF\x03\x30\x96\x9D\xB4\x7A\x16\xDF\x6E\xA7\x30\xEB\x77\xF7\x40\x13\xFB\xF2\xAC\x41\x79\x9D\xDC\xC0\xED\x4B\x8B\x19\xEE\x05\x3D\x61\x20\x39\x7E\x80\x1D\x3A\x23\x69\x48\x43\x60\x8B\x3E\x63\xAD\x01\x7A\xDE\x6F\x01\xBA\x51\xF3\x4B\x14\xBF\x6B\x77\x1A\x32\xC2\x0C\x93\xCC\x35\xBC\x66\xC6\x69', + signer['signature'].native + ) diff --git a/tests/test_crl.py b/tests/test_crl.py new file mode 100644 index 0000000..5367aa3 --- /dev/null +++ b/tests/test_crl.py @@ -0,0 +1,38 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import sys +import os + +from asn1crypto import crl + +if sys.version_info < (3,): + byte_cls = str + num_cls = long #pylint: disable=E0602 +else: + byte_cls = bytes + num_cls = int + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + +class CRLTests(unittest.TestCase): + + def test_parse_crl(self): + with open(os.path.join(fixtures_dir, 'eid2011.crl'), 'rb') as f: + cert_list = crl.CertificateList.load(f.read()) + serial_numbers = [] + for revoked_cert in cert_list['tbs_cert_list']['revoked_certificates']: + serial_numbers.append(revoked_cert['user_certificate'].native) + self.assertEqual( + 15752, + len(serial_numbers) + ) + for serial_number in serial_numbers: + self.assertIsInstance( + serial_number, + num_cls + ) diff --git a/tests/test_keys.py b/tests/test_keys.py new file mode 100644 index 0000000..1d02a3f --- /dev/null +++ b/tests/test_keys.py @@ -0,0 +1,283 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import os +from collections import OrderedDict + +from asn1crypto import keys, core + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + + +class KeysTests(unittest.TestCase): + + def test_parse_rsa_private_key(self): + with open(os.path.join(fixtures_dir, 'keys/test-der.key'), 'rb') as f: + key = keys.RSAPrivateKey.load(f.read()) + + self.assertEqual( + 'two-prime', + key['version'].native + ) + self.assertEqual( + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + key['modulus'].native + ) + self.assertEqual( + 65537, + key['public_exponent'].native + ) + self.assertEqual( + 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, + key['private_exponent'].native + ) + self.assertEqual( + 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, + key['prime1'].native + ) + self.assertEqual( + 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, + key['prime2'].native + ) + self.assertEqual( + 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, + key['exponent1'].native + ) + self.assertEqual( + 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, + key['exponent2'].native + ) + self.assertEqual( + 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, + key['coefficient'].native + ) + self.assertEqual( + None, + key['other_prime_infos'].native + ) + + def test_parse_rsa_private_key_no_spec(self): + with open(os.path.join(fixtures_dir, 'keys/test-der.key'), 'rb') as f: + key = core.Asn1Value.load(f.read()) + + self.assertEqual( + 0, + key[0].native + ) + self.assertEqual( + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + key[1].native + ) + self.assertEqual( + 65537, + key[2].native + ) + self.assertEqual( + 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, + key[3].native + ) + self.assertEqual( + 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, + key[4].native + ) + self.assertEqual( + 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, + key[5].native + ) + self.assertEqual( + 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, + key[6].native + ) + self.assertEqual( + 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, + key[7].native + ) + self.assertEqual( + 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, + key[8].native + ) + + with self.assertRaises(KeyError): + key[9].native #pylint: disable=W0104 + + def test_parse_dsa_private_key(self): + with open(os.path.join(fixtures_dir, 'keys/test-dsa-der.key'), 'rb') as f: + key = keys.DSAPrivateKey.load(f.read()) + + self.assertEqual( + 0, + key['version'].native + ) + self.assertEqual( + 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857, + key['p'].native + ) + self.assertEqual( + 71587850165936478337655415373676526523562874562337607790945426056266440596923, + key['q'].native + ) + self.assertEqual( + 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202, + key['g'].native + ) + self.assertEqual( + 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, + key['public_key'].native + ) + self.assertEqual( + 67419307522580891944110478232775481982040250615628832761657973309422062357004, + key['private_key'].native + ) + + def test_parse_ec_private_key(self): + with open(os.path.join(fixtures_dir, 'keys/test-ec-der.key'), 'rb') as f: + key = keys.ECPrivateKey.load(f.read()) + + self.assertEqual( + 'ecPrivkeyVer1', + key['version'].native + ) + self.assertEqual( + 105342176757643535635985202437872662036661123763048203788770333621775587689309, + key['private_key'].native + ) + self.assertEqual( + OrderedDict([ + ('version', 'ecdpVer1'), + ( + 'field_id', + OrderedDict([ + ('field_type', 'prime_field'), + ('parameters', 115792089210356248762697446949407573530086143415290314195533631308867097853951) + ]) + ), + ( + 'curve', + OrderedDict([ + ('a', b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC'), + ('b', b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B'), + ('seed', b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90'), + ]) + ), + ( + 'base', + b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5' + ), + ( + 'order', + 115792089210356248762697446949407573529996955224135760342422259061068512044369 + ), + ('cofactor', 1), + ('hash', None), + ]), + key['parameters'].native + ) + self.assertEqual( + 60930340107159415611969123187806310044445787587918330601145416939590630530757526035521042297997626933274194649938002969225935746493465239160856198904766465, + key['public_key'].native + ) + + def test_parse_rsa_public_key(self): + with open(os.path.join(fixtures_dir, 'keys/test-public-rsa-der.key'), 'rb') as f: + key = keys.RSAPublicKey.load(f.read()) + + self.assertEqual( + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + key['modulus'].native + ) + self.assertEqual( + 65537, + key['public_exponent'].native + ) + + def test_parse_public_key_info(self): + with open(os.path.join(fixtures_dir, 'keys/test-public-der.key'), 'rb') as f: + key = keys.PublicKeyInfo.load(f.read()) + + public_key = key['public_key'].parsed + + self.assertEqual( + 'rsa', + key['algorithm']['algorithm'].native + ) + self.assertEqual( + None, + key['algorithm']['parameters'].native + ) + self.assertEqual( + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + public_key['modulus'].native + ) + self.assertEqual( + 65537, + public_key['public_exponent'].native + ) + + def test_parse_pkcs8_private_key(self): + with open(os.path.join(fixtures_dir, 'keys/test-pkcs8-der.key'), 'rb') as f: + key_info = keys.PrivateKeyInfo.load(f.read()) + + key = key_info['private_key'].parsed + + self.assertEqual( + 0, + key_info['version'].native + ) + self.assertEqual( + 'rsa', + key_info['private_key_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + key_info['private_key_algorithm']['parameters'].native + ) + + self.assertEqual( + 'two-prime', + key['version'].native + ) + self.assertEqual( + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + key['modulus'].native + ) + self.assertEqual( + 65537, + key['public_exponent'].native + ) + self.assertEqual( + 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, + key['private_exponent'].native + ) + self.assertEqual( + 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, + key['prime1'].native + ) + self.assertEqual( + 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, + key['prime2'].native + ) + self.assertEqual( + 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, + key['exponent1'].native + ) + self.assertEqual( + 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, + key['exponent2'].native + ) + self.assertEqual( + 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, + key['coefficient'].native + ) + self.assertEqual( + None, + key['other_prime_infos'].native + ) + + self.assertEqual( + None, + key_info['attributes'].native + ) diff --git a/tests/test_ocsp.py b/tests/test_ocsp.py new file mode 100644 index 0000000..62240d1 --- /dev/null +++ b/tests/test_ocsp.py @@ -0,0 +1,153 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import sys +import os +from datetime import datetime + +from asn1crypto import ocsp, core + +if sys.version_info < (3,): + byte_cls = str +else: + byte_cls = bytes + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + + +class OCSPTests(unittest.TestCase): + + def test_parse_request(self): + with open(os.path.join(fixtures_dir, 'ocsp_request'), 'rb') as f: + request = ocsp.OCSPRequest.load(f.read()) + + tbs_request = request['tbs_request'] + request_list = tbs_request['request_list'] + single_request = request_list[0] + req_cert = single_request['req_cert'] + + self.assertEqual( + 'v1', + tbs_request['version'].native + ) + self.assertEqual( + None, + tbs_request['requestor_name'].native + ) + self.assertEqual( + 'sha1', + req_cert['hash_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + req_cert['hash_algorithm']['parameters'].native + ) + self.assertEqual( + b'\xAA\x2B\x03\x14\xAF\x64\x2E\x13\x0E\xD6\x92\x25\xE3\xFF\x2A\xBA\xD7\x3D\x62\x30', + req_cert['issuer_name_hash'].native + ) + self.assertEqual( + b'\xDE\xCF\x5C\x50\xB7\xAE\x02\x1F\x15\x17\xAA\x16\xE8\x0D\xB5\x28\x9D\x6A\x5A\xF3', + req_cert['issuer_key_hash'].native + ) + self.assertEqual( + 130338219198307073574879940486642352162, + req_cert['serial_number'].native + ) + + def test_parse_response(self): + with open(os.path.join(fixtures_dir, 'ocsp_response'), 'rb') as f: + response = ocsp.OCSPResponse.load(f.read()) + + response_bytes = response['response_bytes'] + basic_ocsp_response = response_bytes['response'].parsed + tbs_response_data = basic_ocsp_response['tbs_response_data'] + responder_id = tbs_response_data['responder_id'] + single_response = tbs_response_data['responses'][0] + cert_id = single_response['cert_id'] + cert = basic_ocsp_response['certs'][0] + + self.assertEqual( + 'successful', + response['response_status'].native + ) + self.assertEqual( + 'basic_ocsp_response', + response_bytes['response_type'].native + ) + self.assertEqual( + 'sha1_rsa', + basic_ocsp_response['signature_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + basic_ocsp_response['signature_algorithm']['parameters'].native + ) + self.assertEqual( + 'v1', + tbs_response_data['version'].native + ) + self.assertEqual( + b'\x4E\xC5\x63\xD6\xB2\x05\x05\xD7\x76\xF0\x07\xED\xAC\x7D\x5A\x56\x97\x7B\xBD\x3C', + responder_id.native + ) + self.assertEqual( + 'by_key', + responder_id.name + ) + self.assertEqual( + datetime(2015, 5, 22, 16, 24, 8, tzinfo=core.timezone.utc), + tbs_response_data['produced_at'].native + ) + self.assertEqual( + 'sha1', + cert_id['hash_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + cert_id['hash_algorithm']['parameters'].native + ) + self.assertEqual( + b'\xAA\x2B\x03\x14\xAF\x64\x2E\x13\x0E\xD6\x92\x25\xE3\xFF\x2A\xBA\xD7\x3D\x62\x30', + cert_id['issuer_name_hash'].native + ) + self.assertEqual( + b'\xDE\xCF\x5C\x50\xB7\xAE\x02\x1F\x15\x17\xAA\x16\xE8\x0D\xB5\x28\x9D\x6A\x5A\xF3', + cert_id['issuer_key_hash'].native + ) + self.assertEqual( + 130338219198307073574879940486642352162, + cert_id['serial_number'].native + ) + self.assertEqual( + datetime(2015, 5, 22, 16, 24, 8, tzinfo=core.timezone.utc), + single_response['this_update'].native + ) + self.assertEqual( + datetime(2015, 5, 29, 16, 24, 8, tzinfo=core.timezone.utc), + single_response['next_update'].native + ) + self.assertEqual( + None, + single_response['single_extensions'].native + ) + self.assertEqual( + None, + tbs_response_data['response_extensions'].native + ) + self.assertIsInstance( + basic_ocsp_response['certs'].native, + list + ) + self.assertEqual( + 1, + len(basic_ocsp_response['certs']) + ) + self.assertEqual( + 'v3', + cert['tbs_certificate']['version'].native + ) diff --git a/tests/test_tsa.py b/tests/test_tsa.py new file mode 100644 index 0000000..813592d --- /dev/null +++ b/tests/test_tsa.py @@ -0,0 +1,242 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import os +from datetime import datetime +from collections import OrderedDict + +from asn1crypto import tsa, cms, core + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + + +class TSATests(unittest.TestCase): + + def test_parse_request(self): + with open(os.path.join(fixtures_dir, 'tsa_request'), 'rb') as f: + request = tsa.TimeStampReq.load(f.read()) + + self.assertEqual( + 'v1', + request['version'].native + ) + self.assertEqual( + 'sha1', + request['message_imprint']['hash_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + request['message_imprint']['hash_algorithm']['parameters'].native + ) + self.assertEqual( + b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB', + request['message_imprint']['hashed_message'].native + ) + self.assertEqual( + 17842879675353045770, + request['nonce'].native + ) + + def test_parse_response(self): + with open(os.path.join(fixtures_dir, 'tsa_response'), 'rb') as f: + response = tsa.TimeStampResp.load(f.read()) + + status_info = response['status'] + token = response['time_stamp_token'] + signed_data = token['content'] + encap_content_info = signed_data['encap_content_info'] + tst_info = encap_content_info['content'].parsed + signer_infos = signed_data['signer_infos'] + signer_info = signer_infos[0] + signed_attrs = signer_info['signed_attrs'] + + self.assertEqual( + 'granted', + status_info['status'].native + ) + self.assertEqual( + None, + status_info['status_string'].native + ) + self.assertEqual( + None, + status_info['fail_info'].native + ) + self.assertEqual( + 'signed_data', + token['content_type'].native + ) + self.assertIsInstance( + signed_data, + cms.SignedData + ) + self.assertEqual( + 'v3', + signed_data['version'].native + ) + self.assertEqual( + 'sha1', + signed_data['digest_algorithms'][0]['algorithm'].native + ) + self.assertEqual( + 'tst_info', + encap_content_info['content_type'].native + ) + self.assertIsInstance( + tst_info, + tsa.TSTInfo + ) + self.assertEqual( + 'v1', + tst_info['version'].native + ) + self.assertEqual( + '1.1.2', + tst_info['policy'].native + ) + self.assertEqual( + 'sha1', + tst_info['message_imprint']['hash_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + tst_info['message_imprint']['hash_algorithm']['parameters'].native + ) + self.assertEqual( + b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB', + tst_info['message_imprint']['hashed_message'].native + ) + self.assertEqual( + 544918635, + tst_info['serial_number'].native + ) + self.assertEqual( + datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), + tst_info['gen_time'].native + ) + self.assertEqual( + 60, + tst_info['accuracy']['seconds'].native + ) + self.assertEqual( + None, + tst_info['accuracy']['millis'].native + ) + self.assertEqual( + None, + tst_info['accuracy']['micros'].native + ) + self.assertEqual( + False, + tst_info['ordering'].native + ) + self.assertEqual( + 17842879675353045770, + tst_info['nonce'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('organization_name', 'GeoTrust Inc'), + ('common_name', 'GeoTrust Timestamping Signer 1'), + ]), + tst_info['tsa'].native + ) + self.assertEqual( + None, + tst_info['extensions'].native + ) + self.assertEqual( + None, + signed_data['certificates'].native + ) + self.assertEqual( + None, + signed_data['crls'].native + ) + self.assertEqual( + 1, + len(signer_infos) + ) + self.assertEqual( + 'v1', + signer_info['version'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'issuer', + OrderedDict([ + ('country_name', 'ZA'), + ('state_or_province_name', 'Western Cape'), + ('locality_name', 'Durbanville'), + ('organization_name', 'Thawte'), + ('organizational_unit_name', 'Thawte Certification'), + ('common_name', 'Thawte Timestamping CA'), + ]) + ), + ( + 'serial_number', + 125680471847352264461591953321128732863 + ) + ]), + signer_info['sid'].native + ) + self.assertEqual( + 'sha1', + signer_info['digest_algorithm']['algorithm'].native + ) + self.assertEqual( + 4, + len(signed_attrs) + ) + self.assertEqual( + 'content_type', + signed_attrs[0]['type'].native + ) + self.assertEqual( + 'tst_info', + signed_attrs[0]['values'][0].native + ) + self.assertEqual( + 'signing_time', + signed_attrs[1]['type'].native + ) + self.assertEqual( + datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), + signed_attrs[1]['values'][0].native + ) + self.assertEqual( + 'message_digest', + signed_attrs[2]['type'].native + ) + self.assertEqual( + b'\x22\x06\x7D\xA4\xFC\x7B\xC5\x94\x80\xB4\xB0\x78\xC2\x07\x66\x02\xA3\x0D\x62\xAE', + signed_attrs[2]['values'][0].native + ) + self.assertEqual( + 'signing_certificate', + signed_attrs[3]['type'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'certs', + [ + OrderedDict([ + ('cert_hash', b'\x22\x3C\xDA\x27\x07\x96\x73\x81\x6B\x60\x8A\x1B\x8C\xB0\xAB\x02\x30\x10\x7F\xCC'), + ('issuer_serial', None), + ]) + ] + ), + ( + 'policies', + None + ) + ]), + signed_attrs[3]['values'][0].native + ) diff --git a/tests/test_x509.py b/tests/test_x509.py new file mode 100644 index 0000000..30606e8 --- /dev/null +++ b/tests/test_x509.py @@ -0,0 +1,476 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import sys +import os +from collections import OrderedDict +from datetime import datetime + +from asn1crypto import x509, core + +if sys.version_info < (3,): + byte_cls = str +else: + byte_cls = bytes + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + + +class X509Tests(unittest.TestCase): + + def test_parse_certificate(self): + with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + tbs_certificate = cert['tbs_certificate'] + signature = tbs_certificate['signature'] + issuer = tbs_certificate['issuer'] + validity = tbs_certificate['validity'] + subject = tbs_certificate['subject'] + subject_public_key_info = tbs_certificate['subject_public_key_info'] + subject_public_key_algorithm = subject_public_key_info['algorithm'] + subject_public_key = subject_public_key_info['public_key'].parsed + extensions = tbs_certificate['extensions'] + + self.assertEqual( + 'v3', + tbs_certificate['version'].native + ) + self.assertEqual( + 13683582341504654466, + tbs_certificate['serial_number'].native + ) + self.assertEqual( + 'sha256_rsa', + signature['algorithm'].native + ) + self.assertEqual( + None, + signature['parameters'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + issuer.native + ) + self.assertEqual( + datetime(2015, 5, 6, 14, 37, 16, tzinfo=core.timezone.utc), + validity['not_before'].native + ) + self.assertEqual( + datetime(2025, 5, 3, 14, 37, 16, tzinfo=core.timezone.utc), + validity['not_after'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + subject.native + ) + self.assertEqual( + 'rsa', + subject_public_key_algorithm['algorithm'].native + ) + self.assertEqual( + None, + subject_public_key_algorithm['parameters'].native + ) + self.assertEqual( + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + subject_public_key['modulus'].native + ) + self.assertEqual( + 65537, + subject_public_key['public_exponent'].native + ) + self.assertEqual( + None, + tbs_certificate['issuer_unique_id'].native + ) + self.assertIsInstance( + tbs_certificate['issuer_unique_id'], + core.NoValue + ) + self.assertEqual( + None, + tbs_certificate['subject_unique_id'].native + ) + self.assertIsInstance( + tbs_certificate['subject_unique_id'], + core.NoValue + ) + + self.maxDiff = None + for extension in extensions: + self.assertIsInstance( + extension, + x509.Extension + ) + self.assertEqual( + [ + OrderedDict([ + ('extn_id', 'key_identifier'), + ('critical', False), + ('extn_value', b'\xBE\x42\x85\x3D\xCC\xFF\xE3\xF9\x28\x02\x8F\x7E\x58\x56\xB4\xFD\x03\x5C\xEA\x4B'), + ]), + OrderedDict([ + ('extn_id', 'authority_key_identifier'), + ('critical', False), + ( + 'extn_value', + OrderedDict([ + ('key_identifier', b'\xBE\x42\x85\x3D\xCC\xFF\xE3\xF9\x28\x02\x8F\x7E\x58\x56\xB4\xFD\x03\x5C\xEA\x4B'), + ( + 'authority_cert_issuer', + [ + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]) + ] + ), + ('authority_cert_serial_number', 13683582341504654466), + ]) + ), + ]), + OrderedDict([ + ('extn_id', 'basic_constraints'), + ('critical', False), + ( + 'extn_value', + OrderedDict([ + ('ca', True), + ('path_len_constraint', None) + ]) + ), + ]), + ], + extensions.native + ) + + def test_parse_dsa_certificate(self): + with open(os.path.join(fixtures_dir, 'keys/test-dsa-der.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + tbs_certificate = cert['tbs_certificate'] + signature = tbs_certificate['signature'] + issuer = tbs_certificate['issuer'] + validity = tbs_certificate['validity'] + subject = tbs_certificate['subject'] + subject_public_key_info = tbs_certificate['subject_public_key_info'] + subject_public_key_algorithm = subject_public_key_info['algorithm'] + subject_public_key = subject_public_key_info['public_key'].parsed + extensions = tbs_certificate['extensions'] + + self.assertEqual( + 'v3', + tbs_certificate['version'].native + ) + self.assertEqual( + 14308214745771946523, + tbs_certificate['serial_number'].native + ) + self.assertEqual( + 'sha256_dsa', + signature['algorithm'].native + ) + self.assertEqual( + None, + signature['parameters'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + issuer.native + ) + self.assertEqual( + datetime(2015, 5, 20, 13, 9, 2, tzinfo=core.timezone.utc), + validity['not_before'].native + ) + self.assertEqual( + datetime(2025, 5, 17, 13, 9, 2, tzinfo=core.timezone.utc), + validity['not_after'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + subject.native + ) + self.assertEqual( + 'dsa', + subject_public_key_algorithm['algorithm'].native + ) + self.assertEqual( + OrderedDict([ + ('p', 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857), + ('q', 71587850165936478337655415373676526523562874562337607790945426056266440596923), + ('g', 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202), + ]), + subject_public_key_algorithm['parameters'].native + ) + self.assertEqual( + 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, + subject_public_key.native + ) + self.assertEqual( + None, + tbs_certificate['issuer_unique_id'].native + ) + self.assertIsInstance( + tbs_certificate['issuer_unique_id'], + core.NoValue + ) + self.assertEqual( + None, + tbs_certificate['subject_unique_id'].native + ) + self.assertIsInstance( + tbs_certificate['subject_unique_id'], + core.NoValue + ) + + self.maxDiff = None + for extension in extensions: + self.assertIsInstance( + extension, + x509.Extension + ) + self.assertEqual( + [ + OrderedDict([ + ('extn_id', 'key_identifier'), + ('critical', False), + ('extn_value', b'\x81\xA3\x37\x86\xF9\x99\x28\xF2\x74\x70\x60\x87\xF2\xD3\x7E\x8D\x19\x61\xA8\xBE'), + ]), + OrderedDict([ + ('extn_id', 'authority_key_identifier'), + ('critical', False), + ( + 'extn_value', + OrderedDict([ + ('key_identifier', b'\x81\xA3\x37\x86\xF9\x99\x28\xF2\x74\x70\x60\x87\xF2\xD3\x7E\x8D\x19\x61\xA8\xBE'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None), + ]) + ), + ]), + OrderedDict([ + ('extn_id', 'basic_constraints'), + ('critical', False), + ( + 'extn_value', + OrderedDict([ + ('ca', True), + ('path_len_constraint', None) + ]) + ), + ]), + ], + extensions.native + ) + + def test_parse_ec_certificate(self): + with open(os.path.join(fixtures_dir, 'keys/test-ec-der.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + tbs_certificate = cert['tbs_certificate'] + signature = tbs_certificate['signature'] + issuer = tbs_certificate['issuer'] + validity = tbs_certificate['validity'] + subject = tbs_certificate['subject'] + subject_public_key_info = tbs_certificate['subject_public_key_info'] + subject_public_key_algorithm = subject_public_key_info['algorithm'] + public_key_params = subject_public_key_info['algorithm']['parameters'].chosen + field_id = public_key_params['field_id'] + curve = public_key_params['curve'] + subject_public_key = subject_public_key_info['public_key'].parsed + extensions = tbs_certificate['extensions'] + + self.assertEqual( + 'v3', + tbs_certificate['version'].native + ) + self.assertEqual( + 15854128451240978884, + tbs_certificate['serial_number'].native + ) + self.assertEqual( + 'sha256_ecdsa', + signature['algorithm'].native + ) + self.assertEqual( + None, + signature['parameters'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + issuer.native + ) + self.assertEqual( + datetime(2015, 5, 20, 12, 56, 46, tzinfo=core.timezone.utc), + validity['not_before'].native + ) + self.assertEqual( + datetime(2025, 5, 17, 12, 56, 46, tzinfo=core.timezone.utc), + validity['not_after'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + subject.native + ) + self.assertEqual( + 'ecdsa', + subject_public_key_algorithm['algorithm'].native + ) + self.assertEqual( + 'ecdpVer1', + public_key_params['version'].native + ) + self.assertEqual( + 'prime_field', + field_id['field_type'].native + ) + self.assertEqual( + 115792089210356248762697446949407573530086143415290314195533631308867097853951, + field_id['parameters'].native + ) + self.assertEqual( + b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC', + curve['a'].native + ) + self.assertEqual( + b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B', + curve['b'].native + ) + self.assertEqual( + b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90', + curve['seed'].native + ) + self.assertEqual( + b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5', + public_key_params['base'].native + ) + self.assertEqual( + 115792089210356248762697446949407573529996955224135760342422259061068512044369, + public_key_params['order'].native + ) + self.assertEqual( + 1, + public_key_params['cofactor'].native + ) + self.assertEqual( + None, + public_key_params['hash'].native + ) + self.assertEqual( + b'G\x9f\xcbs$\x1d\xc9\xdd\xd1-\xf1:\x9f\xb7\x04\xde \xd0X\x00\x93T\xf6\x89\xc7/\x87+\xf7\xf9=;4\xed\x9e{\x0e=WB\xdfx\x03\x0b\xcc1\xc6\x03\xd7\x9f`\x01', + subject_public_key.native + ) + self.assertEqual( + None, + tbs_certificate['issuer_unique_id'].native + ) + self.assertIsInstance( + tbs_certificate['issuer_unique_id'], + core.NoValue + ) + self.assertEqual( + None, + tbs_certificate['subject_unique_id'].native + ) + self.assertIsInstance( + tbs_certificate['subject_unique_id'], + core.NoValue + ) + + self.maxDiff = None + for extension in extensions: + self.assertIsInstance( + extension, + x509.Extension + ) + self.assertEqual( + [ + OrderedDict([ + ('extn_id', 'key_identifier'), + ('critical', False), + ('extn_value', b'\x54\xAA\x54\x70\x6C\x34\x1A\x6D\xEB\x5D\x97\xD7\x1E\xFC\xD5\x24\x3C\x8A\x0E\xD7'), + ]), + OrderedDict([ + ('extn_id', 'authority_key_identifier'), + ('critical', False), + ( + 'extn_value', + OrderedDict([ + ('key_identifier', b'\x54\xAA\x54\x70\x6C\x34\x1A\x6D\xEB\x5D\x97\xD7\x1E\xFC\xD5\x24\x3C\x8A\x0E\xD7'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None), + ]) + ), + ]), + OrderedDict([ + ('extn_id', 'basic_constraints'), + ('critical', False), + ( + 'extn_value', + OrderedDict([ + ('ca', True), + ('path_len_constraint', None) + ]) + ), + ]), + ], + extensions.native + ) -- cgit v1.2.3 From b9b7597b6f4f770bc7c4045236123aa28a402e2c Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 09:09:17 -0400 Subject: Docs, package infrastructure --- .gitignore | 1 + LICENSE | 19 ++ asn1crypto/__init__.py | 1 + asn1crypto/algos.py | 2 + asn1crypto/core.py | 15 +- asn1crypto/pkcs12.py | 40 +++- asn1crypto/tsa.py | 290 ----------------------- asn1crypto/tsp.py | 290 +++++++++++++++++++++++ changelog.md | 5 + docs/readme.md | 19 ++ docs/tutorial.md | 547 ++++++++++++++++++++++++++++++++++++++++++++ readme.md | 108 +++++++++ setup.py | 34 +++ tests.py | 4 +- tests/fixtures/tsa_request | Bin 51 -> 0 bytes tests/fixtures/tsa_response | Bin 711 -> 0 bytes tests/fixtures/tsp_request | Bin 0 -> 51 bytes tests/fixtures/tsp_response | Bin 0 -> 711 bytes tests/test_tsa.py | 242 -------------------- tests/test_tsp.py | 242 ++++++++++++++++++++ 20 files changed, 1315 insertions(+), 544 deletions(-) create mode 100644 LICENSE delete mode 100644 asn1crypto/tsa.py create mode 100644 asn1crypto/tsp.py create mode 100644 changelog.md create mode 100644 docs/readme.md create mode 100644 docs/tutorial.md create mode 100644 readme.md create mode 100644 setup.py delete mode 100644 tests/fixtures/tsa_request delete mode 100644 tests/fixtures/tsa_response create mode 100644 tests/fixtures/tsp_request create mode 100644 tests/fixtures/tsp_response delete mode 100644 tests/test_tsa.py create mode 100644 tests/test_tsp.py diff --git a/.gitignore b/.gitignore index 0f01e60..2ad65e9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build/ dist/ tests/output/ .DS_Store +tmp/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6352e34 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Will Bond + +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. diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index e69de29..e4e49b3 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -0,0 +1 @@ +__version__ = '0.9.0' diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 78dbef3..cd5e120 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -44,6 +44,8 @@ class DigestAlgorithmId(ObjectIdentifier): '2.16.840.1.101.3.4.2.1': 'sha256', '2.16.840.1.101.3.4.2.2': 'sha384', '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', } diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ce3edeb..b8bbcef 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2,13 +2,15 @@ from __future__ import unicode_literals from __future__ import absolute_import +import sys import re from collections import OrderedDict from datetime import datetime, timedelta, tzinfo from . import teletex_codec -try: +# Python 2 +if sys.version_info <= (3,): str_cls = unicode #pylint: disable=E0602 byte_cls = str py2 = True @@ -53,7 +55,8 @@ try: utc = utc() -except (NameError): +# Python 3 +else: str_cls = str byte_cls = bytes py2 = False @@ -859,8 +862,8 @@ class BitString(Primitive, ValueMap, object): value = ''.join(map(str_cls, value)) elif isinstance(value, int): - self._native = tuple(map(int, tuple(value))) value = '{0:b}'.format(value) + self._native = tuple(map(int, tuple(value))) size = max(self._map.keys()) + 1 if len(value) != size: @@ -2250,18 +2253,12 @@ class UTCTime(AbstractTime): strlen = len(string) - if strlen == 8: - return datetime.strptime(string, '%y%m%d%H') - if strlen == 10: return datetime.strptime(string, '%y%m%d%H%M') if strlen == 12: return datetime.strptime(string, '%y%m%d%H%M%S') - if strlen == 16: - return datetime.strptime(string, '%y%m%d%H%M%S.%f') - return string class GeneralizedTime(AbstractTime): diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 22430c1..17fb1b9 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -5,15 +5,17 @@ from __future__ import absolute_import from .algos import DigestInfo from .core import ( Any, + BMPString, Integer, ObjectIdentifier, OctetString, Sequence, SequenceOf, + SetOf, ) from .cms import ContentInfo, SignedData from .keys import PrivateKeyInfo, EncryptedPrivateKeyInfo -from .x509 import Attributes, Certificate +from .x509 import Certificate @@ -33,6 +35,42 @@ class Version(Integer): } +class AttributeType(ObjectIdentifier): + _map = { + # https://tools.ietf.org/html/rfc2985#page-18 + '1.2.840.113549.1.9.20': 'friendly_name', + '1.2.840.113549.1.9.21': 'local_key_id', + # https://support.microsoft.com/en-us/kb/287547 + '1.3.6.1.4.1.311.17.1': 'microsoft_local_machine_keyset', + } + + +class SetOfBMPString(SetOf): + _child_spec = BMPString + + +class SetOfOctetString(SetOf): + _child_spec = OctetString + + +class Attribute(Sequence): + _fields = [ + ('type', AttributeType), + ('values', SetOf, {'spec': Any}), + ] + + _oid_pair = ('type', 'values') + _oid_specs = { + 'friendly_name': SetOfBMPString, + 'local_key_id': SetOfOctetString, + 'microsoft_csp_name': SetOfBMPString, + } + + +class Attributes(SequenceOf): + _child_spec = Attribute + + class Pfx(Sequence): _fields = [ ('version', Version), diff --git a/asn1crypto/tsa.py b/asn1crypto/tsa.py deleted file mode 100644 index 61fa7ca..0000000 --- a/asn1crypto/tsa.py +++ /dev/null @@ -1,290 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals -from __future__ import absolute_import - -from .algos import DigestAlgorithm -from .core import ( - Any, - BitString, - Boolean, - Choice, - GeneralizedTime, - IA5String, - Integer, - ObjectIdentifier, - OctetString, - Sequence, - SequenceOf, - SetOf, - UTF8String, -) -from .cms import ( - CMSAttribute, - CMSAttributeType, - ContentInfo, - ContentType, - EncapsulatedContentInfo, -) -from .crl import CertificateList -from .x509 import Attributes, CertificatePolicies, GeneralName, GeneralNames - - -# The structures in this file are based on https://tools.ietf.org/html/rfc3161, -# https://tools.ietf.org/html/rfc4998, https://tools.ietf.org/html/rfc5544, -# https://tools.ietf.org/html/rfc5035, https://tools.ietf.org/html/rfc2634 - -class Version(Integer): - _map = { - 0: 'v0', - 1: 'v1', - 2: 'v2', - 3: 'v3', - 4: 'v4', - 5: 'v5', - } - - -class MessageImprint(Sequence): - _fields = [ - ('hash_algorithm', DigestAlgorithm), - ('hashed_message', OctetString), - ] - - -class Accuracy(Sequence): - _fields = [ - ('seconds', Integer, {'optional': True}), - ('millis', Integer, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('micros', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ] - - -class Extension(Sequence): - _fields = [ - ('extn_id', ObjectIdentifier), - ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), - ] - - -class Extensions(SequenceOf): - _child_spec = Extension - - -class TSTInfo(Sequence): - _fields = [ - ('version', Version), - ('policy', ObjectIdentifier), - ('message_imprint', MessageImprint), - ('serial_number', Integer), - ('gen_time', GeneralizedTime), - ('accuracy', Accuracy, {'optional': True}), - ('ordering', Boolean, {'default': False}), - ('nonce', Integer, {'optional': True}), - ('tsa', GeneralName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ] - - -class TimeStampReq(Sequence): - _fields = [ - ('version', Version), - ('message_imprint', MessageImprint), - ('req_policy', ObjectIdentifier, {'optional': True}), - ('nonce', Integer, {'optional': True}), - ('cert_req', Boolean, {'default': False}), - ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ] - - -class PKIStatus(Integer): - _map = { - 0: 'granted', - 1: 'granted_with_mods', - 2: 'rejection', - 3: 'waiting', - 4: 'revocation_warning', - 5: 'revocation_notification', - } - - -class PKIFreeText(SequenceOf): - _child_spec = UTF8String - - -class PKIFailureInfo(BitString): - _map = { - 0: 'bad_alg', - 2: 'bad_request', - 5: 'bad_data_format', - 14: 'time_not_available', - 15: 'unaccepted_policy', - 16: 'unaccepted_extensions', - 17: 'add_info_not_available', - 25: 'system_failure', - } - - -class PKIStatusInfo(Sequence): - _fields = [ - ('status', PKIStatus), - ('status_string', PKIFreeText, {'optional': True}), - ('fail_info', PKIFailureInfo, {'optional': True}), - ] - - -class TimeStampResp(Sequence): - _fields = [ - ('status', PKIStatusInfo), - ('time_stamp_token', ContentInfo), - ] - - -class MetaData(Sequence): - _fields = [ - ('hash_protected', Boolean), - ('file_name', UTF8String, {'optional': True}), - ('media_type', IA5String, {'optional': True}), - ('other_meta_data', Attributes, {'optional': True}), - ] - - -class TimeStampAndCRL(SequenceOf): - _fields = [ - ('time_stamp', EncapsulatedContentInfo), - ('crl', CertificateList, {'optional': True}), - ] - - -class TimeStampTokenEvidence(SequenceOf): - _child_spec = TimeStampAndCRL - - -class DigestAlgorithms(SequenceOf): - _child_spec = DigestAlgorithm - - -class EncryptionInfo(Sequence): - _fields = [ - ('encryption_info_type', ObjectIdentifier), - ('encryption_info_value', Any), - ] - - -class PartialHashtree(SequenceOf): - _child_spec = OctetString - - -class PartialHashtrees(SequenceOf): - _child_spec = PartialHashtree - - -class ArchiveTimeStamp(Sequence): - _fields = [ - ('digest_algorithm', DigestAlgorithm, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('attributes', Attributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('reduced_hashtree', PartialHashtrees, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), - ('time_stamp', ContentInfo), - ] - - -class ArchiveTimeStampSequence(SequenceOf): - _child_spec = ArchiveTimeStamp - - -class EvidenceRecord(Sequence): - _fields = [ - ('version', Version), - ('digest_algorithms', DigestAlgorithms), - ('crypto_infos', Attributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('encryption_info', EncryptionInfo, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('archive_time_stamp_sequence', ArchiveTimeStampSequence), - ] - - -class OtherEvidence(Sequence): - _fields = [ - ('oe_type', ObjectIdentifier), - ('oe_value', Any), - ] - - -class Evidence(Choice): - _alternatives = [ - ('tst_evidence', TimeStampTokenEvidence, {'tag_type': 'implicit', 'tag': 0}), - ('ers_evidence', EvidenceRecord, {'tag_type': 'implicit', 'tag': 1}), - ('other_evidence', OtherEvidence, {'tag_type': 'implicit', 'tag': 2}), - ] - - -class TimeStampedData(Sequence): - _fields = [ - ('version', Version), - ('data_uri', IA5String, {'optional': True}), - ('meta_data', MetaData, {'optional': True}), - ('content', OctetString, {'optional': True}), - ('temporal_evidence', Evidence), - ] - - -class IssuerSerial(Sequence): - _fields = [ - ('issuer', GeneralNames), - ('serial_number', Integer), - ] - - -class ESSCertID(Sequence): - _fields = [ - ('cert_hash', OctetString), - ('issuer_serial', IssuerSerial, {'optional': True}), - ] - - -class ESSCertIDs(SequenceOf): - _child_spec = ESSCertID - - -class SigningCertificate(Sequence): - _fields = [ - ('certs', ESSCertIDs), - ('policies', CertificatePolicies, {'optional': True}), - ] - - -class SetOfSigningCertificates(SetOf): - _child_spec = SigningCertificate - - -class ESSCertIDv2(Sequence): - _fields = [ - ('hash_algorithm', DigestAlgorithm, {'default': 'sha256'}), - ('cert_hash', OctetString), - ('issuer_serial', IssuerSerial, {'optional': True}), - ] - - -class ESSCertIDv2s(SequenceOf): - _child_spec = ESSCertIDv2 - - -class SigningCertificateV2(Sequence): - _fields = [ - ('certs', ESSCertIDv2s), - ('policies', CertificatePolicies, {'optional': True}), - ] - - -class SetOfSigningCertificatesV2(SetOf): - _child_spec = SigningCertificateV2 - - -EncapsulatedContentInfo._oid_specs['tst_info'] = TSTInfo #pylint: disable=W0212 -EncapsulatedContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 -ContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 -ContentType._map['1.2.840.113549.1.9.16.1.4'] = 'tst_info' #pylint: disable=W0212 -ContentType._map['1.2.840.113549.1.9.16.1.31'] = 'timestamped_data' #pylint: disable=W0212 -CMSAttributeType._map['1.2.840.113549.1.9.16.2.12'] = 'signing_certificate' #pylint: disable=W0212 -CMSAttribute._oid_specs['signing_certificate'] = SetOfSigningCertificates #pylint: disable=W0212 -CMSAttributeType._map['1.2.840.113549.1.9.16.2.47'] = 'signing_certificate_v2' #pylint: disable=W0212 -CMSAttribute._oid_specs['signing_certificate_v2'] = SetOfSigningCertificatesV2 #pylint: disable=W0212 diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py new file mode 100644 index 0000000..61fa7ca --- /dev/null +++ b/asn1crypto/tsp.py @@ -0,0 +1,290 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +from .algos import DigestAlgorithm +from .core import ( + Any, + BitString, + Boolean, + Choice, + GeneralizedTime, + IA5String, + Integer, + ObjectIdentifier, + OctetString, + Sequence, + SequenceOf, + SetOf, + UTF8String, +) +from .cms import ( + CMSAttribute, + CMSAttributeType, + ContentInfo, + ContentType, + EncapsulatedContentInfo, +) +from .crl import CertificateList +from .x509 import Attributes, CertificatePolicies, GeneralName, GeneralNames + + +# The structures in this file are based on https://tools.ietf.org/html/rfc3161, +# https://tools.ietf.org/html/rfc4998, https://tools.ietf.org/html/rfc5544, +# https://tools.ietf.org/html/rfc5035, https://tools.ietf.org/html/rfc2634 + +class Version(Integer): + _map = { + 0: 'v0', + 1: 'v1', + 2: 'v2', + 3: 'v3', + 4: 'v4', + 5: 'v5', + } + + +class MessageImprint(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm), + ('hashed_message', OctetString), + ] + + +class Accuracy(Sequence): + _fields = [ + ('seconds', Integer, {'optional': True}), + ('millis', Integer, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('micros', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class Extension(Sequence): + _fields = [ + ('extn_id', ObjectIdentifier), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + +class Extensions(SequenceOf): + _child_spec = Extension + + +class TSTInfo(Sequence): + _fields = [ + ('version', Version), + ('policy', ObjectIdentifier), + ('message_imprint', MessageImprint), + ('serial_number', Integer), + ('gen_time', GeneralizedTime), + ('accuracy', Accuracy, {'optional': True}), + ('ordering', Boolean, {'default': False}), + ('nonce', Integer, {'optional': True}), + ('tsa', GeneralName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class TimeStampReq(Sequence): + _fields = [ + ('version', Version), + ('message_imprint', MessageImprint), + ('req_policy', ObjectIdentifier, {'optional': True}), + ('nonce', Integer, {'optional': True}), + ('cert_req', Boolean, {'default': False}), + ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ] + + +class PKIStatus(Integer): + _map = { + 0: 'granted', + 1: 'granted_with_mods', + 2: 'rejection', + 3: 'waiting', + 4: 'revocation_warning', + 5: 'revocation_notification', + } + + +class PKIFreeText(SequenceOf): + _child_spec = UTF8String + + +class PKIFailureInfo(BitString): + _map = { + 0: 'bad_alg', + 2: 'bad_request', + 5: 'bad_data_format', + 14: 'time_not_available', + 15: 'unaccepted_policy', + 16: 'unaccepted_extensions', + 17: 'add_info_not_available', + 25: 'system_failure', + } + + +class PKIStatusInfo(Sequence): + _fields = [ + ('status', PKIStatus), + ('status_string', PKIFreeText, {'optional': True}), + ('fail_info', PKIFailureInfo, {'optional': True}), + ] + + +class TimeStampResp(Sequence): + _fields = [ + ('status', PKIStatusInfo), + ('time_stamp_token', ContentInfo), + ] + + +class MetaData(Sequence): + _fields = [ + ('hash_protected', Boolean), + ('file_name', UTF8String, {'optional': True}), + ('media_type', IA5String, {'optional': True}), + ('other_meta_data', Attributes, {'optional': True}), + ] + + +class TimeStampAndCRL(SequenceOf): + _fields = [ + ('time_stamp', EncapsulatedContentInfo), + ('crl', CertificateList, {'optional': True}), + ] + + +class TimeStampTokenEvidence(SequenceOf): + _child_spec = TimeStampAndCRL + + +class DigestAlgorithms(SequenceOf): + _child_spec = DigestAlgorithm + + +class EncryptionInfo(Sequence): + _fields = [ + ('encryption_info_type', ObjectIdentifier), + ('encryption_info_value', Any), + ] + + +class PartialHashtree(SequenceOf): + _child_spec = OctetString + + +class PartialHashtrees(SequenceOf): + _child_spec = PartialHashtree + + +class ArchiveTimeStamp(Sequence): + _fields = [ + ('digest_algorithm', DigestAlgorithm, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('attributes', Attributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('reduced_hashtree', PartialHashtrees, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('time_stamp', ContentInfo), + ] + + +class ArchiveTimeStampSequence(SequenceOf): + _child_spec = ArchiveTimeStamp + + +class EvidenceRecord(Sequence): + _fields = [ + ('version', Version), + ('digest_algorithms', DigestAlgorithms), + ('crypto_infos', Attributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('encryption_info', EncryptionInfo, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('archive_time_stamp_sequence', ArchiveTimeStampSequence), + ] + + +class OtherEvidence(Sequence): + _fields = [ + ('oe_type', ObjectIdentifier), + ('oe_value', Any), + ] + + +class Evidence(Choice): + _alternatives = [ + ('tst_evidence', TimeStampTokenEvidence, {'tag_type': 'implicit', 'tag': 0}), + ('ers_evidence', EvidenceRecord, {'tag_type': 'implicit', 'tag': 1}), + ('other_evidence', OtherEvidence, {'tag_type': 'implicit', 'tag': 2}), + ] + + +class TimeStampedData(Sequence): + _fields = [ + ('version', Version), + ('data_uri', IA5String, {'optional': True}), + ('meta_data', MetaData, {'optional': True}), + ('content', OctetString, {'optional': True}), + ('temporal_evidence', Evidence), + ] + + +class IssuerSerial(Sequence): + _fields = [ + ('issuer', GeneralNames), + ('serial_number', Integer), + ] + + +class ESSCertID(Sequence): + _fields = [ + ('cert_hash', OctetString), + ('issuer_serial', IssuerSerial, {'optional': True}), + ] + + +class ESSCertIDs(SequenceOf): + _child_spec = ESSCertID + + +class SigningCertificate(Sequence): + _fields = [ + ('certs', ESSCertIDs), + ('policies', CertificatePolicies, {'optional': True}), + ] + + +class SetOfSigningCertificates(SetOf): + _child_spec = SigningCertificate + + +class ESSCertIDv2(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm, {'default': 'sha256'}), + ('cert_hash', OctetString), + ('issuer_serial', IssuerSerial, {'optional': True}), + ] + + +class ESSCertIDv2s(SequenceOf): + _child_spec = ESSCertIDv2 + + +class SigningCertificateV2(Sequence): + _fields = [ + ('certs', ESSCertIDv2s), + ('policies', CertificatePolicies, {'optional': True}), + ] + + +class SetOfSigningCertificatesV2(SetOf): + _child_spec = SigningCertificateV2 + + +EncapsulatedContentInfo._oid_specs['tst_info'] = TSTInfo #pylint: disable=W0212 +EncapsulatedContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 +ContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 +ContentType._map['1.2.840.113549.1.9.16.1.4'] = 'tst_info' #pylint: disable=W0212 +ContentType._map['1.2.840.113549.1.9.16.1.31'] = 'timestamped_data' #pylint: disable=W0212 +CMSAttributeType._map['1.2.840.113549.1.9.16.2.12'] = 'signing_certificate' #pylint: disable=W0212 +CMSAttribute._oid_specs['signing_certificate'] = SetOfSigningCertificates #pylint: disable=W0212 +CMSAttributeType._map['1.2.840.113549.1.9.16.2.47'] = 'signing_certificate_v2' #pylint: disable=W0212 +CMSAttribute._oid_specs['signing_certificate_v2'] = SetOfSigningCertificatesV2 #pylint: disable=W0212 diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..531ac8e --- /dev/null +++ b/changelog.md @@ -0,0 +1,5 @@ +# changelog + +## 0.9.0 + + - Initial release diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..d41e2e6 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,19 @@ +# asn1crypto docs + +The documentation for *asn1crypto* is composed of a tutorial on basic usage that +covers all of the universal data types, and links to the source for the various +pre-defined data types. + + - [Tutorial](tutorial.md) + + - [Universal types](../asn1crypto/core.py), `asn1crypto.core` + - [Digest, HMAC, signed digest and encryption algorithms](../asn1crypto/algos.py), `asn1crypto.algos` + - [Private and public keys](../asn1crypto/keys.py), `asn1crypto.algos` + - [X509 certificates](../asn1crypto/x509.py), `asn1crypto.x509` + - [Certificate revocation lists (CRLs)](../asn1crypto/crl.py), `asn1crypto.crl` + - [Online certificate status protocol (OCSP)](../asn1crypto/ocsp.py), `asn1crypto.ocsp` + - [Private key encryption (PKCS#5)](../asn1crypto/pkcs5.py), `asn1crypto.pkcs5` + - [Private key/certificate containers (PKCS#12)](../asn1crypto/pkcs12.py), `asn1crypto.pkcs12` + - [Cryptographic message syntax (CMS, PKCS#7)](../asn1crypto/cms.py), `asn1crypto.cms` + - [Time stamp protocol (TSP)](../asn1crypto/tsp.py), `asn1crypto.tsp` + - [PDF signatures](../asn1crypto/pdf.py), `asn1crypto.pdf` diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..b223b16 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,547 @@ +# asn1crypto Tutorial + +The *asn1crypto* library is a combination of a general-purpose BER/DER parser +and DER serializer, along with a number of pre-built cryptographic types +definitions. + +For a general overview of ASN.1 as used in cryptography, please see +[A Layman's Guide to a Subset of ASN.1, BER, and DER](http://luca.ntop.org/Teaching/Appunti/asn1.html). + +## Universal Types + +For general purpose ASN.1 parsing, the `asn1crypto.core` module is used. It +contains the following classes, that parse, represent and serialize all of the +ASN.1 universal types: + +| Class | Native Type | Implementation Notes | +| ------------------ | ------------------- | ------------------------------- | +| `Boolean` | `bool` | | +| `Integer` | `int` | may be `long` on Python 2 | +| `BitString` | `tuple` of `int` | | +| `OctetString` | `bytes` (`str`) | | +| `Null` | `None` | | +| `ObjectIdentifier` | `str` (`unicode`) | string is dotted integer format | +| `ObjectDescriptor` | | no native conversion | +| `InstanceOf` | | no native conversion | +| `Real` | | no native conversion | +| `Enumerated` | `str` (`unicode`) | `_map` must be set | +| `UTF8String` | `str` (`unicode`) | | +| `RelativeOid` | `str` (`unicode`) | string is dotted integer format | +| `Sequence` | `OrderedDict` | | +| `SequenceOf` | `list` | | +| `Set` | `OrderedDict` | | +| `SetOf` | `list` | | +| `EmbeddedPdv` | `OrderedDict` | no named field parsing | +| `NumericString` | `str` (`unicode`) | no charset limitations | +| `PrintableString` | `str` (`unicode`) | no charset limitations | +| `TeletexString` | `str` (`unicode`) | | +| `VideotexString` | `bytes` (`str`) | no unicode conversion | +| `IA5String` | `str` (`unicode`) | | +| `UTCTime` | `datetime.datetime` | | +| `GeneralizedTime` | `datetime.datetime` | treated as UTC when no timezone | +| `GraphicString` | `str` (`unicode`) | unicode conversion as latin1 | +| `VisibleString` | `str` (`unicode`) | no charset limitations | +| `GeneralString` | `str` (`unicode`) | unicode conversion as latin1 | +| `UniversalString` | `str` (`unicode`) | | +| `CharacterString` | `str` (`unicode`) | unicode conversion as latin1 | +| `BMPString` | `str` (`unicode`) | | + +For *Native Type*, the Python 3 type is listed first, with the Python 2 type +in parentheses. + +As mentioned next to some of the types, value parsing may not be implemented +for types not currently used in cryptography (such as `ObjectDescriptor`, +`InstanceOf` and `Real`). Additionally some of the string classes don't +enforce character set limitations, and for some string types that accept all +different encodings, the default encoding is set to latin1. + +In addition, there are a few overridden types where various specifications use +a `BitString` or `OctetString` type to represent a different type. These +include: + +| Class | Native Type | Implementation Notes | +| -------------------- | ------------------- | ------------------------------- | +| `OctetBitString` | `bytes` (`str`) | | +| `IntegerBitString` | `int` | may be `long` on Python 2 | +| `IntegerOctetString` | `int` | may be `long` on Python 2 | + +## Basic Usage + +All of the universal types implement two methods, the class method `.load()` for +parsing and the instance method `.dump()` for serialization. + +```python +from asn1crypto.core import Sequence + +parsed = Sequence.load(der_byte_string) +serialized = parsed.dump() +``` + +In addition to the two primary methods, every instance has a `.native` property +that converts the data into a native Python data type. + +```python +import pprint +from asn1crypto.core import Sequence + +parsed = Sequence.load(der_byte_string) +pprint(parsed.native) +``` + +## Sequence + +One of the core structures when dealing with ASN.1 is the Sequence type. The +`Sequence` class can handle field with universal data types, however in most +situations the `_fields` property will need to be set with the expected +definition of each field in the Sequence. + +### Configuration + +The `_fields` property must be set to a `list` of 2-3 element `tuple`s. The +first element in the tuple must be a unicode string of the field name. The +second must be a type class - either a universal type, or a custom type. The +third, and optional, element is a `dict` with parameters to pass to the type +class for things like default values, marking the field as optional, or +implicit/explicit tagging. + +```python +from asn1crypto.core import Sequence, Integer, OctetString, IA5String + +class MySequence(Sequence): + _fields = [ + ('field_one', Integer), + ('field_two', OctetString), + ('field_three', IA5String, {'optional': True}), + ] +``` + +Implicit and explicit tagging will be covered in more detail later, however +the following are options that can be set for each field type class: + + - `{'default: 1}` sets the field's default value to `1`, allowing it to be + omitted from the serialized form + - `{'optional': True}` set the field to be optional, allowing it to be + omitted + +### Usage + +To access values of the sequence, use dict-like access via `[]` and use the +name of the field: + +```python +seq = MySequence.load(der_byte_string) +print(seq['field_two'].native) +``` + +The values of fields can be set by assigning via `[]`. If the value assigned is +of the correct type class, it will be used as-is. If the value is not of the +correct type class, a new instance of that type class will be created and the +value will be passed to the constructor. + +```python +seq = MySequence.load(der_byte_string) +# These statements will result in the same state +seq['field_one'] = Integer(5) +seq['field_one'] = 5 +``` + +When fields are complex types such as `Sequence` or `SequenceOf`, there is no +way to construct the value out of a native Python data type. + +### Optional Fields + +When a field is configured via the `optional` parameter, not present in the +`Sequence`, but accessed, an instance of the `NoValue` class will be returned. +This class is serialized to an empty byte string and returns `None` when +`.native` is accessed. + +## Set + +The `Set` class is configured in the same was as `Sequence`, however it allows +serialized fields to be in any order, per the ASN.1 standard. + +```python +from asn1crypto.core import Set, Integer, OctetString, IA5String + +class MySet(Set): + _fields = [ + ('field_one', Integer), + ('field_two', OctetString), + ('field_three', IA5String, {'optional': True}), + ] +``` + +## SequenceOf + +The `SequenceOf` class is used to allow for zero or more instances of a type. +The class uses the `_child_spec` property to define the instance class type. + +```python +from asn1crypto.core import SequenceOf, Integer + +class Integers(SequenceOf): + _child_spec = Integer +``` + +Values in the `SequenceOf` can be accessed via `[]` with an integer key. The +length of the `SequenceOf` is determined via `len()`. + +```python +values = Integers.load(der_byte_string) +for i in range(0, len(values)): + print(values[i].native) +``` + +## SetOf + +The `SetOf` class is an exact duplicate of `SequenceOf`. According to the ASN.1 +standard, the difference is that a `SequenceOf` is explicitly ordered, however +`SetOf` may be in any order. This is an equivalent comparison of a Python `list` +and `set`. + +```python +from asn1crypto.core import SetOf, Integer + +class Integers(SetOf): + _child_spec = Integer +``` + +## Integer + +The `Integer` class allows values to be *named*. An `Integer` with named values +may contain any integer, however special values with named will be represented +as those names when `.native` is called. + +Named values are configured via the `_map` property, which must be a `dict` +with the keys being integers and the values being unicode strings. + +```python +from asn1crypto.core import Integer + +class Version(Integer): + _map = { + 1: 'v1', + 2: 'v2', + } + +# Will print: "v1" +print(Version(1).native) + +# Will print: 4 +print(Version(4).native) +``` + +## Enumerated + +The `Enumerated` class is almost identical to `Integer`, however only values in +the `_map` property are valid. + +```python +from asn1crypto.core import Enumerated + +class Version(Enumerated): + _map = { + 1: 'v1', + 2: 'v2', + } + +# Will print: "v1" +print(Version(1).native) + +# Will raise a ValueError exception +print(Version(4).native) +``` + +## ObjectIdentifier + +The `ObjectIdentifier` class represents values of the ASN.1 type of the same +name. `ObjectIdentifier` instances are converted to a unicode string in a +dotted-integer format when `.native` is accessed. + +While this standard conversion is a reasonable baseline, in most situations +it will be more maintainable to map the OID strings to a unicode string +containing a description of what the OID repesents. + +The mapping of OID strings to name strings is configured via the `_map` +property, which is a `dict` object with keys being unicode OID string and the +values being a unicode string. + +```python +from asn1crypto.core import ObjectIdentifier + +class MyType(ObjectIdentifier): + _map = { + '1.8.2.1.23': 'value_name', + '1.8.2.1.24': 'other_value', + } + +# Will print: "value_name" +print(MyType('1.8.2.1.23').native) + +# Will print: "1.8.2.1.25" +print(MyType('1.8.2.1.25').native) +``` + +## BitString + +The `BitString` class has a native representation as a `tuple` of `int`s (being +either `1` or `0`). In addition, it is possible to set the value of a +`BitString` by passing an integer. + +```python +from asn1crypto.core import BitString + +# These are equivalent +b1 = BitString((1, 0, 1)) +b2 = BitString(5) +``` + +Additionally, it is possible to set the `_map` property to a dict where the +keys are bit indexes and the values are unicode string names. This allows +checking the value of a given bit by a property of the name specified. + +```python +from asn1crypto.core import BitString + +class MyFlags(BitString): + _map = { + 0: 'edit', + 1: 'delete', + 2: 'manage_users', + } + +permissions = MyFlags(3) + +# This will be printed +if permissions.edit and permissions.delete: + print('Can edit and delete') + +# This will not +if permissions.manage_users: + print('Is admin') +``` + +## Strings + +ASN.1 contains quite a number of string types: + +| Type | Standard Encoding | Implementation Encoding | Notes | +| ----------------- | --------------------------------- | ----------------------- | ------------------------------------------------------------------------- | +| `UTF8String` | UTF-8 | UTF-8 | | +| `NumericString` | ASCII `[0-9 ]` | ISO 8859-1 | The implementation is a superset of supported characters | +| `PrintableString` | ASCII `[a-zA-Z0-9 '()+,\\-./:=?]` | ISO 8859-1 | The implementation is a superset of supported characters | +| `TeletexString` | ITU T.61 | Custom | The implementation is based off of https://en.wikipedia.org/wiki/ITU_T.61 | +| `VideotexString` | *?* | *None* | This has no set encoding, and it not used in cryptography | +| `IA5String` | ITU T.50 (very similar to ASCII) | ISO 8859-1 | The implementation is a superset of supported characters | +| `GraphicString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | +| `VisibleString` | ASCII (printable) | ISO 8859-1 | The implementation is a superset of supported characters | +| `GeneralString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | +| `UniversalString` | UTF-32 | UTF-32 | | +| `CharacterString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | +| `BMPString` | UTF-16 | UTF-16 | | + +As noted in the table above, many of the implementations are supersets of the +supported characters. This simplifies parsing, but puts the onus of using valid +characters on the developer. However, in general `UTF8String`, `BMPString` or +`UniversalString` should be preferred when a choice is given. + +All string types other than `VideotexString` are created from unicode strings. + +```python +from asn1crypto.core import IA5String + +print(IA5String('Testing!').native) +``` + +## UTCTime + +The class `UTCTime` accepts a unicode string in one of the formats: + + - `%y%m%d%H%MZ` + - `%y%m%d%H%M%SZ` + - `%y%m%d%H%M%z` + - `%y%m%d%H%M%S%z` + +or a `datetime.datetime` instance. See the +[Python datetime strptime() reference](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) +for details of the formats. + +When `.native` is accessed, it returns a `datetime.datetime` object with a +`tzinfo` of `asn1crypto.core.timezone.utc`. + +## GeneralizedTime + +The class `GeneralizedTime` accepts a unicode string in one of the formats: + + - `%Y%m%d%H` + - `%Y%m%d%H%M` + - `%Y%m%d%H%M%S` + - `%Y%m%d%H%M%S.%f` + - `%Y%m%d%HZ` + - `%Y%m%d%H%MZ` + - `%Y%m%d%H%M%SZ` + - `%Y%m%d%H%M%S.%fZ` + - `%Y%m%d%H%z` + - `%Y%m%d%H%M%z` + - `%Y%m%d%H%M%S%z` + - `%Y%m%d%H%M%S.%f%z` + +or a `datetime.datetime` instance. See the +[Python datetime strptime() reference](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) +for details of the formats. + +When `.native` is accessed, it returns a `datetime.datetime` object with a +`tzinfo` of `asn1crypto.core.timezone.utc`. For formats where the time has a +timezone offset is specified (`[+-]\d{4}`), the time is converted to UTC. For +times without a timezone, the time is assumed to be in UTC. + +## Choice + +The `Choice` class allows handling ASN.1 Choice structures. The `_alternatives` +property must be set to a `list` containing 2-3 element `tuple`s. The first +element in the tuple is the alternative name. The second element is the type +class for the alternative. The, optional, third element is a `dict` of +parameters to pass to the type class constructor. This is used primarily for +implicit and explicit tagging. + +```python +from asn1crypto.core import Choice, Integer, OctetString, IA5String + +class MyChoice(Choice): + _alternatives = [ + ('option_one', Integer), + ('option_two', OctetString), + ('option_three', IA5String), + ] +``` + +`Choice` objects has two extra properties, `.name` and `.chosen`. The `.name` +property contains the name of the chosen alternative. The `.chosen` property +contains the instance of the chosen type class. + +```python +parsed = MyChoice.load(der_bytes) +print(parsed.name) +print(type(parsed.chosen)) +``` + +The `.native` property and `.dump()` method work as with the universal type +classes. Under the hood they just proxy the calls to the `.chosen` object. + +## Any + +The `Any` class implements the ASN.1 Any type, which allows any data type. By +default objects of this class do not perform any parsing. However, the +`.parse()` instance method allows parsing the contents of the `Any` object, +either into a universal type, or to a specification pass in via the `spec` +parameter. + +This type is not used as a top-level structure, but instead allows `Sequence` +and `Set` objects to accept varying contents, usually based on some sort of +`ObjectIdentifier`. + +```python +from asn1crypto.core import Sequence, ObjectIdentifier, Any, Integer, OctetString + +class MySequence(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('value', Any), + ] +``` + +## Specification via OID + +Throughout the usage of ASN.1 in cryptography, a pattern is present where an +`ObjectIdenfitier` is used to determine what specification should be used to +interpret another field in a `Sequence`. Usually the other field is an instance +of `Any`, however ocassionally it is an `OctetString`. + +*asn1crypto* provides the `_oid_pair` and `_oid_specs` properties of the +`Sequence` class to allow handling these situations. + +The `_oid_pair` is a tuple with two unicode string elements. The first is the +name of the field that is an `ObjectIdentifier` and the second if the name of +the field that has a variable specification based on the first field. + +The `_oid_specs` property is a `dict` object with `ObjectIdentifier` values as +the keys (either dotted or mapped notation) and a type class as the value. When +the first field in `_oid_pair` has a value equal to one of the keys in +`_oid_specs`, then the corresponding type class will be used as the +specification for the second field of `_oid_pair`. + +```python +from asn1crypto.core import Sequence, ObjectIdentifier, Any, OctetString, Integer + +class MyId(ObjectIdentifier): + _map = { + '1.2.3.4': 'initialization_vector', + '1.2.3.5': 'iterations', + } + +class MySequence(Sequence): + _fields = [ + ('type', MyId), + ('value', Any), + ] + + _oid_pair = ('type', 'value') + _oid_specs = { + 'initialization_vector': OctetString, + 'iterations': Integer, + } +``` + +In some situations, the second field in `_oid_pair` is not an instance of `Any`, +but instead is an instance of `OctetString`. This is dictated by the ASN.1 +specification of the data structures being worked with. + +## Explicit and Implicit Tagging + +When working with `Sequence`, `Set` and `Choice` it is often necessary to +disambiguate between fields because of a number of factors: + + - In `Sequence` the presence of an optional field must be determined by tag number + - In `Set`, each field must have a different tag number since they can be in any order + - In `Choice`, each alternative must have a different tag number to determine which is present + +The universal types all have unique tag numbers. However, if a `Sequence`, `Set` +or `Choice` has more than one field with the same universal type, tagging allows +a way to keep the semantics of the original type, but with a different tag +number. + +Implicit tagging simply changes the tag number of a type to a different value. +However, Explicit tagging wraps the existing type in another tag with the +specified tag number. + +In general, most situations allow for implicit tagging, with the notable +exception than a field that is a `Choice` type must always be explicitly tagged. +Otherwise, using implicit tagging would modify the tag of the chosen +alternative, breaking the mechanism by which `Choice` works. + +Here is an example of implicit and explicit tagging where explicit tagging on +the `Sequence` allows a `Choice` type field to be optional, and where implicit +tagging in the `Choice` structure allows disambiguating between two string of +the same type. + +```python +from asn1crypto.core import Sequence, Choice, IA5String, UTCTime, ObjectIdentifier + +class Person(Choice): + _alternatives = [ + ('name', IA5String), + ('email', IA5String, {'tag_type': 'implicit', 'tag': 0}), + ] + +class Record(Sequence): + _fields = [ + ('id', ObjectIdentifier), + ('created', UTCTime), + ('creator', Person, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] +``` + +As is shown above, the keys `tag_type` and `tag` are used for tagging, and are +passed to a type class constructor via the optional third element of a field +or alternative tuple. The `tag_type` may be the unicode strings `'implicit'` or +`'explicit'` and the `tag` may be any integer. diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ffd6433 --- /dev/null +++ b/readme.md @@ -0,0 +1,108 @@ +# asn1crypto + +A fast, pure Python library for parsing and serializing ASN.1 structures. In +addition to an ASN.1 BER/DER decoder and DER serializer, the project includes +a bunch of ASN.1 structures for use with various common cryptography standards: + +| Standard | Module | Source | +| X509 | `asn1crypto.x509` | [RFC5280](https://tools.ietf.org/html/rfc5280) | +| CRL | `asn1crypto.crl` | [RFC5280](https://tools.ietf.org/html/rfc5280) | +| OCSP | `asn1crypto.ocsp` | [RFC6960](https://tools.ietf.org/html/rfc6960) | +| PKCS#12 | `asn1crypto.pkcs12` | [RFC7292](https://tools.ietf.org/html/rfc7292) | +| PKCS#8 | `asn1crypto.keys` | [RFC5208](https://tools.ietf.org/html/rfc5208) | +| PKCS#1 v2.1 (RSA keys) | `asn1crypto.keys` | [RFC3447](https://tools.ietf.org/html/rfc3447) | +| DSA keys | `asn1crypto.keys` | [RFC3279](https://tools.ietf.org/html/rfc3279) | +| Elliptic curve keys | `asn1crypto.keys` | [SECG SEC1 V2](http://www.secg.org/sec1-v2.pdf) | +| PKCS#5 v2.1 | `asn1crypto.pkcs5` | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | +| CMS (and PKCS#7) | `asn1crypto.cms` | [RFC5652](https://tools.ietf.org/html/rfc5652), [RFC2315](https://tools.ietf.org/html/rfc2315) | +| TSP | `asn1crypto.tsp` | [RFC3161](https://tools.ietf.org/html/rfc3161) | +| PDF signatures | `asn1crypto.pdf` | [PDF 1.7](http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf) | + +## License + +*asn1crypto* is licensed under the terms of the MIT license. See the +[LICENSE](LICENSE) file for the exact license text. + +## Dependencies + +Python 2.7, 3.3, 3.4 or pypy. *No third-party packages required.* + +## Version + +0.9.0 - [changelog](changelog.md) + +## Installation + +```bash +pip install asn1crypto +``` + +## Documentation + + - `asn1crypto.core` + - `asn1crypto.algos` + - `asn1crypto.keys` + - `asn1crypto.x509` + - `asn1crypto.crl` + - `asn1crypto.ocsp` + - `asn1crypto.pkcs5` + - `asn1crypto.pkcs12` + - `asn1crypto.cms` + - `asn1crypto.tsp` + - `asn1crypto.pdf` + +## Why Another Python ASN.1 Library? + +Python has long had the [pyasn1](https://pypi.python.org/pypi/pyasn1) and +[pyasn1_modules](https://pypi.python.org/pypi/pyasn1-modules) available for +parsing and serializing ASN.1 structures. While the project does include a +comprehensive set of tools for parsing and serializing, the performance of the +library can be very poor, especially when dealing with bit fields and parsing +large structures such as CRLs. + +After spending extensive time using *pyasn1*, the following issues were +identified: + + 1. Poor performance + 2. Verbose, non-pythonic API + 3. Out-dated and incomplete definitions in *pyasn1-modules* + 4. No simple way to map data to native Python data structures + 5. No mechanism for overriden universal ASN.1 types + +The *pyasn1* API is largely method driven, and uses extensive configuration +objects and lowerCamelCase names. There were no consistent options for +converting types of native Python data structures. Since the project supports +out-dated versions of Python, many newer language features are unavailable +for use. + +Time was spent trying to profile issues with the performance, however the +architecture made it hard to pin down the primary source of the poor +performance. Attempts were made to improve performance by utilizing unreleased +patches and delaying parsing using the `Any` type. Even with such changes, the +performance was still unacceptably slow. + +Finally, a number of structures in the cryptographic space use universal data +types such as `BitString` and `OctetString`, but interpret the data as other +types. For instance, signatures are really byte strings, but are encoded as +`BitString`. Elliptic curve keys use both `BitString` and `OctetString` to +represent integers. Parsing these structures as the base universal types and +then re-interpreting them wastes computation. + +*asn1crypto* uses the following techniques to improve performance, especially +when extracting one or two fields from large, complex structures: + + - Delayed parsing of byte string values + - Persistence of original ASN.1 encoded data until a value is changed + - Lazy loading of child fields + - Utilization of high-level Python stdlib modules + +While there is no extensive performance test suite, the +`CRLTests.test_parse_crl` test case was used to parse a 21MB CRL file on a +late 2013 rMBP. *asn1crypto* parsed the certificate serial numbers in just +under 8 seconds. With *pyasn1*, using definitions from *pyasn1-modules*, the +same parsing took over 4,100 seconds. + +For smaller structures the performance difference can range from a few times +faster to an order of magnitude of more. + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6df9758 --- /dev/null +++ b/setup.py @@ -0,0 +1,34 @@ +from setuptools import setup, find_packages +import asn1crypto + + +setup( + name='asn1crypto', + version=asn1crypto.__version__, + + description='Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X509 and TSA', + long_description='Docs for this project are maintained at https://github.com/wbond/asn1crypto#readme.', + + url='https://github.com/wbond/asn1crypto', + + author='wbond', + author_email='will@wbond.net', + + license='MIT', + + classifiers=[ + 'Development Status :: 4 - Beta', + + 'Intended Audience :: Developers', + + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], + + keywords='asn1 crypto', + + packages=find_packages(exclude=['tests*']) +) diff --git a/tests.py b/tests.py index 0339b5c..79dff27 100644 --- a/tests.py +++ b/tests.py @@ -7,13 +7,13 @@ from tests.test_cms import CMSTests #pylint: disable=E0611,W0611 from tests.test_crl import CRLTests #pylint: disable=E0611,W0611 from tests.test_keys import KeysTests #pylint: disable=E0611,W0611 from tests.test_ocsp import OCSPTests #pylint: disable=E0611,W0611 -from tests.test_tsa import TSATests #pylint: disable=E0611,W0611 +from tests.test_tsp import TSPTests #pylint: disable=E0611,W0611 from tests.test_x509 import X509Tests #pylint: disable=E0611,W0611 if __name__ == '__main__': suite = unittest.TestSuite() loader = unittest.TestLoader() - for test_class in [CMSTests, CRLTests, KeysTests, OCSPTests, TSATests, X509Tests]: + for test_class in [CMSTests, CRLTests, KeysTests, OCSPTests, TSPTests, X509Tests]: suite.addTest(loader.loadTestsFromTestCase(test_class)) unittest.TextTestRunner().run(suite) diff --git a/tests/fixtures/tsa_request b/tests/fixtures/tsa_request deleted file mode 100644 index 214d28b..0000000 Binary files a/tests/fixtures/tsa_request and /dev/null differ diff --git a/tests/fixtures/tsa_response b/tests/fixtures/tsa_response deleted file mode 100644 index 3921362..0000000 Binary files a/tests/fixtures/tsa_response and /dev/null differ diff --git a/tests/fixtures/tsp_request b/tests/fixtures/tsp_request new file mode 100644 index 0000000..214d28b Binary files /dev/null and b/tests/fixtures/tsp_request differ diff --git a/tests/fixtures/tsp_response b/tests/fixtures/tsp_response new file mode 100644 index 0000000..3921362 Binary files /dev/null and b/tests/fixtures/tsp_response differ diff --git a/tests/test_tsa.py b/tests/test_tsa.py deleted file mode 100644 index 813592d..0000000 --- a/tests/test_tsa.py +++ /dev/null @@ -1,242 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals - -import unittest -import os -from datetime import datetime -from collections import OrderedDict - -from asn1crypto import tsa, cms, core - - -tests_root = os.path.dirname(__file__) -fixtures_dir = os.path.join(tests_root, 'fixtures') - - - -class TSATests(unittest.TestCase): - - def test_parse_request(self): - with open(os.path.join(fixtures_dir, 'tsa_request'), 'rb') as f: - request = tsa.TimeStampReq.load(f.read()) - - self.assertEqual( - 'v1', - request['version'].native - ) - self.assertEqual( - 'sha1', - request['message_imprint']['hash_algorithm']['algorithm'].native - ) - self.assertEqual( - None, - request['message_imprint']['hash_algorithm']['parameters'].native - ) - self.assertEqual( - b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB', - request['message_imprint']['hashed_message'].native - ) - self.assertEqual( - 17842879675353045770, - request['nonce'].native - ) - - def test_parse_response(self): - with open(os.path.join(fixtures_dir, 'tsa_response'), 'rb') as f: - response = tsa.TimeStampResp.load(f.read()) - - status_info = response['status'] - token = response['time_stamp_token'] - signed_data = token['content'] - encap_content_info = signed_data['encap_content_info'] - tst_info = encap_content_info['content'].parsed - signer_infos = signed_data['signer_infos'] - signer_info = signer_infos[0] - signed_attrs = signer_info['signed_attrs'] - - self.assertEqual( - 'granted', - status_info['status'].native - ) - self.assertEqual( - None, - status_info['status_string'].native - ) - self.assertEqual( - None, - status_info['fail_info'].native - ) - self.assertEqual( - 'signed_data', - token['content_type'].native - ) - self.assertIsInstance( - signed_data, - cms.SignedData - ) - self.assertEqual( - 'v3', - signed_data['version'].native - ) - self.assertEqual( - 'sha1', - signed_data['digest_algorithms'][0]['algorithm'].native - ) - self.assertEqual( - 'tst_info', - encap_content_info['content_type'].native - ) - self.assertIsInstance( - tst_info, - tsa.TSTInfo - ) - self.assertEqual( - 'v1', - tst_info['version'].native - ) - self.assertEqual( - '1.1.2', - tst_info['policy'].native - ) - self.assertEqual( - 'sha1', - tst_info['message_imprint']['hash_algorithm']['algorithm'].native - ) - self.assertEqual( - None, - tst_info['message_imprint']['hash_algorithm']['parameters'].native - ) - self.assertEqual( - b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB', - tst_info['message_imprint']['hashed_message'].native - ) - self.assertEqual( - 544918635, - tst_info['serial_number'].native - ) - self.assertEqual( - datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), - tst_info['gen_time'].native - ) - self.assertEqual( - 60, - tst_info['accuracy']['seconds'].native - ) - self.assertEqual( - None, - tst_info['accuracy']['millis'].native - ) - self.assertEqual( - None, - tst_info['accuracy']['micros'].native - ) - self.assertEqual( - False, - tst_info['ordering'].native - ) - self.assertEqual( - 17842879675353045770, - tst_info['nonce'].native - ) - self.assertEqual( - OrderedDict([ - ('country_name', 'US'), - ('organization_name', 'GeoTrust Inc'), - ('common_name', 'GeoTrust Timestamping Signer 1'), - ]), - tst_info['tsa'].native - ) - self.assertEqual( - None, - tst_info['extensions'].native - ) - self.assertEqual( - None, - signed_data['certificates'].native - ) - self.assertEqual( - None, - signed_data['crls'].native - ) - self.assertEqual( - 1, - len(signer_infos) - ) - self.assertEqual( - 'v1', - signer_info['version'].native - ) - self.assertEqual( - OrderedDict([ - ( - 'issuer', - OrderedDict([ - ('country_name', 'ZA'), - ('state_or_province_name', 'Western Cape'), - ('locality_name', 'Durbanville'), - ('organization_name', 'Thawte'), - ('organizational_unit_name', 'Thawte Certification'), - ('common_name', 'Thawte Timestamping CA'), - ]) - ), - ( - 'serial_number', - 125680471847352264461591953321128732863 - ) - ]), - signer_info['sid'].native - ) - self.assertEqual( - 'sha1', - signer_info['digest_algorithm']['algorithm'].native - ) - self.assertEqual( - 4, - len(signed_attrs) - ) - self.assertEqual( - 'content_type', - signed_attrs[0]['type'].native - ) - self.assertEqual( - 'tst_info', - signed_attrs[0]['values'][0].native - ) - self.assertEqual( - 'signing_time', - signed_attrs[1]['type'].native - ) - self.assertEqual( - datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), - signed_attrs[1]['values'][0].native - ) - self.assertEqual( - 'message_digest', - signed_attrs[2]['type'].native - ) - self.assertEqual( - b'\x22\x06\x7D\xA4\xFC\x7B\xC5\x94\x80\xB4\xB0\x78\xC2\x07\x66\x02\xA3\x0D\x62\xAE', - signed_attrs[2]['values'][0].native - ) - self.assertEqual( - 'signing_certificate', - signed_attrs[3]['type'].native - ) - self.assertEqual( - OrderedDict([ - ( - 'certs', - [ - OrderedDict([ - ('cert_hash', b'\x22\x3C\xDA\x27\x07\x96\x73\x81\x6B\x60\x8A\x1B\x8C\xB0\xAB\x02\x30\x10\x7F\xCC'), - ('issuer_serial', None), - ]) - ] - ), - ( - 'policies', - None - ) - ]), - signed_attrs[3]['values'][0].native - ) diff --git a/tests/test_tsp.py b/tests/test_tsp.py new file mode 100644 index 0000000..0944746 --- /dev/null +++ b/tests/test_tsp.py @@ -0,0 +1,242 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import os +from datetime import datetime +from collections import OrderedDict + +from asn1crypto import tsp, cms, core + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + + +class TSPTests(unittest.TestCase): + + def test_parse_request(self): + with open(os.path.join(fixtures_dir, 'tsp_request'), 'rb') as f: + request = tsp.TimeStampReq.load(f.read()) + + self.assertEqual( + 'v1', + request['version'].native + ) + self.assertEqual( + 'sha1', + request['message_imprint']['hash_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + request['message_imprint']['hash_algorithm']['parameters'].native + ) + self.assertEqual( + b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB', + request['message_imprint']['hashed_message'].native + ) + self.assertEqual( + 17842879675353045770, + request['nonce'].native + ) + + def test_parse_response(self): + with open(os.path.join(fixtures_dir, 'tsp_response'), 'rb') as f: + response = tsp.TimeStampResp.load(f.read()) + + status_info = response['status'] + token = response['time_stamp_token'] + signed_data = token['content'] + encap_content_info = signed_data['encap_content_info'] + tst_info = encap_content_info['content'].parsed + signer_infos = signed_data['signer_infos'] + signer_info = signer_infos[0] + signed_attrs = signer_info['signed_attrs'] + + self.assertEqual( + 'granted', + status_info['status'].native + ) + self.assertEqual( + None, + status_info['status_string'].native + ) + self.assertEqual( + None, + status_info['fail_info'].native + ) + self.assertEqual( + 'signed_data', + token['content_type'].native + ) + self.assertIsInstance( + signed_data, + cms.SignedData + ) + self.assertEqual( + 'v3', + signed_data['version'].native + ) + self.assertEqual( + 'sha1', + signed_data['digest_algorithms'][0]['algorithm'].native + ) + self.assertEqual( + 'tst_info', + encap_content_info['content_type'].native + ) + self.assertIsInstance( + tst_info, + tsp.TSTInfo + ) + self.assertEqual( + 'v1', + tst_info['version'].native + ) + self.assertEqual( + '1.1.2', + tst_info['policy'].native + ) + self.assertEqual( + 'sha1', + tst_info['message_imprint']['hash_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + tst_info['message_imprint']['hash_algorithm']['parameters'].native + ) + self.assertEqual( + b'\x53\xC9\xDB\xC1\x6D\xDB\x34\x3B\x28\x4E\xEF\xA6\x03\x0E\x02\x64\x79\x31\xAF\xFB', + tst_info['message_imprint']['hashed_message'].native + ) + self.assertEqual( + 544918635, + tst_info['serial_number'].native + ) + self.assertEqual( + datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), + tst_info['gen_time'].native + ) + self.assertEqual( + 60, + tst_info['accuracy']['seconds'].native + ) + self.assertEqual( + None, + tst_info['accuracy']['millis'].native + ) + self.assertEqual( + None, + tst_info['accuracy']['micros'].native + ) + self.assertEqual( + False, + tst_info['ordering'].native + ) + self.assertEqual( + 17842879675353045770, + tst_info['nonce'].native + ) + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('organization_name', 'GeoTrust Inc'), + ('common_name', 'GeoTrust Timestamping Signer 1'), + ]), + tst_info['tsa'].native + ) + self.assertEqual( + None, + tst_info['extensions'].native + ) + self.assertEqual( + None, + signed_data['certificates'].native + ) + self.assertEqual( + None, + signed_data['crls'].native + ) + self.assertEqual( + 1, + len(signer_infos) + ) + self.assertEqual( + 'v1', + signer_info['version'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'issuer', + OrderedDict([ + ('country_name', 'ZA'), + ('state_or_province_name', 'Western Cape'), + ('locality_name', 'Durbanville'), + ('organization_name', 'Thawte'), + ('organizational_unit_name', 'Thawte Certification'), + ('common_name', 'Thawte Timestamping CA'), + ]) + ), + ( + 'serial_number', + 125680471847352264461591953321128732863 + ) + ]), + signer_info['sid'].native + ) + self.assertEqual( + 'sha1', + signer_info['digest_algorithm']['algorithm'].native + ) + self.assertEqual( + 4, + len(signed_attrs) + ) + self.assertEqual( + 'content_type', + signed_attrs[0]['type'].native + ) + self.assertEqual( + 'tst_info', + signed_attrs[0]['values'][0].native + ) + self.assertEqual( + 'signing_time', + signed_attrs[1]['type'].native + ) + self.assertEqual( + datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), + signed_attrs[1]['values'][0].native + ) + self.assertEqual( + 'message_digest', + signed_attrs[2]['type'].native + ) + self.assertEqual( + b'\x22\x06\x7D\xA4\xFC\x7B\xC5\x94\x80\xB4\xB0\x78\xC2\x07\x66\x02\xA3\x0D\x62\xAE', + signed_attrs[2]['values'][0].native + ) + self.assertEqual( + 'signing_certificate', + signed_attrs[3]['type'].native + ) + self.assertEqual( + OrderedDict([ + ( + 'certs', + [ + OrderedDict([ + ('cert_hash', b'\x22\x3C\xDA\x27\x07\x96\x73\x81\x6B\x60\x8A\x1B\x8C\xB0\xAB\x02\x30\x10\x7F\xCC'), + ('issuer_serial', None), + ]) + ] + ), + ( + 'policies', + None + ) + ]), + signed_attrs[3]['values'][0].native + ) -- cgit v1.2.3 From 4c30ef48614639473ec65af35681f80adf88fc21 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 09:11:00 -0400 Subject: Fix table formatting --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index ffd6433..9137a44 100644 --- a/readme.md +++ b/readme.md @@ -5,6 +5,7 @@ addition to an ASN.1 BER/DER decoder and DER serializer, the project includes a bunch of ASN.1 structures for use with various common cryptography standards: | Standard | Module | Source | +| ---------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | | X509 | `asn1crypto.x509` | [RFC5280](https://tools.ietf.org/html/rfc5280) | | CRL | `asn1crypto.crl` | [RFC5280](https://tools.ietf.org/html/rfc5280) | | OCSP | `asn1crypto.ocsp` | [RFC6960](https://tools.ietf.org/html/rfc6960) | -- cgit v1.2.3 From b84959410119899e16d6419f15e8af546a5e2453 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 09:14:43 -0400 Subject: Add doc links --- readme.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/readme.md b/readme.md index 9137a44..ac1ad13 100644 --- a/readme.md +++ b/readme.md @@ -40,17 +40,24 @@ pip install asn1crypto ## Documentation - - `asn1crypto.core` - - `asn1crypto.algos` - - `asn1crypto.keys` - - `asn1crypto.x509` - - `asn1crypto.crl` - - `asn1crypto.ocsp` - - `asn1crypto.pkcs5` - - `asn1crypto.pkcs12` - - `asn1crypto.cms` - - `asn1crypto.tsp` - - `asn1crypto.pdf` +The documentation for *asn1crypto* is composed of a tutorial on basic usage that +covers all of the universal data types, and links to the source for the various +pre-defined data types. + + - [Tutorial](docs/tutorial.md) + + - [Universal types](asn1crypto/core.py), `asn1crypto.core` + - [Digest, HMAC, signed digest and encryption algorithms](asn1crypto/algos.py), `asn1crypto.algos` + - [Private and public keys](asn1crypto/keys.py), `asn1crypto.algos` + - [X509 certificates](asn1crypto/x509.py), `asn1crypto.x509` + - [Certificate revocation lists (CRLs)](asn1crypto/crl.py), `asn1crypto.crl` + - [Online certificate status protocol (OCSP)](asn1crypto/ocsp.py), `asn1crypto.ocsp` + - [Private key encryption (PKCS#5)](asn1crypto/pkcs5.py), `asn1crypto.pkcs5` + - [Private key/certificate containers (PKCS#12)](asn1crypto/pkcs12.py), `asn1crypto.pkcs12` + - [Cryptographic message syntax (CMS, PKCS#7)](asn1crypto/cms.py), `asn1crypto.cms` + - [Time stamp protocol (TSP)](asn1crypto/tsp.py), `asn1crypto.tsp` + - [PDF signatures](asn1crypto/pdf.py), `asn1crypto.pdf` + ## Why Another Python ASN.1 Library? -- cgit v1.2.3 From a27ca53c325e83008afd26420443eb578c5d8101 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 09:16:47 -0400 Subject: Docs formatting --- docs/readme.md | 2 ++ readme.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/readme.md b/docs/readme.md index d41e2e6..68c30d3 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -6,6 +6,8 @@ pre-defined data types. - [Tutorial](tutorial.md) +## Reference + - [Universal types](../asn1crypto/core.py), `asn1crypto.core` - [Digest, HMAC, signed digest and encryption algorithms](../asn1crypto/algos.py), `asn1crypto.algos` - [Private and public keys](../asn1crypto/keys.py), `asn1crypto.algos` diff --git a/readme.md b/readme.md index ac1ad13..0c3f10f 100644 --- a/readme.md +++ b/readme.md @@ -46,6 +46,8 @@ pre-defined data types. - [Tutorial](docs/tutorial.md) +### Reference + - [Universal types](asn1crypto/core.py), `asn1crypto.core` - [Digest, HMAC, signed digest and encryption algorithms](asn1crypto/algos.py), `asn1crypto.algos` - [Private and public keys](asn1crypto/keys.py), `asn1crypto.algos` -- cgit v1.2.3 From 77bf0ad2ac8a13f5fb2c365e6a0fdb689d22e391 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 09:28:36 -0400 Subject: More docs fixes --- docs/readme.md | 2 +- readme.md | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/readme.md b/docs/readme.md index 68c30d3..b987385 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -10,7 +10,7 @@ pre-defined data types. - [Universal types](../asn1crypto/core.py), `asn1crypto.core` - [Digest, HMAC, signed digest and encryption algorithms](../asn1crypto/algos.py), `asn1crypto.algos` - - [Private and public keys](../asn1crypto/keys.py), `asn1crypto.algos` + - [Private and public keys](../asn1crypto/keys.py), `asn1crypto.keys` - [X509 certificates](../asn1crypto/x509.py), `asn1crypto.x509` - [Certificate revocation lists (CRLs)](../asn1crypto/crl.py), `asn1crypto.crl` - [Online certificate status protocol (OCSP)](../asn1crypto/ocsp.py), `asn1crypto.ocsp` diff --git a/readme.md b/readme.md index 0c3f10f..c9336e9 100644 --- a/readme.md +++ b/readme.md @@ -4,20 +4,20 @@ A fast, pure Python library for parsing and serializing ASN.1 structures. In addition to an ASN.1 BER/DER decoder and DER serializer, the project includes a bunch of ASN.1 structures for use with various common cryptography standards: -| Standard | Module | Source | -| ---------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| X509 | `asn1crypto.x509` | [RFC5280](https://tools.ietf.org/html/rfc5280) | -| CRL | `asn1crypto.crl` | [RFC5280](https://tools.ietf.org/html/rfc5280) | -| OCSP | `asn1crypto.ocsp` | [RFC6960](https://tools.ietf.org/html/rfc6960) | -| PKCS#12 | `asn1crypto.pkcs12` | [RFC7292](https://tools.ietf.org/html/rfc7292) | -| PKCS#8 | `asn1crypto.keys` | [RFC5208](https://tools.ietf.org/html/rfc5208) | -| PKCS#1 v2.1 (RSA keys) | `asn1crypto.keys` | [RFC3447](https://tools.ietf.org/html/rfc3447) | -| DSA keys | `asn1crypto.keys` | [RFC3279](https://tools.ietf.org/html/rfc3279) | -| Elliptic curve keys | `asn1crypto.keys` | [SECG SEC1 V2](http://www.secg.org/sec1-v2.pdf) | -| PKCS#5 v2.1 | `asn1crypto.pkcs5` | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | -| CMS (and PKCS#7) | `asn1crypto.cms` | [RFC5652](https://tools.ietf.org/html/rfc5652), [RFC2315](https://tools.ietf.org/html/rfc2315) | -| TSP | `asn1crypto.tsp` | [RFC3161](https://tools.ietf.org/html/rfc3161) | -| PDF signatures | `asn1crypto.pdf` | [PDF 1.7](http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf) | +| Standard | Module | Source | +| ---------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| X509 | [`asn1crypto.x509`](asn1crypto/x509.py) | [RFC5280](https://tools.ietf.org/html/rfc5280) | +| CRL | [`asn1crypto.crl`](asn1crypto/crl.py) | [RFC5280](https://tools.ietf.org/html/rfc5280) | +| OCSP | [`asn1crypto.ocsp`](asn1crypto/ocsp.py) | [RFC6960](https://tools.ietf.org/html/rfc6960) | +| PKCS#12 | [`asn1crypto.pkcs12`](asn1crypto/pkcs12.py) | [RFC7292](https://tools.ietf.org/html/rfc7292) | +| PKCS#8 | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC5208](https://tools.ietf.org/html/rfc5208) | +| PKCS#1 v2.1 (RSA keys) | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC3447](https://tools.ietf.org/html/rfc3447) | +| DSA keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC3279](https://tools.ietf.org/html/rfc3279) | +| Elliptic curve keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [SECG SEC1 V2](http://www.secg.org/sec1-v2.pdf) | +| PKCS#5 v2.1 | [`asn1crypto.pkcs5`](asn1crypto/pkcs5.py) | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | +| CMS (and PKCS#7) | [`asn1crypto.cms`](asn1crypto/cms.py) | [RFC5652](https://tools.ietf.org/html/rfc5652), [RFC2315](https://tools.ietf.org/html/rfc2315) | +| TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC3161](https://tools.ietf.org/html/rfc3161) | +| PDF signatures | [`asn1crypto.pdf`](asn1crypto/pdf.py) | [PDF 1.7](http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf) | ## License @@ -50,7 +50,7 @@ pre-defined data types. - [Universal types](asn1crypto/core.py), `asn1crypto.core` - [Digest, HMAC, signed digest and encryption algorithms](asn1crypto/algos.py), `asn1crypto.algos` - - [Private and public keys](asn1crypto/keys.py), `asn1crypto.algos` + - [Private and public keys](asn1crypto/keys.py), `asn1crypto.keys` - [X509 certificates](asn1crypto/x509.py), `asn1crypto.x509` - [Certificate revocation lists (CRLs)](asn1crypto/crl.py), `asn1crypto.crl` - [Online certificate status protocol (OCSP)](asn1crypto/ocsp.py), `asn1crypto.ocsp` -- cgit v1.2.3 From 703b4343d59d589460a34f480836be5769cf06b7 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 15:20:40 -0400 Subject: Fixed serialization of ObjectIdentifier --- asn1crypto/core.py | 18 ++++++-- tests.py | 39 +++++++++++++---- tests/fixtures/universal/object_identifier.der | 1 + tests/test_core.py | 33 ++++++++++++++ tests/unittest_data.py | 60 ++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/universal/object_identifier.der create mode 100644 tests/test_core.py create mode 100644 tests/unittest_data.py diff --git a/asn1crypto/core.py b/asn1crypto/core.py index b8bbcef..4b36ea5 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1272,15 +1272,25 @@ class ObjectIdentifier(Primitive, ValueMap): self._native = value if self._map is not None: - if value is self._reverse_map: + if value in self._reverse_map: value = self._reverse_map[value] self.contents = b'' - for part in value.split('.'): - encoded_part = byte_cls(0x7F & part) + first = None + for index, part in enumerate(value.split('.')): + part = int(part) + + # The first two parts are merged into a single byte + if index == 0: + first = part + continue + elif index == 1: + part = (first * 40) + part + + encoded_part = chr_cls(0x7F & part) part = part >> 7 while part > 0: - encoded_part = byte_cls(0x80 | (0x7F & part)) + encoded_part + encoded_part = chr_cls(0x80 | (0x7F & part)) + encoded_part part = part >> 7 self.contents += encoded_part diff --git a/tests.py b/tests.py index 79dff27..333ac61 100644 --- a/tests.py +++ b/tests.py @@ -1,19 +1,42 @@ # coding: utf-8 from __future__ import unicode_literals +import sys import unittest +import re -from tests.test_cms import CMSTests #pylint: disable=E0611,W0611 -from tests.test_crl import CRLTests #pylint: disable=E0611,W0611 -from tests.test_keys import KeysTests #pylint: disable=E0611,W0611 -from tests.test_ocsp import OCSPTests #pylint: disable=E0611,W0611 -from tests.test_tsp import TSPTests #pylint: disable=E0611,W0611 -from tests.test_x509 import X509Tests #pylint: disable=E0611,W0611 +if sys.version_info < (3,): + byte_cls = str +else: + byte_cls = bytes + +from tests.test_cms import CMSTests #pylint: disable=E0611 +from tests.test_crl import CRLTests #pylint: disable=E0611 +from tests.test_keys import KeysTests #pylint: disable=E0611 +from tests.test_ocsp import OCSPTests #pylint: disable=E0611 +from tests.test_tsp import TSPTests #pylint: disable=E0611 +from tests.test_x509 import X509Tests #pylint: disable=E0611 +from tests.test_core import CoreTests #pylint: disable=E0611 + + +test_classes = [CMSTests, CRLTests, KeysTests, OCSPTests, TSPTests, X509Tests, CoreTests] if __name__ == '__main__': + matcher = None + if len(sys.argv) > 1: + matcher = sys.argv[1] + if isinstance(matcher, byte_cls): + matcher = matcher.decode('utf-8') + suite = unittest.TestSuite() loader = unittest.TestLoader() - for test_class in [CMSTests, CRLTests, KeysTests, OCSPTests, TSPTests, X509Tests]: - suite.addTest(loader.loadTestsFromTestCase(test_class)) + for test_class in test_classes: + if matcher: + names = loader.getTestCaseNames(test_class) + for name in names: + if re.search(matcher, name): + suite.addTest(test_class(name)) + else: + suite.addTest(loader.loadTestsFromTestCase(test_class)) unittest.TextTestRunner().run(suite) diff --git a/tests/fixtures/universal/object_identifier.der b/tests/fixtures/universal/object_identifier.der new file mode 100644 index 0000000..c86e6b3 --- /dev/null +++ b/tests/fixtures/universal/object_identifier.der @@ -0,0 +1 @@ + *H  \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..af575e8 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,33 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import os + +from asn1crypto import core + +from .unittest_data import DataDecorator, data + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + +@DataDecorator +class CoreTests(unittest.TestCase): + + #pylint: disable=C0326 + @staticmethod + def type_info(): + return ( + ('universal/object_identifier.der', core.ObjectIdentifier, '1.2.840.113549.1.1.1'), + ) + + @data('type_info') + def parse_universal_type(self, input_filename, type_class, native): + with open(os.path.join(fixtures_dir, input_filename), 'rb') as f: + der = f.read() + parsed = type_class.load(der) + + self.assertEqual(native, parsed.native) + self.assertEqual(der, parsed.dump(force=True)) diff --git a/tests/unittest_data.py b/tests/unittest_data.py new file mode 100644 index 0000000..25dd58c --- /dev/null +++ b/tests/unittest_data.py @@ -0,0 +1,60 @@ +# Written by Will Bond +# +# The author or authors of this code dedicate any and all copyright interest in +# this code to the public domain. We make this dedication for the benefit of the +# public at large and to the detriment of our heirs and successors. We intend +# this dedication to be an overt act of relinquishment in perpetuity of all +# present and future rights to this code under copyright law. + + +def data(provider_method, first_param_name_suffix=False): + """ + A method decorator for unittest.TestCase classes that configured a + static method to be used to provide multiple sets of test data to a single + test + + :param provider_method: + The name of the staticmethod of the class to use as the data provider + + :param first_param_name_suffix: + If the first parameter for each set should be appended to the method + name to generate the name of the test. Otherwise integers are used. + + :return: + The decorated function + """ + + def test_func_decorator(test_func): + test_func._provider_method = provider_method #pylint: disable=W0212 + test_func._provider_name_suffix = first_param_name_suffix #pylint: disable=W0212 + return test_func + return test_func_decorator + + +def DataDecorator(cls): + """ + A class decorator that works with the @provider decorator to generate test + method from a data provider + """ + + def generate_test_func(name, original_function, num, params): + if original_function._provider_name_suffix: #pylint: disable=W0212 + data_name = params[0] + params = params[1:] + else: + data_name = num + expanded_name = 'test_%s_%s' % (name, data_name) + # We used expanded variable names here since this line is present in + # backtraces that are generated from test failures. + generated_test_function = lambda self: original_function(self, *params) + setattr(cls, expanded_name, generated_test_function) + + for name in dir(cls): + func = getattr(cls, name) + if hasattr(func, '_provider_method'): + num = 1 + for params in getattr(cls, func._provider_method)(): #pylint: disable=W0212 + generate_test_func(name, func, num, params) + num += 1 + + return cls -- cgit v1.2.3 From b5354a4dbe7ab115b5695c7123155b446272c6bc Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 15:21:59 -0400 Subject: Fixed bugs related to parsing info about encryption algorithms --- asn1crypto/algos.py | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++ asn1crypto/cms.py | 5 +- asn1crypto/pkcs5.py | 95 ++++---------------------------------- 3 files changed, 141 insertions(+), 88 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index cd5e120..fb801e2 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -150,3 +150,132 @@ class EncryptionAlgorithm(Sequence): 'aes192': OctetString, 'aes256': OctetString, } + + @property + def key_length(self): + """ + Returns the key length to pass to the cipher. The PKCS#5 spec does + not specify a way to store the RC5 key length, however this tends not + to be a problem since OpenSSL does not support RC5 in PKCS#8 and OS X + does not provide an RC5 cipher for use in the Security Transforms + library. + + :raises: + ValueError - when the key length can not be determined + + :return: + An integer representing the length in bytes + """ + + cipher = self['algorithm'].native + + cipher_lengths = { + 'des': 8, + 'tripledes_3key': 24, + 'aes128': 16, + 'aes192': 24, + 'aes256': 32, + } + + if cipher in cipher_lengths: + return cipher_lengths[cipher] + + if cipher == 'rc2': + rc2_params = self['parameters'].parsed['encryption_scheme']['parameters'].parsed + rc2_parameter_version = rc2_params['rc2_parameter_version'].native + + # See page 24 of http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf + encoded_key_bits_map = { + 160: 5, # 40-bit + 120: 8, # 64-bit + 58: 16, # 128-bit + } + + if rc2_parameter_version in encoded_key_bits_map: + return encoded_key_bits_map[rc2_parameter_version] + + if rc2_parameter_version >= 256: + return rc2_parameter_version + + if rc2_parameter_version is None: + return 4 # 32-bit default + + raise ValueError('Invalid RC2 parameter version found in EncryptionAlgorithm parameters') + + raise ValueError('Unable to determine the key length for the encryption scheme "%s"' % cipher) + + @property + def encryption_cipher(self): + """ + Returns the name of the symmetric encryption cipher to use. The key + length can be retrieved via the .key_length property to disabiguate + between different variations of TripleDES, AES, and the RC* ciphers. + + :return: + A unicode string from one of the following: "rc2", "rc5", "des", "tripledes", "aes" + """ + + cipher = self['algorithm'].native + + cipher_map = { + 'des': 'des', + 'tripledes_3key': 'tripledes', + 'aes128': 'aes', + 'aes192': 'aes', + 'aes256': 'aes', + 'rc2': 'rc2', + 'rc5': 'rc5', + } + if cipher in cipher_map: + return cipher_map[cipher] + + raise ValueError('Unrecognized encryption cipher "%s"' % cipher) + + @property + def encryption_block_size(self): + """ + Returns the block size of the encryption cipher, in bytes. + + :return: + An integer that is the block size in bytes + """ + + cipher = self['algorithm'].native + + cipher_map = { + 'des': 8, + 'tripledes_3key': 8, + 'aes128': 16, + 'aes192': 16, + 'aes256': 16, + 'rc2': 8, + } + if cipher in cipher_map: + return cipher_map[cipher] + + if cipher == 'rc5': + return self['parameters'].parsed['block_size_in_bits'].native / 8 + + raise ValueError('Unrecognized encryption cipher "%s", can not determine block size' % cipher) + + @property + def encryption_iv(self): + """ + Returns the byte string of the initialization vector for the encryption + scheme. Only the PBES2 stores the IV in the params. For PBES1, the IV + is derived from the KDF and this property will return None. + + :return: + A byte string or None + """ + + cipher = self['algorithm'].native + + if cipher == 'rc2' or cipher == 'rc5': + return self['parameters'].parsed['iv'].native + + # For DES/Triple DES and AES the IV is the entirety of the parameters + if cipher.find('.') == -1: + return self['parameters'].native + + raise ValueError('Unrecognized encryption cipher "%s", can not determine initialization vector' % cipher) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 28380a6..0c2c42f 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -9,7 +9,6 @@ except (ImportError): from .algos import ( DigestAlgorithm, - EncryptionAlgorithm, SignedDigestAlgorithm, HmacAlgorithm, ) @@ -30,7 +29,7 @@ from .core import ( from .crl import CertificateList from .keys import PublicKeyInfo from .ocsp import OCSPResponse -from .pkcs5 import KdfAlgorithm +from .pkcs5 import KdfAlgorithm, Pkcs5EncryptionAlgorithm from .x509 import Attributes, Certificate, Extensions, GeneralNames, Name @@ -522,7 +521,7 @@ class RecipientInfos(SetOf): class EncryptedContentInfo(Sequence): _fields = [ ('content_type', ContentType), - ('content_encryption_algorithm', EncryptionAlgorithm), + ('content_encryption_algorithm', Pkcs5EncryptionAlgorithm), ('encrypted_content', OctetString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), ] diff --git a/asn1crypto/pkcs5.py b/asn1crypto/pkcs5.py index 17d1874..1043567 100644 --- a/asn1crypto/pkcs5.py +++ b/asn1crypto/pkcs5.py @@ -114,7 +114,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): return self['parameters'].parsed['key_derivation_func']['algorithm'].native if encryption_algo.find('.') == -1: - encryption_algo, _ = self['algorithm'].native.split('_', 2) + encryption_algo, _ = self['algorithm'].native.split('_', 1) if encryption_algo == 'pbes1': return 'pbkdf1' @@ -139,7 +139,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): return self['parameters'].parsed['key_derivation_func']['parameters']['prf']['algorithm'].native if encryption_algo.find('.') == -1: - _, hmac_algo, _ = self['algorithm'].native.split('_', 3) + _, hmac_algo, _ = self['algorithm'].native.split('_', 2) return hmac_algo raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation hmac algorithm' % encryption_algo) @@ -164,7 +164,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): return salt.native if encryption_algo.find('.') == -1: - return self['parameters'].parsed['salt'].native + return self['parameters']['salt'].native raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation salt' % encryption_algo) @@ -180,10 +180,10 @@ class Pkcs5EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - return self['parameters'].parsed['key_derivation_func']['algorithm']['iteration_count'].native + return self['parameters']['key_derivation_func']['algorithm']['iteration_count'].native if encryption_algo.find('.') == -1: - return self['parameters'].parsed['iterations'].native + return self['parameters']['iterations'].native raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation iterations' % encryption_algo) @@ -206,7 +206,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - key_length = self['parameters'].parsed['key_derivation_func']['algorithm']['key_length'].native + key_length = self['parameters']['key_derivation_func']['algorithm']['key_length'].native if key_length is not None: return key_length @@ -215,43 +215,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): # practical terms, neither OpenSSL or OS X support RC5 for PKCS#8 # so it is unlikely to be an issue that is run into. - cipher = self['parameters'].parsed['encryption_scheme']['algorithm'].native - - cipher_lengths = { - 'des': 8, - 'tripledes_3key': 24, - 'aes128': 16, - 'aes192': 24, - 'aes256': 32, - } - - if cipher in cipher_lengths: - return cipher_lengths[cipher] - - if cipher == 'rc2': - rc2_params = self['parameters'].parsed['encryption_scheme']['parameters'].parsed - rc2_parameter_version = rc2_params['rc2_parameter_version'].native - - # See page 24 of http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf - encoded_key_bits_map = { - 160: 5, # 40-bit - 120: 8, # 64-bit - 58: 16, # 128-bit - } - - if rc2_parameter_version in encoded_key_bits_map: - return encoded_key_bits_map[rc2_parameter_version] - - if rc2_parameter_version >= 256: - return rc2_parameter_version - - if rc2_parameter_version is None: - return 4 # 32-bit default - - raise ValueError('Invalid RC2 parameter version found in PBES2 encryption scheme parameters') - - # There - raise ValueError('Unable to determine the key length for the encryption scheme "%s"' % cipher) + return self['parameters']['encryption_scheme'].key_length if encryption_algo.find('.') == -1: return { @@ -285,20 +249,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - cipher = self['parameters'].parsed['encryption_scheme']['algorithm'].native - cipher_map = { - 'des': 'des', - 'tripledes_3key': 'tripledes', - 'aes128': 'aes', - 'aes192': 'aes', - 'aes256': 'aes', - 'rc2': 'rc2', - 'rc5': 'rc5', - } - if cipher in cipher_map: - return cipher_map[cipher] - - raise ValueError('Unrecognized encryption cipher "%s"' % cipher) + return self['parameters']['encryption_scheme'].encryption_cipher if encryption_algo.find('.') == -1: return { @@ -331,23 +282,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - scheme = self['parameters'].parsed['encryption_scheme'] - cipher = scheme['algorithm'].native - cipher_map = { - 'des': 8, - 'tripledes_3key': 8, - 'aes128': 16, - 'aes192': 16, - 'aes256': 16, - 'rc2': 8, - } - if cipher in cipher_map: - return cipher_map[cipher] - - if cipher == 'rc5': - return scheme['parameters'].parsed['block_size_in_bits'].native / 8 - - raise ValueError('Unrecognized encryption cipher "%s", can not determine block size' % cipher) + return self['parameters']['encryption_scheme'].encryption_block_size if encryption_algo.find('.') == -1: return { @@ -381,17 +316,7 @@ class Pkcs5EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - scheme = self['parameters'].parsed['encryption_scheme'] - cipher = scheme['algorithm'].native - - if cipher == 'rc2' or cipher == 'rc5': - return scheme['parameters'].parsed['iv'].native - - # For DES/Triple DES and AES the IV is the entirety of the parameters - if cipher.find('.') == -1: - return scheme['parameters'].native - - raise ValueError('Unrecognized encryption cipher "%s", can not determine initialization vector' % cipher) + return self['parameters']['encryption_scheme'].encryption_iv # All of the PBES1 algos use their KDF to create the IV. For the pbkdf1, # the KDF is told to generate a key that is an extra 8 bytes long, and -- cgit v1.2.3 From 7bc28e34d25573b850dfda538c41e8d766f510c8 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 15:22:23 -0400 Subject: Fixed int to bytes conversion code on Python 3 to use minimal bytes --- asn1crypto/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 4b36ea5..f13a9b2 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -65,7 +65,10 @@ else: return bytes([num]) def int_to_bytes(value, signed=False): - return value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) + result = value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) + if not signed: + return result.lstrip(b'\x00') + return result def int_from_bytes(value, signed=False): return int.from_bytes(value, 'big', signed=signed) -- cgit v1.2.3 From 78f043cbf7d8d22efe240e67c2792cd94e702d0b Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 15:23:18 -0400 Subject: Improved error messaging when unexpected types are passed --- asn1crypto/core.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index f13a9b2..c071dc7 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -205,18 +205,18 @@ class Asn1Value(): if tag_type is not None: if tag_type not in ('implicit', 'explicit'): - raise ValueError('tag_type is not one of "implicit", "explicit"') + raise ValueError('tag_type must be one of "implicit", "explicit" - is %s' % repr(tag_type)) self.tag_type = tag_type if class_ is None: class_ = 'context' if class_ not in CLASS_NAME_TO_NUM_MAP: - raise ValueError('class_ is not one of "universal", "application", "context", "private"') + raise ValueError('class_ must be one of "universal", "application", "context", "private" - is %s' % repr(class_)) class_ = CLASS_NAME_TO_NUM_MAP[class_] if tag is not None: if not isinstance(tag, int): - raise ValueError('tag is not an integer') + raise ValueError('tag must be an integer, not %s' % tag.__class__.__name__) if tag_type == 'implicit': self.class_ = class_ @@ -227,7 +227,7 @@ class Asn1Value(): else: if class_ is not None: if class_ not in CLASS_NUM_TO_NAME_MAP: - raise ValueError('class_ is not one of "universal", "application", "context", "private"') + raise ValueError('class_ must be one of "universal", "application", "context", "private" - is %s' % repr(class_)) self.class_ = CLASS_NAME_TO_NUM_MAP[class_] if tag is not None: @@ -663,7 +663,7 @@ class Primitive(Asn1Value): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string' % self.__class__.__name__) + raise ValueError('%s value must be a byte string, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value self.contents = value @@ -691,7 +691,7 @@ class AbstractString(Primitive): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string' % self.__class__.__name__) + raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value self.contents = value.encode(self._encoding) @@ -858,7 +858,7 @@ class BitString(Primitive, ValueMap, object): """ if not isinstance(value, int) and not isinstance(value, tuple): - raise ValueError('%s value must be an integer or a tuple of ones and zeros' % self.__class__.__name__) + raise ValueError('%s value must be an integer or a tuple of ones and zeros, not %s' % (self.__class__.__name__, value.__class__.__name__)) if isinstance(value, tuple): self._native = value @@ -973,7 +973,7 @@ class OctetBitString(Primitive): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string' % self.__class__.__name__) + raise ValueError('%s value must be a byte string, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value # Set the unused bits to 0 @@ -1070,7 +1070,7 @@ class IntegerBitString(Primitive): """ if not isinstance(value, int): - raise ValueError('%s value must be an integer' % self.__class__.__name__) + raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value # Set the unused bits to 0 @@ -1192,7 +1192,7 @@ class IntegerOctetString(OctetString): """ if not isinstance(value, int): - raise ValueError('%s value must be an integer' % self.__class__.__name__) + raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value # Set the unused bits to 0 @@ -1270,7 +1270,7 @@ class ObjectIdentifier(Primitive, ValueMap): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string' % self.__class__.__name__) + raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value @@ -1390,7 +1390,7 @@ class Enumerated(Integer): """ if not isinstance(value, int) and not isinstance(value, str_cls): - raise ValueError('%s value must be an integer or a unicode string' % self.__class__.__name__) + raise ValueError('%s value must be an integer or a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) if isinstance(value, str_cls): if value not in self._reverse_map: -- cgit v1.2.3 From c33f2e014820d905e47d72bfc384610272f61106 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 15:23:48 -0400 Subject: Fixed tagging on some PKCS#12 structures --- asn1crypto/pkcs12.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 17fb1b9..12eac9e 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -115,7 +115,7 @@ class CertId(ObjectIdentifier): class CertBag(Sequence): _fields = [ ('cert_id', CertId), - ('cert_value', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ('cert_value', OctetString, {'tag_type': 'explicit', 'tag': 0}), ] _oid_pair = ('cert_id', 'cert_value') @@ -127,14 +127,14 @@ class CertBag(Sequence): class CrlBag(Sequence): _fields = [ ('crl_id', ObjectIdentifier), - ('crl_value', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ('crl_value', OctetString, {'tag_type': 'explicit', 'tag': 0}), ] class SecretBag(Sequence): _fields = [ ('secret_type_id', ObjectIdentifier), - ('secret_value', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ('secret_value', OctetString, {'tag_type': 'explicit', 'tag': 0}), ] -- cgit v1.2.3 From 7a5fbf6d0635640321bc1e69b3a5c111cc618cd5 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 15:24:25 -0400 Subject: Added a force parameter to .dump() to ensure DER-encoding --- asn1crypto/core.py | 173 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 13 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index c071dc7..fa3ddab 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -257,10 +257,14 @@ class Asn1Value(): """ return '<%s %s>' % (self.__class__.__name__, repr(self.contents)) - def dump(self): + def dump(self, force=False): """ Encodes the value using DER + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + :return: A byte string of the DER-encoded value """ @@ -354,10 +358,14 @@ class NoValue(Asn1Value): return None - def dump(self): + def dump(self, force=False): """ Encodes the value using DER + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + :return: A byte string of the DER-encoded value """ @@ -428,10 +436,14 @@ class Any(Asn1Value): self._parsed = (parsed_value, spec, spec_params) return self._parsed[0] - def dump(self): + def dump(self, force=False): """ Encodes the value using DER + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + :return: A byte string of the DER-encoded value """ @@ -439,7 +451,7 @@ class Any(Asn1Value): if self._parsed is None: self.parse() - return self._parsed[0].dump() + return self._parsed[0].dump(force=force) class Choice(Asn1Value): @@ -615,15 +627,19 @@ class Choice(Asn1Value): return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag) - def dump(self): + def dump(self, force=False): """ Encodes the value using DER + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + :return: A byte string of the DER-encoded value """ - return self.chosen.dump() + return self.chosen.dump(force=force) class Primitive(Asn1Value): @@ -671,6 +687,25 @@ class Primitive(Asn1Value): if self.trailer != b'': self.trailer = b'' + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + if force: + native = self.native + self.contents = None + self.set(native) + + return Asn1Value.dump(self) + class AbstractString(Primitive): """ @@ -1050,6 +1085,28 @@ class OctetBitString(Primitive): return self._parsed[0] + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + if force: + if self._parsed is not None: + native = self.parsed.dump(force=force) + else: + native = self.native + self.contents = None + self.set(native) + + return Asn1Value.dump(self) + class IntegerBitString(Primitive): """ @@ -1174,6 +1231,28 @@ class OctetString(Primitive): return self._parsed[0] + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + if force: + if self._parsed is not None: + native = self.parsed.dump(force=force) + else: + native = self.native + self.contents = None + self.set(native) + + return Asn1Value.dump(self) + class IntegerOctetString(OctetString): """ @@ -1235,7 +1314,7 @@ class Null(Primitive): None """ - pass + self.contents = b'' @property def native(self): @@ -1651,19 +1730,29 @@ class Sequence(Asn1Value): for info in self._fields: yield info[0] - def _set_contents(self): + def _set_contents(self, force=False): """ Updates the .contents attribute of the value with the encoded value of all of the child objects + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data """ + if self.children is None: + self._parse_children() + self.contents = b'' for index, info in enumerate(self._fields): child = self.children[index] if isinstance(child, tuple): - child_dump = child[3] + child[4] + child[5] + if force: + child_dump = self._lazy_child(index).dump(force=force) + else: + child_dump = child[3] + child[4] + child[5] else: - child_dump = child.dump() + child_dump = child.dump(force=force) # Skip values that are the same as the default if len(info) > 2 and 'default' in info[2]: default_value = info[1](**info[2]) @@ -1704,6 +1793,10 @@ class Sequence(Asn1Value): ValueError - when an error occurs parsing child objects """ + if self.contents is None: + self.children = [None] * len(self._fields) + return + try: self.children = [] contents_length = len(self.contents) @@ -1803,6 +1896,23 @@ class Sequence(Asn1Value): self._native[self._fields[index][0]] = child.native return self._native + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + if force: + self._set_contents(force=force) + + return Asn1Value.dump(self) + class SequenceOf(Asn1Value): """ @@ -1943,14 +2053,34 @@ class SequenceOf(Asn1Value): for index in range(0, len(self.children)): yield self._lazy_child(index) - def _set_contents(self): + def _set_contents(self, force=False): """ Encodes all child objects into the contents for this object + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data """ + if self.children is None: + self._parse_children() + self.contents = b'' - for child in self: - self.contents += child.dump() + for index, info in enumerate(self._fields): + child = self.children[index] + if isinstance(child, tuple): + if force: + child_dump = self._lazy_child(index).dump(force=force) + else: + child_dump = child[3] + child[4] + child[5] + else: + child_dump = child.dump(force=force) + # Skip values that are the same as the default + if len(info) > 2 and 'default' in info[2]: + default_value = info[1](**info[2]) + if default_value.dump() == child_dump: + continue + self.contents += child_dump self.header = None if self.trailer != b'': self.trailer = b'' @@ -2008,6 +2138,23 @@ class SequenceOf(Asn1Value): self._native = [child.native for child in self] return self._native + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + if force: + self._set_contents(force=force) + + return Asn1Value.dump(self) + class Set(Sequence): """ -- cgit v1.2.3 From 59af99deea7a0b93b6a901b8effddbab5426f65c Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 16:19:04 -0400 Subject: Folded PKCS#5 algos into asn1crypto.algos --- asn1crypto/algos.py | 329 ++++++++++++++++++++++++++++++++++++++++++++---- asn1crypto/cms.py | 7 +- asn1crypto/keys.py | 5 +- asn1crypto/pkcs5.py | 354 ---------------------------------------------------- docs/readme.md | 1 - lint.py | 1 - readme.md | 3 +- 7 files changed, 313 insertions(+), 387 deletions(-) delete mode 100644 asn1crypto/pkcs5.py diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index fb801e2..eb32a60 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -2,11 +2,13 @@ from __future__ import unicode_literals from __future__ import absolute_import -from .core import Any, Integer, ObjectIdentifier, OctetString, Sequence +from .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence -# OID in this file are pulled from https://tools.ietf.org/html/rfc3279, -# https://tools.ietf.org/html/rfc4055 and https://tools.ietf.org/html/rfc5758 +# Structures and OIDs in this file are pulled from +# https://tools.ietf.org/html/rfc3279, https://tools.ietf.org/html/rfc4055, +# https://tools.ietf.org/html/rfc5758, https://tools.ietf.org/html/rfc7292, +# http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf class AlgorithmIdentifier(Sequence): _fields = [ @@ -100,6 +102,39 @@ class SignedDigestAlgorithm(Sequence): ] +class Pbkdf2Salt(Choice): + _fields = [ + ('specified', OctetString), + ('other_source', AlgorithmIdentifier), + ] + + +class Pbkdf2Params(Sequence): + _fields = [ + ('salt', Pbkdf2Salt), + ('iteration_count', Integer), + ('key_length', Integer, {'optional': True}), + ('prf', HmacAlgorithm, {'optional': True}), + ] + + +class KdfAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.5.12': 'pbkdf2' + } + + +class KdfAlgorithm(Sequence): + _fields = [ + ('algorithm', KdfAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'pbkdf2': Pbkdf2Params + } + + class Rc2Params(Sequence): _fields = [ ('rc2_parameter_version', Integer, {'optional': True}), @@ -122,6 +157,13 @@ class Rc5Params(Sequence): ] +class Pbes1Params(Sequence): + _fields = [ + ('salt', OctetString), + ('iterations', Integer), + ] + + class EncryptionAlgorithmId(ObjectIdentifier): _map = { '1.3.14.3.2.7': 'des', @@ -131,6 +173,21 @@ class EncryptionAlgorithmId(ObjectIdentifier): '2.16.840.1.101.3.4.1.2': 'aes128', '2.16.840.1.101.3.4.1.22': 'aes192', '2.16.840.1.101.3.4.1.42': 'aes256', + # From PKCS#5 + '1.2.840.113549.1.5.13': 'pbes2', + '1.2.840.113549.1.5.1': 'pbes1_md2_des', + '1.2.840.113549.1.5.3': 'pbes1_md5_des', + '1.2.840.113549.1.5.4': 'pbes1_md2_rc2', + '1.2.840.113549.1.5.6': 'pbes1_md5_rc2', + '1.2.840.113549.1.5.10': 'pbes1_sha1_des', + '1.2.840.113549.1.5.11': 'pbes1_sha1_rc2', + # From PKCS#12 + '1.2.840.113549.1.12.1.1': 'pkcs12_sha1_rc4_128', + '1.2.840.113549.1.12.1.2': 'pkcs12_sha1_rc4_40', + '1.2.840.113549.1.12.1.3': 'pkcs12_sha1_tripledes_3key', + '1.2.840.113549.1.12.1.4': 'pkcs12_sha1_tripledes_2key', + '1.2.840.113549.1.12.1.5': 'pkcs12_sha1_rc2_128', + '1.2.840.113549.1.12.1.6': 'pkcs12_sha1_rc2_40', } @@ -149,12 +206,126 @@ class EncryptionAlgorithm(Sequence): 'aes128': OctetString, 'aes192': OctetString, 'aes256': OctetString, + # From PKCS#5 + 'pbes1_md2_des': Pbes1Params, + 'pbes1_md5_des': Pbes1Params, + 'pbes1_md2_rc2': Pbes1Params, + 'pbes1_md5_rc2': Pbes1Params, + 'pbes1_sha1_des': Pbes1Params, + 'pbes1_sha1_rc2': Pbes1Params, + # From PKCS#12 + 'pkcs12_sha1_rc4_128': Pbes1Params, + 'pkcs12_sha1_rc4_40': Pbes1Params, + 'pkcs12_sha1_tripledes_3key': Pbes1Params, + 'pkcs12_sha1_tripledes_2key': Pbes1Params, + 'pkcs12_sha1_rc2_128': Pbes1Params, + 'pkcs12_sha1_rc2_40': Pbes1Params, } + @property + def kdf(self): + """ + Returns the name of the key derivation function to use. + + :return: + A unicode from of one of the following: "pbkdf1", "pbkdf2", "pkcs12_kdf" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + return self['parameters'].parsed['key_derivation_func']['algorithm'].native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + encryption_algo, _ = encryption_algo.split('_', 1) + + if encryption_algo == 'pbes1': + return 'pbkdf1' + + if encryption_algo == 'pkcs12': + return 'pkcs12_kdf' + + raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation function' % encryption_algo) + + @property + def kdf_hmac(self): + """ + Returns the HMAC algorithm to use with the KDF. + + :return: + A unicode string of one of the following: "md2", "md5", "sha1", "sha224", "sha256", "sha384", "sha512" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + return self['parameters'].parsed['key_derivation_func']['parameters']['prf']['algorithm'].native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + _, hmac_algo, _ = encryption_algo.split('_', 2) + return hmac_algo + + raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation hmac algorithm' % encryption_algo) + + @property + def kdf_salt(self): + """ + Returns the byte string to use as the salt for the KDF. + + :return: + A byte string + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + salt = self['parameters'].parsed['key_derivation_func']['algorithm']['salt'] + + if salt.name == 'other_source': + raise ValueError('Can not determine key derivation salt - the reversed-for-future-use other source salt choice was specified in the PBKDF2 params structure') + + return salt.native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + return self['parameters']['salt'].native + + raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation salt' % encryption_algo) + + @property + def kdf_iterations(self): + """ + Returns the number of iterations that should be run via the KDF. + + :return: + An integer + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo == 'pbes2': + return self['parameters']['key_derivation_func']['algorithm']['iteration_count'].native + + if encryption_algo.find('.') == -1: + if encryption_algo.find('_') != -1: + return self['parameters']['iterations'].native + + raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) + + raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation iterations' % encryption_algo) + @property def key_length(self): """ - Returns the key length to pass to the cipher. The PKCS#5 spec does + Returns the key length to pass to the cipher/kdf. The PKCS#5 spec does not specify a way to store the RC5 key length, however this tends not to be a problem since OpenSSL does not support RC5 in PKCS#8 and OS X does not provide an RC5 cipher for use in the Security Transforms @@ -167,7 +338,7 @@ class EncryptionAlgorithm(Sequence): An integer representing the length in bytes """ - cipher = self['algorithm'].native + encryption_algo = self['algorithm'].native cipher_lengths = { 'des': 8, @@ -177,10 +348,10 @@ class EncryptionAlgorithm(Sequence): 'aes256': 32, } - if cipher in cipher_lengths: - return cipher_lengths[cipher] + if encryption_algo in cipher_lengths: + return cipher_lengths[encryption_algo] - if cipher == 'rc2': + if encryption_algo == 'rc2': rc2_params = self['parameters'].parsed['encryption_scheme']['parameters'].parsed rc2_parameter_version = rc2_params['rc2_parameter_version'].native @@ -202,7 +373,35 @@ class EncryptionAlgorithm(Sequence): raise ValueError('Invalid RC2 parameter version found in EncryptionAlgorithm parameters') - raise ValueError('Unable to determine the key length for the encryption scheme "%s"' % cipher) + if encryption_algo == 'pbes2': + key_length = self['parameters']['key_derivation_func']['algorithm']['key_length'].native + if key_length is not None: + return key_length + + # If the KDF params don't specify the key size, we can infer it from + # the encryption scheme for all schemes except for RC5. However, in + # practical terms, neither OpenSSL or OS X support RC5 for PKCS#8 + # so it is unlikely to be an issue that is run into. + + return self['parameters']['encryption_scheme'].key_length + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 8, + 'pbes1_md5_des': 8, + 'pbes1_md2_rc2': 8, + 'pbes1_md5_rc2': 8, + 'pbes1_sha1_des': 8, + 'pbes1_sha1_rc2': 8, + 'pkcs12_sha1_rc4_128': 16, + 'pkcs12_sha1_rc4_40': 5, + 'pkcs12_sha1_tripledes_3key': 24, + 'pkcs12_sha1_tripledes_2key': 16, + 'pkcs12_sha1_rc2_128': 16, + 'pkcs12_sha1_rc2_40': 5, + }[encryption_algo] + + raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) @property def encryption_cipher(self): @@ -215,7 +414,7 @@ class EncryptionAlgorithm(Sequence): A unicode string from one of the following: "rc2", "rc5", "des", "tripledes", "aes" """ - cipher = self['algorithm'].native + encryption_algo = self['algorithm'].native cipher_map = { 'des': 'des', @@ -226,10 +425,29 @@ class EncryptionAlgorithm(Sequence): 'rc2': 'rc2', 'rc5': 'rc5', } - if cipher in cipher_map: - return cipher_map[cipher] - - raise ValueError('Unrecognized encryption cipher "%s"' % cipher) + if encryption_algo in cipher_map: + return cipher_map[encryption_algo] + + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_cipher + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 'des', + 'pbes1_md5_des': 'des', + 'pbes1_md2_rc2': 'rc2', + 'pbes1_md5_rc2': 'rc2', + 'pbes1_sha1_des': 'des', + 'pbes1_sha1_rc2': 'rc2', + 'pkcs12_sha1_rc4_128': 'rc4', + 'pkcs12_sha1_rc4_40': 'rc4', + 'pkcs12_sha1_tripledes_3key': 'tripledes', + 'pkcs12_sha1_tripledes_2key': 'tripledes', + 'pkcs12_sha1_rc2_128': 'rc2', + 'pkcs12_sha1_rc2_40': 'rc2', + }[encryption_algo] + + raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) @property def encryption_block_size(self): @@ -240,7 +458,7 @@ class EncryptionAlgorithm(Sequence): An integer that is the block size in bytes """ - cipher = self['algorithm'].native + encryption_algo = self['algorithm'].native cipher_map = { 'des': 8, @@ -250,13 +468,32 @@ class EncryptionAlgorithm(Sequence): 'aes256': 16, 'rc2': 8, } - if cipher in cipher_map: - return cipher_map[cipher] + if encryption_algo in cipher_map: + return cipher_map[encryption_algo] - if cipher == 'rc5': + if encryption_algo == 'rc5': return self['parameters'].parsed['block_size_in_bits'].native / 8 - raise ValueError('Unrecognized encryption cipher "%s", can not determine block size' % cipher) + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_block_size + + if encryption_algo.find('.') == -1: + return { + 'pbes1_md2_des': 8, + 'pbes1_md5_des': 8, + 'pbes1_md2_rc2': 8, + 'pbes1_md5_rc2': 8, + 'pbes1_sha1_des': 8, + 'pbes1_sha1_rc2': 8, + 'pkcs12_sha1_rc4_128': 0, + 'pkcs12_sha1_rc4_40': 0, + 'pkcs12_sha1_tripledes_3key': 8, + 'pkcs12_sha1_tripledes_2key': 8, + 'pkcs12_sha1_rc2_128': 8, + 'pkcs12_sha1_rc2_40': 8, + }[encryption_algo] + + raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) @property def encryption_iv(self): @@ -269,13 +506,59 @@ class EncryptionAlgorithm(Sequence): A byte string or None """ - cipher = self['algorithm'].native + encryption_algo = self['algorithm'].native - if cipher == 'rc2' or cipher == 'rc5': + if encryption_algo in ('rc2', 'rc5'): return self['parameters'].parsed['iv'].native # For DES/Triple DES and AES the IV is the entirety of the parameters - if cipher.find('.') == -1: + if encryption_algo in ('des', 'tripledes_3key', 'aes128', 'aes192', 'aes256'): return self['parameters'].native - raise ValueError('Unrecognized encryption cipher "%s", can not determine initialization vector' % cipher) + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_iv + + # All of the PBES1 algos use their KDF to create the IV. For the pbkdf1, + # the KDF is told to generate a key that is an extra 8 bytes long, and + # that is used for the IV. For the PKCS#12 KDF, it is called with an id + # of 2 to generate the IV. In either case, we can't return the IV + # without knowing the user's password. + if encryption_algo.find('.') == -1: + return None + + raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) + + +class Pbes2Params(Sequence): + _fields = [ + ('key_derivation_func', KdfAlgorithm), + ('encryption_scheme', EncryptionAlgorithm), + ] + + +class Pbmac1Params(Sequence): + _fields = [ + ('key_derivation_func', KdfAlgorithm), + ('message_auth_scheme', HmacAlgorithm), + ] + + +class Pkcs5MacId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.5.14': 'pbmac1', + } + + +class Pkcs5MacAlgorithm(Sequence): + _fields = [ + ('algorithm', Pkcs5MacId), + ('parameters', Any), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'pbmac1': Pbmac1Params, + } + + +EncryptionAlgorithm._oid_specs['pbes2'] = Pbes2Params #pylint: disable=W0212 diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 0c2c42f..85fe9d0 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -9,8 +9,10 @@ except (ImportError): from .algos import ( DigestAlgorithm, - SignedDigestAlgorithm, + EncryptionAlgorithm, HmacAlgorithm, + KdfAlgorithm, + SignedDigestAlgorithm, ) from .core import ( Any, @@ -29,7 +31,6 @@ from .core import ( from .crl import CertificateList from .keys import PublicKeyInfo from .ocsp import OCSPResponse -from .pkcs5 import KdfAlgorithm, Pkcs5EncryptionAlgorithm from .x509 import Attributes, Certificate, Extensions, GeneralNames, Name @@ -521,7 +522,7 @@ class RecipientInfos(SetOf): class EncryptedContentInfo(Sequence): _fields = [ ('content_type', ContentType), - ('content_encryption_algorithm', Pkcs5EncryptionAlgorithm), + ('content_encryption_algorithm', EncryptionAlgorithm), ('encrypted_content', OctetString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), ] diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 76ba948..9632b33 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -5,7 +5,7 @@ from __future__ import absolute_import import hashlib from decimal import localcontext -from .algos import DigestAlgorithm +from .algos import DigestAlgorithm, EncryptionAlgorithm from .core import ( Any, Choice, @@ -20,7 +20,6 @@ from .core import ( SequenceOf, SetOf, ) -from .pkcs5 import Pkcs5EncryptionAlgorithm try: # Python 2 @@ -497,7 +496,7 @@ class EncryptedPrivateKeyInfo(Sequence): """ _fields = [ - ('encryption_algorithm', Pkcs5EncryptionAlgorithm), + ('encryption_algorithm', EncryptionAlgorithm), ('encrypted_data', OctetString), ] diff --git a/asn1crypto/pkcs5.py b/asn1crypto/pkcs5.py deleted file mode 100644 index 1043567..0000000 --- a/asn1crypto/pkcs5.py +++ /dev/null @@ -1,354 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals -from __future__ import absolute_import - -from .algos import AlgorithmIdentifier, EncryptionAlgorithm, HmacAlgorithm -from .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence - - -# The structures in this file are taken from -# http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf -# with extra OIDs from https://tools.ietf.org/html/rfc7292 (PKCS#12) - - -class Pbes1Params(Sequence): - _fields = [ - ('salt', OctetString), - ('iterations', Integer), - ] - - -class Pbkdf2Salt(Choice): - _fields = [ - ('specified', OctetString), - ('other_source', AlgorithmIdentifier), - ] - - -class Pbkdf2Params(Sequence): - _fields = [ - ('salt', Pbkdf2Salt), - ('iteration_count', Integer), - ('key_length', Integer, {'optional': True}), - ('prf', HmacAlgorithm, {'optional': True}), - ] - - -class KdfAlgorithmId(ObjectIdentifier): - _map = { - '1.2.840.113549.1.5.12': 'pbkdf2' - } - - -class KdfAlgorithm(Sequence): - _fields = [ - ('algorithm', KdfAlgorithmId), - ('parameters', Any, {'optional': True}), - ] - _oid_pair = ('algorithm', 'parameters') - _oid_specs = { - 'pbkdf2': Pbkdf2Params - } - - -class Pbes2Params(Sequence): - _fields = [ - ('key_derivation_func', KdfAlgorithm), - ('encryption_scheme', EncryptionAlgorithm), - ] - - -class Pkcs5EncryptionId(ObjectIdentifier): - _map = { - '1.2.840.113549.1.5.13': 'pbes2', - '1.2.840.113549.1.5.1': 'pbes1_md2_des', - '1.2.840.113549.1.5.3': 'pbes1_md5_des', - '1.2.840.113549.1.5.4': 'pbes1_md2_rc2', - '1.2.840.113549.1.5.6': 'pbes1_md5_rc2', - '1.2.840.113549.1.5.10': 'pbes1_sha1_des', - '1.2.840.113549.1.5.11': 'pbes1_sha1_rc2', - '1.2.840.113549.1.12.1.1': 'pkcs12_sha1_rc4_128', - '1.2.840.113549.1.12.1.2': 'pkcs12_sha1_rc4_40', - '1.2.840.113549.1.12.1.3': 'pkcs12_sha1_tripledes_3key', - '1.2.840.113549.1.12.1.4': 'pkcs12_sha1_tripledes_2key', - '1.2.840.113549.1.12.1.5': 'pkcs12_sha1_rc2_128', - '1.2.840.113549.1.12.1.6': 'pkcs12_sha1_rc2_40', - } - - -class Pkcs5EncryptionAlgorithm(Sequence): - _fields = [ - ('algorithm', Pkcs5EncryptionId), - ('parameters', Any), - ] - - _oid_pair = ('algorithm', 'parameters') - _oid_specs = { - 'pbes2': Pbes2Params, - 'pbes1_md2_des': Pbes1Params, - 'pbes1_md5_des': Pbes1Params, - 'pbes1_md2_rc2': Pbes1Params, - 'pbes1_md5_rc2': Pbes1Params, - 'pbes1_sha1_des': Pbes1Params, - 'pbes1_sha1_rc2': Pbes1Params, - 'pkcs12_sha1_rc4_128': Pbes1Params, - 'pkcs12_sha1_rc4_40': Pbes1Params, - 'pkcs12_sha1_tripledes_3key': Pbes1Params, - 'pkcs12_sha1_tripledes_2key': Pbes1Params, - 'pkcs12_sha1_rc2_128': Pbes1Params, - 'pkcs12_sha1_rc2_40': Pbes1Params, - } - - @property - def kdf(self): - """ - Returns the name of the key derivation function to use. - - :return: - A unicode from of one of the following: "pbkdf1", "pbkdf2", "pkcs12_kdf" - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - return self['parameters'].parsed['key_derivation_func']['algorithm'].native - - if encryption_algo.find('.') == -1: - encryption_algo, _ = self['algorithm'].native.split('_', 1) - - if encryption_algo == 'pbes1': - return 'pbkdf1' - - if encryption_algo == 'pkcs12': - return 'pkcs12_kdf' - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation function' % encryption_algo) - - @property - def kdf_hmac(self): - """ - Returns the HMAC algorithm to use with the KDF. - - :return: - A unicode string of one of the following: "md2", "md5", "sha1", "sha224", "sha256", "sha384", "sha512" - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - return self['parameters'].parsed['key_derivation_func']['parameters']['prf']['algorithm'].native - - if encryption_algo.find('.') == -1: - _, hmac_algo, _ = self['algorithm'].native.split('_', 2) - return hmac_algo - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation hmac algorithm' % encryption_algo) - - @property - def kdf_salt(self): - """ - Returns the byte string to use as the salt for the KDF. - - :return: - A byte string - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - salt = self['parameters'].parsed['key_derivation_func']['algorithm']['salt'] - - if salt.name == 'other_source': - raise ValueError('Can not determine key derivation salt - the reversed-for-future-use other source salt choice was specified in the PBKDF2 params structure') - - return salt.native - - if encryption_algo.find('.') == -1: - return self['parameters']['salt'].native - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation salt' % encryption_algo) - - @property - def kdf_iterations(self): - """ - Returns the number of iterations that should be run via the KDF. - - :return: - An integer - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - return self['parameters']['key_derivation_func']['algorithm']['iteration_count'].native - - if encryption_algo.find('.') == -1: - return self['parameters']['iterations'].native - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation iterations' % encryption_algo) - - @property - def key_length(self): - """ - Returns the key length to pass to the KDF/cipher. The PKCS#5 spec does - not specify a way to store the RC5 key length, however this tends not - to be a problem since OpenSSL does not support RC5 in PKCS#8 and OS X - does not provide an RC5 cipher for use in the Security Transforms - library. - - :raises: - ValueError - when the key length can not be determined - - :return: - An integer representing the length in bytes - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - key_length = self['parameters']['key_derivation_func']['algorithm']['key_length'].native - if key_length is not None: - return key_length - - # If the KDF params don't specify the key size, we can infer it from - # the encryption scheme for all schemes except for RC5. However, in - # practical terms, neither OpenSSL or OS X support RC5 for PKCS#8 - # so it is unlikely to be an issue that is run into. - - return self['parameters']['encryption_scheme'].key_length - - if encryption_algo.find('.') == -1: - return { - 'pbes1_md2_des': 8, - 'pbes1_md5_des': 8, - 'pbes1_md2_rc2': 8, - 'pbes1_md5_rc2': 8, - 'pbes1_sha1_des': 8, - 'pbes1_sha1_rc2': 8, - 'pkcs12_sha1_rc4_128': 16, - 'pkcs12_sha1_rc4_40': 5, - 'pkcs12_sha1_tripledes_3key': 24, - 'pkcs12_sha1_tripledes_2key': 16, - 'pkcs12_sha1_rc2_128': 16, - 'pkcs12_sha1_rc2_40': 5, - }[encryption_algo] - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation function key length' % encryption_algo) - - @property - def encryption_cipher(self): - """ - Returns the name of the symmetric encryption cipher to use. The key - length can be retrieved via the .key_length property to disabiguate - between different variations of TripleDES, AES, and the RC* ciphers. - - :return: - A unicode string from one of the following: "rc2", "rc4", "rc5", "des", "tripledes", "aes" - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - return self['parameters']['encryption_scheme'].encryption_cipher - - if encryption_algo.find('.') == -1: - return { - 'pbes1_md2_des': 'des', - 'pbes1_md5_des': 'des', - 'pbes1_md2_rc2': 'rc2', - 'pbes1_md5_rc2': 'rc2', - 'pbes1_sha1_des': 'des', - 'pbes1_sha1_rc2': 'rc2', - 'pkcs12_sha1_rc4_128': 'rc4', - 'pkcs12_sha1_rc4_40': 'rc4', - 'pkcs12_sha1_tripledes_3key': 'tripledes', - 'pkcs12_sha1_tripledes_2key': 'tripledes', - 'pkcs12_sha1_rc2_128': 'rc2', - 'pkcs12_sha1_rc2_40': 'rc2', - }[encryption_algo] - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine encryption cipher' % encryption_algo) - - @property - def encryption_block_size(self): - """ - Returns the block size of the encryption cipher, in bytes. For RC4, a - stream cipher, 0 is returned. - - :return: - An integer that is the block size in bytes - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - return self['parameters']['encryption_scheme'].encryption_block_size - - if encryption_algo.find('.') == -1: - return { - 'pbes1_md2_des': 8, - 'pbes1_md5_des': 8, - 'pbes1_md2_rc2': 8, - 'pbes1_md5_rc2': 8, - 'pbes1_sha1_des': 8, - 'pbes1_sha1_rc2': 8, - 'pkcs12_sha1_rc4_128': 0, - 'pkcs12_sha1_rc4_40': 0, - 'pkcs12_sha1_tripledes_3key': 8, - 'pkcs12_sha1_tripledes_2key': 8, - 'pkcs12_sha1_rc2_128': 8, - 'pkcs12_sha1_rc2_40': 8, - }[encryption_algo] - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine encryption block size' % encryption_algo) - - @property - def encryption_iv(self): - """ - Returns the byte string of the initialization vector for the encryption - scheme. Only the PBES2 stores the IV in the params. For PBES1, the IV - is derived from the KDF and this property will return None. - - :return: - A byte string or None - """ - - encryption_algo = self['algorithm'].native - - if encryption_algo == 'pbes2': - return self['parameters']['encryption_scheme'].encryption_iv - - # All of the PBES1 algos use their KDF to create the IV. For the pbkdf1, - # the KDF is told to generate a key that is an extra 8 bytes long, and - # that is used for the IV. For the PKCS#12 KDF, it is called with an id - # of 2 to generate the IV. In either case, we can't return the IV - # without knowing the user's password. - if encryption_algo.find('.') == -1: - return None - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine initialization vector' % encryption_algo) - - -class Pbmac1Params(Sequence): - _fields = [ - ('key_derivation_func', KdfAlgorithm), - ('message_auth_scheme', HmacAlgorithm), - ] - - -class Pkcs5MacId(ObjectIdentifier): - _map = { - '1.2.840.113549.1.5.14': 'pbmac1', - } - - -class Pkcs5MacAlgorithm(Sequence): - _fields = [ - ('algorithm', Pkcs5MacId), - ('parameters', Any), - ] - - _oid_pair = ('algorithm', 'parameters') - _oid_specs = { - 'pbmac1': Pbmac1Params, - } diff --git a/docs/readme.md b/docs/readme.md index b987385..7dc216f 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -14,7 +14,6 @@ pre-defined data types. - [X509 certificates](../asn1crypto/x509.py), `asn1crypto.x509` - [Certificate revocation lists (CRLs)](../asn1crypto/crl.py), `asn1crypto.crl` - [Online certificate status protocol (OCSP)](../asn1crypto/ocsp.py), `asn1crypto.ocsp` - - [Private key encryption (PKCS#5)](../asn1crypto/pkcs5.py), `asn1crypto.pkcs5` - [Private key/certificate containers (PKCS#12)](../asn1crypto/pkcs12.py), `asn1crypto.pkcs12` - [Cryptographic message syntax (CMS, PKCS#7)](../asn1crypto/cms.py), `asn1crypto.cms` - [Time stamp protocol (TSP)](../asn1crypto/tsp.py), `asn1crypto.tsp` diff --git a/lint.py b/lint.py index bead1e8..557f0b9 100644 --- a/lint.py +++ b/lint.py @@ -21,7 +21,6 @@ files = [ 'ocsp.py', 'pdf.py', 'pkcs12.py', - 'pkcs5.py', 'teletex_codec.py', 'tsa.py', 'x509.py', diff --git a/readme.md b/readme.md index c9336e9..2c9df2b 100644 --- a/readme.md +++ b/readme.md @@ -14,7 +14,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: | PKCS#1 v2.1 (RSA keys) | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC3447](https://tools.ietf.org/html/rfc3447) | | DSA keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC3279](https://tools.ietf.org/html/rfc3279) | | Elliptic curve keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [SECG SEC1 V2](http://www.secg.org/sec1-v2.pdf) | -| PKCS#5 v2.1 | [`asn1crypto.pkcs5`](asn1crypto/pkcs5.py) | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | +| PKCS#5 v2.1 | [`asn1crypto.algos`](asn1crypto/algos.py) | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | | CMS (and PKCS#7) | [`asn1crypto.cms`](asn1crypto/cms.py) | [RFC5652](https://tools.ietf.org/html/rfc5652), [RFC2315](https://tools.ietf.org/html/rfc2315) | | TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC3161](https://tools.ietf.org/html/rfc3161) | | PDF signatures | [`asn1crypto.pdf`](asn1crypto/pdf.py) | [PDF 1.7](http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf) | @@ -54,7 +54,6 @@ pre-defined data types. - [X509 certificates](asn1crypto/x509.py), `asn1crypto.x509` - [Certificate revocation lists (CRLs)](asn1crypto/crl.py), `asn1crypto.crl` - [Online certificate status protocol (OCSP)](asn1crypto/ocsp.py), `asn1crypto.ocsp` - - [Private key encryption (PKCS#5)](asn1crypto/pkcs5.py), `asn1crypto.pkcs5` - [Private key/certificate containers (PKCS#12)](asn1crypto/pkcs12.py), `asn1crypto.pkcs12` - [Cryptographic message syntax (CMS, PKCS#7)](asn1crypto/cms.py), `asn1crypto.cms` - [Time stamp protocol (TSP)](asn1crypto/tsp.py), `asn1crypto.tsp` -- cgit v1.2.3 From a19cfe1f8ac5f06c2e29d5190b877158ec2bfb37 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 15 Jun 2015 16:32:52 -0400 Subject: Add normal_tagging parameter to .dump() --- asn1crypto/core.py | 92 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index fa3ddab..3e3634f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -257,7 +257,7 @@ class Asn1Value(): """ return '<%s %s>' % (self.__class__.__name__, repr(self.contents)) - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -265,26 +265,34 @@ class Asn1Value(): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ - if self.header is None: + if (self.header is None) or normal_tagging: header = b'' + trailer = b'' id_num = 0 id_num |= self.class_ << 6 id_num |= self.method << 5 - if self.tag >= 31: - header += chr_cls(id_num | 31) + if normal_tagging: + tag = self.__class__.tag + else: tag = self.tag + + if tag >= 31: + header += chr_cls(id_num | 31) while tag > 0: continuation_bit = 0x80 if tag > 0x7F else 0 header += chr_cls(continuation_bit | (tag & 0x7F)) tag = tag >> 7 else: - header += chr_cls(id_num | self.tag) + header += chr_cls(id_num | tag) length = len(self.contents) if length <= 127: @@ -294,20 +302,26 @@ class Asn1Value(): header += chr_cls(0x80 | len(length_bytes)) header += length_bytes - self.header = header - - if self.tag_type == 'explicit': + if self.tag_type == 'explicit' and not normal_tagging: container = Asn1Value() container.method = 1 container.class_ = self.explicit_class container.tag = self.explicit_tag - container.contents = self.header + self.contents + self.trailer + container.contents = header + self.contents + trailer # Force the container to generate the header and footer container.dump() - self.header = container.header + self.header - self.trailer += container.trailer + header = container.header + header + trailer += container.trailer + + if not normal_tagging: + self.header = header + self.trailer = trailer + + else: + header = self.header + trailer = self.trailer - return self.header + self.contents + self.trailer + return header + self.contents + trailer class ValueMap(): @@ -358,7 +372,7 @@ class NoValue(Asn1Value): return None - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -366,6 +380,9 @@ class NoValue(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ @@ -436,7 +453,7 @@ class Any(Asn1Value): self._parsed = (parsed_value, spec, spec_params) return self._parsed[0] - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -444,6 +461,9 @@ class Any(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ @@ -451,7 +471,7 @@ class Any(Asn1Value): if self._parsed is None: self.parse() - return self._parsed[0].dump(force=force) + return self._parsed[0].dump(force=force, normal_tagging=normal_tagging) class Choice(Asn1Value): @@ -627,7 +647,7 @@ class Choice(Asn1Value): return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag) - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -635,11 +655,14 @@ class Choice(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ - return self.chosen.dump(force=force) + return self.chosen.dump(force=force, normal_tagging=normal_tagging) class Primitive(Asn1Value): @@ -687,7 +710,7 @@ class Primitive(Asn1Value): if self.trailer != b'': self.trailer = b'' - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -695,6 +718,9 @@ class Primitive(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ @@ -704,7 +730,7 @@ class Primitive(Asn1Value): self.contents = None self.set(native) - return Asn1Value.dump(self) + return Asn1Value.dump(self, normal_tagging=normal_tagging) class AbstractString(Primitive): @@ -1085,7 +1111,7 @@ class OctetBitString(Primitive): return self._parsed[0] - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -1093,6 +1119,9 @@ class OctetBitString(Primitive): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ @@ -1105,7 +1134,7 @@ class OctetBitString(Primitive): self.contents = None self.set(native) - return Asn1Value.dump(self) + return Asn1Value.dump(self, normal_tagging=normal_tagging) class IntegerBitString(Primitive): @@ -1231,7 +1260,7 @@ class OctetString(Primitive): return self._parsed[0] - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -1239,6 +1268,9 @@ class OctetString(Primitive): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ @@ -1251,7 +1283,7 @@ class OctetString(Primitive): self.contents = None self.set(native) - return Asn1Value.dump(self) + return Asn1Value.dump(self, normal_tagging=normal_tagging) class IntegerOctetString(OctetString): @@ -1896,7 +1928,7 @@ class Sequence(Asn1Value): self._native[self._fields[index][0]] = child.native return self._native - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -1904,6 +1936,9 @@ class Sequence(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ @@ -1911,7 +1946,7 @@ class Sequence(Asn1Value): if force: self._set_contents(force=force) - return Asn1Value.dump(self) + return Asn1Value.dump(self, normal_tagging=normal_tagging) class SequenceOf(Asn1Value): @@ -2138,7 +2173,7 @@ class SequenceOf(Asn1Value): self._native = [child.native for child in self] return self._native - def dump(self, force=False): + def dump(self, force=False, normal_tagging=False): """ Encodes the value using DER @@ -2146,6 +2181,9 @@ class SequenceOf(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format + :param normal_tagging: + Ignore implicit or explicit tagging when serializing + :return: A byte string of the DER-encoded value """ @@ -2153,7 +2191,7 @@ class SequenceOf(Asn1Value): if force: self._set_contents(force=force) - return Asn1Value.dump(self) + return Asn1Value.dump(self, normal_tagging=normal_tagging) class Set(Sequence): -- cgit v1.2.3 From 225418c220d38b97594788a71b4193959e2c3250 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 16 Jun 2015 00:08:37 -0400 Subject: Fixed various bugs with EncryptionAlgorithm and related structures --- asn1crypto/algos.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index eb32a60..8ee6abb 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -32,7 +32,7 @@ class HmacAlgorithmId(ObjectIdentifier): class HmacAlgorithm(Sequence): _fields = [ - ('algorithm', HmacAlgorithmId, {'default': 'sha1'}), + ('algorithm', HmacAlgorithmId), ('parameters', Any, {'optional': True}), ] @@ -103,7 +103,7 @@ class SignedDigestAlgorithm(Sequence): class Pbkdf2Salt(Choice): - _fields = [ + _alternatives = [ ('specified', OctetString), ('other_source', AlgorithmIdentifier), ] @@ -114,7 +114,7 @@ class Pbkdf2Params(Sequence): ('salt', Pbkdf2Salt), ('iteration_count', Integer), ('key_length', Integer, {'optional': True}), - ('prf', HmacAlgorithm, {'optional': True}), + ('prf', HmacAlgorithm, {'default': {'algorithm': 'sha1'}}), ] @@ -234,7 +234,7 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - return self['parameters'].parsed['key_derivation_func']['algorithm'].native + return self['parameters']['key_derivation_func']['algorithm'].native if encryption_algo.find('.') == -1: if encryption_algo.find('_') != -1: @@ -262,7 +262,7 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - return self['parameters'].parsed['key_derivation_func']['parameters']['prf']['algorithm'].native + return self['parameters']['key_derivation_func']['parameters']['prf']['algorithm'].native if encryption_algo.find('.') == -1: if encryption_algo.find('_') != -1: @@ -285,7 +285,7 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - salt = self['parameters'].parsed['key_derivation_func']['algorithm']['salt'] + salt = self['parameters']['key_derivation_func']['parameters']['salt'] if salt.name == 'other_source': raise ValueError('Can not determine key derivation salt - the reversed-for-future-use other source salt choice was specified in the PBKDF2 params structure') @@ -312,7 +312,7 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native if encryption_algo == 'pbes2': - return self['parameters']['key_derivation_func']['algorithm']['iteration_count'].native + return self['parameters']['key_derivation_func']['parameters']['iteration_count'].native if encryption_algo.find('.') == -1: if encryption_algo.find('_') != -1: @@ -374,7 +374,7 @@ class EncryptionAlgorithm(Sequence): raise ValueError('Invalid RC2 parameter version found in EncryptionAlgorithm parameters') if encryption_algo == 'pbes2': - key_length = self['parameters']['key_derivation_func']['algorithm']['key_length'].native + key_length = self['parameters']['key_derivation_func']['parameters']['key_length'].native if key_length is not None: return key_length -- cgit v1.2.3 From 3fd1e78f5e26f0259d1c4c0572f86d4623fbf2b2 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 16 Jun 2015 00:10:04 -0400 Subject: Allow setting Sequence/Set and SequenceOf/SetOf values in constructor via a dict --- asn1crypto/core.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 3e3634f..ca95669 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1596,6 +1596,27 @@ class Sequence(Asn1Value): # A 2-element tuple of the indexes in _fields of the OID and value fields _oid_nums = None + def __init__(self, value=None, default=None, **kwargs): + """ + Allows setting field values before passing everything else along to + Asn1Value.__init__() + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified + """ + + Asn1Value.__init__(self, **kwargs) + + if value is None and default is not None: + value = default + + if value is not None: + for key, child in value.items(): + self.__setitem__(key, child) + def _lazy_child(self, index): """ Builds a child object if the child has only been parsed into a tuple so far @@ -1966,10 +1987,16 @@ class SequenceOf(Asn1Value): # An Asn1Value class to use when parsing children _child_spec = None - def __init__(self, spec=None, **kwargs): + def __init__(self, value=None, default=None, spec=None, **kwargs): """ - Allows setting the _child_spec via the spec parameter before - passing everything else along to Asn1Value.__init__() + Allows setting child objects and the _child_spec via the spec parameter + before passing everything else along to Asn1Value.__init__() + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified :param spec: A class derived from Asn1Value to use to parse children @@ -1980,6 +2007,13 @@ class SequenceOf(Asn1Value): Asn1Value.__init__(self, **kwargs) + if value is None and default is not None: + value = default + + if value is not None: + for index, child in enumerate(value): + self.__setitem__(index, child) + def _lazy_child(self, index): """ Builds a child object if the child has only been parsed into a tuple so far -- cgit v1.2.3 From 902d59787a3cca51bfaf0ee37a9bd38d1e30fd2e Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 16 Jun 2015 00:10:47 -0400 Subject: Fix bug with wrapped field being double wrapped --- asn1crypto/core.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ca95669..dc62f21 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1721,19 +1721,22 @@ class Sequence(Asn1Value): else: new_value = value - elif isinstance(value, value_spec): + elif isinstance(value, field_spec): new_value = value else: - new_value = value_spec(value, **(field_info[2] if len(field_info) > 2 else {})) - - # For when the field is OctetString or OctetBitString with embedded - # values we need to wrap the value in the field spec to get the - # appropriate encoded value. - if field_spec != value_spec and not issubclass(field_spec, Any): - wrapper = field_spec(value=new_value.dump()) - wrapper._parsed = new_value #pylint: disable=W0212 - new_value = wrapper + if isinstance(value, value_spec): + new_value = value + else: + new_value = value_spec(value, **(field_info[2] if len(field_info) > 2 else {})) + + # For when the field is OctetString or OctetBitString with embedded + # values we need to wrap the value in the field spec to get the + # appropriate encoded value. + if field_spec != value_spec and not issubclass(field_spec, Any): + wrapper = field_spec(value=new_value.dump()) + wrapper._parsed = new_value #pylint: disable=W0212 + new_value = wrapper if new_value.contents is None: raise ValueError('Value for field "%s" of %s is not set' % (field_info[0], self.__class__.__name__)) -- cgit v1.2.3 From 971b5c0a93c08d7a73fc70e56f64f107fe8c9efc Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 16 Jun 2015 00:11:43 -0400 Subject: Fix some serialization bugs in Sequence and SequenceOf --- asn1crypto/core.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index dc62f21..e13d85b 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1745,6 +1745,7 @@ class Sequence(Asn1Value): if self._native is not None: self._native[self._fields[key][0]] = self.children[key].native + self._set_contents() def __delitem__(self, key): """ @@ -1776,6 +1777,7 @@ class Sequence(Asn1Value): self._native[info[0]] = None else: self.__setitem__(key, None) + self._set_contents() def __iter__(self): #pylint: disable=W0234 """ @@ -1802,7 +1804,9 @@ class Sequence(Asn1Value): self.contents = b'' for index, info in enumerate(self._fields): child = self.children[index] - if isinstance(child, tuple): + if child is None: + child_dump = b'' + elif isinstance(child, tuple): if force: child_dump = self._lazy_child(index).dump(force=force) else: @@ -2140,7 +2144,9 @@ class SequenceOf(Asn1Value): self.contents = b'' for index, info in enumerate(self._fields): child = self.children[index] - if isinstance(child, tuple): + if child is None: + child_dump = b'' + elif isinstance(child, tuple): if force: child_dump = self._lazy_child(index).dump(force=force) else: -- cgit v1.2.3 From abda3a66dc85b485318e9780543b090e665128ee Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 16 Jun 2015 00:12:08 -0400 Subject: Make sure trailing default fields in a Sequence get initialized --- asn1crypto/core.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index e13d85b..5f58ea6 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1924,8 +1924,16 @@ class Sequence(Asn1Value): field += 1 total_fields = len(self._fields) - while len(self.children) < total_fields: - self.children.append(NoValue()) + index = len(self.children) + while index < total_fields: + field_info = self._fields[index] + field_spec = field_info[1] + field_params = field_info[2] if len(field_info) > 2 else {} + if 'default' in field_params: + self.children.append(field_spec(**field_params)) + else: + self.children.append(NoValue()) + index += 1 except (ValueError) as e: args = e.args[1:] -- cgit v1.2.3 From 1e2a60260f9d48f20cd535c680e7d0b55be1d744 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 16 Jun 2015 00:12:40 -0400 Subject: Allow adding children to SequenceOf/SetOf via [] assignment --- asn1crypto/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 5f58ea6..d351869 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2081,6 +2081,10 @@ class SequenceOf(Asn1Value): if self.children is None: self._parse_children() + # If adding at the end, create a space for the new value + if key == len(self.children): + self.children.append(None) + if issubclass(self._child_spec, Any): if isinstance(value, Asn1Value): self.chilren[key] = value -- cgit v1.2.3 From 8b59e05f6b3888b3cb0a6323b1b97aea6f5e7de9 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 16 Jun 2015 00:13:05 -0400 Subject: Add .wrap() methods to PublicKeyInfo and PrivateKeyInfo --- asn1crypto/keys.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 9632b33..22746cf 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -8,6 +8,7 @@ from decimal import localcontext from .algos import DigestAlgorithm, EncryptionAlgorithm from .core import ( Any, + Asn1Value, Choice, Integer, IntegerBitString, @@ -420,6 +421,53 @@ class PrivateKeyInfo(Sequence): _fingerprint = None + @classmethod + def wrap(cls, private_key, algorithm): + """ + Wraps a private key in a PrivateKeyInfo structure + + :param private_key: + A byte string or Asn1Value object of the private key + + :param algorithm: + A unicode string of "rsa", "dsa" or "ecdsa" + + :return: + A PrivateKeyInfo object + """ + + if not isinstance(private_key, byte_cls) and not isinstance(private_key, Asn1Value): + raise ValueError('private_key must be a byte string or Asn1Value, not %s' % private_key.__class__.__name__) + + if algorithm == 'rsa': + if not isinstance(private_key, RSAPrivateKey): + private_key = RSAPrivateKey.load(private_key) + params = Null() + elif algorithm == 'dsa': + if not isinstance(private_key, DSAPrivateKey): + private_key = DSAPrivateKey.load(private_key) + params = DSAParams() + params['p'] = private_key['p'] + params['q'] = private_key['q'] + params['g'] = private_key['g'] + elif algorithm == 'ecdsa': + if not isinstance(private_key, ECPrivateKey): + private_key = ECPrivateKey.load(private_key) + params = private_key['parameters'] + else: + raise ValueError('algorithm must be one of "rsa", "dsa", "ecdsa" - is %s' % repr(algorithm)) + + private_key_algo = PrivateKeyAlgorithm() + private_key_algo['algorithm'] = PrivateKeyAlgorithmId(algorithm) + private_key_algo['parameters'] = params + + container = cls() + container['version'] = Integer(0) + container['private_key_algorithm'] = private_key_algo + container['private_key'] = OctetString(private_key.dump(normal_tagging=True)) + + return container + @property def fingerprint(self): """ @@ -565,6 +613,39 @@ class PublicKeyInfo(Sequence): _fingerprint = None + @classmethod + def wrap(cls, public_key, algorithm): + """ + Wraps a public key in a PublicKeyInfo structure + + :param public_key: + A byte string or Asn1Value object of the public key + + :param algorithm: + A unicode string of "rsa" + + :return: + A PublicKeyInfo object + """ + + if not isinstance(public_key, byte_cls) and not isinstance(public_key, Asn1Value): + raise ValueError('public_key must be a byte string or Asn1Value, not %s' % public_key.__class__.__name__) + + if algorithm != 'rsa': + raise ValueError('algorithm must be one of "rsa" - is %s' % repr(algorithm)) + + algo = PublicKeyAlgorithm() + algo['algorithm'] = PublicKeyAlgorithmId(algorithm) + algo['parameters'] = Null() + + container = cls() + container['algorithm'] = algo + if isinstance(public_key, Asn1Value): + public_key = public_key.dump(normal_tagging=True) + container['public_key'] = OctetBitString(public_key) + + return container + @property def fingerprint(self): """ -- cgit v1.2.3 From 40a32f0ebd0f6badf6fdd743864e5833fe1f8eb1 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 22:29:56 -0400 Subject: Fixed CompressionAlgorithm to make parameters optional --- asn1crypto/cms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 85fe9d0..5264823 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -621,7 +621,7 @@ class CompressionAlgorithmId(ObjectIdentifier): class CompressionAlgorithm(Sequence): _fields = [ ('algorithm', CompressionAlgorithmId), - ('parameters', Any), + ('parameters', Any, {'optional': True}), ] -- cgit v1.2.3 From 73c17c9acec0d1f295a57c1bc2228e904bea1834 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 22:31:02 -0400 Subject: Added Asn1Value.pprint() --- asn1crypto/core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d351869..f6cc3d5 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -6,6 +6,7 @@ import sys import re from collections import OrderedDict from datetime import datetime, timedelta, tzinfo +from pprint import pprint from . import teletex_codec @@ -323,6 +324,13 @@ class Asn1Value(): return header + self.contents + trailer + def pprint(self): + """ + Pretty prints the native representation of the value + """ + + pprint(self.native) + class ValueMap(): """ -- cgit v1.2.3 From 438f877046d85ae78963896425d369bd4747113a Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 22:32:17 -0400 Subject: Mark teletex_codex as private --- asn1crypto/_teletex_codec.py | 317 +++++++++++++++++++++++++++++++++++++++++++ asn1crypto/core.py | 4 +- asn1crypto/teletex_codec.py | 317 ------------------------------------------- 3 files changed, 319 insertions(+), 319 deletions(-) create mode 100644 asn1crypto/_teletex_codec.py delete mode 100644 asn1crypto/teletex_codec.py diff --git a/asn1crypto/_teletex_codec.py b/asn1crypto/_teletex_codec.py new file mode 100644 index 0000000..473ba9c --- /dev/null +++ b/asn1crypto/_teletex_codec.py @@ -0,0 +1,317 @@ +# coding: utf-8 +from __future__ import unicode_literals +from __future__ import absolute_import + +import codecs + + +class TeletexCodec(codecs.Codec): + + def encode(self, input_, errors='strict'): + return codecs.charmap_encode(input_, errors, ENCODING_TABLE) + + def decode(self, input_, errors='strict'): + return codecs.charmap_decode(input_, errors, DECODING_TABLE) + + +class TeletexIncrementalEncoder(codecs.IncrementalEncoder): + + def encode(self, input_, final=False): + return codecs.charmap_encode(input_, self.errors, ENCODING_TABLE)[0] + + +class TeletexIncrementalDecoder(codecs.IncrementalDecoder): + + def decode(self, input_, final=False): + return codecs.charmap_decode(input_, self.errors, DECODING_TABLE)[0] + + +class TeletexStreamWriter(TeletexCodec, codecs.StreamWriter): + + pass + + +class TeletexStreamReader(TeletexCodec, codecs.StreamReader): + + pass + + +def teletex_search_function(name): + if name != 'teletex': + return None + + return codecs.CodecInfo( + name='teletex', + encode=TeletexCodec().encode, + decode=TeletexCodec().decode, + incrementalencoder=TeletexIncrementalEncoder, + incrementaldecoder=TeletexIncrementalDecoder, + streamreader=TeletexStreamReader, + streamwriter=TeletexStreamWriter, + ) + + +def register(): + codecs.register(teletex_search_function) + + +# http://en.wikipedia.org/wiki/ITU_T.61 +DECODING_TABLE = ( + '\u0000' + '\u0001' + '\u0002' + '\u0003' + '\u0004' + '\u0005' + '\u0006' + '\u0007' + '\u0008' + '\u0009' + '\u000A' + '\u000B' + '\u000C' + '\u000D' + '\u000E' + '\u000F' + '\u0010' + '\u0011' + '\u0012' + '\u0013' + '\u0014' + '\u0015' + '\u0016' + '\u0017' + '\u0018' + '\u0019' + '\u001A' + '\u001B' + '\u001C' + '\u001D' + '\u001E' + '\u001F' + '\u0020' + '\u0021' + '\u0022' + '\ufffe' + '\ufffe' + '\u0025' + '\u0026' + '\u0027' + '\u0028' + '\u0029' + '\u002A' + '\u002B' + '\u002C' + '\u002D' + '\u002E' + '\u002F' + '\u0030' + '\u0031' + '\u0032' + '\u0033' + '\u0034' + '\u0035' + '\u0036' + '\u0037' + '\u0038' + '\u0039' + '\u003A' + '\u003B' + '\u003C' + '\u003D' + '\u003E' + '\u003F' + '\u0040' + '\u0041' + '\u0042' + '\u0043' + '\u0044' + '\u0045' + '\u0046' + '\u0047' + '\u0048' + '\u0049' + '\u004A' + '\u004B' + '\u004C' + '\u004D' + '\u004E' + '\u004F' + '\u0050' + '\u0051' + '\u0052' + '\u0053' + '\u0054' + '\u0055' + '\u0056' + '\u0057' + '\u0058' + '\u0059' + '\u005A' + '\u005B' + '\ufffe' + '\u005D' + '\ufffe' + '\u005F' + '\ufffe' + '\u0061' + '\u0062' + '\u0063' + '\u0064' + '\u0065' + '\u0066' + '\u0067' + '\u0068' + '\u0069' + '\u006A' + '\u006B' + '\u006C' + '\u006D' + '\u006E' + '\u006F' + '\u0070' + '\u0071' + '\u0072' + '\u0073' + '\u0074' + '\u0075' + '\u0076' + '\u0077' + '\u0078' + '\u0079' + '\u007A' + '\ufffe' + '\u007C' + '\ufffe' + '\ufffe' + '\u007F' + '\u0080' + '\u0081' + '\u0082' + '\u0083' + '\u0084' + '\u0085' + '\u0086' + '\u0087' + '\u0088' + '\u0089' + '\u008A' + '\u008B' + '\u008C' + '\u008D' + '\u008E' + '\u008F' + '\u0090' + '\u0091' + '\u0092' + '\u0093' + '\u0094' + '\u0095' + '\u0096' + '\u0097' + '\u0098' + '\u0099' + '\u009A' + '\u009B' + '\u009C' + '\u009D' + '\u009E' + '\u009F' + '\u00A0' + '\u00A1' + '\u00A2' + '\u00A3' + '\u0024' + '\u00A5' + '\u0023' + '\u00A7' + '\u00A4' + '\ufffe' + '\ufffe' + '\u00AB' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\u00B0' + '\u00B1' + '\u00B2' + '\u00B3' + '\u00D7' + '\u00B5' + '\u00B6' + '\u00B7' + '\u00F7' + '\ufffe' + '\ufffe' + '\u00BB' + '\u00BC' + '\u00BD' + '\u00BE' + '\u00BF' + '\ufffe' + '\u0300' + '\u0301' + '\u0302' + '\u0303' + '\u0304' + '\u0306' + '\u0307' + '\u0308' + '\ufffe' + '\u030A' + '\u0327' + '\u0332' + '\u030B' + '\u0328' + '\u030C' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\ufffe' + '\u2126' + '\u00C6' + '\u00D0' + '\u00AA' + '\u0126' + '\ufffe' + '\u0132' + '\u013F' + '\u0141' + '\u00D8' + '\u0152' + '\u00BA' + '\u00DE' + '\u0166' + '\u014A' + '\u0149' + '\u0138' + '\u00E6' + '\u0111' + '\u00F0' + '\u0127' + '\u0131' + '\u0133' + '\u0140' + '\u0142' + '\u00F8' + '\u0153' + '\u00DF' + '\u00FE' + '\u0167' + '\u014B' + '\ufffe' +) +ENCODING_TABLE = codecs.charmap_build(DECODING_TABLE) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index f6cc3d5..fba2679 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -8,7 +8,7 @@ from collections import OrderedDict from datetime import datetime, timedelta, tzinfo from pprint import pprint -from . import teletex_codec +from . import _teletex_codec # Python 2 if sys.version_info <= (3,): @@ -78,7 +78,7 @@ else: -teletex_codec.register() +_teletex_codec.register() CLASS_NUM_TO_NAME_MAP = { diff --git a/asn1crypto/teletex_codec.py b/asn1crypto/teletex_codec.py deleted file mode 100644 index 473ba9c..0000000 --- a/asn1crypto/teletex_codec.py +++ /dev/null @@ -1,317 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals -from __future__ import absolute_import - -import codecs - - -class TeletexCodec(codecs.Codec): - - def encode(self, input_, errors='strict'): - return codecs.charmap_encode(input_, errors, ENCODING_TABLE) - - def decode(self, input_, errors='strict'): - return codecs.charmap_decode(input_, errors, DECODING_TABLE) - - -class TeletexIncrementalEncoder(codecs.IncrementalEncoder): - - def encode(self, input_, final=False): - return codecs.charmap_encode(input_, self.errors, ENCODING_TABLE)[0] - - -class TeletexIncrementalDecoder(codecs.IncrementalDecoder): - - def decode(self, input_, final=False): - return codecs.charmap_decode(input_, self.errors, DECODING_TABLE)[0] - - -class TeletexStreamWriter(TeletexCodec, codecs.StreamWriter): - - pass - - -class TeletexStreamReader(TeletexCodec, codecs.StreamReader): - - pass - - -def teletex_search_function(name): - if name != 'teletex': - return None - - return codecs.CodecInfo( - name='teletex', - encode=TeletexCodec().encode, - decode=TeletexCodec().decode, - incrementalencoder=TeletexIncrementalEncoder, - incrementaldecoder=TeletexIncrementalDecoder, - streamreader=TeletexStreamReader, - streamwriter=TeletexStreamWriter, - ) - - -def register(): - codecs.register(teletex_search_function) - - -# http://en.wikipedia.org/wiki/ITU_T.61 -DECODING_TABLE = ( - '\u0000' - '\u0001' - '\u0002' - '\u0003' - '\u0004' - '\u0005' - '\u0006' - '\u0007' - '\u0008' - '\u0009' - '\u000A' - '\u000B' - '\u000C' - '\u000D' - '\u000E' - '\u000F' - '\u0010' - '\u0011' - '\u0012' - '\u0013' - '\u0014' - '\u0015' - '\u0016' - '\u0017' - '\u0018' - '\u0019' - '\u001A' - '\u001B' - '\u001C' - '\u001D' - '\u001E' - '\u001F' - '\u0020' - '\u0021' - '\u0022' - '\ufffe' - '\ufffe' - '\u0025' - '\u0026' - '\u0027' - '\u0028' - '\u0029' - '\u002A' - '\u002B' - '\u002C' - '\u002D' - '\u002E' - '\u002F' - '\u0030' - '\u0031' - '\u0032' - '\u0033' - '\u0034' - '\u0035' - '\u0036' - '\u0037' - '\u0038' - '\u0039' - '\u003A' - '\u003B' - '\u003C' - '\u003D' - '\u003E' - '\u003F' - '\u0040' - '\u0041' - '\u0042' - '\u0043' - '\u0044' - '\u0045' - '\u0046' - '\u0047' - '\u0048' - '\u0049' - '\u004A' - '\u004B' - '\u004C' - '\u004D' - '\u004E' - '\u004F' - '\u0050' - '\u0051' - '\u0052' - '\u0053' - '\u0054' - '\u0055' - '\u0056' - '\u0057' - '\u0058' - '\u0059' - '\u005A' - '\u005B' - '\ufffe' - '\u005D' - '\ufffe' - '\u005F' - '\ufffe' - '\u0061' - '\u0062' - '\u0063' - '\u0064' - '\u0065' - '\u0066' - '\u0067' - '\u0068' - '\u0069' - '\u006A' - '\u006B' - '\u006C' - '\u006D' - '\u006E' - '\u006F' - '\u0070' - '\u0071' - '\u0072' - '\u0073' - '\u0074' - '\u0075' - '\u0076' - '\u0077' - '\u0078' - '\u0079' - '\u007A' - '\ufffe' - '\u007C' - '\ufffe' - '\ufffe' - '\u007F' - '\u0080' - '\u0081' - '\u0082' - '\u0083' - '\u0084' - '\u0085' - '\u0086' - '\u0087' - '\u0088' - '\u0089' - '\u008A' - '\u008B' - '\u008C' - '\u008D' - '\u008E' - '\u008F' - '\u0090' - '\u0091' - '\u0092' - '\u0093' - '\u0094' - '\u0095' - '\u0096' - '\u0097' - '\u0098' - '\u0099' - '\u009A' - '\u009B' - '\u009C' - '\u009D' - '\u009E' - '\u009F' - '\u00A0' - '\u00A1' - '\u00A2' - '\u00A3' - '\u0024' - '\u00A5' - '\u0023' - '\u00A7' - '\u00A4' - '\ufffe' - '\ufffe' - '\u00AB' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\u00B0' - '\u00B1' - '\u00B2' - '\u00B3' - '\u00D7' - '\u00B5' - '\u00B6' - '\u00B7' - '\u00F7' - '\ufffe' - '\ufffe' - '\u00BB' - '\u00BC' - '\u00BD' - '\u00BE' - '\u00BF' - '\ufffe' - '\u0300' - '\u0301' - '\u0302' - '\u0303' - '\u0304' - '\u0306' - '\u0307' - '\u0308' - '\ufffe' - '\u030A' - '\u0327' - '\u0332' - '\u030B' - '\u0328' - '\u030C' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\ufffe' - '\u2126' - '\u00C6' - '\u00D0' - '\u00AA' - '\u0126' - '\ufffe' - '\u0132' - '\u013F' - '\u0141' - '\u00D8' - '\u0152' - '\u00BA' - '\u00DE' - '\u0166' - '\u014A' - '\u0149' - '\u0138' - '\u00E6' - '\u0111' - '\u00F0' - '\u0127' - '\u0131' - '\u0133' - '\u0140' - '\u0142' - '\u00F8' - '\u0153' - '\u00DF' - '\u00FE' - '\u0167' - '\u014B' - '\ufffe' -) -ENCODING_TABLE = codecs.charmap_build(DECODING_TABLE) -- cgit v1.2.3 From 9c9e2eeaa4314b06d95bc2815457b4a54deda283 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 22:33:07 -0400 Subject: Fixed bug allowing non optional fields in a Sequence to be missing, improved error messaging --- asn1crypto/core.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index fba2679..eea12cd 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1939,8 +1939,10 @@ class Sequence(Asn1Value): field_params = field_info[2] if len(field_info) > 2 else {} if 'default' in field_params: self.children.append(field_spec(**field_params)) - else: + elif 'optional' in field_params: self.children.append(NoValue()) + else: + raise ValueError('Field "%s" is missing from structure' % field_info[0]) index += 1 except (ValueError) as e: @@ -2875,12 +2877,17 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value.explicit_class = original_value.explicit_class value.explicit_tag = original_value.explicit_tag - # Force parsing the Choice now - if isinstance(value, Choice): - value.parse() - - if nested_spec: - value.parse(nested_spec) + try: + # Force parsing the Choice now + if isinstance(value, Choice): + value.parse() + + if nested_spec: + value.parse(nested_spec) + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % value.__class__.__name__,) + args + raise e return value -- cgit v1.2.3 From 92954e28bae935515bd97e729490eb6b417ba5fe Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 22:33:49 -0400 Subject: Moved int_*_bytes() functions to separate file --- asn1crypto/_int_conversion.py | 42 ++++++++++++++++++++++++++++++++++++++++++ asn1crypto/core.py | 33 +-------------------------------- 2 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 asn1crypto/_int_conversion.py diff --git a/asn1crypto/_int_conversion.py b/asn1crypto/_int_conversion.py new file mode 100644 index 0000000..382b2d9 --- /dev/null +++ b/asn1crypto/_int_conversion.py @@ -0,0 +1,42 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import sys + + +# Python 2 +if sys.version_info <= (3,): + def int_to_bytes(value, signed=False): + # Handle negatives in two's complement + if signed and value < 0: + value = (~value) + 1 + + hex_str = '%x' % value + if len(hex_str) & 1: + hex_str = '0' + hex_str + return hex_str.decode('hex') + + def int_from_bytes(value, signed=False): + num = long(value.encode("hex"), 16) #pylint: disable=E0602 + + if not signed: + return num + + # Check for sign bit and handle two's complement + if ord(value[0:1]) & 0x80: + bit_len = len(value) * 8 + return num - (1 << bit_len) + + return num + +# Python 3 +else: + + def int_to_bytes(value, signed=False): + result = value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) + if not signed: + return result.lstrip(b'\x00') + return result + + def int_from_bytes(value, signed=False): + return int.from_bytes(value, 'big', signed=signed) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index eea12cd..0fe88f5 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -9,6 +9,7 @@ from datetime import datetime, timedelta, tzinfo from pprint import pprint from . import _teletex_codec +from ._int_conversion import int_to_bytes, int_from_bytes # Python 2 if sys.version_info <= (3,): @@ -18,29 +19,6 @@ if sys.version_info <= (3,): chr_cls = chr range = xrange #pylint: disable=E0602,W0622 - def int_to_bytes(value, signed=False): - # Handle negatives in two's complement - if signed and value < 0: - value = (~value) + 1 - - hex_str = '%x' % value - if len(hex_str) & 1: - hex_str = '0' + hex_str - return hex_str.decode('hex') - - def int_from_bytes(value, signed=False): - num = long(value.encode("hex"), 16) #pylint: disable=E0602 - - if not signed: - return num - - # Check for sign bit and handle two's complement - if ord(value[0:1]) & 0x80: - bit_len = len(value) * 8 - return num - (1 << bit_len) - - return num - class utc(tzinfo): def tzname(self, _): @@ -65,15 +43,6 @@ else: def chr_cls(num): return bytes([num]) - def int_to_bytes(value, signed=False): - result = value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) - if not signed: - return result.lstrip(b'\x00') - return result - - def int_from_bytes(value, signed=False): - return int.from_bytes(value, 'big', signed=signed) - from datetime import timezone -- cgit v1.2.3 From 29f8149dbe50c8f82ca0f339edf9711a2de4ae48 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 22:35:33 -0400 Subject: Corrected data type of ECPrivateKey.public_key field --- asn1crypto/keys.py | 4 +--- tests/test_keys.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 22746cf..6ec6224 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -11,7 +11,6 @@ from .core import ( Asn1Value, Choice, Integer, - IntegerBitString, IntegerOctetString, Null, ObjectIdentifier, @@ -318,11 +317,10 @@ class ECPrivateKey(Sequence): ('version', ECPrivateKeyVersion), ('private_key', IntegerOctetString), ('parameters', ECDomainParameters, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('public_key', IntegerBitString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('public_key', OctetBitString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), ] - class DSAParams(Sequence): """ Parameters for a DSA public or private key diff --git a/tests/test_keys.py b/tests/test_keys.py index 1d02a3f..e47ce41 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -177,7 +177,7 @@ class KeysTests(unittest.TestCase): key['parameters'].native ) self.assertEqual( - 60930340107159415611969123187806310044445787587918330601145416939590630530757526035521042297997626933274194649938002969225935746493465239160856198904766465, + b'\x04\x8B\x5D\x4C\x71\xF7\xD6\xC6\xA3\x49\x63\x42\x5C\x47\x9F\xCB\x73\x24\x1D\xC9\xDD\xD1\x2D\xF1\x3A\x9F\xB7\x04\xDE\x20\xD0\x58\x00\x93\x54\xF6\x89\xC7\x2F\x87\x2B\xF7\xF9\x3D\x3B\x34\xED\x9E\x7B\x0E\x3D\x57\x42\xDF\x78\x03\x0B\xCC\x31\xC6\x03\xD7\x9F\x60\x01', key['public_key'].native ) -- cgit v1.2.3 From 0a7fead977f7ce8073a28309c89964651394f111 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 22:36:37 -0400 Subject: Added missing DSA public key files --- tests/fixtures/keys/test-public-dsa-der.key | Bin 0 -> 1226 bytes tests/fixtures/keys/test-public-dsa.key | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/fixtures/keys/test-public-dsa-der.key create mode 100644 tests/fixtures/keys/test-public-dsa.key diff --git a/tests/fixtures/keys/test-public-dsa-der.key b/tests/fixtures/keys/test-public-dsa-der.key new file mode 100644 index 0000000..d54122a Binary files /dev/null and b/tests/fixtures/keys/test-public-dsa-der.key differ diff --git a/tests/fixtures/keys/test-public-dsa.key b/tests/fixtures/keys/test-public-dsa.key new file mode 100644 index 0000000..56143cd --- /dev/null +++ b/tests/fixtures/keys/test-public-dsa.key @@ -0,0 +1,28 @@ +-----BEGIN PUBLIC KEY----- +MIIExjCCAzkGByqGSM44BAEwggMsAoIBgQDGz00klLP8CyoVk216zk+ldYapofKC +/Mdxv5SvVQKqpr7UEhCPECn/kwLGjoKtby8m/s3DJ2Y1/59LiZhR8RZPFUGO73ln +xvguMRaY1lKMW1Z3Ylcn2wYyjA+4tSIxW4Nrt12JQvDhlYonPjNYqjWyasHI/aK3 +ySMlVlb1gw1t4NCfisC0JIdgzWXc589OM7sWIztzEFGs+EEjCV0BOVi8WfRMjaRu +o9hw+Ev5LbMTy7rEK4BK5hPcMdFwTO1SFMxxJopUkafl/PK1zyNYy1rYrDWDioqP +hcDQt/1U+nfvGUoKvBiEHfH1T467v5UMUizLglJ4EWCeYHU/7tQmep3FCf8buySZ +YzmMvgeK8anFKsjukGtxQwcnvCPD/bLTm4evbNKIK30jIRcAICD+OXJyAsU284y9 +2MO4OW6a0dzam2vGqw9zmpjNZlitvnvCcuSw7t282ty/poJqRxLQjbgsoqpjwwYO +CemgYSlCNJ7XQ0/kUrHTPrbGCl8Yvx0LCkkCIQCeRUmNf/OhUimhQrhBgfqoHymV +cB9RxFr4JwpXUGlduwKCAYAhjX0YsOUsVCaRuSMrJBcdr8NeuhIZmnPIGPWMrrXx +uUA/XbLYG2meHp4uL7Xumx0ahAKG1yc9ZM12UW+cZm1Gmf+agZHcJUAMq1x/w6fz +aKy+7qLU4yutVe0uBE9Z55ml9YiWuKN/XK9IA6H+7K4C5gtCKFCH9d1wuiTLEWbO +B+PeYb0N/UbDfqou49y/YiWEG2lAuwYGQzYrKEbhFqQJEzw+cbMbX6bg85teACja +M6G1CPvktBIJkwjM6rxuGzGN8wFuoTk/wCBQ8t3W33XIS+okTEHkbY6Ez46yYNDH ++ItJarltZaWA/I03plMqjwlAKpUrXcapk+wKnyP3sTrSmiQvPWYa0r3iLfg48fU4 +NO/VKS/WDYCIuL+dVwcMRFyI3i4GzVwQqmGLzMFqVAKw1GxAwn0l/lZ9GkYoAy2g +1DmfGNXL8V7IIRqudz/OVvvoXbfl+lRfpaZJUJ9zeDoLa/X4JAKTJl3Kw0aCj0T/ +rGOVd+Bx0kFrOI9fiLo0XWoDggGFAAKCAYApKrbhVeOVtIj66/sjETnKGSXYKXCg +eRn5uZQnQHkfEzniwdMeQue1jFyJ8Dp/mhmA3ZkRf1Jd9yYlBsVjGmISqbsfBwrA +1VeRkBTW5YrEDlr1+1uZYA7ZFRft+hqXkTBfgBaldvb1A8qJBVYSv4SDy65LkUA+ +02h9Oc5kFn7/XmgJprvyQRDA9c/9RCEsq8iI1ypupEjXsc+bwdoCq/kGufubywwq +IvRvAz5WxfzG6q6QHYkNVUNp15rWg2gTnrpRRZlqyL2zTd7t0x4z1SbtAsBFbB5K +/CmvnIX8UmmNPhaFJh5KH67jSpWyryiUzUSkM3Vu6teg9gNcfR+RnqNAHthR3iYT +ipeu3VdW4h7GhMqy3cpjvQWCwWKSSStSQbQzYZbX/lOjM8+3JdkfdoCnLadoAz4z +xwYtUwfES0NdKGwpVtin3UzWAlMciDtsNZMf95Rofb2E1k225w7RmL0jvW+xpg05 +rYqOttN+bF8Nv4v66yJFtro88gMTfMtvmUQ= +-----END PUBLIC KEY----- -- cgit v1.2.3 From 0a689b9f849d3e08891113aa5bd9416ca86fe664 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 23:28:07 -0400 Subject: Ensure int type checking works on Python 2 --- asn1crypto/core.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 0fe88f5..ab05196 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -15,6 +15,7 @@ from ._int_conversion import int_to_bytes, int_from_bytes if sys.version_info <= (3,): str_cls = unicode #pylint: disable=E0602 byte_cls = str + int_types = (int, long) py2 = True chr_cls = chr range = xrange #pylint: disable=E0602,W0622 @@ -38,6 +39,7 @@ if sys.version_info <= (3,): else: str_cls = str byte_cls = bytes + int_types = int py2 = False def chr_cls(num): @@ -185,7 +187,7 @@ class Asn1Value(): class_ = CLASS_NAME_TO_NUM_MAP[class_] if tag is not None: - if not isinstance(tag, int): + if not isinstance(tag, int_types): raise ValueError('tag must be an integer, not %s' % tag.__class__.__name__) if tag_type == 'implicit': @@ -841,7 +843,7 @@ class Integer(Primitive, ValueMap): value = self._reverse_map[value] - elif not isinstance(value, int): + elif not isinstance(value, int_types): raise ValueError('%s value must be an integer or unicode string when a name_map is provided' % self.__class__.__name__) self._native = self._map[value] if self._map and value in self._map else value @@ -895,14 +897,14 @@ class BitString(Primitive, ValueMap, object): ValueError - when an invalid value is passed """ - if not isinstance(value, int) and not isinstance(value, tuple): + if not isinstance(value, int_types) and not isinstance(value, tuple): raise ValueError('%s value must be an integer or a tuple of ones and zeros, not %s' % (self.__class__.__name__, value.__class__.__name__)) if isinstance(value, tuple): self._native = value value = ''.join(map(str_cls, value)) - elif isinstance(value, int): + elif isinstance(value, int_types): value = '{0:b}'.format(value) self._native = tuple(map(int, tuple(value))) @@ -1132,7 +1134,7 @@ class IntegerBitString(Primitive): ValueError - when an invalid value is passed """ - if not isinstance(value, int): + if not isinstance(value, int_types): raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value @@ -1279,7 +1281,7 @@ class IntegerOctetString(OctetString): ValueError - when an invalid value is passed """ - if not isinstance(value, int): + if not isinstance(value, int_types): raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value @@ -1477,7 +1479,7 @@ class Enumerated(Integer): ValueError - when an invalid value is passed """ - if not isinstance(value, int) and not isinstance(value, str_cls): + if not isinstance(value, int_types) and not isinstance(value, str_cls): raise ValueError('%s value must be an integer or a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) if isinstance(value, str_cls): @@ -1634,7 +1636,7 @@ class Sequence(Asn1Value): if self.children is None: self._parse_children() - if not isinstance(key, int): + if not isinstance(key, int_types): if key not in self._field_map: raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) key = self._field_map[key] @@ -1663,7 +1665,7 @@ class Sequence(Asn1Value): if self.children is None: self._parse_children() - if not isinstance(key, int): + if not isinstance(key, int_types): if key not in self._field_map: raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) key = self._field_map[key] @@ -1739,7 +1741,7 @@ class Sequence(Asn1Value): if self.children is None: self._parse_children() - if not isinstance(key, int): + if not isinstance(key, int_types): if key not in self._field_map: raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) key = self._field_map[key] -- cgit v1.2.3 From 488eac3f6a77aac265996374f68c4b45270407e1 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 23:35:05 -0400 Subject: Added ability to compute the public key from a private key --- asn1crypto/_elliptic_curve.py | 385 +++++++++++++++++++++++ asn1crypto/keys.py | 76 ++++- tests/fixtures/keys/test-ec-named-der.crt | Bin 0 -> 647 bytes tests/fixtures/keys/test-ec-named-der.key | Bin 0 -> 121 bytes tests/fixtures/keys/test-ec-named.crt | 16 + tests/fixtures/keys/test-ec-named.key | 5 + tests/fixtures/keys/test-pkcs8-dsa-der.key | Bin 0 -> 873 bytes tests/fixtures/keys/test-pkcs8-dsa.key | 21 ++ tests/fixtures/keys/test-pkcs8-ec-der.key | Bin 0 -> 381 bytes tests/fixtures/keys/test-pkcs8-ec-named-der.key | Bin 0 -> 138 bytes tests/fixtures/keys/test-pkcs8-ec-named.key | 5 + tests/fixtures/keys/test-pkcs8-ec.key | 10 + tests/fixtures/keys/test-public-ec-der.key | Bin 0 -> 335 bytes tests/fixtures/keys/test-public-ec-named-der.key | Bin 0 -> 91 bytes tests/fixtures/keys/test-public-ec-named.key | 4 + tests/fixtures/keys/test-public-ec.key | 9 + tests/test_keys.py | 23 +- 17 files changed, 551 insertions(+), 3 deletions(-) create mode 100644 asn1crypto/_elliptic_curve.py create mode 100644 tests/fixtures/keys/test-ec-named-der.crt create mode 100644 tests/fixtures/keys/test-ec-named-der.key create mode 100644 tests/fixtures/keys/test-ec-named.crt create mode 100644 tests/fixtures/keys/test-ec-named.key create mode 100644 tests/fixtures/keys/test-pkcs8-dsa-der.key create mode 100644 tests/fixtures/keys/test-pkcs8-dsa.key create mode 100644 tests/fixtures/keys/test-pkcs8-ec-der.key create mode 100644 tests/fixtures/keys/test-pkcs8-ec-named-der.key create mode 100644 tests/fixtures/keys/test-pkcs8-ec-named.key create mode 100644 tests/fixtures/keys/test-pkcs8-ec.key create mode 100644 tests/fixtures/keys/test-public-ec-der.key create mode 100644 tests/fixtures/keys/test-public-ec-named-der.key create mode 100644 tests/fixtures/keys/test-public-ec-named.key create mode 100644 tests/fixtures/keys/test-public-ec.key diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py new file mode 100644 index 0000000..a71a658 --- /dev/null +++ b/asn1crypto/_elliptic_curve.py @@ -0,0 +1,385 @@ +# coding: utf-8 + +""" +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, and to support loading and +serializing to/from ECPrimePoint format. 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 +from __future__ import division + +import sys +import math + +from ._int_conversion import int_to_bytes, int_from_bytes + +if sys.version_info < (3,): + byte_cls = str + int_types = (int, long) #pylint: disable=E0602 + +else: + byte_cls = bytes + int_types = int + + + +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 + """ + + @classmethod + def load(cls, curve, data): + """ + Loads a Point from the ECPoint OctetString representation + + :param curve: + A PrimeCurve object of the curve the point is on + + :param data: + A byte string of the ECPoint representation of the point. This + representation uses the first byte as a compression indicator, and + then lists the X and Y as base256 byte string integers + + :return: + A PrimePoint object + """ + + first_byte = data[0:1] + + # Uncompressed + if first_byte == b'\x04': + remaining = data[1:] + field_len = len(remaining) // 2 + x = int_from_bytes(remaining[0:field_len]) + y = int_from_bytes(remaining[field_len:]) + return cls(curve, x, y) + + if first_byte not in (b'\x02', b'\x03'): + raise ValueError('Invalid ECPoint representation of a point - first byte is incorrect') + + raise ValueError('Compressed ECPoint representations are not supported') + + 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: + assert self.curve.contains(self) + + if self.order: + assert self * self.order == INFINITY + + 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) + + def dump(self): + """ + Dumps a PrimePoint to the ECPoint OctetString representation + + :return: + A byte string of the ECPoint representation + """ + + field_len = int(math.ceil(math.log(self.curve.p, 2) / 8)) + + xb = int_to_bytes(self.x) + yb = int_to_bytes(self.y) + + # Make sure the fields are full width, according to the spec + while len(xb) < field_len: + xb = b'\x00' + xb + while len(yb) < field_len: + yb = b'\x00' + yb + + return b'\x04' + xb + yb + + +# This one point is the Point At Infinity for all purposes: +INFINITY = PrimePoint(None, None, None) + + +def inverse_mod(a, m): + """Inverse of a mod m.""" + + if a < 0 or m <= a: + a = a % m + + # From Ferguson and Schneier, roughly: + + c, d = a, m + 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*m = d. + # If d == 1, this means that ud is a inverse. + + assert d == 1 + if ud > 0: + return ud + else: + return ud + m + + +# NIST Curve P-192: +NIST_192_CURVE = PrimeCurve( + 6277101735386680763835789423207666416083908700390324961279, + -3, + 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1 +) +NIST_P192_BASE_POINT = PrimePoint( + NIST_192_CURVE, + 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012, + 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811, + 6277101735386680763835789423176059013767194773182842284081 +) + + +# NIST Curve P-224: +NIST_P224_CURVE = PrimeCurve( + 26959946667150639794667015087019630673557916260026308143510066298881, + -3, + 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4 +) +NIST_P224_BASE_POINT = PrimePoint( + NIST_P224_CURVE, + 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21, + 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34, + 26959946667150639794667015087019625940457807714424391721682722368061 +) + + +# NIST Curve P-256: +NIST_P256_CURVE = PrimeCurve( + 115792089210356248762697446949407573530086143415290314195533631308867097853951, + -3, + 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b +) +NIST_P256_BASE_POINT = PrimePoint( + NIST_P256_CURVE, + 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, + 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5, + 115792089210356248762697446949407573529996955224135760342422259061068512044369 +) + + +# NIST Curve P-384: +NIST_P384_CURVE = PrimeCurve( + 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, + -3, + 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef +) +NIST_P384_BASE_POINT = PrimePoint( + NIST_P384_CURVE, + 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7, + 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f, + 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643 +) + + +# NIST Curve P-521: +NIST_P521_CURVE = PrimeCurve( + 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, + -3, + 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 +) +NIST_P521_BASE_POINT = PrimePoint( + NIST_P521_CURVE, + 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, + 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, + 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 +) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 6ec6224..da6f822 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from __future__ import absolute_import import hashlib -from decimal import localcontext from .algos import DigestAlgorithm, EncryptionAlgorithm from .core import ( @@ -20,6 +19,16 @@ from .core import ( SequenceOf, SetOf, ) +from ._elliptic_curve import ( + NIST_P192_BASE_POINT, + NIST_P224_BASE_POINT, + NIST_P256_BASE_POINT, + NIST_P384_BASE_POINT, + NIST_P521_BASE_POINT, + PrimeCurve, + PrimePoint, +) +from ._int_conversion import int_from_bytes try: # Python 2 @@ -417,6 +426,7 @@ class PrivateKeyInfo(Sequence): 'private_key': _private_key_spec } + _computed_public_key = None _fingerprint = None @classmethod @@ -466,6 +476,68 @@ class PrivateKeyInfo(Sequence): return container + def compute_public_key(self): + """ + Computes the public key corresponding to the current private key. + + :return: + For RSA keys, an RSAPublicKey object. For DSA keys, an Integer + object. For ECDSA keys, an OctetString. + """ + + if self._computed_public_key is None: + algo = self['private_key_algorithm']['algorithm'].native + + if algo == 'dsa': + params = self['private_key_algorithm']['parameters'] + self._computed_public_key = Integer(pow( + params['g'].native, + self['private_key'].native, + params['p'].native + )) + + elif algo == 'rsa': + key = self['private_key'].parsed + self._computed_public_key = RSAPublicKey({ + 'modulus': key['modulus'], + 'public_exponent': key['public_exponent'], + }) + + elif algo == 'ecdsa': + params = self['private_key_algorithm']['parameters'] + chosen = params.chosen + + if params.name == 'implicit_ca': + raise ValueError('Unable to compute public key for ECDSA key using Implicit CA parameters') + + if params.name == 'specified': + if chosen['field_id']['field_type'] == 'characteristic_two_field': + raise ValueError('Unable to compute public key for ECDSA key over a characteristic two field') + + curve = PrimeCurve( + chosen['field_id']['parameters'].native, + int_from_bytes(chosen['curve']['a'].native), + int_from_bytes(chosen['curve']['b'].native) + ) + base_point = PrimePoint.load(curve, chosen['base'].native) + + elif params.name == 'named': + if chosen.native not in ('prime192v1', 'secp224r1', 'prime256v1', 'secp384r1', 'secp521r1'): + raise ValueError('Unable to compute public key for ECDSA named curve %s, parameters not currently included' % chosen.native) + + base_point = { + 'prime192v1': NIST_P192_BASE_POINT, + 'secp224r1': NIST_P224_BASE_POINT, + 'prime256v1': NIST_P256_BASE_POINT, + 'secp384r1': NIST_P384_BASE_POINT, + 'secp521r1': NIST_P521_BASE_POINT, + }[chosen.native] + + public_point = base_point * self['private_key'].parsed['private_key'].native + self._computed_public_key = OctetString(public_point.dump()) + + return self._computed_public_key + @property def fingerprint(self): """ @@ -602,7 +674,7 @@ class PublicKeyInfo(Sequence): 'dsa': Integer, # ECSDA's public key is an ECPoint, which is an OctetString. Since # we are using OctetBitString here, we don't need further parsing. - 'ecdsa': OctetString, + 'ecdsa': None, }[algorithm] _spec_callbacks = { diff --git a/tests/fixtures/keys/test-ec-named-der.crt b/tests/fixtures/keys/test-ec-named-der.crt new file mode 100644 index 0000000..4a78f03 Binary files /dev/null and b/tests/fixtures/keys/test-ec-named-der.crt differ diff --git a/tests/fixtures/keys/test-ec-named-der.key b/tests/fixtures/keys/test-ec-named-der.key new file mode 100644 index 0000000..6c98561 Binary files /dev/null and b/tests/fixtures/keys/test-ec-named-der.key differ diff --git a/tests/fixtures/keys/test-ec-named.crt b/tests/fixtures/keys/test-ec-named.crt new file mode 100644 index 0000000..df87560 --- /dev/null +++ b/tests/fixtures/keys/test-ec-named.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICgzCCAimgAwIBAgIJAOWsWhjG4kOgMAoGCCqGSM49BAMCMIGdMQswCQYDVQQG +EwJVUzEWMBQGA1UECAwNTWFzc2FjaHVzZXR0czEQMA4GA1UEBwwHTmV3YnVyeTEe +MBwGA1UECgwVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLDAdUZXN0aW5n +MRIwEAYDVQQDDAlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhu +cy5pbzAeFw0xNTA2MTcxNTQ5MDNaFw0xNTA3MTcxNTQ5MDNaMIGdMQswCQYDVQQG +EwJVUzEWMBQGA1UECAwNTWFzc2FjaHVzZXR0czEQMA4GA1UEBwwHTmV3YnVyeTEe +MBwGA1UECgwVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLDAdUZXN0aW5n +MRIwEAYDVQQDDAlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhu +cy5pbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIWbAAvPCfySlPRi3IxJ//es +1oz5ISNlGbiZKSDFRmQUuGMvuNHLcxV65xlabTQCtU06tWXhO19oKUFGEfqjfvuj +UDBOMB0GA1UdDgQWBBQjje7uR0gq5DVUuP1WaBZf4qrNgTAfBgNVHSMEGDAWgBQj +je7uR0gq5DVUuP1WaBZf4qrNgTAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gA +MEUCIQDGxGHBYh2VpgzM6GFctdVxwEo8oumb+Mv/2G9YyNH/bgIgVO4qPsVFvE6G +hY9L6tWXKiU4OD8E7GrySzDBRqjhIDU= +-----END CERTIFICATE----- diff --git a/tests/fixtures/keys/test-ec-named.key b/tests/fixtures/keys/test-ec-named.key new file mode 100644 index 0000000..93a9c31 --- /dev/null +++ b/tests/fixtures/keys/test-ec-named.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIQtgbLFPj1EGIBWhUjb8MKem8OaYkgy1mtqzxacT+n3oAoGCCqGSM49 +AwEHoUQDQgAEhZsAC88J/JKU9GLcjEn/96zWjPkhI2UZuJkpIMVGZBS4Yy+40ctz +FXrnGVptNAK1TTq1ZeE7X2gpQUYR+qN++w== +-----END EC PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-pkcs8-dsa-der.key b/tests/fixtures/keys/test-pkcs8-dsa-der.key new file mode 100644 index 0000000..c1b723a Binary files /dev/null and b/tests/fixtures/keys/test-pkcs8-dsa-der.key differ diff --git a/tests/fixtures/keys/test-pkcs8-dsa.key b/tests/fixtures/keys/test-pkcs8-dsa.key new file mode 100644 index 0000000..575ab80 --- /dev/null +++ b/tests/fixtures/keys/test-pkcs8-dsa.key @@ -0,0 +1,21 @@ +-----BEGIN PRIVATE KEY----- +MIIDZQIBADCCAzkGByqGSM44BAEwggMsAoIBgQDGz00klLP8CyoVk216zk+ldYap +ofKC/Mdxv5SvVQKqpr7UEhCPECn/kwLGjoKtby8m/s3DJ2Y1/59LiZhR8RZPFUGO +73lnxvguMRaY1lKMW1Z3Ylcn2wYyjA+4tSIxW4Nrt12JQvDhlYonPjNYqjWyasHI +/aK3ySMlVlb1gw1t4NCfisC0JIdgzWXc589OM7sWIztzEFGs+EEjCV0BOVi8WfRM +jaRuo9hw+Ev5LbMTy7rEK4BK5hPcMdFwTO1SFMxxJopUkafl/PK1zyNYy1rYrDWD +ioqPhcDQt/1U+nfvGUoKvBiEHfH1T467v5UMUizLglJ4EWCeYHU/7tQmep3FCf8b +uySZYzmMvgeK8anFKsjukGtxQwcnvCPD/bLTm4evbNKIK30jIRcAICD+OXJyAsU2 +84y92MO4OW6a0dzam2vGqw9zmpjNZlitvnvCcuSw7t282ty/poJqRxLQjbgsoqpj +wwYOCemgYSlCNJ7XQ0/kUrHTPrbGCl8Yvx0LCkkCIQCeRUmNf/OhUimhQrhBgfqo +HymVcB9RxFr4JwpXUGlduwKCAYAhjX0YsOUsVCaRuSMrJBcdr8NeuhIZmnPIGPWM +rrXxuUA/XbLYG2meHp4uL7Xumx0ahAKG1yc9ZM12UW+cZm1Gmf+agZHcJUAMq1x/ +w6fzaKy+7qLU4yutVe0uBE9Z55ml9YiWuKN/XK9IA6H+7K4C5gtCKFCH9d1wuiTL +EWbOB+PeYb0N/UbDfqou49y/YiWEG2lAuwYGQzYrKEbhFqQJEzw+cbMbX6bg85te +ACjaM6G1CPvktBIJkwjM6rxuGzGN8wFuoTk/wCBQ8t3W33XIS+okTEHkbY6Ez46y +YNDH+ItJarltZaWA/I03plMqjwlAKpUrXcapk+wKnyP3sTrSmiQvPWYa0r3iLfg4 +8fU4NO/VKS/WDYCIuL+dVwcMRFyI3i4GzVwQqmGLzMFqVAKw1GxAwn0l/lZ9GkYo +Ay2g1DmfGNXL8V7IIRqudz/OVvvoXbfl+lRfpaZJUJ9zeDoLa/X4JAKTJl3Kw0aC +j0T/rGOVd+Bx0kFrOI9fiLo0XWoEIwIhAJUN+c1g4U0Po+7Wx2LUiiZhEHwwn4c/ +ItFl1L631UoM +-----END PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-pkcs8-ec-der.key b/tests/fixtures/keys/test-pkcs8-ec-der.key new file mode 100644 index 0000000..0194426 Binary files /dev/null and b/tests/fixtures/keys/test-pkcs8-ec-der.key differ diff --git a/tests/fixtures/keys/test-pkcs8-ec-named-der.key b/tests/fixtures/keys/test-pkcs8-ec-named-der.key new file mode 100644 index 0000000..86b55f8 Binary files /dev/null and b/tests/fixtures/keys/test-pkcs8-ec-named-der.key differ diff --git a/tests/fixtures/keys/test-pkcs8-ec-named.key b/tests/fixtures/keys/test-pkcs8-ec-named.key new file mode 100644 index 0000000..1de8e29 --- /dev/null +++ b/tests/fixtures/keys/test-pkcs8-ec-named.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQghC2BssU+PUQYgFaF +SNvwwp6bw5piSDLWa2rPFpxP6fehRANCAASFmwALzwn8kpT0YtyMSf/3rNaM+SEj +ZRm4mSkgxUZkFLhjL7jRy3MVeucZWm00ArVNOrVl4TtfaClBRhH6o377 +-----END PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-pkcs8-ec.key b/tests/fixtures/keys/test-pkcs8-ec.key new file mode 100644 index 0000000..0751d92 --- /dev/null +++ b/tests/fixtures/keys/test-pkcs8-ec.key @@ -0,0 +1,10 @@ +-----BEGIN PRIVATE KEY----- +MIIBeQIBADCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAAB +AAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA +///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMV +AMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg +9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8A +AAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBBG0wawIBAQQg6OWPI5AB6xIv +ibu6NwIv2Myye+QfQp4+U/mJ0GmFQ12hRANCAASLXUxx99bGo0ljQlxHn8tzJB3J +3dEt8TqftwTeINBYAJNU9onHL4cr9/k9OzTtnnsOPVdC33gDC8wxxgPXn2AB +-----END PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-public-ec-der.key b/tests/fixtures/keys/test-public-ec-der.key new file mode 100644 index 0000000..98b9a9d Binary files /dev/null and b/tests/fixtures/keys/test-public-ec-der.key differ diff --git a/tests/fixtures/keys/test-public-ec-named-der.key b/tests/fixtures/keys/test-public-ec-named-der.key new file mode 100644 index 0000000..667b5fd Binary files /dev/null and b/tests/fixtures/keys/test-public-ec-named-der.key differ diff --git a/tests/fixtures/keys/test-public-ec-named.key b/tests/fixtures/keys/test-public-ec-named.key new file mode 100644 index 0000000..55af596 --- /dev/null +++ b/tests/fixtures/keys/test-public-ec-named.key @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhZsAC88J/JKU9GLcjEn/96zWjPkh +I2UZuJkpIMVGZBS4Yy+40ctzFXrnGVptNAK1TTq1ZeE7X2gpQUYR+qN++w== +-----END PUBLIC KEY----- diff --git a/tests/fixtures/keys/test-public-ec.key b/tests/fixtures/keys/test-public-ec.key new file mode 100644 index 0000000..7a93268 --- /dev/null +++ b/tests/fixtures/keys/test-public-ec.key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA +AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA//// +///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd +NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5 +RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA +//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABItdTHH31sajSWNCXEefy3Mk +Hcnd0S3xOp+3BN4g0FgAk1T2iccvhyv3+T07NO2eew49V0LfeAMLzDHGA9efYAE= +-----END PUBLIC KEY----- diff --git a/tests/test_keys.py b/tests/test_keys.py index e47ce41..cbc342e 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -7,12 +7,14 @@ from collections import OrderedDict from asn1crypto import keys, core +from .unittest_data import DataDecorator, data + tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') - +@DataDecorator class KeysTests(unittest.TestCase): def test_parse_rsa_private_key(self): @@ -281,3 +283,22 @@ class KeysTests(unittest.TestCase): None, key_info['attributes'].native ) + + #pylint: disable=C0326 + @staticmethod + def key_pairs(): + return ( + ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-public-dsa-der.key'), + ('ecdsa_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-public-ec-named-der.key'), + ('ecdsa', 'keys/test-pkcs8-ec-der.key', 'keys/test-public-ec-der.key'), + ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-public-der.key'), + ) + + @data('key_pairs', True) + def compute_public_key(self, private_key_file, public_key_file): + with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(public_key['public_key'].native, private_key.compute_public_key().native) -- cgit v1.2.3 From a4bfddd12053d5afb33e455a37788a5bb93540bd Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 23:36:16 -0400 Subject: Fixed bugs in PrivateKeyInfo.wrap() --- asn1crypto/keys.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index da6f822..0075277 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -458,10 +458,12 @@ class PrivateKeyInfo(Sequence): params['p'] = private_key['p'] params['q'] = private_key['q'] params['g'] = private_key['g'] + private_key = private_key['private_key'] elif algorithm == 'ecdsa': if not isinstance(private_key, ECPrivateKey): private_key = ECPrivateKey.load(private_key) params = private_key['parameters'] + del private_key['parameters'] else: raise ValueError('algorithm must be one of "rsa", "dsa", "ecdsa" - is %s' % repr(algorithm)) -- cgit v1.2.3 From eb02d766de42b0b21ddbb49bf7139fa5a6707b46 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 17 Jun 2015 23:37:30 -0400 Subject: Improved key fingerprinting for ECDSA --- asn1crypto/keys.py | 70 +++++++++++++++++++++++++----------------------------- tests/test_keys.py | 9 +++++++ 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 0075277..0d15043 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -34,13 +34,11 @@ try: # Python 2 str_cls = unicode #pylint: disable=E0602 byte_cls = str - import cPickle as pickle #pylint: disable=F0401 except NameError: # Python 3 str_cls = str byte_cls = bytes - import pickle @@ -559,7 +557,7 @@ class PrivateKeyInfo(Sequence): if self._fingerprint is None: key_type = self['private_key_algorithm']['algorithm'].native - params = self['private_key_algorithm']['parameters'].native + params = self['private_key_algorithm']['parameters'] key = self['private_key'].parsed if key_type == 'rsa': @@ -569,38 +567,33 @@ class PrivateKeyInfo(Sequence): ) elif key_type == 'dsa': - # The private key structure for PKCS#8 does not include the - # public key, so we must calculate it here - with localcontext() as ctx: - ctx.prec = 200 - public_key = ctx.power(params['g'].native, key.native, params['p'].native) - + public_key = self.compute_public_key() to_hash = '%d:%d:%d:%d' % ( params['p'].native, params['q'].native, params['g'].native, - int(public_key), + public_key.native, ) elif key_type == 'ecdsa': public_key = key['public_key'].native if public_key is None: - raise ValueError('Unable to compute fingerprint of ecdsa private key since the public_key field is empty') + public_key = self.compute_public_key().native if params.name == 'named': - to_hash = '%s:%d' % ( - params.chosen.native, - public_key, - ) + to_hash = '%s:' % params.chosen.native + to_hash = to_hash.encode('utf-8') + to_hash += public_key - elif params.named == 'implicit_ca': - to_hash = str_cls(public_key) + elif params.name == 'implicit_ca': + to_hash = public_key - elif params.named == 'specified': - to_hash = b'%s:%s' % ( - pickle.dumps(params.native), - str_cls(public_key).encode('utf-8'), - ) + 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') @@ -734,16 +727,17 @@ class PublicKeyInfo(Sequence): if self._fingerprint is None: key_type = self['algorithm']['algorithm'].native - params = self['algorithm']['parameters'].native - key = self['public_key'].parsed + 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, @@ -752,20 +746,22 @@ class PublicKeyInfo(Sequence): ) elif key_type == 'ecdsa': - if params.name == 'named': - to_hash = '%s:%d' % ( - params.chosen.native, - key.native, - ) - - elif params.named == 'implicit_ca': - to_hash = str_cls(key.native) + key = self['public_key'] - elif params.named == 'specified': - to_hash = b'%s:%s' % ( - pickle.dumps(params.native), - str_cls(key.native).encode('utf-8'), - ) + 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') diff --git a/tests/test_keys.py b/tests/test_keys.py index cbc342e..1738fa9 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -294,6 +294,15 @@ class KeysTests(unittest.TestCase): ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-public-der.key'), ) + @data('key_pairs', True) + def compare_fingerprints(self, private_key_file, public_key_file): + with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(private_key.fingerprint, public_key.fingerprint) + @data('key_pairs', True) def compute_public_key(self, private_key_file, public_key_file): with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: -- cgit v1.2.3 From 500703aa01096694547a324a7598fe840ccf4f92 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 18 Jun 2015 14:13:39 -0400 Subject: Improved the performance of computing an elliptic curve public key from the private key --- asn1crypto/_elliptic_curve.py | 138 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 112 insertions(+), 26 deletions(-) diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index a71a658..33566c4 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -33,6 +33,8 @@ from __future__ import division import sys import math +from ctypes.util import find_library +from ctypes import CDLL, c_int, c_char_p, c_void_p, create_string_buffer from ._int_conversion import int_to_bytes, int_from_bytes @@ -257,8 +259,7 @@ class PrimePoint(): p = self.curve.p a = self.curve.a - l = ((3 * self.x * self.x + a) * \ - inverse_mod(2 * self.y, p)) % p + 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 @@ -291,30 +292,6 @@ class PrimePoint(): INFINITY = PrimePoint(None, None, None) -def inverse_mod(a, m): - """Inverse of a mod m.""" - - if a < 0 or m <= a: - a = a % m - - # From Ferguson and Schneier, roughly: - - c, d = a, m - 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*m = d. - # If d == 1, this means that ud is a inverse. - - assert d == 1 - if ud > 0: - return ud - else: - return ud + m - - # NIST Curve P-192: NIST_192_CURVE = PrimeCurve( 6277101735386680763835789423207666416083908700390324961279, @@ -383,3 +360,112 @@ NIST_P521_BASE_POINT = PrimePoint( 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 ) + + + +use_python_inverse_mod = True + +libcrypto_path = find_library('libcrypto') +if libcrypto_path: + try: + libcrypto = CDLL(libcrypto_path) + + BN_new = libcrypto.BN_new + BN_new.argtypes = [] + BN_new.restype = c_void_p + + BN_bin2bn = libcrypto.BN_bin2bn + BN_bin2bn.argtypes = [c_char_p, c_int, c_void_p] + BN_bin2bn.restype = c_void_p + + BN_bn2bin = libcrypto.BN_bn2bin + BN_bn2bin.argtypes = [c_void_p, c_char_p] + BN_bn2bin.restype = c_int + + BN_set_negative = libcrypto.BN_set_negative + BN_set_negative.argtypes = [c_void_p, c_int] + BN_set_negative.restype = None + + BN_num_bits = libcrypto.BN_num_bits + BN_num_bits.argtypes = [c_void_p] + BN_num_bits.restype = c_int + + BN_free = libcrypto.BN_free + BN_free.argtypes = [c_void_p] + BN_free.restype = None + + BN_CTX_new = libcrypto.BN_CTX_new + BN_CTX_new.argtypes = [] + BN_CTX_new.restype = c_void_p + + BN_CTX_free = libcrypto.BN_CTX_free + BN_CTX_free.argtypes = [c_void_p] + BN_CTX_free.restype = None + + BN_mod_inverse = libcrypto.BN_mod_inverse + BN_mod_inverse.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] + BN_mod_inverse.restype = c_void_p + + use_python_inverse_mod = False + + except (AttributeError): #pylint: disable=W0704 + pass + +if not use_python_inverse_mod: + + def inverse_mod(a, p): + """ + Compute the inverse modul + """ + + ctx = BN_CTX_new() + + a_bytes = int_to_bytes(abs(a)) + p_bytes = int_to_bytes(abs(p)) + + a_buf = create_string_buffer(a_bytes) + a_bn = BN_bin2bn(a_buf, len(a_bytes), None) + if a < 0: + BN_set_negative(a_bn, 1) + + p_buf = create_string_buffer(p_bytes) + p_bn = BN_bin2bn(p_buf, len(p_bytes), None) + if p < 0: + BN_set_negative(p_bn, 1) + + r_bn = BN_mod_inverse(None, a_bn, p_bn, ctx) + r_len_bits = BN_num_bits(r_bn) + r_len = math.ceil(r_len_bits / 8) + r_buf = create_string_buffer(r_len) + BN_bn2bin(r_bn, r_buf) + result = int_from_bytes(r_buf.raw) + + BN_free(a_bn) + BN_free(p_bn) + BN_free(r_bn) + BN_CTX_free(ctx) + + return result + +else: + + def inverse_mod(a, m): + if a < 0 or m <= a: + a = a % m + + # From Ferguson and Schneier, roughly: + + c, d = a, m + 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*m = d. + # If d == 1, this means that ud is a inverse. + + assert d == 1 + if ud > 0: + return ud + else: + return ud + m -- cgit v1.2.3 From 156877f2bff06efce70423e1b693d1e460c038eb Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 18 Jun 2015 14:13:58 -0400 Subject: Fixed a linting error in core --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ab05196..fe834f1 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -15,7 +15,7 @@ from ._int_conversion import int_to_bytes, int_from_bytes if sys.version_info <= (3,): str_cls = unicode #pylint: disable=E0602 byte_cls = str - int_types = (int, long) + int_types = (int, long) #pylint: disable=E0602 py2 = True chr_cls = chr range = xrange #pylint: disable=E0602,W0622 -- cgit v1.2.3 From eb5eb6ea737f50e53b3a60e95b46303860d28998 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 18 Jun 2015 14:14:12 -0400 Subject: Add the .public_key property to PrivateKeyInfo and cached values for better performance. .compute_public_key() was renamed to ._compute_public_key() and no longer caches, but is only called when really needed. --- asn1crypto/keys.py | 154 +++++++++++++++++++++++++++++++++++------------------ tests/test_keys.py | 11 +++- 2 files changed, 111 insertions(+), 54 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 0d15043..dcec539 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -424,7 +424,7 @@ class PrivateKeyInfo(Sequence): 'private_key': _private_key_spec } - _computed_public_key = None + _public_key = None _fingerprint = None @classmethod @@ -456,6 +456,7 @@ class PrivateKeyInfo(Sequence): params['p'] = private_key['p'] params['q'] = private_key['q'] params['g'] = private_key['g'] + public_key = private_key['public_key'] private_key = private_key['private_key'] elif algorithm == 'ecdsa': if not isinstance(private_key, ECPrivateKey): @@ -474,9 +475,14 @@ class PrivateKeyInfo(Sequence): container['private_key_algorithm'] = private_key_algo container['private_key'] = OctetString(private_key.dump(normal_tagging=True)) + # Here we save the DSA public key if possible since it is not contained + # within the PKCS#8 structure for a DSA key + if algorithm == 'dsa': + container._public_key = public_key #pylint: disable=W0212 + return container - def compute_public_key(self): + def _compute_public_key(self): """ Computes the public key corresponding to the current private key. @@ -485,58 +491,100 @@ class PrivateKeyInfo(Sequence): object. For ECDSA keys, an OctetString. """ - if self._computed_public_key is None: - algo = self['private_key_algorithm']['algorithm'].native + algo = self['private_key_algorithm']['algorithm'].native - if algo == 'dsa': - params = self['private_key_algorithm']['parameters'] - self._computed_public_key = Integer(pow( - params['g'].native, - self['private_key'].native, - params['p'].native - )) + if algo == 'dsa': + params = self['private_key_algorithm']['parameters'] + return Integer(pow( + params['g'].native, + self['private_key'].native, + params['p'].native + )) + + if algo == 'rsa': + key = self['private_key'].parsed + return RSAPublicKey({ + 'modulus': key['modulus'], + 'public_exponent': key['public_exponent'], + }) + + if algo == 'ecdsa': + params = self['private_key_algorithm']['parameters'] + chosen = params.chosen + + if params.name == 'implicit_ca': + raise ValueError('Unable to compute public key for ECDSA key using Implicit CA parameters') + + if params.name == 'specified': + if chosen['field_id']['field_type'] == 'characteristic_two_field': + raise ValueError('Unable to compute public key for ECDSA key over a characteristic two field') + + curve = PrimeCurve( + chosen['field_id']['parameters'].native, + int_from_bytes(chosen['curve']['a'].native), + int_from_bytes(chosen['curve']['b'].native) + ) + base_point = PrimePoint.load(curve, chosen['base'].native) + + elif params.name == 'named': + if chosen.native not in ('prime192v1', 'secp224r1', 'prime256v1', 'secp384r1', 'secp521r1'): + raise ValueError('Unable to compute public key for ECDSA named curve %s, parameters not currently included' % chosen.native) + + base_point = { + 'prime192v1': NIST_P192_BASE_POINT, + 'secp224r1': NIST_P224_BASE_POINT, + 'prime256v1': NIST_P256_BASE_POINT, + 'secp384r1': NIST_P384_BASE_POINT, + 'secp521r1': NIST_P521_BASE_POINT, + }[chosen.native] - elif algo == 'rsa': + public_point = base_point * self['private_key'].parsed['private_key'].native + return OctetString(public_point.dump()) + + def unwrap(self): + """ + Unwraps the private key into an RSAPrivateKey, DSAPrivateKey or + ECPrivateKey object + + :return: + An RSAPrivateKey, DSAPrivateKey or ECPrivateKey object + """ + + algo = self['private_key_algorithm']['algorithm'].native + + if algo == 'rsa': + return self['private_key'] + + if algo == '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'], + }) + + if algo == 'ecdsa': + output = self['private_key'] + output['parameters'] = self['private_key_algorithm']['parameters'] + output['public_key'] = OctetBitString(self.public_key.native) + return output + + @property + def public_key(self): + if self._public_key is None: + if self['private_key_algorithm']['algorithm'].native == 'ecdsa': key = self['private_key'].parsed - self._computed_public_key = RSAPublicKey({ - 'modulus': key['modulus'], - 'public_exponent': key['public_exponent'], - }) - - elif algo == 'ecdsa': - params = self['private_key_algorithm']['parameters'] - chosen = params.chosen - - if params.name == 'implicit_ca': - raise ValueError('Unable to compute public key for ECDSA key using Implicit CA parameters') - - if params.name == 'specified': - if chosen['field_id']['field_type'] == 'characteristic_two_field': - raise ValueError('Unable to compute public key for ECDSA key over a characteristic two field') - - curve = PrimeCurve( - chosen['field_id']['parameters'].native, - int_from_bytes(chosen['curve']['a'].native), - int_from_bytes(chosen['curve']['b'].native) - ) - base_point = PrimePoint.load(curve, chosen['base'].native) - - elif params.name == 'named': - if chosen.native not in ('prime192v1', 'secp224r1', 'prime256v1', 'secp384r1', 'secp521r1'): - raise ValueError('Unable to compute public key for ECDSA named curve %s, parameters not currently included' % chosen.native) - - base_point = { - 'prime192v1': NIST_P192_BASE_POINT, - 'secp224r1': NIST_P224_BASE_POINT, - 'prime256v1': NIST_P256_BASE_POINT, - 'secp384r1': NIST_P384_BASE_POINT, - 'secp521r1': NIST_P521_BASE_POINT, - }[chosen.native] - - public_point = base_point * self['private_key'].parsed['private_key'].native - self._computed_public_key = OctetString(public_point.dump()) - - return self._computed_public_key + if key['public_key']: + self._public_key = OctetString(key['public_key'].native) + else: + self._public_key = self._compute_public_key() + else: + self._public_key = self._compute_public_key() + + return self._public_key @property def fingerprint(self): @@ -567,7 +615,7 @@ class PrivateKeyInfo(Sequence): ) elif key_type == 'dsa': - public_key = self.compute_public_key() + public_key = self.public_key to_hash = '%d:%d:%d:%d' % ( params['p'].native, params['q'].native, @@ -578,7 +626,7 @@ class PrivateKeyInfo(Sequence): elif key_type == 'ecdsa': public_key = key['public_key'].native if public_key is None: - public_key = self.compute_public_key().native + public_key = self.public_key.native if params.name == 'named': to_hash = '%s:' % params.chosen.native diff --git a/tests/test_keys.py b/tests/test_keys.py index 1738fa9..07b4539 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -310,4 +310,13 @@ class KeysTests(unittest.TestCase): with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: public_key = keys.PublicKeyInfo.load(f.read()) - self.assertEqual(public_key['public_key'].native, private_key.compute_public_key().native) + self.assertEqual(public_key['public_key'].native, private_key._compute_public_key().native) #pylint: disable=W0212 + + @data('key_pairs', True) + def public_key_property(self, private_key_file, public_key_file): + with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(public_key['public_key'].native, private_key.public_key.native) -- cgit v1.2.3 From e95da2c3a5f5ce9931c2d8a3ab05120cf8f684b6 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 01:49:44 -0400 Subject: Removed normal_tagging parameter for .dump(), added .untag() and .retag() --- asn1crypto/core.py | 252 +++++++++++++++++++++++++++++++++++------------------ asn1crypto/keys.py | 4 +- 2 files changed, 170 insertions(+), 86 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index fe834f1..00f147b 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -229,7 +229,54 @@ class Asn1Value(): """ return '<%s %s>' % (self.__class__.__name__, repr(self.contents)) - def dump(self, force=False, normal_tagging=False): + def retag(self, tag_type, tag): + """ + Copies the object, applying a new tagging to it + + :param tag_type: + A unicode string of "implicit" or "explicit" + + :param tag: + A integer tag number + + :return: + An Asn1Value object + """ + + new_obj = self.__class__(tag_type=tag_type, tag=tag) + new_obj._copy(self) #pylint: disable=W0212 + return new_obj + + def untag(self): + """ + Copies the object, removing any special tagging from it + + :return: + An Asn1Value object + """ + + new_obj = self.__class__() + new_obj._copy(self) #pylint: disable=W0212 + return new_obj + + #pylint: disable=W0212 + def _copy(self, other): + """ + Copies the contents of another Asn1Value object to itself + + :param object: + Another instance of the same class + """ + + if self.__class__ != other.__class__: + raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + + self.contents = other.contents + self._native = other._native + if hasattr(other, '_parsed'): + self._parsed = other._parsed + + def dump(self, force=False): """ Encodes the value using DER @@ -237,44 +284,15 @@ class Asn1Value(): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ - if (self.header is None) or normal_tagging: - header = b'' + if self.header is None: + header = _dump_header(self.class_, self.method, self.tag, self.contents) trailer = b'' - id_num = 0 - id_num |= self.class_ << 6 - id_num |= self.method << 5 - - if normal_tagging: - tag = self.__class__.tag - else: - tag = self.tag - - if tag >= 31: - header += chr_cls(id_num | 31) - while tag > 0: - continuation_bit = 0x80 if tag > 0x7F else 0 - header += chr_cls(continuation_bit | (tag & 0x7F)) - tag = tag >> 7 - else: - header += chr_cls(id_num | tag) - - length = len(self.contents) - if length <= 127: - header += chr_cls(length) - else: - length_bytes = int_to_bytes(length) - header += chr_cls(0x80 | len(length_bytes)) - header += length_bytes - - if self.tag_type == 'explicit' and not normal_tagging: + if self.tag_type == 'explicit': container = Asn1Value() container.method = 1 container.class_ = self.explicit_class @@ -285,15 +303,10 @@ class Asn1Value(): header = container.header + header trailer += container.trailer - if not normal_tagging: - self.header = header - self.trailer = trailer - - else: - header = self.header - trailer = self.trailer + self.header = header + self.trailer = trailer - return header + self.contents + trailer + return self.header + self.contents + self.trailer def pprint(self): """ @@ -351,7 +364,7 @@ class NoValue(Asn1Value): return None - def dump(self, force=False, normal_tagging=False): + def dump(self, force=False): """ Encodes the value using DER @@ -359,9 +372,6 @@ class NoValue(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ @@ -432,7 +442,7 @@ class Any(Asn1Value): self._parsed = (parsed_value, spec, spec_params) return self._parsed[0] - def dump(self, force=False, normal_tagging=False): + def dump(self, force=False): """ Encodes the value using DER @@ -440,9 +450,6 @@ class Any(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ @@ -450,7 +457,7 @@ class Any(Asn1Value): if self._parsed is None: self.parse() - return self._parsed[0].dump(force=force, normal_tagging=normal_tagging) + return self._parsed[0].dump(force=force) class Choice(Asn1Value): @@ -559,7 +566,7 @@ class Choice(Asn1Value): try: info = self._alternatives[self._choice] params = info[2] if len(info) > 2 else {} - self._parsed, _ = _parse_build(self.header + self.contents + self.trailer, spec=info[1], spec_params=params) + self._parsed, _ = _parse_build(self.contents, spec=info[1], spec_params=params) except (ValueError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args @@ -626,7 +633,25 @@ class Choice(Asn1Value): return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag) - def dump(self, force=False, normal_tagging=False): + #pylint: disable=W0212 + def _copy(self, other): + """ + Copies the contents of another Asn1Value object to itself + + :param object: + Another instance of the same class + """ + + if self.__class__ != other.__class__: + raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + + self.contents = other.contents + self._native = other._native + self._choice = other._choice + self._name = other._name + self._parsed = other._parsed + + def dump(self, force=False): """ Encodes the value using DER @@ -634,14 +659,17 @@ class Choice(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ - return self.chosen.dump(force=force, normal_tagging=normal_tagging) + self.contents = self.chosen.dump(force=force) + if self.header is None: + if self.tag_type == 'explicit': + self.header = _dump_header(self.explicit_class, 1, self.explicit_tag, self.contents) + else: + self.header = b'' + return self.header + self.contents class Primitive(Asn1Value): @@ -689,7 +717,7 @@ class Primitive(Asn1Value): if self.trailer != b'': self.trailer = b'' - def dump(self, force=False, normal_tagging=False): + def dump(self, force=False): """ Encodes the value using DER @@ -697,9 +725,6 @@ class Primitive(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ @@ -709,7 +734,7 @@ class Primitive(Asn1Value): self.contents = None self.set(native) - return Asn1Value.dump(self, normal_tagging=normal_tagging) + return Asn1Value.dump(self) class AbstractString(Primitive): @@ -844,7 +869,7 @@ class Integer(Primitive, ValueMap): value = self._reverse_map[value] elif not isinstance(value, int_types): - raise ValueError('%s value must be an integer or unicode string when a name_map is provided' % self.__class__.__name__) + raise ValueError('%s value must be an integer or unicode string when a name_map is provided, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = self._map[value] if self._map and value in self._map else value @@ -1090,7 +1115,7 @@ class OctetBitString(Primitive): return self._parsed[0] - def dump(self, force=False, normal_tagging=False): + def dump(self, force=False): """ Encodes the value using DER @@ -1098,9 +1123,6 @@ class OctetBitString(Primitive): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ @@ -1113,7 +1135,7 @@ class OctetBitString(Primitive): self.contents = None self.set(native) - return Asn1Value.dump(self, normal_tagging=normal_tagging) + return Asn1Value.dump(self) class IntegerBitString(Primitive): @@ -1239,7 +1261,7 @@ class OctetString(Primitive): return self._parsed[0] - def dump(self, force=False, normal_tagging=False): + def dump(self, force=False): """ Encodes the value using DER @@ -1247,9 +1269,6 @@ class OctetString(Primitive): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ @@ -1262,7 +1281,7 @@ class OctetString(Primitive): self.contents = None self.set(native) - return Asn1Value.dump(self, normal_tagging=normal_tagging) + return Asn1Value.dump(self) class IntegerOctetString(OctetString): @@ -1672,6 +1691,7 @@ class Sequence(Asn1Value): field_info = self._fields[key] field_spec = field_info[1] + field_params = field_info[2] if len(field_info) > 2 else {} value_spec = field_spec if self._spec_callbacks is not None and field_info[0] in self._spec_callbacks: @@ -1702,12 +1722,14 @@ class Sequence(Asn1Value): elif isinstance(value, field_spec): new_value = value + if value_spec != field_spec: + new_value.parse(value_spec) else: if isinstance(value, value_spec): new_value = value else: - new_value = value_spec(value, **(field_info[2] if len(field_info) > 2 else {})) + new_value = value_spec(value) # For when the field is OctetString or OctetBitString with embedded # values we need to wrap the value in the field spec to get the @@ -1717,6 +1739,10 @@ class Sequence(Asn1Value): wrapper._parsed = new_value #pylint: disable=W0212 new_value = wrapper + if field_params and 'tag_type' in field_params and 'tag' in field_params: + if field_params['tag_type'] != new_value.tag_type or field_params['tag'] != new_value.tag: + new_value = new_value.retag(tag_type=field_params['tag_type'], tag=field_params['tag']) + if new_value.contents is None: raise ValueError('Value for field "%s" of %s is not set' % (field_info[0], self.__class__.__name__)) @@ -1833,7 +1859,7 @@ class Sequence(Asn1Value): """ if self.contents is None: - self.children = [None] * len(self._fields) + self.children = [NoValue()] * len(self._fields) return try: @@ -1945,7 +1971,23 @@ class Sequence(Asn1Value): self._native[self._fields[index][0]] = child.native return self._native - def dump(self, force=False, normal_tagging=False): + #pylint: disable=W0212 + def _copy(self, other): + """ + Copies the contents of another Asn1Value object to itself + + :param object: + Another instance of the same class + """ + + if self.__class__ != other.__class__: + raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + + self.contents = other.contents + self._native = other._native + self.children = other.children + + def dump(self, force=False): """ Encodes the value using DER @@ -1953,9 +1995,6 @@ class Sequence(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ @@ -1963,7 +2002,7 @@ class Sequence(Asn1Value): if force: self._set_contents(force=force) - return Asn1Value.dump(self, normal_tagging=normal_tagging) + return Asn1Value.dump(self) class SequenceOf(Asn1Value): @@ -2209,7 +2248,23 @@ class SequenceOf(Asn1Value): self._native = [child.native for child in self] return self._native - def dump(self, force=False, normal_tagging=False): + #pylint: disable=W0212 + def _copy(self, other): + """ + Copies the contents of another Asn1Value object to itself + + :param object: + Another instance of the same class + """ + + if self.__class__ != other.__class__: + raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + + self.contents = other.contents + self._native = other._native + self.children = other.children + + def dump(self, force=False): """ Encodes the value using DER @@ -2217,9 +2272,6 @@ class SequenceOf(Asn1Value): If the encoded contents already exist, clear them and regenerate to ensure they are in DER format instead of BER format - :param normal_tagging: - Ignore implicit or explicit tagging when serializing - :return: A byte string of the DER-encoded value """ @@ -2227,7 +2279,7 @@ class SequenceOf(Asn1Value): if force: self._set_contents(force=force) - return Asn1Value.dump(self, normal_tagging=normal_tagging) + return Asn1Value.dump(self) class Set(Sequence): @@ -2607,6 +2659,35 @@ class BMPString(AbstractString): _encoding = 'utf-16-be' +def _dump_header(class_, method, tag, contents): + header = b'' + + id_num = 0 + id_num |= class_ << 6 + id_num |= method << 5 + + tag = tag + + if tag >= 31: + header += chr_cls(id_num | 31) + while tag > 0: + continuation_bit = 0x80 if tag > 0x7F else 0 + header += chr_cls(continuation_bit | (tag & 0x7F)) + tag = tag >> 7 + else: + header += chr_cls(id_num | tag) + + length = len(contents) + if length <= 127: + header += chr_cls(length) + else: + length_bytes = int_to_bytes(length) + header += chr_cls(0x80 | len(length_bytes)) + header += length_bytes + + return header + + def _build_id_tuple(params, spec): """ Builds a 2-element tuple used to identify fields by grabbing the class_ @@ -2847,6 +2928,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class value.explicit_tag = original_value.explicit_tag + elif isinstance(value, Choice): + value.contents = value.header + value.contents + value.header = b'' try: # Force parsing the Choice now diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index dcec539..5b4f27d 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -473,7 +473,7 @@ class PrivateKeyInfo(Sequence): container = cls() container['version'] = Integer(0) container['private_key_algorithm'] = private_key_algo - container['private_key'] = OctetString(private_key.dump(normal_tagging=True)) + container['private_key'] = OctetString(private_key.untag().dump()) # Here we save the DSA public key if possible since it is not contained # within the PKCS#8 structure for a DSA key @@ -754,7 +754,7 @@ class PublicKeyInfo(Sequence): container = cls() container['algorithm'] = algo if isinstance(public_key, Asn1Value): - public_key = public_key.dump(normal_tagging=True) + public_key = public_key.untag().dump() container['public_key'] = OctetBitString(public_key) return container -- cgit v1.2.3 From f937f48708d530f8031770ecddb9024e20013c4c Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 01:51:11 -0400 Subject: Added .bit_size, .algorithm and .curve to PublicKeyInfo and PrivateKeyInfo --- asn1crypto/keys.py | 192 +++++++++++++++++++++++++++++++++++++++++++---------- tests/test_keys.py | 122 ++++++++++++++++++++++++++++++++-- 2 files changed, 272 insertions(+), 42 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 5b4f27d..4ebc07e 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from __future__ import absolute_import import hashlib +import math from .algos import DigestAlgorithm, EncryptionAlgorithm from .core import ( @@ -268,18 +269,18 @@ class NamedCurve(ObjectIdentifier): '1.2.840.10045.3.0.18': 'c2tnb359v1', '1.2.840.10045.3.0.19': 'c2pnb368w1', '1.2.840.10045.3.0.20': 'c2tnb431r1', - '1.2.840.10045.3.1.1': 'prime192v1', '1.2.840.10045.3.1.2': 'prime192v2', '1.2.840.10045.3.1.3': 'prime192v3', '1.2.840.10045.3.1.4': 'prime239v1', '1.2.840.10045.3.1.5': 'prime239v2', '1.2.840.10045.3.1.6': 'prime239v3', - '1.2.840.10045.3.1.7': 'prime256v1', # https://tools.ietf.org/html/rfc5480#page-5 '1.3.132.0.1': 'sect163k1', '1.3.132.0.15': 'sect163r2', + '1.2.840.10045.3.1.1': 'secp192r1', '1.3.132.0.33': 'secp224r1', '1.3.132.0.26': 'sect233k1', + '1.2.840.10045.3.1.7': 'secp256r1', '1.3.132.0.27': 'sect233r1', '1.3.132.0.16': 'sect283k1', '1.3.132.0.17': 'sect283r1', @@ -424,6 +425,8 @@ class PrivateKeyInfo(Sequence): 'private_key': _private_key_spec } + _algorithm = None + _bit_size = None _public_key = None _fingerprint = None @@ -471,6 +474,7 @@ class PrivateKeyInfo(Sequence): private_key_algo['parameters'] = params container = cls() + container._algorithm = algorithm #pylint: disable=W0212 container['version'] = Integer(0) container['private_key_algorithm'] = private_key_algo container['private_key'] = OctetString(private_key.untag().dump()) @@ -491,9 +495,7 @@ class PrivateKeyInfo(Sequence): object. For ECDSA keys, an OctetString. """ - algo = self['private_key_algorithm']['algorithm'].native - - if algo == 'dsa': + if self.algorithm == 'dsa': params = self['private_key_algorithm']['parameters'] return Integer(pow( params['g'].native, @@ -501,42 +503,41 @@ class PrivateKeyInfo(Sequence): params['p'].native )) - if algo == 'rsa': + if self.algorithm == 'rsa': key = self['private_key'].parsed return RSAPublicKey({ 'modulus': key['modulus'], 'public_exponent': key['public_exponent'], }) - if algo == 'ecdsa': - params = self['private_key_algorithm']['parameters'] - chosen = params.chosen + if self.algorithm == 'ecdsa': + curve_type, details = self.curve - if params.name == 'implicit_ca': + if curve_type == 'implicit_ca': raise ValueError('Unable to compute public key for ECDSA key using Implicit CA parameters') - if params.name == 'specified': - if chosen['field_id']['field_type'] == 'characteristic_two_field': + if curve_type == 'specified': + if details['field_id']['field_type'] == 'characteristic_two_field': raise ValueError('Unable to compute public key for ECDSA key over a characteristic two field') curve = PrimeCurve( - chosen['field_id']['parameters'].native, - int_from_bytes(chosen['curve']['a'].native), - int_from_bytes(chosen['curve']['b'].native) + details['field_id']['parameters'], + int_from_bytes(details['curve']['a']), + int_from_bytes(details['curve']['b']) ) - base_point = PrimePoint.load(curve, chosen['base'].native) + base_point = PrimePoint.load(curve, details['base']) - elif params.name == 'named': - if chosen.native not in ('prime192v1', 'secp224r1', 'prime256v1', 'secp384r1', 'secp521r1'): - raise ValueError('Unable to compute public key for ECDSA named curve %s, parameters not currently included' % chosen.native) + elif curve_type == 'named': + if details not in ('secp192r1', 'secp224r1', 'secp256r1', 'secp384r1', 'secp521r1'): + raise ValueError('Unable to compute public key for ECDSA named curve %s, parameters not currently included' % details) base_point = { - 'prime192v1': NIST_P192_BASE_POINT, + 'secp192r1': NIST_P192_BASE_POINT, 'secp224r1': NIST_P224_BASE_POINT, - 'prime256v1': NIST_P256_BASE_POINT, + 'secp256r1': NIST_P256_BASE_POINT, 'secp384r1': NIST_P384_BASE_POINT, 'secp521r1': NIST_P521_BASE_POINT, - }[chosen.native] + }[details] public_point = base_point * self['private_key'].parsed['private_key'].native return OctetString(public_point.dump()) @@ -550,12 +551,10 @@ class PrivateKeyInfo(Sequence): An RSAPrivateKey, DSAPrivateKey or ECPrivateKey object """ - algo = self['private_key_algorithm']['algorithm'].native + if self.algorithm == 'rsa': + return self['private_key'].parsed - if algo == 'rsa': - return self['private_key'] - - if algo == 'dsa': + if self.algorithm == 'dsa': params = self['private_key_algorithm']['parameters'] return DSAPrivateKey({ 'version': 0, @@ -563,19 +562,82 @@ class PrivateKeyInfo(Sequence): 'q': params['q'], 'g': params['g'], 'public_key': self.public_key, - 'private_key': self['private_key'], + 'private_key': self['private_key'].parsed, }) - if algo == 'ecdsa': - output = self['private_key'] + if self.algorithm == 'ecdsa': + output = self['private_key'].parsed output['parameters'] = self['private_key_algorithm']['parameters'] output['public_key'] = OctetBitString(self.public_key.native) return output + @property + def curve(self): + """ + Returns information about the curve used for an ECDSA key + + :raises: + ValueError - when the key is no an ECDSA key + + :return: + A two-element tuple, with the first element being a unicode string + of "implicit_ca", "specified" or "named". If the first element is + "implicit_ca", the second is None. If "specified", the second is + an OrderedDict that is the native version of SpecifiedECDomain. If + "named", the second is a unicode string of the curve name. + """ + + if self.algorithm != 'ecdsa': + raise ValueError('Only ECDSA keys have a curve, this key is %s' % self.algorithm.upper()) + + params = self['private_key_algorithm']['parameters'] + chosen = params.chosen + + if params.name == 'implicit_ca': + value = None + else: + value = chosen.native + + return (params.name, value) + + @property + def algorithm(self): + """ + :return: + A unicode string of "rsa", "dsa" or "ecdsa" + """ + + if self._algorithm is None: + self._algorithm = self['private_key_algorithm']['algorithm'].native + return self._algorithm + + @property + def bit_size(self): + """ + :return: + The bit size of the private, as an integer + """ + + if self._bit_size is None: + if self.algorithm == 'rsa': + prime = self['private_key'].parsed['private_exponent'].native + elif self.algorithm == 'dsa': + prime = self['private_key_algorithm']['parameters']['p'].native + elif self.algorithm == 'ecdsa': + prime = self['private_key'].parsed['private_key'].native + self._bit_size = int(math.ceil(math.log(prime, 2) / 8) * 8) + return self._bit_size + @property def public_key(self): + """ + :return: + If an RSA key, an RSAPublicKey object. If a DSA key, an Integer + object. If an ECDSA key, an OctetString object. + """ + if self._public_key is None: - if self['private_key_algorithm']['algorithm'].native == 'ecdsa': + if self.algorithm == 'ecdsa': key = self['private_key'].parsed if key['public_key']: self._public_key = OctetString(key['public_key'].native) @@ -604,17 +666,16 @@ class PrivateKeyInfo(Sequence): """ if self._fingerprint is None: - key_type = self['private_key_algorithm']['algorithm'].native params = self['private_key_algorithm']['parameters'] key = self['private_key'].parsed - if key_type == 'rsa': + if self.algorithm == 'rsa': to_hash = '%d:%d' % ( key['modulus'].native, key['public_exponent'].native, ) - elif key_type == 'dsa': + elif self.algorithm == 'dsa': public_key = self.public_key to_hash = '%d:%d:%d:%d' % ( params['p'].native, @@ -623,7 +684,7 @@ class PrivateKeyInfo(Sequence): public_key.native, ) - elif key_type == 'ecdsa': + elif self.algorithm == 'ecdsa': public_key = key['public_key'].native if public_key is None: public_key = self.public_key.native @@ -724,6 +785,8 @@ class PublicKeyInfo(Sequence): 'public_key': _public_key_spec } + _algorithm = None + _bit_size = None _fingerprint = None @classmethod @@ -759,6 +822,65 @@ class PublicKeyInfo(Sequence): return container + @property + def curve(self): + """ + Returns information about the curve used for an ECDSA key + + :raises: + ValueError - when the key is no an ECDSA key + + :return: + A two-element tuple, with the first element being a unicode string + of "implicit_ca", "specified" or "named". If the first element is + "implicit_ca", the second is None. If "specified", the second is + an OrderedDict that is the native version of SpecifiedECDomain. If + "named", the second is a unicode string of the curve name. + """ + + if self.algorithm != 'ecdsa': + raise ValueError('Only ECDSA keys have a curve, this key is %s' % self.algorithm.upper()) + + params = self['algorithm']['parameters'] + chosen = params.chosen + + if params.name == 'implicit_ca': + value = None + else: + value = chosen.native + + return (params.name, value) + + @property + def algorithm(self): + """ + :return: + A unicode string of "rsa", "dsa" or "ecdsa" + """ + + if self._algorithm is None: + self._algorithm = self['algorithm']['algorithm'].native + return self._algorithm + + @property + def bit_size(self): + """ + :return: + The bit size of the private, as an integer + """ + + if self._bit_size is None: + if self.algorithm == 'ecdsa': + self._bit_size = ((len(self['public_key'].native) - 1) / 2) * 8 + else: + if self.algorithm == 'rsa': + prime = self['public_key'].parsed['modulus'].native + elif self.algorithm == 'dsa': + prime = self['public_key'].parsed.native + self._bit_size = int(math.ceil(math.log(prime, 2) / 8) * 8) + + return self._bit_size + @property def fingerprint(self): """ diff --git a/tests/test_keys.py b/tests/test_keys.py index 07b4539..e7bee70 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -288,14 +288,14 @@ class KeysTests(unittest.TestCase): @staticmethod def key_pairs(): return ( - ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-public-dsa-der.key'), - ('ecdsa_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-public-ec-named-der.key'), - ('ecdsa', 'keys/test-pkcs8-ec-der.key', 'keys/test-public-ec-der.key'), - ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-public-der.key'), + ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-public-dsa-der.key', 'dsa', 3072), + ('ecdsa_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-public-ec-named-der.key', 'ecdsa', 256), + ('ecdsa', 'keys/test-pkcs8-ec-der.key', 'keys/test-public-ec-der.key', 'ecdsa', 256), + ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-public-der.key', 'rsa', 2048), ) @data('key_pairs', True) - def compare_fingerprints(self, private_key_file, public_key_file): + def compare_fingerprints(self, private_key_file, public_key_file, *_): with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: private_key = keys.PrivateKeyInfo.load(f.read()) with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: @@ -304,7 +304,7 @@ class KeysTests(unittest.TestCase): self.assertEqual(private_key.fingerprint, public_key.fingerprint) @data('key_pairs', True) - def compute_public_key(self, private_key_file, public_key_file): + def compute_public_key(self, private_key_file, public_key_file, *_): with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: private_key = keys.PrivateKeyInfo.load(f.read()) with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: @@ -313,10 +313,118 @@ class KeysTests(unittest.TestCase): self.assertEqual(public_key['public_key'].native, private_key._compute_public_key().native) #pylint: disable=W0212 @data('key_pairs', True) - def public_key_property(self, private_key_file, public_key_file): + def public_key_property(self, private_key_file, public_key_file, *_): with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: private_key = keys.PrivateKeyInfo.load(f.read()) with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: public_key = keys.PublicKeyInfo.load(f.read()) self.assertEqual(public_key['public_key'].native, private_key.public_key.native) + + @data('key_pairs', True) + def algorithm_name(self, private_key_file, public_key_file, algorithm, _): + with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(algorithm, private_key.algorithm) + self.assertEqual(algorithm, public_key.algorithm) + + @data('key_pairs', True) + def bit_size(self, private_key_file, public_key_file, _, bit_size): + with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(bit_size, private_key.bit_size) + self.assertEqual(bit_size, public_key.bit_size) + + #pylint: disable=C0326 + @staticmethod + def key_variations(): + return ( + ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-dsa-der.key',), + ('ecdsa_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-ec-named-der.key',), + ('ecdsa', 'keys/test-pkcs8-ec-der.key', 'keys/test-ec-der.key',), + ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-der.key',), + ) + + @data('key_variations', True) + def unwrap(self, wrapped_private_key_file, unwrapped_private_key_file): + with open(os.path.join(fixtures_dir, wrapped_private_key_file), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + with open(os.path.join(fixtures_dir, unwrapped_private_key_file), 'rb') as f: + unwrapped_bytes = f.read() + + self.assertEqual(unwrapped_bytes, private_key.unwrap().dump()) + + def test_curve_invalid(self): + with open(os.path.join(fixtures_dir, 'keys/test-pkcs8-der.key'), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + + with self.assertRaises(ValueError) as _: + _ = private_key.curve + + with open(os.path.join(fixtures_dir, 'keys/test-public-rsa-der.key'), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + with self.assertRaises(ValueError) as _: + _ = public_key.curve + + def test_curve_info_name(self): + with open(os.path.join(fixtures_dir, 'keys/test-pkcs8-ec-named-der.key'), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + + curve = ('named', 'secp256r1') + + self.assertEqual(curve, private_key.curve) + + with open(os.path.join(fixtures_dir, 'keys/test-public-ec-named-der.key'), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(curve, public_key.curve) + + def test_curve_info_specified(self): + with open(os.path.join(fixtures_dir, 'keys/test-pkcs8-ec-der.key'), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + + curve = ( + 'specified', + OrderedDict([ + ('version', 'ecdpVer1'), + ( + 'field_id', + OrderedDict([ + ('field_type', 'prime_field'), + ('parameters', 115792089210356248762697446949407573530086143415290314195533631308867097853951) + ]) + ), + ( + 'curve', + OrderedDict([ + ('a', b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC'), + ('b', b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B'), + ('seed', b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90'), + ]) + ), + ( + 'base', + b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5' + ), + ( + 'order', + 115792089210356248762697446949407573529996955224135760342422259061068512044369 + ), + ('cofactor', 1), + ('hash', None), + ]) + ) + + self.assertEqual(curve, private_key.curve) + + with open(os.path.join(fixtures_dir, 'keys/test-public-ec-der.key'), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(curve, public_key.curve) -- cgit v1.2.3 From e7c390cc5898976743a51a4823fbf4fff2c81ee5 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 02:01:55 -0400 Subject: Improved consistency of elliptic curve names --- asn1crypto/_elliptic_curve.py | 30 +++++++++++++++--------------- asn1crypto/keys.py | 20 ++++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index 33566c4..0da844a 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -293,13 +293,13 @@ INFINITY = PrimePoint(None, None, None) # NIST Curve P-192: -NIST_192_CURVE = PrimeCurve( +SECP192R1_CURVE = PrimeCurve( 6277101735386680763835789423207666416083908700390324961279, -3, 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1 ) -NIST_P192_BASE_POINT = PrimePoint( - NIST_192_CURVE, +SECP192R1_BASE_POINT = PrimePoint( + SECP192R1_CURVE, 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012, 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811, 6277101735386680763835789423176059013767194773182842284081 @@ -307,13 +307,13 @@ NIST_P192_BASE_POINT = PrimePoint( # NIST Curve P-224: -NIST_P224_CURVE = PrimeCurve( +SECP224R1_CURVE = PrimeCurve( 26959946667150639794667015087019630673557916260026308143510066298881, -3, 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4 ) -NIST_P224_BASE_POINT = PrimePoint( - NIST_P224_CURVE, +SECP224R1_BASE_POINT = PrimePoint( + SECP224R1_CURVE, 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21, 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34, 26959946667150639794667015087019625940457807714424391721682722368061 @@ -321,13 +321,13 @@ NIST_P224_BASE_POINT = PrimePoint( # NIST Curve P-256: -NIST_P256_CURVE = PrimeCurve( +SECP256R1_CURVE = PrimeCurve( 115792089210356248762697446949407573530086143415290314195533631308867097853951, -3, 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b ) -NIST_P256_BASE_POINT = PrimePoint( - NIST_P256_CURVE, +SECP256R1_BASE_POINT = PrimePoint( + SECP256R1_CURVE, 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296, 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5, 115792089210356248762697446949407573529996955224135760342422259061068512044369 @@ -335,13 +335,13 @@ NIST_P256_BASE_POINT = PrimePoint( # NIST Curve P-384: -NIST_P384_CURVE = PrimeCurve( +SECP384R1_CURVE = PrimeCurve( 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, -3, 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef ) -NIST_P384_BASE_POINT = PrimePoint( - NIST_P384_CURVE, +SECP384R1_BASE_POINT = PrimePoint( + SECP384R1_CURVE, 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7, 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f, 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643 @@ -349,13 +349,13 @@ NIST_P384_BASE_POINT = PrimePoint( # NIST Curve P-521: -NIST_P521_CURVE = PrimeCurve( +SECP521R1_CURVE = PrimeCurve( 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, -3, 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 ) -NIST_P521_BASE_POINT = PrimePoint( - NIST_P521_CURVE, +SECP521R1_BASE_POINT = PrimePoint( + SECP521R1_CURVE, 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 4ebc07e..ab20e70 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -21,11 +21,11 @@ from .core import ( SetOf, ) from ._elliptic_curve import ( - NIST_P192_BASE_POINT, - NIST_P224_BASE_POINT, - NIST_P256_BASE_POINT, - NIST_P384_BASE_POINT, - NIST_P521_BASE_POINT, + SECP192R1_BASE_POINT, + SECP224R1_BASE_POINT, + SECP256R1_BASE_POINT, + SECP384R1_BASE_POINT, + SECP521R1_BASE_POINT, PrimeCurve, PrimePoint, ) @@ -532,11 +532,11 @@ class PrivateKeyInfo(Sequence): raise ValueError('Unable to compute public key for ECDSA named curve %s, parameters not currently included' % details) base_point = { - 'secp192r1': NIST_P192_BASE_POINT, - 'secp224r1': NIST_P224_BASE_POINT, - 'secp256r1': NIST_P256_BASE_POINT, - 'secp384r1': NIST_P384_BASE_POINT, - 'secp521r1': NIST_P521_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 -- cgit v1.2.3 From cd967c32b8cc4ad1bc39320b0f28b2ee411d728c Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 15:05:57 -0400 Subject: Fixed up __future__ imports --- asn1crypto/__init__.py | 4 ++++ asn1crypto/_teletex_codec.py | 1 - asn1crypto/algos.py | 1 - asn1crypto/cms.py | 1 - asn1crypto/core.py | 1 - asn1crypto/crl.py | 1 - asn1crypto/keys.py | 1 - asn1crypto/ocsp.py | 1 - asn1crypto/pdf.py | 1 - asn1crypto/pkcs12.py | 1 - asn1crypto/tsp.py | 1 - asn1crypto/x509.py | 1 - 12 files changed, 4 insertions(+), 11 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index e4e49b3..ad4e556 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -1 +1,5 @@ +# coding: utf-8 +from __future__ import unicode_literals + + __version__ = '0.9.0' diff --git a/asn1crypto/_teletex_codec.py b/asn1crypto/_teletex_codec.py index 473ba9c..837d5c2 100644 --- a/asn1crypto/_teletex_codec.py +++ b/asn1crypto/_teletex_codec.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import import codecs diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 8ee6abb..416a912 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import from .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 5264823..ed216a8 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import try: import zlib diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 00f147b..46ed9d2 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import import sys import re diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index e7d80c7..3708685 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import from .algos import SignedDigestAlgorithm from .core import ( diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index ab20e70..fc929ab 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import import hashlib import math diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 69f9abf..af913b0 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import from .algos import DigestAlgorithm, SignedDigestAlgorithm from .core import ( diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index f707682..71e78d1 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import from .cms import CMSAttributeType, CMSAttribute from .core import ( diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 12eac9e..103d8d5 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import from .algos import DigestInfo from .core import ( diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py index 61fa7ca..e828d43 100644 --- a/asn1crypto/tsp.py +++ b/asn1crypto/tsp.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import from .algos import DigestAlgorithm from .core import ( diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index bfdf473..69a21d0 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from __future__ import absolute_import from collections import OrderedDict -- cgit v1.2.3 From ea25fc2504a2b449e2af029e581a70a131aab8a2 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 15:07:04 -0400 Subject: Added module and function docstrings --- asn1crypto/_elliptic_curve.py | 21 ++++++++++++++- asn1crypto/_int_conversion.py | 62 +++++++++++++++++++++++++++++++++++++++++++ asn1crypto/_teletex_codec.py | 15 +++++++++++ asn1crypto/algos.py | 17 ++++++++++++ asn1crypto/cms.py | 18 +++++++++++++ asn1crypto/core.py | 45 +++++++++++++++++++++++++++++++ asn1crypto/crl.py | 10 +++++++ asn1crypto/keys.py | 15 +++++++++++ asn1crypto/ocsp.py | 11 ++++++++ asn1crypto/pdf.py | 6 +++++ asn1crypto/pkcs12.py | 13 +++++++++ asn1crypto/tsp.py | 16 +++++++++++ asn1crypto/x509.py | 19 +++++++++++++ 13 files changed, 267 insertions(+), 1 deletion(-) diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index 0da844a..a1568b9 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -1,7 +1,26 @@ # coding: utf-8 """ -The following source code is derived from +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, and to support loading and serializing to/from ECPrimePoint format. The original project license is listed diff --git a/asn1crypto/_int_conversion.py b/asn1crypto/_int_conversion.py index 382b2d9..275d930 100644 --- a/asn1crypto/_int_conversion.py +++ b/asn1crypto/_int_conversion.py @@ -1,4 +1,13 @@ # coding: utf-8 + +""" +Functions to convert from integers to byte string and byte string to integers. +Exports the following items: + + - int_from_bytes() + - int_to_bytes() +""" + from __future__ import unicode_literals import sys @@ -6,7 +15,21 @@ import sys # Python 2 if sys.version_info <= (3,): + def int_to_bytes(value, signed=False): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + If the byte string should be encoded using two's complement + + :return: + A byte string + """ + # Handle negatives in two's complement if signed and value < 0: value = (~value) + 1 @@ -17,6 +40,19 @@ if sys.version_info <= (3,): return hex_str.decode('hex') def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + num = long(value.encode("hex"), 16) #pylint: disable=E0602 if not signed: @@ -33,10 +69,36 @@ if sys.version_info <= (3,): else: def int_to_bytes(value, signed=False): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + If the byte string should be encoded using two's complement + + :return: + A byte string + """ + result = value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) if not signed: return result.lstrip(b'\x00') return result def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + return int.from_bytes(value, 'big', signed=signed) diff --git a/asn1crypto/_teletex_codec.py b/asn1crypto/_teletex_codec.py index 837d5c2..34ddd8a 100644 --- a/asn1crypto/_teletex_codec.py +++ b/asn1crypto/_teletex_codec.py @@ -1,4 +1,11 @@ # coding: utf-8 + +""" +Implementation of the teletex T.61 codec. Exports the following items: + + - register() +""" + from __future__ import unicode_literals import codecs @@ -36,6 +43,10 @@ class TeletexStreamReader(TeletexCodec, codecs.StreamReader): def teletex_search_function(name): + """ + Search function for teletex codec that is passed to codecs.register() + """ + if name != 'teletex': return None @@ -51,6 +62,10 @@ def teletex_search_function(name): def register(): + """ + Registers the teletex codec + """ + codecs.register(teletex_search_function) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 416a912..7134e5b 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -1,4 +1,21 @@ # coding: utf-8 + +""" +ASN.1 type classes for various algorithms using in various aspects of public +key cryptography. Exports the following items: + + - AlgorithmIdentifier() + - DigestAlgorithm() + - DigestInfo() + - EncryptionAlgorithm() + - HmacAlgorithm() + - KdfAlgorithm() + - Pkcs5MacAlgorithm() + - SignedDigestAlgorithm() + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals from .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index ed216a8..cc1ff20 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -1,4 +1,22 @@ # coding: utf-8 + +""" +ASN.1 type classes for cryptographic message syntax (CMS). Structures are also +compatible with PKCS#7. Exports the following items: + + - AuthenticatedData() + - AuthEnvelopedData() + - CompressedData() + - ContentInfo() + - EncryptedData() + - EnvelopedData() + - EnvelopedData() + - SignedAndEnvelopedData() + - SignedData() + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals try: diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 46ed9d2..c8443ce 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1,4 +1,49 @@ # coding: utf-8 + +""" +ASN.1 type classes for universal types. Exports the following items: + + - Any() + - Asn1Value() + - BitString() + - BMPString() + - Boolean() + - CharacterString() + - Choice() + - EmbeddedPdv() + - Enumerated() + - GeneralizedTime() + - GeneralString() + - GraphicString() + - IA5String() + - InstanceOf() + - Integer() + - IntegerBitString() + - IntegerOctetString() + - NoValue() + - Null() + - NumericString() + - ObjectDescriptor() + - ObjectIdentifier() + - OctetBitString() + - OctetString() + - PrintableString() + - Real() + - RelativeOid() + - Sequence() + - SequenceOf() + - Set() + - SetOf() + - TeletexString() + - UniversalString() + - UTCTime() + - UTF8String() + - VideotexString() + - VisibleString() + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals import sys diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 3708685..4d0c34a 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -1,4 +1,14 @@ # coding: utf-8 + +""" +ASN.1 type classes for certificate revocation lists (CRL). Exports the +following items: + + - CertificateList() + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals from .algos import SignedDigestAlgorithm diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index fc929ab..99f8940 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -1,4 +1,19 @@ # coding: utf-8 + +""" +ASN.1 type classes for public and private keys. Exports the following items: + + - DSAPrivateKey() + - ECPrivateKey() + - EncryptedPrivateKeyInfo() + - PrivateKeyInfo() + - PublicKeyInfo() + - RSAPrivateKey() + - RSAPublicKey() + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals import hashlib diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index af913b0..18a2e03 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -1,4 +1,15 @@ # coding: utf-8 + +""" +ASN.1 type classes for the online certificate status protocol (OCSP). Exports +the following items: + + - OCSPRequest() + - OCSPResponse() + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals from .algos import DigestAlgorithm, SignedDigestAlgorithm diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index 71e78d1..47f7724 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -1,4 +1,10 @@ # coding: utf-8 + +""" +ASN.1 type classes for PDF signature structures. Adds extra oid mapping and +value parsing to asn1crypto.x509.Extension() and asn1crypto.xms.CMSAttribute(). +""" + from __future__ import unicode_literals from .cms import CMSAttributeType, CMSAttribute diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 103d8d5..dbc164d 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -1,4 +1,17 @@ # coding: utf-8 + +""" +ASN.1 type classes for PKCS#12 files. Exports the following items: + + - CertBag() + - CrlBag() + - Pfx() + - SafeBag() + - SecretBag() + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals from .algos import DigestInfo diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py index e828d43..b5ba67e 100644 --- a/asn1crypto/tsp.py +++ b/asn1crypto/tsp.py @@ -1,4 +1,20 @@ # coding: utf-8 + +""" +ASN.1 type classes for the time stamp protocol (TSP). Exports the following +items: + + - TimeStampReq() + - TimeStampResp() + +Also adds TimeStampedData() support to asn1crypto.cms.ContentInfo(), +TimeStampedData() and TSTInfo() support to +asn1crypto.cms.EncapsulatedContentInfo() and some oids and value parsers to +asn1crypto.cms.CMSAttribute(). + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals from .algos import DigestAlgorithm diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 69a21d0..6a9782c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1,4 +1,23 @@ # coding: utf-8 + +""" +ASN.1 type classes for X509 certificates. Exports the following items: + + - Attributes() + - Certificate() + - Extensions() + - GeneralName() + - GeneralNames() + - Name() + +Also adds TimeStampedData() support to asn1crypto.cms.ContentInfo(), +TimeStampedData() and TSTInfo() support to +asn1crypto.cms.EncapsulatedContentInfo() and some oids and value parsers to +asn1crypto.cms.CMSAttribute(). + +Other type classes are defined that help compose the types listed above. +""" + from __future__ import unicode_literals from collections import OrderedDict -- cgit v1.2.3 From 300b6129ead17026c61e9ad3127469d8a6922706 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 15:08:04 -0400 Subject: Changed inverse_mod() function to use the following in order: cffi, ctypes, pure python --- asn1crypto/_big_num_cffi.py | 74 ++++++++++++++++++++++++ asn1crypto/_big_num_ctypes.py | 77 +++++++++++++++++++++++++ asn1crypto/_elliptic_curve.py | 112 +----------------------------------- asn1crypto/_ffi.py | 26 +++++++++ asn1crypto/_inverse_mod.py | 128 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 306 insertions(+), 111 deletions(-) create mode 100644 asn1crypto/_big_num_cffi.py create mode 100644 asn1crypto/_big_num_ctypes.py create mode 100644 asn1crypto/_ffi.py create mode 100644 asn1crypto/_inverse_mod.py diff --git a/asn1crypto/_big_num_cffi.py b/asn1crypto/_big_num_cffi.py new file mode 100644 index 0000000..4906702 --- /dev/null +++ b/asn1crypto/_big_num_cffi.py @@ -0,0 +1,74 @@ +# coding: utf-8 + +""" +cffi interface for BN_mod_inverse() function from OpenSSL. Provides the +following items to be imported: + + - buffer_from_bytes() + - bytes_from_buffer() + - libcrypto + - BN_new() + - BN_bin2bin() + - BN_set_negative() + - BN_num_bits() + - BN_free() + - BN_CTX_new() + - BN_CTX_free() + - BN_mod_inverse() + - null() + +Will raise asn1crypto._ffi.LibraryNotFoundError() if libcrypto can not be +found. Will raise asn1crypto._ffi.FFIEngineError() if cffi is not instaled +or there is an error interfacing with libcrypto. +""" + +from __future__ import unicode_literals + +from ctypes.util import find_library + +from ._ffi import LibraryNotFoundError, FFIEngineError + +try: + from cffi import FFI + +except (ImportError): + raise FFIEngineError('Error importing cffi') + + +try: + ffi = FFI() + ffi.cdef(""" + void *BN_new(void); + + int BN_bn2bin(const void *a, unsigned char *to); + void *BN_bin2bn(const unsigned char *s, int len, void *ret); + + void BN_set_negative(void *a, int n); + + int BN_num_bits(const void *a); + + void BN_free(void *a); + + void *BN_CTX_new(void); + void BN_CTX_free(void *c); + + void *BN_mod_inverse(void *r, void *a, const void *n, void *ctx); + """) + + libcrypto_path = find_library('libcrypto') + if not libcrypto_path: + raise LibraryNotFoundError('The library libcrypto could not be found') + + libcrypto = ffi.dlopen(libcrypto_path) + + def buffer_from_bytes(initializer): + return ffi.new('char[]', initializer) + + def bytes_from_buffer(buffer, maxlen=None): + return ffi.buffer(buffer, maxlen)[:] + + def null(): + return ffi.NULL + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_big_num_ctypes.py b/asn1crypto/_big_num_ctypes.py new file mode 100644 index 0000000..0c36f7f --- /dev/null +++ b/asn1crypto/_big_num_ctypes.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +""" +ctypes interface for BN_mod_inverse() function from OpenSSL. Provides the +following items to be imported: + + - buffer_from_bytes() + - bytes_from_buffer() + - libcrypto + - BN_bin2bin() + - BN_CTX_free() + - BN_CTX_new() + - BN_free() + - BN_mod_inverse() + - BN_new() + - BN_num_bits() + - BN_set_negative() + - null() + +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 + +from ctypes.util import find_library +from ctypes import CDLL, c_int, c_char_p, c_void_p, create_string_buffer + +from ._ffi import LibraryNotFoundError, FFIEngineError + + +try: + libcrypto_path = find_library('libcrypto') + 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 + + def buffer_from_bytes(initializer): + return create_string_buffer(initializer) + + def bytes_from_buffer(buffer, maxlen=None): #pylint: disable=W0613 + return buffer.raw + + def null(): + return None + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index a1568b9..ad28a77 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -52,9 +52,8 @@ from __future__ import division import sys import math -from ctypes.util import find_library -from ctypes import CDLL, c_int, c_char_p, c_void_p, create_string_buffer +from ._inverse_mod import inverse_mod from ._int_conversion import int_to_bytes, int_from_bytes if sys.version_info < (3,): @@ -379,112 +378,3 @@ SECP521R1_BASE_POINT = PrimePoint( 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 ) - - - -use_python_inverse_mod = True - -libcrypto_path = find_library('libcrypto') -if libcrypto_path: - try: - libcrypto = CDLL(libcrypto_path) - - BN_new = libcrypto.BN_new - BN_new.argtypes = [] - BN_new.restype = c_void_p - - BN_bin2bn = libcrypto.BN_bin2bn - BN_bin2bn.argtypes = [c_char_p, c_int, c_void_p] - BN_bin2bn.restype = c_void_p - - BN_bn2bin = libcrypto.BN_bn2bin - BN_bn2bin.argtypes = [c_void_p, c_char_p] - BN_bn2bin.restype = c_int - - BN_set_negative = libcrypto.BN_set_negative - BN_set_negative.argtypes = [c_void_p, c_int] - BN_set_negative.restype = None - - BN_num_bits = libcrypto.BN_num_bits - BN_num_bits.argtypes = [c_void_p] - BN_num_bits.restype = c_int - - BN_free = libcrypto.BN_free - BN_free.argtypes = [c_void_p] - BN_free.restype = None - - BN_CTX_new = libcrypto.BN_CTX_new - BN_CTX_new.argtypes = [] - BN_CTX_new.restype = c_void_p - - BN_CTX_free = libcrypto.BN_CTX_free - BN_CTX_free.argtypes = [c_void_p] - BN_CTX_free.restype = None - - BN_mod_inverse = libcrypto.BN_mod_inverse - BN_mod_inverse.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] - BN_mod_inverse.restype = c_void_p - - use_python_inverse_mod = False - - except (AttributeError): #pylint: disable=W0704 - pass - -if not use_python_inverse_mod: - - def inverse_mod(a, p): - """ - Compute the inverse modul - """ - - ctx = BN_CTX_new() - - a_bytes = int_to_bytes(abs(a)) - p_bytes = int_to_bytes(abs(p)) - - a_buf = create_string_buffer(a_bytes) - a_bn = BN_bin2bn(a_buf, len(a_bytes), None) - if a < 0: - BN_set_negative(a_bn, 1) - - p_buf = create_string_buffer(p_bytes) - p_bn = BN_bin2bn(p_buf, len(p_bytes), None) - if p < 0: - BN_set_negative(p_bn, 1) - - r_bn = BN_mod_inverse(None, a_bn, p_bn, ctx) - r_len_bits = BN_num_bits(r_bn) - r_len = math.ceil(r_len_bits / 8) - r_buf = create_string_buffer(r_len) - BN_bn2bin(r_bn, r_buf) - result = int_from_bytes(r_buf.raw) - - BN_free(a_bn) - BN_free(p_bn) - BN_free(r_bn) - BN_CTX_free(ctx) - - return result - -else: - - def inverse_mod(a, m): - if a < 0 or m <= a: - a = a % m - - # From Ferguson and Schneier, roughly: - - c, d = a, m - 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*m = d. - # If d == 1, this means that ud is a inverse. - - assert d == 1 - if ud > 0: - return ud - else: - return ud + m diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py new file mode 100644 index 0000000..7522efe --- /dev/null +++ b/asn1crypto/_ffi.py @@ -0,0 +1,26 @@ +# coding: utf-8 + +""" +Exceptions for help trying to use cffi, then ctypes for shared library access +""" + +from __future__ import unicode_literals + + + +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/_inverse_mod.py b/asn1crypto/_inverse_mod.py new file mode 100644 index 0000000..3e53fa4 --- /dev/null +++ b/asn1crypto/_inverse_mod.py @@ -0,0 +1,128 @@ +# coding: utf-8 + +""" +Allows computing the modular inverse using either OpenSSL or a pure python +fallback. Exports the following items: + + - inverse_mod() + +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 +from __future__ import division + +import math + +from ._ffi import LibraryNotFoundError, FFIEngineError +from ._int_conversion import int_to_bytes, int_from_bytes + + +try: + try: + from ._big_num_cffi import libcrypto, buffer_from_bytes, bytes_from_buffer, null + except (FFIEngineError) as e: + from ._big_num_ctypes import libcrypto, buffer_from_bytes, bytes_from_buffer, null + + 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): + + 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 -- cgit v1.2.3 From 1315aa111c81b34bcbf46ec8d936fd96beed3c50 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 15:53:37 -0400 Subject: Fix some module docstring verbiage --- asn1crypto/_big_num_cffi.py | 4 ++-- asn1crypto/_big_num_ctypes.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/asn1crypto/_big_num_cffi.py b/asn1crypto/_big_num_cffi.py index 4906702..678b961 100644 --- a/asn1crypto/_big_num_cffi.py +++ b/asn1crypto/_big_num_cffi.py @@ -1,8 +1,8 @@ # coding: utf-8 """ -cffi interface for BN_mod_inverse() function from OpenSSL. Provides the -following items to be imported: +cffi interface for BN_mod_inverse() function from OpenSSL. Exports the +following items: - buffer_from_bytes() - bytes_from_buffer() diff --git a/asn1crypto/_big_num_ctypes.py b/asn1crypto/_big_num_ctypes.py index 0c36f7f..c0da3c4 100644 --- a/asn1crypto/_big_num_ctypes.py +++ b/asn1crypto/_big_num_ctypes.py @@ -1,8 +1,8 @@ # coding: utf-8 """ -ctypes interface for BN_mod_inverse() function from OpenSSL. Provides the -following items to be imported: +ctypes interface for BN_mod_inverse() function from OpenSSL. Exports the +following items: - buffer_from_bytes() - bytes_from_buffer() -- cgit v1.2.3 From db507c136a3751e3af9090227da2d6c47fccf09a Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 19 Jun 2015 15:54:09 -0400 Subject: Use typedef void instead of just void so the cdef is clearer --- asn1crypto/_big_num_cffi.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/asn1crypto/_big_num_cffi.py b/asn1crypto/_big_num_cffi.py index 678b961..59c97e3 100644 --- a/asn1crypto/_big_num_cffi.py +++ b/asn1crypto/_big_num_cffi.py @@ -38,21 +38,24 @@ except (ImportError): try: ffi = FFI() ffi.cdef(""" - void *BN_new(void); + typedef void BIGNUM; + typedef void BN_CTX; - int BN_bn2bin(const void *a, unsigned char *to); - void *BN_bin2bn(const unsigned char *s, int len, void *ret); + BIGNUM *BN_new(void); - void BN_set_negative(void *a, int n); + int BN_bn2bin(const BIGNUM *a, unsigned char *to); + BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); - int BN_num_bits(const void *a); + void BN_set_negative(BIGNUM *a, int n); - void BN_free(void *a); + int BN_num_bits(const BIGNUM *a); - void *BN_CTX_new(void); - void BN_CTX_free(void *c); + void BN_free(BIGNUM *a); - void *BN_mod_inverse(void *r, void *a, const void *n, void *ctx); + BN_CTX *BN_CTX_new(void); + void BN_CTX_free(BN_CTX *c); + + BIGNUM *BN_mod_inverse(BIGNUM *r, BIGNUM *a, const BIGNUM *n, BN_CTX *ctx); """) libcrypto_path = find_library('libcrypto') -- cgit v1.2.3 From 6b66ab5657a0da2e398ba738504c15fafa6aa035 Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 21 Jun 2015 10:26:45 -0400 Subject: Make __future__ imports consistent for better compat between 2 and 3 --- asn1crypto/__init__.py | 2 +- asn1crypto/_big_num_cffi.py | 2 +- asn1crypto/_big_num_ctypes.py | 2 +- asn1crypto/_elliptic_curve.py | 3 +-- asn1crypto/_ffi.py | 2 +- asn1crypto/_int_conversion.py | 2 +- asn1crypto/_inverse_mod.py | 3 +-- asn1crypto/_teletex_codec.py | 2 +- asn1crypto/algos.py | 2 +- asn1crypto/cms.py | 2 +- asn1crypto/core.py | 2 +- asn1crypto/crl.py | 2 +- asn1crypto/keys.py | 2 +- asn1crypto/ocsp.py | 2 +- asn1crypto/pdf.py | 2 +- asn1crypto/pkcs12.py | 2 +- asn1crypto/tsp.py | 2 +- asn1crypto/x509.py | 2 +- 18 files changed, 18 insertions(+), 20 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index ad4e556..c65130b 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function __version__ = '0.9.0' diff --git a/asn1crypto/_big_num_cffi.py b/asn1crypto/_big_num_cffi.py index 59c97e3..ec573c1 100644 --- a/asn1crypto/_big_num_cffi.py +++ b/asn1crypto/_big_num_cffi.py @@ -22,7 +22,7 @@ found. Will raise asn1crypto._ffi.FFIEngineError() if cffi is not instaled or there is an error interfacing with libcrypto. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from ctypes.util import find_library diff --git a/asn1crypto/_big_num_ctypes.py b/asn1crypto/_big_num_ctypes.py index c0da3c4..25bfd5a 100644 --- a/asn1crypto/_big_num_ctypes.py +++ b/asn1crypto/_big_num_ctypes.py @@ -22,7 +22,7 @@ found. Will raise asn1crypto._ffi.FFIEngineError() if there is an error interfacing with libcrypto. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from ctypes.util import find_library from ctypes import CDLL, c_int, c_char_p, c_void_p, create_string_buffer diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index ad28a77..f085467 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -47,8 +47,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from __future__ import unicode_literals -from __future__ import division +from __future__ import unicode_literals, division, absolute_import, print_function import sys import math diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py index 7522efe..64032f3 100644 --- a/asn1crypto/_ffi.py +++ b/asn1crypto/_ffi.py @@ -4,7 +4,7 @@ Exceptions for help trying to use cffi, then ctypes for shared library access """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function diff --git a/asn1crypto/_int_conversion.py b/asn1crypto/_int_conversion.py index 275d930..387a341 100644 --- a/asn1crypto/_int_conversion.py +++ b/asn1crypto/_int_conversion.py @@ -8,7 +8,7 @@ Exports the following items: - int_to_bytes() """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import sys diff --git a/asn1crypto/_inverse_mod.py b/asn1crypto/_inverse_mod.py index 3e53fa4..3adf99b 100644 --- a/asn1crypto/_inverse_mod.py +++ b/asn1crypto/_inverse_mod.py @@ -32,8 +32,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from __future__ import unicode_literals -from __future__ import division +from __future__ import unicode_literals, division, absolute_import, print_function import math diff --git a/asn1crypto/_teletex_codec.py b/asn1crypto/_teletex_codec.py index 34ddd8a..b5991aa 100644 --- a/asn1crypto/_teletex_codec.py +++ b/asn1crypto/_teletex_codec.py @@ -6,7 +6,7 @@ Implementation of the teletex T.61 codec. Exports the following items: - register() """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import codecs diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 7134e5b..14cce07 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -16,7 +16,7 @@ key cryptography. Exports the following items: Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index cc1ff20..2d6be49 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -17,7 +17,7 @@ compatible with PKCS#7. Exports the following items: Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function try: import zlib diff --git a/asn1crypto/core.py b/asn1crypto/core.py index c8443ce..5f39885 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -44,7 +44,7 @@ ASN.1 type classes for universal types. Exports the following items: Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import sys import re diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 4d0c34a..e90ffd2 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -9,7 +9,7 @@ following items: Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from .algos import SignedDigestAlgorithm from .core import ( diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 99f8940..6816c3a 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -14,7 +14,7 @@ ASN.1 type classes for public and private keys. Exports the following items: Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import hashlib import math diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 18a2e03..e5bd5db 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -10,7 +10,7 @@ the following items: Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from .algos import DigestAlgorithm, SignedDigestAlgorithm from .core import ( diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index 47f7724..5a23323 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -5,7 +5,7 @@ ASN.1 type classes for PDF signature structures. Adds extra oid mapping and value parsing to asn1crypto.x509.Extension() and asn1crypto.xms.CMSAttribute(). """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from .cms import CMSAttributeType, CMSAttribute from .core import ( diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index dbc164d..9cf8e11 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -12,7 +12,7 @@ ASN.1 type classes for PKCS#12 files. Exports the following items: Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from .algos import DigestInfo from .core import ( diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py index b5ba67e..a3e851c 100644 --- a/asn1crypto/tsp.py +++ b/asn1crypto/tsp.py @@ -15,7 +15,7 @@ asn1crypto.cms.CMSAttribute(). Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from .algos import DigestAlgorithm from .core import ( diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 6a9782c..233ddb6 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -18,7 +18,7 @@ asn1crypto.cms.CMSAttribute(). Other type classes are defined that help compose the types listed above. """ -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function from collections import OrderedDict -- cgit v1.2.3 From ba4a69314583b9224a209d40f91de76b62252d6f Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 21 Jun 2015 11:22:53 -0400 Subject: Switch to 'typedef ...' instead of 'typedef void' --- asn1crypto/_big_num_cffi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/_big_num_cffi.py b/asn1crypto/_big_num_cffi.py index ec573c1..8f24328 100644 --- a/asn1crypto/_big_num_cffi.py +++ b/asn1crypto/_big_num_cffi.py @@ -38,8 +38,8 @@ except (ImportError): try: ffi = FFI() ffi.cdef(""" - typedef void BIGNUM; - typedef void BN_CTX; + typedef ... BIGNUM; + typedef ... BN_CTX; BIGNUM *BN_new(void); -- cgit v1.2.3 From 6b926f6392b1fa5c785ae2a5d463bf0f50035ae5 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 24 Jun 2015 08:50:02 -0400 Subject: Search for crypto instead of libcrypto for Linux compat --- asn1crypto/_big_num_cffi.py | 2 +- asn1crypto/_big_num_ctypes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/_big_num_cffi.py b/asn1crypto/_big_num_cffi.py index 8f24328..cc4fdac 100644 --- a/asn1crypto/_big_num_cffi.py +++ b/asn1crypto/_big_num_cffi.py @@ -58,7 +58,7 @@ try: BIGNUM *BN_mod_inverse(BIGNUM *r, BIGNUM *a, const BIGNUM *n, BN_CTX *ctx); """) - libcrypto_path = find_library('libcrypto') + libcrypto_path = find_library('crypto') if not libcrypto_path: raise LibraryNotFoundError('The library libcrypto could not be found') diff --git a/asn1crypto/_big_num_ctypes.py b/asn1crypto/_big_num_ctypes.py index 25bfd5a..d764223 100644 --- a/asn1crypto/_big_num_ctypes.py +++ b/asn1crypto/_big_num_ctypes.py @@ -31,7 +31,7 @@ from ._ffi import LibraryNotFoundError, FFIEngineError try: - libcrypto_path = find_library('libcrypto') + libcrypto_path = find_library('crypto') if not libcrypto_path: raise LibraryNotFoundError('The library libcrypto could not be found') -- cgit v1.2.3 From 1b00a5954e1996fb6c2f67f6f385d80d9a8dfd48 Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 28 Jun 2015 12:58:26 -0400 Subject: Added the .hash_algo property to PublicKeyInfo and PrivateKeyInfo --- asn1crypto/keys.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 6816c3a..5bf04bb 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -614,6 +614,26 @@ class PrivateKeyInfo(Sequence): return (params.name, value) + @property + def hash_algo(self): + """ + Returns the name of the family of hash algorithms used to generate a + DSA key + + :raises: + ValueError - when the key is not a DSA key + + :return: + A unicode string of "sha1" or "sha2" + """ + + if self.algorithm != 'dsa': + raise ValueError('Only DSA keys are generated using a hash algorithm, this key is %s' % self.algorithm.upper()) + + byte_len = math.log(self['private_key_algorithm']['parameters']['q'].native, 2) / 8 + + return 'sha1' if byte_len <= 20 else 'sha2' + @property def algorithm(self): """ @@ -865,6 +885,26 @@ class PublicKeyInfo(Sequence): return (params.name, value) + @property + def hash_algo(self): + """ + Returns the name of the family of hash algorithms used to generate a + DSA key + + :raises: + ValueError - when the key is not a DSA key + + :return: + A unicode string of "sha1" or "sha2" + """ + + if self.algorithm != 'dsa': + raise ValueError('Only DSA keys are generated using a hash algorithm, this key is %s' % self.algorithm.upper()) + + byte_len = math.log(self['algorithm']['parameters']['q'].native, 2) / 8 + + return 'sha1' if byte_len <= 20 else 'sha2' + @property def algorithm(self): """ -- cgit v1.2.3 From 216fd136fc2797e9446ac28dcfabd8ba95dd126f Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 28 Jun 2015 12:59:33 -0400 Subject: Fix .curve property docstrings --- asn1crypto/keys.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 5bf04bb..e174d0d 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -591,7 +591,7 @@ class PrivateKeyInfo(Sequence): Returns information about the curve used for an ECDSA key :raises: - ValueError - when the key is no an ECDSA key + ValueError - when the key is not an ECDSA key :return: A two-element tuple, with the first element being a unicode string @@ -862,7 +862,7 @@ class PublicKeyInfo(Sequence): Returns information about the curve used for an ECDSA key :raises: - ValueError - when the key is no an ECDSA key + ValueError - when the key is not an ECDSA key :return: A two-element tuple, with the first element being a unicode string -- cgit v1.2.3 From 11f875bf40f7e9e36fc8fb2740ca146aa1896b87 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 29 Jun 2015 19:44:08 -0400 Subject: Fixed some bugs with serializing integers --- asn1crypto/_int_conversion.py | 5 ++++- asn1crypto/core.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/asn1crypto/_int_conversion.py b/asn1crypto/_int_conversion.py index 387a341..13c1c96 100644 --- a/asn1crypto/_int_conversion.py +++ b/asn1crypto/_int_conversion.py @@ -37,7 +37,10 @@ if sys.version_info <= (3,): hex_str = '%x' % value if len(hex_str) & 1: hex_str = '0' + hex_str - return hex_str.decode('hex') + output = hex_str.decode('hex') + if signed and ord(output[0:1]) & 0x80: + output = b'\x00' + output + return output def int_from_bytes(value, signed=False): """ diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 5f39885..4c61ea4 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1205,7 +1205,7 @@ class IntegerBitString(Primitive): self._native = value # Set the unused bits to 0 - self.contents = b'\x00' + int_to_bytes(value) + self.contents = b'\x00' + int_to_bytes(value, signed=True) self.header = None if self.trailer != b'': self.trailer = b'' @@ -1349,7 +1349,7 @@ class IntegerOctetString(OctetString): self._native = value # Set the unused bits to 0 - self.contents = int_to_bytes(value) + self.contents = int_to_bytes(value, signed=True) self.header = None if self.trailer != b'': self.trailer = b'' -- cgit v1.2.3 From d691c889141760be61df70db43e35a4dec12f6f6 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 29 Jun 2015 19:44:30 -0400 Subject: Make it clear point compressions support is patented --- asn1crypto/_elliptic_curve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index f085467..e5f82cb 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -138,7 +138,7 @@ class PrimePoint(): if first_byte not in (b'\x02', b'\x03'): raise ValueError('Invalid ECPoint representation of a point - first byte is incorrect') - raise ValueError('Compressed ECPoint representations are not supported') + raise ValueError('Compressed ECPoint representations are not supported due to patent US6252960') def __init__(self, curve, x, y, order=None): """ -- cgit v1.2.3 From dbd7240c00d7dcc7e370e134704ced3dadbbe23c Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 30 Jun 2015 06:33:40 -0400 Subject: Added run.py for executing tests, code coverage and linting --- dev/__init__.py | 0 dev/coverage.py | 19 +++++++++++++++++++ dev/lint.py | 25 +++++++++++++++++++++++++ dev/tests.py | 31 +++++++++++++++++++++++++++++++ lint.py | 32 -------------------------------- readme.md | 15 +++++++++++++++ run.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- tests.py | 42 ------------------------------------------ 9 files changed, 142 insertions(+), 75 deletions(-) create mode 100644 dev/__init__.py create mode 100644 dev/coverage.py create mode 100644 dev/lint.py create mode 100644 dev/tests.py delete mode 100644 lint.py create mode 100644 run.py delete mode 100644 tests.py diff --git a/dev/__init__.py b/dev/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev/coverage.py b/dev/coverage.py new file mode 100644 index 0000000..8961f6f --- /dev/null +++ b/dev/coverage.py @@ -0,0 +1,19 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import coverage + + + +def run(): + cov = coverage.Coverage(include='asn1crypto/*.py') + cov.start() + + from .tests import run as run_tests + run_tests() + print() + + cov.stop() + cov.save() + + cov.report(show_missing=False) diff --git a/dev/lint.py b/dev/lint.py new file mode 100644 index 0000000..7f5f15b --- /dev/null +++ b/dev/lint.py @@ -0,0 +1,25 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os + +from pylint.lint import Run + + +cur_dir = os.path.dirname(__file__) +rc_path = os.path.join(cur_dir, '..', '.pylintrc') + + +def run(): + print('Running pylint...') + + files = [] + for root, _, filenames in os.walk('../asn1crypto/'): + for filename in filenames: + if not filename.endswith('.py'): + continue + files.append(os.path.join(root, filename)) + + args = ['--rcfile=%s' % rc_path] + files + + Run(args) diff --git a/dev/tests.py b/dev/tests.py new file mode 100644 index 0000000..fcb53cd --- /dev/null +++ b/dev/tests.py @@ -0,0 +1,31 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import unittest +import re + +from tests.test_cms import CMSTests +from tests.test_crl import CRLTests +from tests.test_keys import KeysTests +from tests.test_ocsp import OCSPTests +from tests.test_tsp import TSPTests +from tests.test_x509 import X509Tests +from tests.test_core import CoreTests + + +test_classes = [CMSTests, CRLTests, KeysTests, OCSPTests, TSPTests, X509Tests, CoreTests] + + +def run(matcher=None): + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for test_class in test_classes: + if matcher: + names = loader.getTestCaseNames(test_class) + for name in names: + if re.search(matcher, name): + suite.addTest(test_class(name)) + else: + suite.addTest(loader.loadTestsFromTestCase(test_class)) + verbosity = 2 if matcher else 1 + unittest.TextTestRunner(verbosity=verbosity).run(suite) diff --git a/lint.py b/lint.py deleted file mode 100644 index 557f0b9..0000000 --- a/lint.py +++ /dev/null @@ -1,32 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals - -import os - -from pylint.lint import Run - - -cur_dir = os.path.dirname(__file__) -rc_path = os.path.join(cur_dir, './.pylintrc') - -print('Running pylint...') - -files = [ - '__init__.py', - 'algos.py', - 'cms.py', - 'core.py', - 'crl.py', - 'keys.py', - 'ocsp.py', - 'pdf.py', - 'pkcs12.py', - 'teletex_codec.py', - 'tsa.py', - 'x509.py', -] - -args = ['--rcfile=%s' % rc_path] -args += ['asn1crypto/' + f for f in files] - -Run(args) diff --git a/readme.md b/readme.md index 2c9df2b..6b0d686 100644 --- a/readme.md +++ b/readme.md @@ -59,6 +59,21 @@ pre-defined data types. - [Time stamp protocol (TSP)](asn1crypto/tsp.py), `asn1crypto.tsp` - [PDF signatures](asn1crypto/pdf.py), `asn1crypto.pdf` +## Development + +The following commands will run the test suite, linter and test coverage: + +```bash +python run.py tests +python run.py lint +python run.py coverage +``` + +To run only some tests, pass a regular expression as a parameter to `tests`. + +```bash +python run.py tests ocsp +``` ## Why Another Python ASN.1 Library? diff --git a/run.py b/run.py new file mode 100644 index 0000000..65f6be6 --- /dev/null +++ b/run.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +if sys.version_info < (3,): + byte_cls = str +else: + byte_cls = bytes + + +def show_usage(): + print('Usage: run.py (lint | tests [regex] | coverage)', file=sys.stderr) + sys.exit(1) + + +def get_arg(num): + if len(sys.argv) < num + 1: + return None + arg = sys.argv[num] + if isinstance(arg, byte_cls): + arg = arg.decode('utf-8') + return arg + + +if len(sys.argv) < 2 or len(sys.argv) > 3: + show_usage() + +task = get_arg(1) + +if task not in ('lint', 'tests', 'coverage'): + show_usage() + +if task != 'tests' and len(sys.argv) == 3: + show_usage() + +params = [] +if task == 'lint': + from dev.lint import run + +elif task == 'tests': + from dev.tests import run + matcher = get_arg(2) + if matcher: + params.append(matcher) + +elif task == 'coverage': + from dev.coverage import run + +run(*params) diff --git a/setup.py b/setup.py index 6df9758..8652410 100644 --- a/setup.py +++ b/setup.py @@ -30,5 +30,5 @@ setup( keywords='asn1 crypto', - packages=find_packages(exclude=['tests*']) + packages=find_packages(exclude=['tests*', 'dev*']) ) diff --git a/tests.py b/tests.py deleted file mode 100644 index 333ac61..0000000 --- a/tests.py +++ /dev/null @@ -1,42 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals - -import sys -import unittest -import re - -if sys.version_info < (3,): - byte_cls = str -else: - byte_cls = bytes - -from tests.test_cms import CMSTests #pylint: disable=E0611 -from tests.test_crl import CRLTests #pylint: disable=E0611 -from tests.test_keys import KeysTests #pylint: disable=E0611 -from tests.test_ocsp import OCSPTests #pylint: disable=E0611 -from tests.test_tsp import TSPTests #pylint: disable=E0611 -from tests.test_x509 import X509Tests #pylint: disable=E0611 -from tests.test_core import CoreTests #pylint: disable=E0611 - - -test_classes = [CMSTests, CRLTests, KeysTests, OCSPTests, TSPTests, X509Tests, CoreTests] - - -if __name__ == '__main__': - matcher = None - if len(sys.argv) > 1: - matcher = sys.argv[1] - if isinstance(matcher, byte_cls): - matcher = matcher.decode('utf-8') - - suite = unittest.TestSuite() - loader = unittest.TestLoader() - for test_class in test_classes: - if matcher: - names = loader.getTestCaseNames(test_class) - for name in names: - if re.search(matcher, name): - suite.addTest(test_class(name)) - else: - suite.addTest(loader.loadTestsFromTestCase(test_class)) - unittest.TextTestRunner().run(suite) -- cgit v1.2.3 From 25e96a9bb795bec7d12f9338f571256a0e2cf40d Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Jul 2015 09:23:12 -0400 Subject: Change NameTypeAndValue to use DirectoryString for all OIDs --- asn1crypto/x509.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 233ddb6..c571229 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -133,8 +133,8 @@ class NameTypeAndValue(Sequence): _oid_specs = { 'common_name': DirectoryString, 'surname': DirectoryString, - 'serial_number': PrintableString, - 'country_name': PrintableString, + 'serial_number': DirectoryString, + 'country_name': DirectoryString, 'locality_name': DirectoryString, 'state_or_province_name': DirectoryString, 'organization_name': DirectoryString, @@ -144,13 +144,13 @@ class NameTypeAndValue(Sequence): 'given_name': DirectoryString, 'initials': DirectoryString, 'generation_qualifier': DirectoryString, - 'dn_qualifier': PrintableString, + 'dn_qualifier': DirectoryString, # https://tools.ietf.org/html/rfc2985#page-26 'email_address': IA5String, # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf 'incorporation_locality': DirectoryString, 'incorporation_state_or_province': DirectoryString, - 'incorporation_country': PrintableString, + 'incorporation_country': DirectoryString, } -- cgit v1.2.3 From 9c8f2036953d7a93d65ebae961236a41efe9a530 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Jul 2015 16:50:03 -0400 Subject: Handle explicitly tagged Any value with no concrete spec --- asn1crypto/core.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 4c61ea4..48bce3c 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -482,7 +482,12 @@ class Any(Asn1Value): """ if self._parsed is None or self._parsed[1:3] != (spec, spec_params): - parsed_value, _ = _parse_build(self.header + self.contents + self.trailer, spec=spec, spec_params=spec_params) + passed_params = spec_params + if self.tag_type == 'explicit': + passed_params = {} if not spec_params else spec_params.copy() + passed_params['tag_type'] = self.tag_type + passed_params['tag'] = self.tag + parsed_value, _ = _parse_build(self.header + self.contents + self.trailer, spec=spec, spec_params=passed_params) self._parsed = (parsed_value, spec, spec_params) return self._parsed[0] @@ -2918,6 +2923,12 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param ) ) + # For explicitly tagged, un-speced parsings, we use a generic container + # since we will be parsing the contents and discarding the outer object + # anyway a little further on + elif spec_params and 'tag_type' in spec_params and spec_params['tag_type'] == 'explicit': + value = Asn1Value(**spec_params) + # If no spec was specified, allow anything and just process what # is in the input data else: -- cgit v1.2.3 From a9aee32f1df6aefe7caa0715c5f806b4f3193ae2 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Jul 2015 16:50:52 -0400 Subject: Properly handle Choice fields that are optional --- asn1crypto/core.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 48bce3c..d8816fb 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1948,13 +1948,25 @@ class Sequence(Asn1Value): if 'optional' in field_params or 'default' in field_params: id_ = (parts[0], parts[2]) - if self._field_ids[field] != id_ and field_spec != Any: - if 'optional' in field_params: - self.children.append(NoValue()) - else: - self.children.append(field_spec(**field_params)) - field += 1 - continue + no_id_match = self._field_ids[field] != id_ + not_any = field_spec != Any + if no_id_match and not_any: + choice_match = False + if issubclass(field_spec, Choice): + try: + tester = field_spec(**field_params) + tester.validate(*id_) + choice_match = True + except (ValueError): #pylint: disable=W0704 + pass + + if not choice_match: + if 'optional' in field_params: + self.children.append(NoValue()) + else: + self.children.append(field_spec(**field_params)) + field += 1 + continue if field_spec is None or (issubclass(field_spec, Any) and spec_override): field_spec = value_spec -- cgit v1.2.3 From bb57b4e32feba2139bcbdd9566cea0e2f3fea6b7 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Jul 2015 16:51:41 -0400 Subject: Improve error handling in situations where specs and data don't align --- asn1crypto/core.py | 82 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d8816fb..8934878 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -482,13 +482,18 @@ class Any(Asn1Value): """ if self._parsed is None or self._parsed[1:3] != (spec, spec_params): - passed_params = spec_params - if self.tag_type == 'explicit': - passed_params = {} if not spec_params else spec_params.copy() - passed_params['tag_type'] = self.tag_type - passed_params['tag'] = self.tag - parsed_value, _ = _parse_build(self.header + self.contents + self.trailer, spec=spec, spec_params=passed_params) - self._parsed = (parsed_value, spec, spec_params) + try: + passed_params = spec_params + if self.tag_type == 'explicit': + passed_params = {} if not spec_params else spec_params.copy() + passed_params['tag_type'] = self.tag_type + passed_params['tag'] = self.tag + parsed_value, _ = _parse_build(self.header + self.contents + self.trailer, spec=spec, spec_params=passed_params) + self._parsed = (parsed_value, spec, spec_params) + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + raise e return self._parsed[0] def dump(self, force=False): @@ -1916,6 +1921,7 @@ class Sequence(Asn1Value): contents_length = len(self.contents) child_pointer = 0 field = 0 + seen_field = 1 while child_pointer < contents_length: parts, num_bytes = _parse(self.contents, pointer=child_pointer) @@ -1977,6 +1983,31 @@ class Sequence(Asn1Value): else: child = parts + (field_spec, field_params) + # Handle situations where an optional or defaulted field definition is incorrect + elif len(self._fields) > 0 and seen_field <= len(self._fields): + missed_fields = [] + prev_field = field - 1 + while prev_field >= 0: + prev_field_info = self._fields[prev_field] + if len(prev_field_info) < 3: + break + if 'optional' in prev_field_info[2] or 'default' in prev_field_info[2]: + missed_fields.append(prev_field_info[0]) + prev_field -= 1 + plural = 's' if len(missed_fields) > 1 else '' + missed_field_names = ', '.join(missed_fields) + raise ValueError( + 'Data for field %s (%s class, %s method, tag %s) does not match the field definition%s of %s' % + ( + seen_field, + CLASS_NUM_TO_NAME_MAP.get(parts[0]), + METHOD_NUM_TO_NAME_MAP.get(parts[1]), + parts[2], + plural, + missed_field_names + ) + ) + else: child = parts @@ -1988,6 +2019,7 @@ class Sequence(Asn1Value): self.children.append(child) child_pointer += num_bytes field += 1 + seen_field += 1 total_fields = len(self._fields) index = len(self.children) @@ -2022,14 +2054,19 @@ class Sequence(Asn1Value): return None if self._native is None: - if self.children is None: - self._parse_children(recurse=True) - self._native = OrderedDict() - for index, child in enumerate(self.children): - if isinstance(child, tuple): - child = _build(*child) - self.children[index] = child - self._native[self._fields[index][0]] = child.native + try: + if self.children is None: + self._parse_children(recurse=True) + self._native = OrderedDict() + for index, child in enumerate(self.children): + if isinstance(child, tuple): + child = _build(*child) + self.children[index] = child + self._native[self._fields[index][0]] = child.native + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + raise e return self._native #pylint: disable=W0212 @@ -2944,7 +2981,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # If no spec was specified, allow anything and just process what # is in the input data else: - spec = { + universal_specs = { 1: Boolean, 2: Integer, 3: BitString, @@ -2973,7 +3010,18 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param 28: UniversalString, 29: CharacterString, 30: BMPString - }[tag] + } + if tag not in universal_specs: + raise ValueError( + 'Unknown element - %s class, %s method, tag %s' % + ( + CLASS_NUM_TO_NAME_MAP.get(class_), + METHOD_NUM_TO_NAME_MAP.get(method), + tag, + ) + ) + + spec = universal_specs[tag] value = spec(class_=class_) -- cgit v1.2.3 From af737d9225540ce7f2c3459daf67ad86dafc12c9 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Jul 2015 16:52:05 -0400 Subject: Fix SafeBag attributes to be a set instead of a sequence --- asn1crypto/pkcs12.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 9cf8e11..3256403 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -79,7 +79,7 @@ class Attribute(Sequence): } -class Attributes(SequenceOf): +class Attributes(SetOf): _child_spec = Attribute -- cgit v1.2.3 From 680cba11136ec5617303b3425bd6d797f9a8d5c8 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Jul 2015 23:53:54 -0400 Subject: Changed key algo name from 'ecdsa' to just 'ec' since EC keys can be used for DH too --- asn1crypto/keys.py | 65 ++++++++++++++++++++++++++---------------------------- tests/test_keys.py | 8 +++---- tests/test_x509.py | 2 +- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index e174d0d..145e403 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -392,7 +392,7 @@ class PrivateKeyAlgorithmId(ObjectIdentifier): # https://tools.ietf.org/html/rfc3279#page-18 '1.2.840.10040.4.1': 'dsa', # https://tools.ietf.org/html/rfc3279#page-13 - '1.2.840.10045.2.1': 'ecdsa', + '1.2.840.10045.2.1': 'ec', } @@ -411,7 +411,7 @@ class PrivateKeyAlgorithm(Sequence): _oid_specs = { 'rsa': Null, 'dsa': DSAParams, - 'ecdsa': ECDomainParameters, + 'ec': ECDomainParameters, } @@ -432,7 +432,7 @@ class PrivateKeyInfo(Sequence): return { 'rsa': RSAPrivateKey, 'dsa': Integer, - 'ecdsa': ECPrivateKey, + 'ec': ECPrivateKey, }[algorithm] _spec_callbacks = { @@ -453,7 +453,7 @@ class PrivateKeyInfo(Sequence): A byte string or Asn1Value object of the private key :param algorithm: - A unicode string of "rsa", "dsa" or "ecdsa" + A unicode string of "rsa", "dsa" or "ec" :return: A PrivateKeyInfo object @@ -475,13 +475,13 @@ class PrivateKeyInfo(Sequence): params['g'] = private_key['g'] public_key = private_key['public_key'] private_key = private_key['private_key'] - elif algorithm == 'ecdsa': + elif algorithm == 'ec': if not isinstance(private_key, ECPrivateKey): private_key = ECPrivateKey.load(private_key) params = private_key['parameters'] del private_key['parameters'] else: - raise ValueError('algorithm must be one of "rsa", "dsa", "ecdsa" - is %s' % repr(algorithm)) + raise ValueError('algorithm must be one of "rsa", "dsa", "ec" - is %s' % repr(algorithm)) private_key_algo = PrivateKeyAlgorithm() private_key_algo['algorithm'] = PrivateKeyAlgorithmId(algorithm) @@ -506,7 +506,7 @@ class PrivateKeyInfo(Sequence): :return: For RSA keys, an RSAPublicKey object. For DSA keys, an Integer - object. For ECDSA keys, an OctetString. + object. For EC keys, an OctetString. """ if self.algorithm == 'dsa': @@ -524,15 +524,15 @@ class PrivateKeyInfo(Sequence): 'public_exponent': key['public_exponent'], }) - if self.algorithm == 'ecdsa': + if self.algorithm == 'ec': curve_type, details = self.curve if curve_type == 'implicit_ca': - raise ValueError('Unable to compute public key for ECDSA key using Implicit CA parameters') + raise ValueError('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('Unable to compute public key for ECDSA key over a characteristic two field') + raise ValueError('Unable to compute public key for EC key over a characteristic two field') curve = PrimeCurve( details['field_id']['parameters'], @@ -543,7 +543,7 @@ class PrivateKeyInfo(Sequence): elif curve_type == 'named': if details not in ('secp192r1', 'secp224r1', 'secp256r1', 'secp384r1', 'secp521r1'): - raise ValueError('Unable to compute public key for ECDSA named curve %s, parameters not currently included' % details) + raise ValueError('Unable to compute public key for EC named curve %s, parameters not currently included' % details) base_point = { 'secp192r1': SECP192R1_BASE_POINT, @@ -579,7 +579,7 @@ class PrivateKeyInfo(Sequence): 'private_key': self['private_key'].parsed, }) - if self.algorithm == 'ecdsa': + if self.algorithm == 'ec': output = self['private_key'].parsed output['parameters'] = self['private_key_algorithm']['parameters'] output['public_key'] = OctetBitString(self.public_key.native) @@ -588,10 +588,10 @@ class PrivateKeyInfo(Sequence): @property def curve(self): """ - Returns information about the curve used for an ECDSA key + Returns information about the curve used for an EC key :raises: - ValueError - when the key is not an ECDSA key + ValueError - when the key is not an EC key :return: A two-element tuple, with the first element being a unicode string @@ -601,8 +601,8 @@ class PrivateKeyInfo(Sequence): "named", the second is a unicode string of the curve name. """ - if self.algorithm != 'ecdsa': - raise ValueError('Only ECDSA keys have a curve, this key is %s' % self.algorithm.upper()) + if self.algorithm != 'ec': + raise ValueError('Only EC keys have a curve, this key is %s' % self.algorithm.upper()) params = self['private_key_algorithm']['parameters'] chosen = params.chosen @@ -638,7 +638,7 @@ class PrivateKeyInfo(Sequence): def algorithm(self): """ :return: - A unicode string of "rsa", "dsa" or "ecdsa" + A unicode string of "rsa", "dsa" or "ec" """ if self._algorithm is None: @@ -657,7 +657,7 @@ class PrivateKeyInfo(Sequence): prime = self['private_key'].parsed['private_exponent'].native elif self.algorithm == 'dsa': prime = self['private_key_algorithm']['parameters']['p'].native - elif self.algorithm == 'ecdsa': + elif self.algorithm == 'ec': prime = self['private_key'].parsed['private_key'].native self._bit_size = int(math.ceil(math.log(prime, 2) / 8) * 8) return self._bit_size @@ -667,11 +667,11 @@ class PrivateKeyInfo(Sequence): """ :return: If an RSA key, an RSAPublicKey object. If a DSA key, an Integer - object. If an ECDSA key, an OctetString object. + object. If an EC key, an OctetString object. """ if self._public_key is None: - if self.algorithm == 'ecdsa': + if self.algorithm == 'ec': key = self['private_key'].parsed if key['public_key']: self._public_key = OctetString(key['public_key'].native) @@ -691,9 +691,6 @@ class PrivateKeyInfo(Sequence): This fingerprint is not compatiable with fingerprints generated by any other software. - :raises: - ValueError - when the private key is ECDSA, but the public_key field is empty - :return: A byte string that is a sha256 hash of selected components (based on the key type) @@ -718,7 +715,7 @@ class PrivateKeyInfo(Sequence): public_key.native, ) - elif self.algorithm == 'ecdsa': + elif self.algorithm == 'ec': public_key = key['public_key'].native if public_key is None: public_key = self.public_key.native @@ -771,7 +768,7 @@ class PublicKeyAlgorithmId(ObjectIdentifier): # https://tools.ietf.org/html/rfc3279#page-18 '1.2.840.10040.4.1': 'dsa', # https://tools.ietf.org/html/rfc3279#page-13 - '1.2.840.10045.2.1': 'ecdsa', + '1.2.840.10045.2.1': 'ec', } @@ -790,7 +787,7 @@ class PublicKeyAlgorithm(Sequence): _oid_specs = { 'rsa': Null, 'dsa': DSAParams, - 'ecdsa': ECDomainParameters, + 'ec': ECDomainParameters, } @@ -812,7 +809,7 @@ class PublicKeyInfo(Sequence): 'dsa': Integer, # ECSDA's public key is an ECPoint, which is an OctetString. Since # we are using OctetBitString here, we don't need further parsing. - 'ecdsa': None, + 'ec': None, }[algorithm] _spec_callbacks = { @@ -859,10 +856,10 @@ class PublicKeyInfo(Sequence): @property def curve(self): """ - Returns information about the curve used for an ECDSA key + Returns information about the curve used for an EC key :raises: - ValueError - when the key is not an ECDSA key + ValueError - when the key is not an EC key :return: A two-element tuple, with the first element being a unicode string @@ -872,8 +869,8 @@ class PublicKeyInfo(Sequence): "named", the second is a unicode string of the curve name. """ - if self.algorithm != 'ecdsa': - raise ValueError('Only ECDSA keys have a curve, this key is %s' % self.algorithm.upper()) + if self.algorithm != 'ec': + raise ValueError('Only EC keys have a curve, this key is %s' % self.algorithm.upper()) params = self['algorithm']['parameters'] chosen = params.chosen @@ -909,7 +906,7 @@ class PublicKeyInfo(Sequence): def algorithm(self): """ :return: - A unicode string of "rsa", "dsa" or "ecdsa" + A unicode string of "rsa", "dsa" or "ec" """ if self._algorithm is None: @@ -924,7 +921,7 @@ class PublicKeyInfo(Sequence): """ if self._bit_size is None: - if self.algorithm == 'ecdsa': + if self.algorithm == 'ec': self._bit_size = ((len(self['public_key'].native) - 1) / 2) * 8 else: if self.algorithm == 'rsa': @@ -969,7 +966,7 @@ class PublicKeyInfo(Sequence): key.native, ) - elif key_type == 'ecdsa': + elif key_type == 'ec': key = self['public_key'] if params.name == 'named': diff --git a/tests/test_keys.py b/tests/test_keys.py index e7bee70..6b4bf8e 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -289,8 +289,8 @@ class KeysTests(unittest.TestCase): def key_pairs(): return ( ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-public-dsa-der.key', 'dsa', 3072), - ('ecdsa_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-public-ec-named-der.key', 'ecdsa', 256), - ('ecdsa', 'keys/test-pkcs8-ec-der.key', 'keys/test-public-ec-der.key', 'ecdsa', 256), + ('ec_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-public-ec-named-der.key', 'ec', 256), + ('ec', 'keys/test-pkcs8-ec-der.key', 'keys/test-public-ec-der.key', 'ec', 256), ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-public-der.key', 'rsa', 2048), ) @@ -346,8 +346,8 @@ class KeysTests(unittest.TestCase): def key_variations(): return ( ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-dsa-der.key',), - ('ecdsa_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-ec-named-der.key',), - ('ecdsa', 'keys/test-pkcs8-ec-der.key', 'keys/test-ec-der.key',), + ('ec_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-ec-named-der.key',), + ('ec', 'keys/test-pkcs8-ec-der.key', 'keys/test-ec-der.key',), ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-der.key',), ) diff --git a/tests/test_x509.py b/tests/test_x509.py index 30606e8..ab6c55e 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -371,7 +371,7 @@ class X509Tests(unittest.TestCase): subject.native ) self.assertEqual( - 'ecdsa', + 'ec', subject_public_key_algorithm['algorithm'].native ) self.assertEqual( -- cgit v1.2.3 From f4f176cfd74f141f3984d9cadf8fc48b407d65da Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 7 Jul 2015 23:27:19 -0400 Subject: Added .byte_size property to PublicKeyInfo and PrivateKeyInfo, fix .bit_size to not adjust to byte boundary --- asn1crypto/keys.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 145e403..9ab8735 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -649,19 +649,28 @@ class PrivateKeyInfo(Sequence): def bit_size(self): """ :return: - The bit size of the private, as an integer + The bit size of the private key, as an integer """ if self._bit_size is None: if self.algorithm == 'rsa': - prime = self['private_key'].parsed['private_exponent'].native + prime = self['private_key'].parsed['modulus'].native elif self.algorithm == 'dsa': prime = self['private_key_algorithm']['parameters']['p'].native elif self.algorithm == 'ec': prime = self['private_key'].parsed['private_key'].native - self._bit_size = int(math.ceil(math.log(prime, 2) / 8) * 8) + self._bit_size = int(math.ceil(math.log(prime, 2))) return self._bit_size + @property + def byte_size(self): + """ + :return: + The byte size of the private key, as an integer + """ + + return int(math.ceil(self.bit_size / 8)) + @property def public_key(self): """ @@ -917,7 +926,7 @@ class PublicKeyInfo(Sequence): def bit_size(self): """ :return: - The bit size of the private, as an integer + The bit size of the public key, as an integer """ if self._bit_size is None: @@ -928,10 +937,19 @@ class PublicKeyInfo(Sequence): prime = self['public_key'].parsed['modulus'].native elif self.algorithm == 'dsa': prime = self['public_key'].parsed.native - self._bit_size = int(math.ceil(math.log(prime, 2) / 8) * 8) + self._bit_size = int(math.ceil(math.log(prime, 2))) return self._bit_size + @property + def byte_size(self): + """ + :return: + The byte size of the public key, as an integer + """ + + return int(math.ceil(self.bit_size / 8)) + @property def fingerprint(self): """ -- cgit v1.2.3 From b924d3348a76d94f3d7981735f3f69e02f638ba2 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 7 Jul 2015 23:27:48 -0400 Subject: Added PublicKeyInfo.unwrap() for RSA keys --- asn1crypto/keys.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 9ab8735..ae3b0eb 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -862,6 +862,22 @@ class PublicKeyInfo(Sequence): return container + def unwrap(self): + """ + Unwraps an RSA public key into an RSAPublicKey object. Does not support + DSA or EC public keys since they do not have an unwrapped form. + + :return: + An RSAPublicKey object + """ + + if self.algorithm == 'rsa': + return self['public_key'].parsed + + key_type = self.algorithm.upper() + a_an = 'an' if key_type == 'EC' else 'a' + raise ValueError('Only RSA public keys may be unwrapped - this key is %s %s public key' % (a_an, key_type)) + @property def curve(self): """ -- cgit v1.2.3 From 2592ed9ff297bb521b4461549f0b38b69e7cef85 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 7 Jul 2015 23:31:10 -0400 Subject: Added params for RSAES-OAEP and RSASSA-PSS, tweaked name for RSASSA-PKCS#1v1.5 OID --- asn1crypto/algos.py | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 14cce07..0b619c4 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -82,6 +82,74 @@ class DigestInfo(Sequence): ] +class MaskGenAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.1.8': 'mgf1', + } + + +class MaskGenAlgorithm(Sequence): + _fields = [ + ('algorithm', MaskGenAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'mgf1': DigestAlgorithm + } + + +class TrailerField(Integer): + _map = { + 1: 'trailer_field_bc', + } + + +class RSASSAPSSParams(Sequence): + _fields = [ + ( + 'hash_algorithm', + DigestAlgorithm, + { + 'tag_type': 'explicit', + 'tag': 0, + 'default': {'algorithm': 'sha1'}, + } + ), + ( + 'mask_gen_algorithm', + MaskGenAlgorithm, + { + 'tag_type': 'explicit', + 'tag': 1, + 'default': { + 'algorithm': 'mgf1', + 'parameters': {'algorithm': 'sha1'}, + }, + } + ), + ( + 'salt_length', + Integer, + { + 'tag_type': 'explicit', + 'tag': 2, + 'default': 20, + } + ), + ( + 'trailer_field', + TrailerField, + { + 'tag_type': 'explicit', + 'tag': 3, + 'default': 'trailer_field_bc', + } + ), + ] + + class SignedDigestAlgorithmId(ObjectIdentifier): _map = { '1.3.14.3.2.3': 'md5_rsa', @@ -94,6 +162,7 @@ class SignedDigestAlgorithmId(ObjectIdentifier): '1.2.840.113549.1.1.11': 'sha256_rsa', '1.2.840.113549.1.1.12': 'sha384_rsa', '1.2.840.113549.1.1.13': 'sha512_rsa', + '1.2.840.113549.1.1.10': 'rsassa_pss', '1.2.840.10040.4.3': 'sha1_dsa', '1.3.14.3.2.13': 'sha1_dsa', '1.3.14.3.2.27': 'sha1_dsa', @@ -105,7 +174,7 @@ class SignedDigestAlgorithmId(ObjectIdentifier): '1.2.840.10045.4.3.3': 'sha384_ecdsa', '1.2.840.10045.4.3.4': 'sha512_ecdsa', # For when the digest is specified elsewhere in a Sequence - '1.2.840.113549.1.1.1': 'rsa', + '1.2.840.113549.1.1.1': 'rsassa_pkcs1v15', '1.2.840.10040.4.1': 'dsa', '1.2.840.10045.4': 'ecdsa', } @@ -117,6 +186,11 @@ class SignedDigestAlgorithm(Sequence): ('parameters', Any, {'optional': True}), ] + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'rsa_pss': RSASSAPSSParams, + } + class Pbkdf2Salt(Choice): _alternatives = [ @@ -180,6 +254,62 @@ class Pbes1Params(Sequence): ] +class PSourceAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.1.9': 'p_specified', + } + + +class PSourceAlgorithm(Sequence): + _fields = [ + ('algorithm', PSourceAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'p_specified': OctetString + } + + +class RSAESOAEPParams(Sequence): + _fields = [ + ( + 'hash_algorithm', + DigestAlgorithm, + { + 'tag_type': 'explicit', + 'tag': 0, + 'default': {'algorithm': 'sha1'} + } + ), + ( + 'mask_gen_algorithm', + MaskGenAlgorithm, + { + 'tag_type': 'explicit', + 'tag': 1, + 'default': { + 'algorithm': 'mgf1', + 'parameters': {'algorithm': 'sha1'} + } + } + ), + ( + 'p_source_algorithm', + PSourceAlgorithm, + { + 'tag_type': 'explicit', + 'tag': 2, + 'default': { + 'algorithm': 'p_specified', + 'parameters': b'' + } + } + ), + ] + + class EncryptionAlgorithmId(ObjectIdentifier): _map = { '1.3.14.3.2.7': 'des', @@ -204,6 +334,9 @@ class EncryptionAlgorithmId(ObjectIdentifier): '1.2.840.113549.1.12.1.4': 'pkcs12_sha1_tripledes_2key', '1.2.840.113549.1.12.1.5': 'pkcs12_sha1_rc2_128', '1.2.840.113549.1.12.1.6': 'pkcs12_sha1_rc2_40', + # PKCS#1 v2.2 + '1.2.840.113549.1.1.1': 'rsaes_pkcs1v15', + '1.2.840.113549.1.1.7': 'rsaes_oaep', } @@ -236,6 +369,8 @@ class EncryptionAlgorithm(Sequence): 'pkcs12_sha1_tripledes_2key': Pbes1Params, 'pkcs12_sha1_rc2_128': Pbes1Params, 'pkcs12_sha1_rc2_40': Pbes1Params, + # PKCS#1 v2.2 + 'rsaes_oaep': RSAESOAEPParams, } @property -- cgit v1.2.3 From c9fec246b3ea6ddee7cf8587ab5744b159e3995a Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 09:03:11 -0400 Subject: Moved files to clean up main folder and make intent of performance optimization clear --- asn1crypto/_big_num_cffi.py | 77 ---------------------- asn1crypto/_big_num_ctypes.py | 77 ---------------------- asn1crypto/_elliptic_curve.py | 4 +- asn1crypto/_ffi.py | 26 -------- asn1crypto/_int.py | 107 ++++++++++++++++++++++++++++++ asn1crypto/_int_conversion.py | 107 ------------------------------ asn1crypto/_inverse_mod.py | 127 ------------------------------------ asn1crypto/_perf/__init__.py | 0 asn1crypto/_perf/_big_num_cffi.py | 77 ++++++++++++++++++++++ asn1crypto/_perf/_big_num_ctypes.py | 77 ++++++++++++++++++++++ asn1crypto/_perf/_ffi.py | 26 ++++++++ asn1crypto/_perf/_inverse_mod.py | 127 ++++++++++++++++++++++++++++++++++++ asn1crypto/core.py | 2 +- asn1crypto/keys.py | 2 +- 14 files changed, 418 insertions(+), 418 deletions(-) delete mode 100644 asn1crypto/_big_num_cffi.py delete mode 100644 asn1crypto/_big_num_ctypes.py delete mode 100644 asn1crypto/_ffi.py create mode 100644 asn1crypto/_int.py delete mode 100644 asn1crypto/_int_conversion.py delete mode 100644 asn1crypto/_inverse_mod.py create mode 100644 asn1crypto/_perf/__init__.py create mode 100644 asn1crypto/_perf/_big_num_cffi.py create mode 100644 asn1crypto/_perf/_big_num_ctypes.py create mode 100644 asn1crypto/_perf/_ffi.py create mode 100644 asn1crypto/_perf/_inverse_mod.py diff --git a/asn1crypto/_big_num_cffi.py b/asn1crypto/_big_num_cffi.py deleted file mode 100644 index cc4fdac..0000000 --- a/asn1crypto/_big_num_cffi.py +++ /dev/null @@ -1,77 +0,0 @@ -# coding: utf-8 - -""" -cffi interface for BN_mod_inverse() function from OpenSSL. Exports the -following items: - - - buffer_from_bytes() - - bytes_from_buffer() - - libcrypto - - BN_new() - - BN_bin2bin() - - BN_set_negative() - - BN_num_bits() - - BN_free() - - BN_CTX_new() - - BN_CTX_free() - - BN_mod_inverse() - - null() - -Will raise asn1crypto._ffi.LibraryNotFoundError() if libcrypto can not be -found. Will raise asn1crypto._ffi.FFIEngineError() if cffi is not instaled -or there is an error interfacing with libcrypto. -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -from ctypes.util import find_library - -from ._ffi import LibraryNotFoundError, FFIEngineError - -try: - from cffi import FFI - -except (ImportError): - raise FFIEngineError('Error importing cffi') - - -try: - ffi = FFI() - ffi.cdef(""" - typedef ... BIGNUM; - typedef ... BN_CTX; - - BIGNUM *BN_new(void); - - int BN_bn2bin(const BIGNUM *a, unsigned char *to); - BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); - - void BN_set_negative(BIGNUM *a, int n); - - int BN_num_bits(const BIGNUM *a); - - void BN_free(BIGNUM *a); - - BN_CTX *BN_CTX_new(void); - void BN_CTX_free(BN_CTX *c); - - BIGNUM *BN_mod_inverse(BIGNUM *r, BIGNUM *a, const BIGNUM *n, BN_CTX *ctx); - """) - - libcrypto_path = find_library('crypto') - if not libcrypto_path: - raise LibraryNotFoundError('The library libcrypto could not be found') - - libcrypto = ffi.dlopen(libcrypto_path) - - def buffer_from_bytes(initializer): - return ffi.new('char[]', initializer) - - def bytes_from_buffer(buffer, maxlen=None): - return ffi.buffer(buffer, maxlen)[:] - - def null(): - return ffi.NULL - -except (AttributeError): - raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_big_num_ctypes.py b/asn1crypto/_big_num_ctypes.py deleted file mode 100644 index d764223..0000000 --- a/asn1crypto/_big_num_ctypes.py +++ /dev/null @@ -1,77 +0,0 @@ -# coding: utf-8 - -""" -ctypes interface for BN_mod_inverse() function from OpenSSL. Exports the -following items: - - - buffer_from_bytes() - - bytes_from_buffer() - - libcrypto - - BN_bin2bin() - - BN_CTX_free() - - BN_CTX_new() - - BN_free() - - BN_mod_inverse() - - BN_new() - - BN_num_bits() - - BN_set_negative() - - null() - -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 - -from ctypes.util import find_library -from ctypes import CDLL, c_int, c_char_p, c_void_p, create_string_buffer - -from ._ffi import LibraryNotFoundError, FFIEngineError - - -try: - libcrypto_path = find_library('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 - - def buffer_from_bytes(initializer): - return create_string_buffer(initializer) - - def bytes_from_buffer(buffer, maxlen=None): #pylint: disable=W0613 - return buffer.raw - - def null(): - return None - -except (AttributeError): - raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index e5f82cb..14a950c 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -52,8 +52,8 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import math -from ._inverse_mod import inverse_mod -from ._int_conversion import int_to_bytes, int_from_bytes +from ._perf._inverse_mod import inverse_mod +from ._int import int_to_bytes, int_from_bytes if sys.version_info < (3,): byte_cls = str diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py deleted file mode 100644 index 64032f3..0000000 --- a/asn1crypto/_ffi.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding: utf-8 - -""" -Exceptions for help trying to use cffi, then ctypes for shared library access -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - - - -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 new file mode 100644 index 0000000..13c1c96 --- /dev/null +++ b/asn1crypto/_int.py @@ -0,0 +1,107 @@ +# coding: utf-8 + +""" +Functions to convert from integers to byte string and byte string to integers. +Exports the following items: + + - int_from_bytes() + - int_to_bytes() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + + +# Python 2 +if sys.version_info <= (3,): + + def int_to_bytes(value, signed=False): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + If the byte string should be encoded using two's complement + + :return: + A byte string + """ + + # Handle negatives in two's complement + if signed and value < 0: + value = (~value) + 1 + + hex_str = '%x' % value + if len(hex_str) & 1: + hex_str = '0' + hex_str + output = hex_str.decode('hex') + if signed and ord(output[0:1]) & 0x80: + output = b'\x00' + output + return output + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + num = long(value.encode("hex"), 16) #pylint: disable=E0602 + + if not signed: + return num + + # Check for sign bit and handle two's complement + if ord(value[0:1]) & 0x80: + bit_len = len(value) * 8 + return num - (1 << bit_len) + + return num + +# Python 3 +else: + + def int_to_bytes(value, signed=False): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + If the byte string should be encoded using two's complement + + :return: + A byte string + """ + + result = value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) + if not signed: + return result.lstrip(b'\x00') + return result + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + return int.from_bytes(value, 'big', signed=signed) diff --git a/asn1crypto/_int_conversion.py b/asn1crypto/_int_conversion.py deleted file mode 100644 index 13c1c96..0000000 --- a/asn1crypto/_int_conversion.py +++ /dev/null @@ -1,107 +0,0 @@ -# coding: utf-8 - -""" -Functions to convert from integers to byte string and byte string to integers. -Exports the following items: - - - int_from_bytes() - - int_to_bytes() -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -import sys - - -# Python 2 -if sys.version_info <= (3,): - - def int_to_bytes(value, signed=False): - """ - Converts an integer to a byte string - - :param value: - The integer to convert - - :param signed: - If the byte string should be encoded using two's complement - - :return: - A byte string - """ - - # Handle negatives in two's complement - if signed and value < 0: - value = (~value) + 1 - - hex_str = '%x' % value - if len(hex_str) & 1: - hex_str = '0' + hex_str - output = hex_str.decode('hex') - if signed and ord(output[0:1]) & 0x80: - output = b'\x00' + output - return output - - def int_from_bytes(value, signed=False): - """ - Converts a byte string to an integer - - :param value: - The byte string to convert - - :param signed: - If the byte string should be interpreted using two's complement - - :return: - An integer - """ - - num = long(value.encode("hex"), 16) #pylint: disable=E0602 - - if not signed: - return num - - # Check for sign bit and handle two's complement - if ord(value[0:1]) & 0x80: - bit_len = len(value) * 8 - return num - (1 << bit_len) - - return num - -# Python 3 -else: - - def int_to_bytes(value, signed=False): - """ - Converts an integer to a byte string - - :param value: - The integer to convert - - :param signed: - If the byte string should be encoded using two's complement - - :return: - A byte string - """ - - result = value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) - if not signed: - return result.lstrip(b'\x00') - return result - - def int_from_bytes(value, signed=False): - """ - Converts a byte string to an integer - - :param value: - The byte string to convert - - :param signed: - If the byte string should be interpreted using two's complement - - :return: - An integer - """ - - return int.from_bytes(value, 'big', signed=signed) diff --git a/asn1crypto/_inverse_mod.py b/asn1crypto/_inverse_mod.py deleted file mode 100644 index 3adf99b..0000000 --- a/asn1crypto/_inverse_mod.py +++ /dev/null @@ -1,127 +0,0 @@ -# coding: utf-8 - -""" -Allows computing the modular inverse using either OpenSSL or a pure python -fallback. Exports the following items: - - - inverse_mod() - -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 - -import math - -from ._ffi import LibraryNotFoundError, FFIEngineError -from ._int_conversion import int_to_bytes, int_from_bytes - - -try: - try: - from ._big_num_cffi import libcrypto, buffer_from_bytes, bytes_from_buffer, null - except (FFIEngineError) as e: - from ._big_num_ctypes import libcrypto, buffer_from_bytes, bytes_from_buffer, null - - 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): - - 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 diff --git a/asn1crypto/_perf/__init__.py b/asn1crypto/_perf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/asn1crypto/_perf/_big_num_cffi.py b/asn1crypto/_perf/_big_num_cffi.py new file mode 100644 index 0000000..3b47200 --- /dev/null +++ b/asn1crypto/_perf/_big_num_cffi.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +""" +cffi interface for BN_mod_inverse() function from OpenSSL. Exports the +following items: + + - buffer_from_bytes() + - bytes_from_buffer() + - libcrypto + - BN_bin2bin() + - BN_CTX_free() + - BN_CTX_new() + - BN_free() + - BN_mod_inverse() + - BN_new() + - BN_num_bits() + - BN_set_negative() + - null() + +Will raise asn1crypto._ffi.LibraryNotFoundError() if libcrypto can not be +found. Will raise asn1crypto._ffi.FFIEngineError() if cffi is not instaled +or there is an error interfacing with libcrypto. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from ctypes.util import find_library + +from ._ffi import LibraryNotFoundError, FFIEngineError + +try: + from cffi import FFI + +except (ImportError): + raise FFIEngineError('Error importing cffi') + + +try: + ffi = FFI() + ffi.cdef(""" + typedef ... BIGNUM; + typedef ... BN_CTX; + + BIGNUM *BN_new(void); + + int BN_bn2bin(const BIGNUM *a, unsigned char *to); + BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); + + void BN_set_negative(BIGNUM *a, int n); + + int BN_num_bits(const BIGNUM *a); + + void BN_free(BIGNUM *a); + + BN_CTX *BN_CTX_new(void); + void BN_CTX_free(BN_CTX *c); + + BIGNUM *BN_mod_inverse(BIGNUM *r, BIGNUM *a, const BIGNUM *n, BN_CTX *ctx); + """) + + libcrypto_path = find_library('crypto') + if not libcrypto_path: + raise LibraryNotFoundError('The library libcrypto could not be found') + + libcrypto = ffi.dlopen(libcrypto_path) + + def buffer_from_bytes(initializer): + return ffi.new('char[]', initializer) + + def bytes_from_buffer(buffer, maxlen=None): + return ffi.buffer(buffer, maxlen)[:] + + def null(): + return ffi.NULL + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_perf/_big_num_ctypes.py b/asn1crypto/_perf/_big_num_ctypes.py new file mode 100644 index 0000000..d764223 --- /dev/null +++ b/asn1crypto/_perf/_big_num_ctypes.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +""" +ctypes interface for BN_mod_inverse() function from OpenSSL. Exports the +following items: + + - buffer_from_bytes() + - bytes_from_buffer() + - libcrypto + - BN_bin2bin() + - BN_CTX_free() + - BN_CTX_new() + - BN_free() + - BN_mod_inverse() + - BN_new() + - BN_num_bits() + - BN_set_negative() + - null() + +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 + +from ctypes.util import find_library +from ctypes import CDLL, c_int, c_char_p, c_void_p, create_string_buffer + +from ._ffi import LibraryNotFoundError, FFIEngineError + + +try: + libcrypto_path = find_library('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 + + def buffer_from_bytes(initializer): + return create_string_buffer(initializer) + + def bytes_from_buffer(buffer, maxlen=None): #pylint: disable=W0613 + return buffer.raw + + def null(): + return None + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_perf/_ffi.py b/asn1crypto/_perf/_ffi.py new file mode 100644 index 0000000..64032f3 --- /dev/null +++ b/asn1crypto/_perf/_ffi.py @@ -0,0 +1,26 @@ +# coding: utf-8 + +""" +Exceptions for help trying to use cffi, then ctypes for shared library access +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + + + +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/_perf/_inverse_mod.py b/asn1crypto/_perf/_inverse_mod.py new file mode 100644 index 0000000..2a3e15b --- /dev/null +++ b/asn1crypto/_perf/_inverse_mod.py @@ -0,0 +1,127 @@ +# coding: utf-8 + +""" +Allows computing the modular inverse using either OpenSSL or a pure python +fallback. Exports the following items: + + - inverse_mod() + +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 + +import math + +from ._ffi import LibraryNotFoundError, FFIEngineError +from .._int import int_to_bytes, int_from_bytes + + +try: + try: + from ._big_num_cffi import libcrypto, buffer_from_bytes, bytes_from_buffer, null + except (FFIEngineError) as e: + from ._big_num_ctypes import libcrypto, buffer_from_bytes, bytes_from_buffer, null + + 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): + + 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 diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8934878..4db0f2b 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -53,7 +53,7 @@ from datetime import datetime, timedelta, tzinfo from pprint import pprint from . import _teletex_codec -from ._int_conversion import int_to_bytes, int_from_bytes +from ._int import int_to_bytes, int_from_bytes # Python 2 if sys.version_info <= (3,): diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index ae3b0eb..38ccd45 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -43,7 +43,7 @@ from ._elliptic_curve import ( PrimeCurve, PrimePoint, ) -from ._int_conversion import int_from_bytes +from ._int import int_from_bytes try: # Python 2 -- cgit v1.2.3 From d6b8b2ecae22f6d86b6b2f50fbe0c29c22cfff24 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 09:08:23 -0400 Subject: Correct PublicKeyInfo to calculate the bit size of a DSA key from the parameter p and not the public key integer --- asn1crypto/keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 38ccd45..07b9576 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -952,7 +952,7 @@ class PublicKeyInfo(Sequence): if self.algorithm == 'rsa': prime = self['public_key'].parsed['modulus'].native elif self.algorithm == 'dsa': - prime = self['public_key'].parsed.native + prime = self['algorithm']['parameters']['p'].native self._bit_size = int(math.ceil(math.log(prime, 2))) return self._bit_size -- cgit v1.2.3 From 1bfa4dee79e1fd385df003379e49e63064151716 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 09:10:31 -0400 Subject: Updated tests to reflect new, more specific name for RSA signatures --- tests/test_cms.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_cms.py b/tests/test_cms.py index fde2214..a1475b2 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -329,7 +329,7 @@ class CMSTests(unittest.TestCase): self.assertEqual( OrderedDict([ - ('algorithm', 'rsa'), + ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), signer['signature_algorithm'].native @@ -455,7 +455,7 @@ class CMSTests(unittest.TestCase): self.assertEqual( OrderedDict([ - ('algorithm', 'rsa'), + ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), signer['signature_algorithm'].native @@ -577,7 +577,7 @@ class CMSTests(unittest.TestCase): self.assertEqual( OrderedDict([ - ('algorithm', 'rsa'), + ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), signer['signature_algorithm'].native @@ -700,7 +700,7 @@ class CMSTests(unittest.TestCase): self.assertEqual( OrderedDict([ - ('algorithm', 'rsa'), + ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), signer['signature_algorithm'].native -- cgit v1.2.3 From d8a54449b6345206fb7e4b0c737d962a66c29e37 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 09:16:18 -0400 Subject: Moved inverse_mod() function into _int.py since it itself is not strictly for performance --- asn1crypto/_elliptic_curve.py | 3 +- asn1crypto/_int.py | 123 ++++++++++++++++++++++++++++++++++++- asn1crypto/_perf/_inverse_mod.py | 127 --------------------------------------- 3 files changed, 122 insertions(+), 131 deletions(-) delete mode 100644 asn1crypto/_perf/_inverse_mod.py diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index 14a950c..0b000b8 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -52,8 +52,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import math -from ._perf._inverse_mod import inverse_mod -from ._int import int_to_bytes, int_from_bytes +from ._int import int_to_bytes, int_from_bytes, inverse_mod if sys.version_info < (3,): byte_cls = str diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index 13c1c96..902ce81 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -1,16 +1,46 @@ # coding: utf-8 """ -Functions to convert from integers to byte string and byte string to integers. -Exports the following items: +Functions for converting integers to and from bytes, and calculating the modular +inverse. Exports the following items: - int_from_bytes() - int_to_bytes() + - inverse_mod() + +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 import sys +import math + +from ._perf._ffi import LibraryNotFoundError, FFIEngineError + # Python 2 @@ -105,3 +135,92 @@ else: """ return int.from_bytes(value, 'big', signed=signed) + + +# First try to use ctypes or cffi with OpenSSL for better performance +try: + try: + from ._perf._big_num_cffi import libcrypto, buffer_from_bytes, bytes_from_buffer, null + except (FFIEngineError) as e: + from ._perf._big_num_ctypes import libcrypto, buffer_from_bytes, bytes_from_buffer, null + + 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 + +# If there was an issue using OpenSSL, we fall back to pure python +except (LibraryNotFoundError, FFIEngineError): + + 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 diff --git a/asn1crypto/_perf/_inverse_mod.py b/asn1crypto/_perf/_inverse_mod.py deleted file mode 100644 index 2a3e15b..0000000 --- a/asn1crypto/_perf/_inverse_mod.py +++ /dev/null @@ -1,127 +0,0 @@ -# coding: utf-8 - -""" -Allows computing the modular inverse using either OpenSSL or a pure python -fallback. Exports the following items: - - - inverse_mod() - -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 - -import math - -from ._ffi import LibraryNotFoundError, FFIEngineError -from .._int import int_to_bytes, int_from_bytes - - -try: - try: - from ._big_num_cffi import libcrypto, buffer_from_bytes, bytes_from_buffer, null - except (FFIEngineError) as e: - from ._big_num_ctypes import libcrypto, buffer_from_bytes, bytes_from_buffer, null - - 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): - - 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 -- cgit v1.2.3 From f0b67392be595a8e83c4bf61a851b886c3beb92c Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 10:19:46 -0400 Subject: Handle situations where .native is called but _fields has not been set --- asn1crypto/core.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 4db0f2b..43fb21a 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2062,7 +2062,11 @@ class Sequence(Asn1Value): if isinstance(child, tuple): child = _build(*child) self.children[index] = child - self._native[self._fields[index][0]] = child.native + try: + name = self._fields[index][0] + except (IndexError): + name = str_cls(index) + self._native[name] = child.native except (ValueError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args -- cgit v1.2.3 From 3855a15533b5a8f9f51dd301d245ce92bb58e48e Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 10:47:32 -0400 Subject: Added CSR structures, tests and reference links --- asn1crypto/csr.py | 89 ++++++++++++++++++++++++ dev/tests.py | 3 +- docs/readme.md | 1 + readme.md | 2 + tests/fixtures/test-inter-der.csr | Bin 0 -> 756 bytes tests/fixtures/test-inter.csr | 18 +++++ tests/fixtures/test-third-der.csr | Bin 0 -> 805 bytes tests/fixtures/test-third.csr | 19 ++++++ tests/test_csr.py | 140 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 asn1crypto/csr.py create mode 100644 tests/fixtures/test-inter-der.csr create mode 100644 tests/fixtures/test-inter.csr create mode 100644 tests/fixtures/test-third-der.csr create mode 100644 tests/fixtures/test-third.csr create mode 100644 tests/test_csr.py diff --git a/asn1crypto/csr.py b/asn1crypto/csr.py new file mode 100644 index 0000000..734c9f1 --- /dev/null +++ b/asn1crypto/csr.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +""" +ASN.1 type classes for certificate signing requests (CSR). Exports the +following items: + + - CertificatationRequest() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from .algos import SignedDigestAlgorithm +from .core import Any, Integer, ObjectIdentifier, OctetBitString, Sequence, SetOf +from .keys import PublicKeyInfo +from .x509 import DirectoryString, Extensions, Name + + + +# The structures in this file are taken from https://tools.ietf.org/html/rfc2986 + + +class Version(Integer): + _map = { + 0: 'v1', + } + + +class CSRAttributeType(ObjectIdentifier): + _map = { + '1.2.840.113549.1.9.7': 'challenge_password', + '1.2.840.113549.1.9.9': 'extended_certificate_attributes', + '1.2.840.113549.1.9.14': 'extension_request', + } + + +class SetOfDirectoryString(SetOf): + _child_spec = DirectoryString + + +class Attribute(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('values', SetOf, {'spec': Any}), + ] + + +class SetOfAttributes(SetOf): + _child_spec = Attribute + + +class SetOfExtensions(SetOf): + _child_spec = Extensions + + +class CRIAttribute(Sequence): + _fields = [ + ('type', CSRAttributeType), + ('values', Any), + ] + + _oid_pair = ('type', 'values') + _oid_specs = { + 'challenge_password': SetOfDirectoryString, + 'extended_certificate_attributes': SetOfAttributes, + 'extension_request': SetOfExtensions, + } + + +class CRIAttributes(SetOf): + _child_spec = CRIAttribute + + +class CertificationRequestInfo(Sequence): + _fields = [ + ('version', Version), + ('subject', Name), + ('subject_pk_info', PublicKeyInfo), + ('attributes', CRIAttributes, {'tag_type': 'implicit', 'tag': 0}), + ] + + +class CertificationRequest(Sequence): + _fields = [ + ('certification_request_info', CertificationRequestInfo), + ('signature_algorithm', SignedDigestAlgorithm), + ('signature', OctetBitString), + ] diff --git a/dev/tests.py b/dev/tests.py index fcb53cd..39f5d8b 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -6,6 +6,7 @@ import re from tests.test_cms import CMSTests from tests.test_crl import CRLTests +from tests.test_csr import CSRTests from tests.test_keys import KeysTests from tests.test_ocsp import OCSPTests from tests.test_tsp import TSPTests @@ -13,7 +14,7 @@ from tests.test_x509 import X509Tests from tests.test_core import CoreTests -test_classes = [CMSTests, CRLTests, KeysTests, OCSPTests, TSPTests, X509Tests, CoreTests] +test_classes = [CMSTests, CRLTests, CSRTests, KeysTests, OCSPTests, TSPTests, X509Tests, CoreTests] def run(matcher=None): diff --git a/docs/readme.md b/docs/readme.md index 7dc216f..bb8ab62 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -14,6 +14,7 @@ pre-defined data types. - [X509 certificates](../asn1crypto/x509.py), `asn1crypto.x509` - [Certificate revocation lists (CRLs)](../asn1crypto/crl.py), `asn1crypto.crl` - [Online certificate status protocol (OCSP)](../asn1crypto/ocsp.py), `asn1crypto.ocsp` + - [Certificate signing requests (CSRs)](../asn1crypto/csr.py), `asn1crypto.csr` - [Private key/certificate containers (PKCS#12)](../asn1crypto/pkcs12.py), `asn1crypto.pkcs12` - [Cryptographic message syntax (CMS, PKCS#7)](../asn1crypto/cms.py), `asn1crypto.cms` - [Time stamp protocol (TSP)](../asn1crypto/tsp.py), `asn1crypto.tsp` diff --git a/readme.md b/readme.md index 6b0d686..de112ab 100644 --- a/readme.md +++ b/readme.md @@ -8,6 +8,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: | ---------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | X509 | [`asn1crypto.x509`](asn1crypto/x509.py) | [RFC5280](https://tools.ietf.org/html/rfc5280) | | CRL | [`asn1crypto.crl`](asn1crypto/crl.py) | [RFC5280](https://tools.ietf.org/html/rfc5280) | +| CSR | [`asn1crypto.csr`](asn1crypto/csr.py) | [RFC2986](https://tools.ietf.org/html/rfc2986), [RFC2985](https://tools.ietf.org/html/rfc2985) | | OCSP | [`asn1crypto.ocsp`](asn1crypto/ocsp.py) | [RFC6960](https://tools.ietf.org/html/rfc6960) | | PKCS#12 | [`asn1crypto.pkcs12`](asn1crypto/pkcs12.py) | [RFC7292](https://tools.ietf.org/html/rfc7292) | | PKCS#8 | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC5208](https://tools.ietf.org/html/rfc5208) | @@ -54,6 +55,7 @@ pre-defined data types. - [X509 certificates](asn1crypto/x509.py), `asn1crypto.x509` - [Certificate revocation lists (CRLs)](asn1crypto/crl.py), `asn1crypto.crl` - [Online certificate status protocol (OCSP)](asn1crypto/ocsp.py), `asn1crypto.ocsp` + - [Certificate signing requests (CSRs)](asn1crypto/csr.py), `asn1crypto.csr` - [Private key/certificate containers (PKCS#12)](asn1crypto/pkcs12.py), `asn1crypto.pkcs12` - [Cryptographic message syntax (CMS, PKCS#7)](asn1crypto/cms.py), `asn1crypto.cms` - [Time stamp protocol (TSP)](asn1crypto/tsp.py), `asn1crypto.tsp` diff --git a/tests/fixtures/test-inter-der.csr b/tests/fixtures/test-inter-der.csr new file mode 100644 index 0000000..c213cea Binary files /dev/null and b/tests/fixtures/test-inter-der.csr differ diff --git a/tests/fixtures/test-inter.csr b/tests/fixtures/test-inter.csr new file mode 100644 index 0000000..7b89854 --- /dev/null +++ b/tests/fixtures/test-inter.csr @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC8DCCAdgCAQAwgaoxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNl +dHRzMRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmlj +aXQgTEMxHTAbBgNVBAsTFFRlc3RpbmcgSW50ZXJtZWRpYXRlMRIwEAYDVQQDEwlX +aWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5pbzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL89U52iC2p7aCpdnbR1YHk0XGMlfrPo +DnvMapPXU5+I2ZyQirExErx5OhHr38MmZU1tBMnL9wV7Fnfd1v5Su0Qi3E6SUUVs +b/pOOaqw5gXztOQmoZfvNez9nsPVeXT/YI2Hq8889uL+jUUe0qeTgdg6fJQtEzHG +23To7+7//B2dZeJIA4o5qwtPt4/oW6iTbz4F5Db0YHdQ0vygLtuLKG/z3I1WEOUc +CST7N+sY/h02rE5jxKTAjQZiSL2yQRyTB/s3LP68DzNUlojKa1795GJwIUVgXGH5 +C1CHLEXsPCwHn9ciARYpFed3+K/I5EyUhVzzcIcIzivK9eAbT4jta5kCAwEAAaAA +MA0GCSqGSIb3DQEBCwUAA4IBAQBMlRtzNp//q2kFXLYaQbWv6q87Z32HMp6Jq814 +TP0lz2CYZQ2KXtk+AHc+cKU8XHvVRFfydd15xikWM8z4Sn4LgXLD9UvKswFeTnQV +gHkFC41Cp5H5K5shB0dNtIoKh/Lip7EIH2EyH/6ocgqrymqd2VoO3nIE5ysyzGrW ++BVaZtmXVbyWDhQK10TmPe4GJN22VPzkvlPlqDRQC70IfJoFkZAdd085FT+sq7ny +X7D73jBfYgfd9Z9h0t+vKN5NWWN5OI3IZADkMOYhest7RgZTFX2NBqHbigCJaUk8 +f/KDImyMWk+vaDGrkUoOfKKXvdRSUL1mQDgt/FmPq9EdZt4j +-----END CERTIFICATE REQUEST----- diff --git a/tests/fixtures/test-third-der.csr b/tests/fixtures/test-third-der.csr new file mode 100644 index 0000000..6a42474 Binary files /dev/null and b/tests/fixtures/test-third-der.csr differ diff --git a/tests/fixtures/test-third.csr b/tests/fixtures/test-third.csr new file mode 100644 index 0000000..47b07e5 --- /dev/null +++ b/tests/fixtures/test-third.csr @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDITCCAgkCAQAwgbIxCzAJBgNVBAYTAlVTMRYwFAYDVQQIEw1NYXNzYWNodXNl +dHRzMRAwDgYDVQQHEwdOZXdidXJ5MR4wHAYDVQQKExVDb2RleCBOb24gU3VmZmlj +aXQgTEMxJTAjBgNVBAsTHFRlc3QgVGhpcmQtTGV2ZWwgQ2VydGlmaWNhdGUxEjAQ +BgNVBAMTCVdpbGwgQm9uZDEeMBwGCSqGSIb3DQEJARYPd2lsbEBjb2RleG5zLmlv +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwAos1V0CURRsF4Ap9LPd +iZ3vy23qXaOMR3PhAyoWRgNJuXmkwHRE/7JEodVKv6Av30XfABcegxvzVsVUi0t3 +LHe8rWorYWEy+a7DzI1TLejhojAT816hunh5YtY1bB2qY/I2m9zyDwd1WyeT3u71 +lQaiIcRuW8tny6KO9vErmFsKQoeM6EcINPl6C08o7IWo5YnVbMg0gJF8R4iNkzRy +4tRZUmbNv1AnwKY3g7pHWtgrXfV4hnoVaUlOnHWfsy/KBM6YOLJUTQ0x8kVPN2Nr +yRDGLB3Ko30j058epF5+HxMJAG4CC67A9TIC71V7jl4OrN95auCjKKdnQpeKqsfk +NQIDAQABoCkwJwYJKoZIhvcNAQkOMRowGDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIF +4DANBgkqhkiG9w0BAQsFAAOCAQEAdg+HQ3MVFJHnY3Lb+5vSXaYA4F1rXTSZ1jJg +P+TACsuqZ0PaFsg+6OCec78F3yDvntXu3KfUwvshP6PmkIaVjahdLEp0ng9nwtWq +d0DMoUFKeEYhr8T8Z+dbCsZfvUyk8KNMsWuPncCWPQduz5i0g/5vZ9G5na/cmqtJ +GmSY4XLkb3StXJ6sk+uEhlPCuCfL9Dq0r87COTFj4pC7x2+PrDdLu5YJXdTR8mmn +GA44HDNW6XX8t5A5YC5iFnFIgQO2z+B9X2UETajoMxL+HhyE+AopU/n1Wxm1c4oW +1/q3lvrYLiZ+XX76QbIY/4IgAlry7B1eyxHe732lroEvKgezjA== +-----END CERTIFICATE REQUEST----- diff --git a/tests/test_csr.py b/tests/test_csr.py new file mode 100644 index 0000000..932cab9 --- /dev/null +++ b/tests/test_csr.py @@ -0,0 +1,140 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import sys +import os +from collections import OrderedDict + +from asn1crypto import csr + +if sys.version_info < (3,): + byte_cls = str + num_cls = long #pylint: disable=E0602 +else: + byte_cls = bytes + num_cls = int + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + +class CSRTests(unittest.TestCase): + + def test_parse_csr(self): + with open(os.path.join(fixtures_dir, 'test-inter-der.csr'), 'rb') as f: + certification_request = csr.CertificationRequest.load(f.read()) + + cri = certification_request['certification_request_info'] + + self.assertEqual( + 'v1', + cri['version'].native + ) + + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing Intermediate'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + cri['subject'].native + ) + self.assertEqual( + OrderedDict([ + ('algorithm', 'rsa'), + ('parameters', None), + ]), + cri['subject_pk_info']['algorithm'].native + ) + self.assertEqual( + 24141757533938720807477509823483015516687050697622322097001928034085434547050399731881871694642845241206788286795830006142635608141713689209738431462004600429798152826994774062467402648660593454536565119527837471261495586474194846971065722669734666949739228862107500673350843489920495869942508240779131331715037662761414997889327943217889802893638175792326783316531272170879284118280173511200768884738639370318760377047837471530387161553030663446359575963736475504659902898072137674205021477968813148345198711103071746476009234601299344030395455052526948041544669303473529511160643491569274897838845918784633403435929, + cri['subject_pk_info']['public_key'].parsed['modulus'].native + ) + self.assertEqual( + 65537, + cri['subject_pk_info']['public_key'].parsed['public_exponent'].native + ) + self.assertEqual( + [], + cri['attributes'].native + ) + + + def test_parse_csr2(self): + with open(os.path.join(fixtures_dir, 'test-third-der.csr'), 'rb') as f: + certification_request = csr.CertificationRequest.load(f.read()) + + cri = certification_request['certification_request_info'] + + self.assertEqual( + 'v1', + cri['version'].native + ) + + self.assertEqual( + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Test Third-Level Certificate'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io'), + ]), + cri['subject'].native + ) + self.assertEqual( + OrderedDict([ + ('algorithm', 'rsa'), + ('parameters', None), + ]), + cri['subject_pk_info']['algorithm'].native + ) + self.assertEqual( + 24242772097421005542208203320016703216069397492249392798445262959177221203301502279838173203064357049006693856302147277901773700963054800321566171864477088538775137040886151390015408166478059887940234405152693144166884492162723776487601158833605063151869850475289834250129252480954724818505034734280077580919995584375189497366089269712298471489896645221362055822887892887126082288043106492130176555423739906252380437817155678204772878611148787130925042126257401487070141904017757131876614711613405231164930930771261221451019736883391322299033324412671768599041417705072563016759224152503535867541947310239343903761461, + cri['subject_pk_info']['public_key'].parsed['modulus'].native + ) + self.assertEqual( + 65537, + cri['subject_pk_info']['public_key'].parsed['public_exponent'].native + ) + self.assertEqual( + [ + OrderedDict([ + ('type', 'extension_request'), + ( + 'values', + [ + [ + OrderedDict([ + ('extn_id', 'basic_constraints'), + ('critical', False), + ( + 'extn_value', + OrderedDict([ + ('ca', False), + ('path_len_constraint', None), + ]) + ), + ]), + OrderedDict([ + ('extn_id', 'key_usage'), + ('critical', False), + ( + 'extn_value', + (1, 1, 1), + ), + ]) + ] + ] + ), + ]), + ], + cri['attributes'].native + ) -- cgit v1.2.3 From d7837d000e632198db6edcf3fd64eb7802c8f3d2 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 17:42:48 -0400 Subject: Changed BitString to use item access instead of attributes for named bits --- asn1crypto/core.py | 29 +++++++++++++------- docs/tutorial.md | 78 ++++++++++++++++++++++++++++-------------------------- tests/test_csr.py | 12 ++++++++- 3 files changed, 71 insertions(+), 48 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 43fb21a..d737f1f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1000,9 +1000,10 @@ class BitString(Primitive, ValueMap, object): if self.trailer != b'': self.trailer = b'' - def __getattr__(self, key): + def __getitem__(self, key): """ - Retrieves one of the bits based on a name from the _map + Retrieves a boolean version of one of the bits based on a name from the + _map :param key: The unicode string of one of the bit names @@ -1011,7 +1012,7 @@ class BitString(Primitive, ValueMap, object): ValueError - when _map is not set or the key name is invalid :return: - A 1 or a 0 + A boolean if the bit is set """ if not isinstance(self._map, dict): @@ -1023,9 +1024,9 @@ class BitString(Primitive, ValueMap, object): if self._native is None: _ = self.native - return self._native[self._reverse_map[key]] + return self._native[key] - def __setattr__(self, key, value): + def __setitem__(self, key, value): """ Sets one of the bits based on a name from the _map @@ -1033,7 +1034,7 @@ class BitString(Primitive, ValueMap, object): The unicode string of one of the bit names :param value: - A 1 or a 0 + A boolean value :raises: ValueError - when _map is not set or the key name is invalid @@ -1045,7 +1046,7 @@ class BitString(Primitive, ValueMap, object): if self._native is None: _ = self.native - self._native[self._reverse_map[key]] = 1 if value else 0 + self._native[key] = bool(value) self.set(self._native) @property @@ -1054,7 +1055,8 @@ class BitString(Primitive, ValueMap, object): The a native Python datatype representation of this value :return: - The tuple of integers 1 and 0, or None + If a _map is set, an OrdredDict of names as keys and boolean values + or if no _map is set, a tuple of integers 1 and 0. None if no value. """ # For BitString we default the value to be all zeros @@ -1067,7 +1069,16 @@ class BitString(Primitive, ValueMap, object): bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) if extra_bits > 0: bit_string = bit_string[0:0-extra_bits] - self._native = tuple(map(int, tuple(bit_string))) + bits = tuple(map(int, tuple(bit_string))) + if self._map: + self._native = OrderedDict() + for i, bit in enumerate(bits): + self._native[self._map.get(i, i)] = bool(bit) + for i, name in self._map.items(): + if name not in self._native: + self._native[name] = False + else: + self._native = bits return self._native diff --git a/docs/tutorial.md b/docs/tutorial.md index b223b16..b29d14f 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -13,38 +13,38 @@ For general purpose ASN.1 parsing, the `asn1crypto.core` module is used. It contains the following classes, that parse, represent and serialize all of the ASN.1 universal types: -| Class | Native Type | Implementation Notes | -| ------------------ | ------------------- | ------------------------------- | -| `Boolean` | `bool` | | -| `Integer` | `int` | may be `long` on Python 2 | -| `BitString` | `tuple` of `int` | | -| `OctetString` | `bytes` (`str`) | | -| `Null` | `None` | | -| `ObjectIdentifier` | `str` (`unicode`) | string is dotted integer format | -| `ObjectDescriptor` | | no native conversion | -| `InstanceOf` | | no native conversion | -| `Real` | | no native conversion | -| `Enumerated` | `str` (`unicode`) | `_map` must be set | -| `UTF8String` | `str` (`unicode`) | | -| `RelativeOid` | `str` (`unicode`) | string is dotted integer format | -| `Sequence` | `OrderedDict` | | -| `SequenceOf` | `list` | | -| `Set` | `OrderedDict` | | -| `SetOf` | `list` | | -| `EmbeddedPdv` | `OrderedDict` | no named field parsing | -| `NumericString` | `str` (`unicode`) | no charset limitations | -| `PrintableString` | `str` (`unicode`) | no charset limitations | -| `TeletexString` | `str` (`unicode`) | | -| `VideotexString` | `bytes` (`str`) | no unicode conversion | -| `IA5String` | `str` (`unicode`) | | -| `UTCTime` | `datetime.datetime` | | -| `GeneralizedTime` | `datetime.datetime` | treated as UTC when no timezone | -| `GraphicString` | `str` (`unicode`) | unicode conversion as latin1 | -| `VisibleString` | `str` (`unicode`) | no charset limitations | -| `GeneralString` | `str` (`unicode`) | unicode conversion as latin1 | -| `UniversalString` | `str` (`unicode`) | | -| `CharacterString` | `str` (`unicode`) | unicode conversion as latin1 | -| `BMPString` | `str` (`unicode`) | | +| Class | Native Type | Implementation Notes | +| ------------------ | ------------------------------------ | ------------------------------------ | +| `Boolean` | `bool` | | +| `Integer` | `int` | may be `long` on Python 2 | +| `BitString` | `tuple` of `int` or `OrderedDict` | `OrderedDict` used if `_map` present | +| `OctetString` | `bytes` (`str`) | | +| `Null` | `None` | | +| `ObjectIdentifier` | `str` (`unicode`) | string is dotted integer format | +| `ObjectDescriptor` | | no native conversion | +| `InstanceOf` | | no native conversion | +| `Real` | | no native conversion | +| `Enumerated` | `str` (`unicode`) | `_map` must be set | +| `UTF8String` | `str` (`unicode`) | | +| `RelativeOid` | `str` (`unicode`) | string is dotted integer format | +| `Sequence` | `OrderedDict` | | +| `SequenceOf` | `list` | | +| `Set` | `OrderedDict` | | +| `SetOf` | `list` | | +| `EmbeddedPdv` | `OrderedDict` | no named field parsing | +| `NumericString` | `str` (`unicode`) | no charset limitations | +| `PrintableString` | `str` (`unicode`) | no charset limitations | +| `TeletexString` | `str` (`unicode`) | | +| `VideotexString` | `bytes` (`str`) | no unicode conversion | +| `IA5String` | `str` (`unicode`) | | +| `UTCTime` | `datetime.datetime` | | +| `GeneralizedTime` | `datetime.datetime` | treated as UTC when no timezone | +| `GraphicString` | `str` (`unicode`) | unicode conversion as latin1 | +| `VisibleString` | `str` (`unicode`) | no charset limitations | +| `GeneralString` | `str` (`unicode`) | unicode conversion as latin1 | +| `UniversalString` | `str` (`unicode`) | | +| `CharacterString` | `str` (`unicode`) | unicode conversion as latin1 | +| `BMPString` | `str` (`unicode`) | | For *Native Type*, the Python 3 type is listed first, with the Python 2 type in parentheses. @@ -284,9 +284,9 @@ print(MyType('1.8.2.1.25').native) ## BitString -The `BitString` class has a native representation as a `tuple` of `int`s (being -either `1` or `0`). In addition, it is possible to set the value of a -`BitString` by passing an integer. +When no `_map` is set for a `BitString` class, the native representation is a +`tuple` of `int`s (being either `1` or `0`). In addition, it is possible to set +the value of a `BitString` by passing an integer. ```python from asn1crypto.core import BitString @@ -298,7 +298,9 @@ b2 = BitString(5) Additionally, it is possible to set the `_map` property to a dict where the keys are bit indexes and the values are unicode string names. This allows -checking the value of a given bit by a property of the name specified. +checking the value of a given bit by item access, and the native representation +becomes an `OrderedDict` where keys are the unicode strings and values are +either `True` or `False`. ```python from asn1crypto.core import BitString @@ -313,11 +315,11 @@ class MyFlags(BitString): permissions = MyFlags(3) # This will be printed -if permissions.edit and permissions.delete: +if permissions['edit'] and permissions['delete']: print('Can edit and delete') # This will not -if permissions.manage_users: +if permissions['manage_users']: print('Is admin') ``` diff --git a/tests/test_csr.py b/tests/test_csr.py index 932cab9..d2d4a70 100644 --- a/tests/test_csr.py +++ b/tests/test_csr.py @@ -128,7 +128,17 @@ class CSRTests(unittest.TestCase): ('critical', False), ( 'extn_value', - (1, 1, 1), + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', True), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]), ), ]) ] -- cgit v1.2.3 From 8bb77d01e61afbbb0f8bec7a6643624e4316ec8c Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 17:44:29 -0400 Subject: Add convenience attributes to x509.Certificate for accessing extensions used in path validation --- asn1crypto/x509.py | 189 ++++++++++++++++++++- .../Equifax_Secure_Certificate_Authority.crt | Bin 0 -> 804 bytes .../Equifax_Secure_Certificate_Authority.pem | 19 +++ .../geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.cer | 26 +++ .../geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt | Bin 0 -> 1138 bytes .../geotrust_certs/GeoTrust_Primary_CA.crt | Bin 0 -> 896 bytes .../geotrust_certs/GeoTrust_Primary_CA.pem | 21 +++ .../geotrust_certs/GeoTrust_Universal_CA.crt | Bin 0 -> 1388 bytes .../geotrust_certs/GeoTrust_Universal_CA.pem | 31 ++++ tests/fixtures/keys/test-inter-der.crt | Bin 0 -> 1032 bytes tests/fixtures/keys/test-third-der.crt | Bin 0 -> 1023 bytes tests/test_x509.py | 189 +++++++++++++++++++++ 12 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.crt create mode 100644 tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.pem create mode 100644 tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.cer create mode 100644 tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt create mode 100644 tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.crt create mode 100644 tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.pem create mode 100644 tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.crt create mode 100644 tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.pem create mode 100644 tests/fixtures/keys/test-inter-der.crt create mode 100644 tests/fixtures/keys/test-third-der.crt diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index c571229..0dc0257 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -581,9 +581,15 @@ class PolicyQualifierInfos(SequenceOf): _child_spec = PolicyQualifierInfo +class PolicyIdentifier(ObjectIdentifier): + _map = { + '2.5.29.32.0': 'any_policy', + } + + class PolicyInformation(Sequence): _fields = [ - ('policy_identifier', ObjectIdentifier), + ('policy_identifier', PolicyIdentifier), ('policy_qualifiers', PolicyQualifierInfos, {'optional': True}) ] @@ -733,3 +739,184 @@ class Certificate(Sequence): ('signature_algorithm', SignedDigestAlgorithm), ('signature_value', OctetBitString), ] + + _processed_extensions = False + _critical_extensions = None + _key_identifier_value = None + _key_usage_value = None + _subject_alt_name_value = None + _basic_constraints_value = None + _name_constraints_value = None + _crl_distribution_points_value = None + _certificate_policies_value = None + _policy_mappings_value = None + _authority_key_identifier_value = None + _policy_constraints_value = None + _extended_key_usage_value = None + _ocsp_no_check_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = [] + + for extension in self['tbs_certificate']['extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.append(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a list of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A list of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def key_identifier_value(self): + """ + :return: + None or the parsed value of the key identifier extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._key_identifier_value + + @property + def key_usage_value(self): + """ + :return: + None or the parsed value of the key usage extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._key_usage_value + + @property + def subject_alt_name_value(self): + """ + :return: + None or the parsed value of the subject alt name extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._subject_alt_name_value + + @property + def basic_constraints_value(self): + """ + :return: + None or the parsed value of the basic constraints extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._basic_constraints_value + + @property + def name_constraints_value(self): + """ + :return: + None or the parsed value of the name constraints extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._name_constraints_value + + @property + def crl_distribution_points_value(self): + """ + :return: + None or the parsed value of the CRL distribution points + extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._crl_distribution_points_value + + @property + def certificate_policies_value(self): + """ + :return: + None or the parsed value of the certificate policies extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._certificate_policies_value + + @property + def policy_mappings_value(self): + """ + :return: + None or the parsed value of the policy mappings extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._policy_mappings_value + + @property + def authority_key_identifier_value(self): + """ + :return: + None or the parsed value of the authority key identifier + extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._authority_key_identifier_value + + @property + def policy_constraints_value(self): + """ + :return: + None or the parsed value of the policy constraints extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._policy_constraints_value + + @property + def extended_key_usage_value(self): + """ + :return: + None or the parsed value of the extended key usage extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._extended_key_usage_value + + @property + def ocsp_no_check_value(self): + """ + :return: + None or the parsed value of the OCSP no check extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._ocsp_no_check_value diff --git a/tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.crt b/tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.crt new file mode 100644 index 0000000..c44db27 Binary files /dev/null and b/tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.crt differ diff --git a/tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.pem b/tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.pem new file mode 100644 index 0000000..7a36225 --- /dev/null +++ b/tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.cer b/tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.cer new file mode 100644 index 0000000..078ee1b --- /dev/null +++ b/tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.cer @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEbjCCA1agAwIBAgIQboqQ68/wRIpyDQgF0IKlRDANBgkqhkiG9w0BAQsFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xMzEw +MzEwMDAwMDBaFw0yMzEwMzAyMzU5NTlaMEcxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMSAwHgYDVQQDExdHZW9UcnVzdCBFViBTU0wgQ0EgLSBH +NDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANm0BfI4Zw8J53z1Yyrl +uV6oEa51cdlMhGetiV38KD0qsKXV1OYwCoTU5BjLhTfFRnHrHHtp22VpjDAFPgfh +bzzBC2HmOET8vIwvTnVX9ZaZfD6HHw+QS3DDPzlFOzpry7t7QFTRi0uhctIE6eBy +GpMRei/xq52cmFiuLOp3Xy8uh6+4a+Pi4j/WPeCWRN8RVWNSL/QmeMQPIE0KwGhw +FYY47rd2iKsYj081HtSMydt+PUTUNozBN7VZW4f56fHUxSi9HdzMlnLReqGnILW4 +r/hupWB7K40f7vQr1mnNr8qAWCnoTAAgikkKbo6MqNEAEoS2xeKVosA7pGvwgtCW +XSUCAwEAAaOCAUMwggE/MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQD +AgEGMC8GCCsGAQUFBwEBBCMwITAfBggrBgEFBQcwAYYTaHR0cDovL2cyLnN5bWNi +LmNvbTBHBgNVHSAEQDA+MDwGBFUdIAAwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93 +d3cuZ2VvdHJ1c3QuY29tL3Jlc291cmNlcy9jcHMwNAYDVR0fBC0wKzApoCegJYYj +aHR0cDovL2cxLnN5bWNiLmNvbS9HZW9UcnVzdFBDQS5jcmwwKQYDVR0RBCIwIKQe +MBwxGjAYBgNVBAMTEVN5bWFudGVjUEtJLTEtNTM4MB0GA1UdDgQWBBTez1xQt64C +HxUXqhboDbUonWpa8zAfBgNVHSMEGDAWgBQs1VBBlxWL8I82YVtK+2vZmckzkjAN +BgkqhkiG9w0BAQsFAAOCAQEAtI69B7mahew7Z70HYGHmhNHU7+sbuguCS5VktmZT +I723hN3ke40J2s+y9fHDv4eEvk6mqMLnEjkoNOCkVkRADJ+IoxXT6NNe4xwEYPtp +Nk9qfgwqKMHzqlgObM4dB8NKwJyNw3SxroLwGuH5Tim9Rt63Hfl929kPhMuSRcwc +sxj2oM9xbwwum9Its5mTg0SsFaqbLmfsT4hpBVZ7i7JDqTpsHBMzJRv9qMhXAvsc +4NG9O1ZEZcNj9Rvv7DDZ424uE+k5CCoMcvOazPYnKYTT70zHhBFlH8bjgQPbh8x4 +97Wdlj5qf7wRhXp15kF9Dc/55YVpJY/HjQct+GkPy0FTAA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt b/tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt new file mode 100644 index 0000000..c18ef50 Binary files /dev/null and b/tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt differ diff --git a/tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.crt b/tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.crt new file mode 100644 index 0000000..3a1ea37 Binary files /dev/null and b/tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.crt differ diff --git a/tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.pem b/tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.pem new file mode 100644 index 0000000..6635993 --- /dev/null +++ b/tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo +R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx +MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK +Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 +AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA +ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 +7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W +kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI +mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ +KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 +6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl +4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K +oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj +UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU +AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.crt b/tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.crt new file mode 100644 index 0000000..3e67770 Binary files /dev/null and b/tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.crt differ diff --git a/tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.pem b/tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.pem new file mode 100644 index 0000000..6534f82 --- /dev/null +++ b/tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW +MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy +c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 +IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV +VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 +cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT +QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh +F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v +c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w +mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd +VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX +teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ +f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe +Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ +nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY +MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc +aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX +IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn +ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z +uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN +Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja +QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW +koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 +ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt +DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm +bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/keys/test-inter-der.crt b/tests/fixtures/keys/test-inter-der.crt new file mode 100644 index 0000000..1aa7f40 Binary files /dev/null and b/tests/fixtures/keys/test-inter-der.crt differ diff --git a/tests/fixtures/keys/test-third-der.crt b/tests/fixtures/keys/test-third-der.crt new file mode 100644 index 0000000..8ecaa5b Binary files /dev/null and b/tests/fixtures/keys/test-third-der.crt differ diff --git a/tests/test_x509.py b/tests/test_x509.py index ab6c55e..a530cf5 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -22,6 +22,195 @@ fixtures_dir = os.path.join(tests_root, 'fixtures') class X509Tests(unittest.TestCase): + def test_extensions(self): + with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + self.assertEqual([], cert.critical_extensions) + self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.key_identifier_value.native) + self.assertEqual(None, cert.key_usage_value) + self.assertEqual(None, cert.subject_alt_name_value) + self.assertEqual(True, cert.basic_constraints_value['ca'].native) + self.assertEqual(None, cert.basic_constraints_value['path_len_constraint'].native) + self.assertEqual(None, cert.name_constraints_value) + self.assertEqual(None, cert.crl_distribution_points_value) + self.assertEqual(None, cert.certificate_policies_value) + self.assertEqual(None, cert.policy_mappings_value) + self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.authority_key_identifier_value['key_identifier'].native) + self.assertEqual(None, cert.policy_constraints_value) + self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.ocsp_no_check_value) + + def test_extensions2(self): + with open(os.path.join(fixtures_dir, 'keys/test-inter-der.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + self.assertEqual([], cert.critical_extensions) + self.assertEqual(b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02', cert.key_identifier_value.native) + self.assertEqual(None, cert.key_usage_value) + self.assertEqual(None, cert.subject_alt_name_value) + self.assertEqual(True, cert.basic_constraints_value['ca'].native) + self.assertEqual(None, cert.basic_constraints_value['path_len_constraint'].native) + self.assertEqual(None, cert.name_constraints_value) + self.assertEqual(None, cert.crl_distribution_points_value) + self.assertEqual(None, cert.certificate_policies_value) + self.assertEqual(None, cert.policy_mappings_value) + self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.authority_key_identifier_value['key_identifier'].native) + self.assertEqual(None, cert.policy_constraints_value) + self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.ocsp_no_check_value) + + def test_extensions3(self): + with open(os.path.join(fixtures_dir, 'keys/test-third-der.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + self.assertEqual([], cert.critical_extensions) + self.assertEqual(b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r', cert.key_identifier_value.native) + self.assertEqual(None, cert.key_usage_value) + self.assertEqual(None, cert.subject_alt_name_value) + self.assertEqual(None, cert.basic_constraints_value) + self.assertEqual(None, cert.name_constraints_value) + self.assertEqual(None, cert.crl_distribution_points_value) + self.assertEqual(None, cert.certificate_policies_value) + self.assertEqual(None, cert.policy_mappings_value) + self.assertEqual(b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02', cert.authority_key_identifier_value['key_identifier'].native) + self.assertEqual(None, cert.policy_constraints_value) + self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.ocsp_no_check_value) + + def test_extensions4(self): + with open(os.path.join(fixtures_dir, 'geotrust_certs/GeoTrust_Universal_CA.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + self.assertEqual(['basic_constraints', 'key_usage'], cert.critical_extensions) + self.assertEqual(b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6', cert.key_identifier_value.native) + self.assertEqual( + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', True), + ('crl_sign', True), + ('encipher_only', False), + ('decipher_only', False), + ]), + cert.key_usage_value.native + ) + self.assertEqual(None, cert.subject_alt_name_value) + self.assertEqual( + OrderedDict([ + ('ca', True), + ('path_len_constraint', None), + ]), + cert.basic_constraints_value.native + ) + self.assertEqual(None, cert.name_constraints_value) + self.assertEqual(None, cert.crl_distribution_points_value) + self.assertEqual(None, cert.certificate_policies_value) + self.assertEqual(None, cert.policy_mappings_value) + self.assertEqual(b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6', cert.authority_key_identifier_value['key_identifier'].native) + self.assertEqual(None, cert.policy_constraints_value) + self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.ocsp_no_check_value) + + def test_extensions5(self): + with open(os.path.join(fixtures_dir, 'geotrust_certs/GeoTrust_Primary_CA.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + self.assertEqual(['basic_constraints', 'key_usage'], cert.critical_extensions) + self.assertEqual(b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92', cert.key_identifier_value.native) + self.assertEqual( + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]), + cert.key_usage_value.native + ) + self.assertEqual(None, cert.subject_alt_name_value) + self.assertEqual(True, cert.basic_constraints_value['ca'].native) + self.assertEqual(None, cert.basic_constraints_value['path_len_constraint'].native) + self.assertEqual(None, cert.name_constraints_value) + self.assertEqual(None, cert.crl_distribution_points_value) + self.assertEqual(None, cert.certificate_policies_value) + self.assertEqual(None, cert.policy_mappings_value) + self.assertEqual(None, cert.authority_key_identifier_value) + self.assertEqual(None, cert.policy_constraints_value) + self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.ocsp_no_check_value) + + def test_extensions6(self): + with open(os.path.join(fixtures_dir, 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt'), 'rb') as f: + cert = x509.Certificate.load(f.read()) + + self.assertEqual(['basic_constraints', 'key_usage'], cert.critical_extensions) + self.assertEqual(b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3', cert.key_identifier_value.native) + self.assertEqual( + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]), + cert.key_usage_value.native + ) + self.assertEqual( + [ + OrderedDict([ + ('common_name', 'SymantecPKI-1-538') + ]) + ], + cert.subject_alt_name_value.native + ) + self.assertEqual(True, cert.basic_constraints_value['ca'].native) + self.assertEqual(0, cert.basic_constraints_value['path_len_constraint'].native) + self.assertEqual(None, cert.name_constraints_value) + self.assertEqual( + [ + OrderedDict([ + ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ], + cert.crl_distribution_points_value.native + ) + self.assertEqual( + [ + OrderedDict([ + ('policy_identifier', 'any_policy'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'https://www.geotrust.com/resources/cps') + ]) + ] + ) + ]) + ], + cert.certificate_policies_value.native + ) + self.assertEqual(None, cert.policy_mappings_value) + self.assertEqual(b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92', cert.authority_key_identifier_value['key_identifier'].native) + self.assertEqual(None, cert.policy_constraints_value) + self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.ocsp_no_check_value) + def test_parse_certificate(self): with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: cert = x509.Certificate.load(f.read()) -- cgit v1.2.3 From a43b3fb3cb180da29b8af02a4ddc1d8f5460fe39 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 17:52:25 -0400 Subject: Add detection of the inhibit anyPolicy extension --- asn1crypto/x509.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 0dc0257..a6b7868 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -671,6 +671,7 @@ class ExtensionId(ObjectIdentifier): '2.5.29.35': 'authority_key_identifier', '2.5.29.36': 'policy_constraints', '2.5.29.37': 'extended_key_usage', + '2.5.29.54': 'inhibit_any_policy', '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', '1.2.840.113533.7.65.0': 'entrust_version_extension', '2.16.840.1.113730.1.1': 'netscape_certificate_type', @@ -700,6 +701,7 @@ class Extension(Sequence): 'authority_key_identifier': AuthorityKeyIdentifier, 'policy_constraints': PolicyConstraints, 'extended_key_usage': ExtKeyUsageSyntax, + 'inhibit_any_policy': Integer, 'ocsp_no_check': Null, 'entrust_version_extension': EntrustVersionInfo, 'netscape_certificate_type': NetscapeCertificateType, -- cgit v1.2.3 From a0d4548e73de9be16be02854f3fa42b5eecbd066 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 22:10:20 -0400 Subject: Added authority info access syntax extension to Certificate in addition to CRL --- asn1crypto/crl.py | 20 +------------------- asn1crypto/x509.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index e90ffd2..63c376c 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -24,10 +24,10 @@ from .core import ( SequenceOf, ) from .x509 import ( + AuthorityInfoAccessSyntax, AuthorityKeyIdentifier, CRLDistributionPoints, DistributionPointName, - GeneralName, GeneralNames, Name, ReasonFlags, @@ -58,24 +58,6 @@ class IssuingDistributionPoint(Sequence): ] -class AccessMethod(ObjectIdentifier): - _map = { - '1.3.6.1.5.5.7.48.1': 'ocsp', - '1.3.6.1.5.5.7.48.2': 'ca_issuers', - } - - -class AccessDescription(Sequence): - _fields = [ - ('access_method', AccessMethod), - ('access_location', GeneralName), - ] - - -class AuthorityInfoAccessSyntax(SequenceOf): - _child_spec = AccessDescription - - class TBSCertListExtensionId(ObjectIdentifier): _map = { '2.5.29.18': 'issuer_alt_name', diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index a6b7868..e932d0c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -635,6 +635,24 @@ class ExtKeyUsageSyntax(SequenceOf): _child_spec = KeyPurposeId +class AccessMethod(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1': 'ocsp', + '1.3.6.1.5.5.7.48.2': 'ca_issuers', + } + + +class AccessDescription(Sequence): + _fields = [ + ('access_method', AccessMethod), + ('access_location', GeneralName), + ] + + +class AuthorityInfoAccessSyntax(SequenceOf): + _child_spec = AccessDescription + + class EntrustVersionInfo(Sequence): _fields = [ ('entrust_vers', GeneralString), @@ -672,6 +690,7 @@ class ExtensionId(ObjectIdentifier): '2.5.29.36': 'policy_constraints', '2.5.29.37': 'extended_key_usage', '2.5.29.54': 'inhibit_any_policy', + '1.3.6.1.5.5.7.1.1': 'authority_information_access', '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', '1.2.840.113533.7.65.0': 'entrust_version_extension', '2.16.840.1.113730.1.1': 'netscape_certificate_type', @@ -702,6 +721,7 @@ class Extension(Sequence): 'policy_constraints': PolicyConstraints, 'extended_key_usage': ExtKeyUsageSyntax, 'inhibit_any_policy': Integer, + 'authority_information_access': AuthorityInfoAccessSyntax, 'ocsp_no_check': Null, 'entrust_version_extension': EntrustVersionInfo, 'netscape_certificate_type': NetscapeCertificateType, -- cgit v1.2.3 From 08c60fa10e6b604c1be9ab4227d338897cedbf68 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 13 Jul 2015 23:02:13 -0400 Subject: Add convenience property and tests for authority information access value on Certificate objects --- asn1crypto/x509.py | 12 ++++++++++++ tests/test_x509.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index e932d0c..951e19f 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -775,6 +775,7 @@ class Certificate(Sequence): _authority_key_identifier_value = None _policy_constraints_value = None _extended_key_usage_value = None + _authority_information_access_value = None _ocsp_no_check_value = None def _set_extensions(self): @@ -932,6 +933,17 @@ class Certificate(Sequence): self._set_extensions() return self._extended_key_usage_value + @property + def authority_information_access_value(self): + """ + :return: + None or the parsed value of the authority information access extension + """ + + if not self._processed_extensions: + self._set_extensions() + return self._authority_information_access_value + @property def ocsp_no_check_value(self): """ diff --git a/tests/test_x509.py b/tests/test_x509.py index a530cf5..a1868b8 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -39,6 +39,7 @@ class X509Tests(unittest.TestCase): self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.authority_key_identifier_value['key_identifier'].native) self.assertEqual(None, cert.policy_constraints_value) self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.authority_information_access_value) self.assertEqual(None, cert.ocsp_no_check_value) def test_extensions2(self): @@ -58,6 +59,7 @@ class X509Tests(unittest.TestCase): self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.authority_key_identifier_value['key_identifier'].native) self.assertEqual(None, cert.policy_constraints_value) self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.authority_information_access_value) self.assertEqual(None, cert.ocsp_no_check_value) def test_extensions3(self): @@ -76,6 +78,7 @@ class X509Tests(unittest.TestCase): self.assertEqual(b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02', cert.authority_key_identifier_value['key_identifier'].native) self.assertEqual(None, cert.policy_constraints_value) self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.authority_information_access_value) self.assertEqual(None, cert.ocsp_no_check_value) def test_extensions4(self): @@ -113,6 +116,7 @@ class X509Tests(unittest.TestCase): self.assertEqual(b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6', cert.authority_key_identifier_value['key_identifier'].native) self.assertEqual(None, cert.policy_constraints_value) self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.authority_information_access_value) self.assertEqual(None, cert.ocsp_no_check_value) def test_extensions5(self): @@ -145,6 +149,7 @@ class X509Tests(unittest.TestCase): self.assertEqual(None, cert.authority_key_identifier_value) self.assertEqual(None, cert.policy_constraints_value) self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual(None, cert.authority_information_access_value) self.assertEqual(None, cert.ocsp_no_check_value) def test_extensions6(self): @@ -209,6 +214,15 @@ class X509Tests(unittest.TestCase): self.assertEqual(b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92', cert.authority_key_identifier_value['key_identifier'].native) self.assertEqual(None, cert.policy_constraints_value) self.assertEqual(None, cert.extended_key_usage_value) + self.assertEqual( + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://g2.symcb.com') + ]) + ], + cert.authority_information_access_value.native + ) self.assertEqual(None, cert.ocsp_no_check_value) def test_parse_certificate(self): -- cgit v1.2.3 From d90cf5b81cf412c53287041446c61c36da810861 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 15 Jul 2015 13:15:03 -0400 Subject: Added PEM functionality --- asn1crypto/pem.py | 195 +++++++++++ dev/tests.py | 3 +- docs/pem.md | 79 +++++ docs/readme.md | 12 +- docs/tutorial.md | 549 ------------------------------- docs/universal_types.md | 550 ++++++++++++++++++++++++++++++++ readme.md | 10 +- tests/fixtures/keys/test-aes128-der.key | Bin 0 -> 1200 bytes tests/fixtures/keys/test-aes128.key | 30 ++ tests/fixtures/keys/test-inter.crt | 24 ++ tests/fixtures/keys/test-third.crt | 24 ++ tests/test_pem.py | 77 +++++ 12 files changed, 994 insertions(+), 559 deletions(-) create mode 100644 asn1crypto/pem.py create mode 100644 docs/pem.md delete mode 100644 docs/tutorial.md create mode 100644 docs/universal_types.md create mode 100644 tests/fixtures/keys/test-aes128-der.key create mode 100644 tests/fixtures/keys/test-aes128.key create mode 100644 tests/fixtures/keys/test-inter.crt create mode 100644 tests/fixtures/keys/test-third.crt create mode 100644 tests/test_pem.py diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py new file mode 100644 index 0000000..c812d03 --- /dev/null +++ b/asn1crypto/pem.py @@ -0,0 +1,195 @@ +# coding: utf-8 + +""" +Encoding DER to PEM and decoding PEM to DER +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import base64 +import re + +if sys.version_info < (3,): + str_cls = unicode #pylint: disable=E0602 + byte_cls = str + from cStringIO import StringIO as BytesIO #pylint: disable=F0401 +else: + str_cls = str + byte_cls = bytes + from io import BytesIO + + + +def detect(byte_string): + """ + Detect if a byte string seems to contain a PEM-encoded block + + :param byte_string: + A byte string to look through + + :return: + A boolean, indicating if a PEM-encoded block is contained in the byte + string + """ + + if not isinstance(byte_string, byte_cls): + raise ValueError('byte_string must be a byte string, not %s' % byte_string.__class__.__name__) + + return byte_string.find(b'-----BEGIN') != -1 or byte_string.find(b'---- BEGIN') != -1 + + +def armor(type_name, der_bytes, headers=None): + """ + Armors a DER-encoded byte string in PEM + + :param der_bytes: + A byte string to be armored + + :param type_name: + A unicode string that will be capitalized and placed in the header + and footer of the block. E.g. "CERTIFICATE", "PRIVATE KEY", etc. This + will appear as "-----BEGIN CERTIFICATE-----" and + "-----END CERTIFICATE-----". + + :param headers: + An OrderedDict of the header lines to write after the BEGIN line + + :return: + A byte string of the PEM block + """ + + if not isinstance(der_bytes, byte_cls): + raise ValueError('der_bytes must be a byte string, not %s' % der_bytes.__class__.__name__) + + if not isinstance(type_name, str_cls): + raise ValueError('type_name must be a unicode string, not %s' % type_name.__class__.__name__) + + type_name = type_name.upper().encode('ascii') + + output = BytesIO() + output.write(b'-----BEGIN ') + output.write(type_name) + output.write(b'-----\n') + if headers: + for key in headers: + output.write(key.encode('ascii')) + output.write(b': ') + output.write(headers[key].encode('ascii')) + output.write(b'\n') + output.write(b'\n') + b64_bytes = base64.b64encode(der_bytes) + b64_len = len(b64_bytes) + i = 0 + while i < b64_len: + output.write(b64_bytes[i:i+64]) + output.write(b'\n') + i += 64 + output.write(b'-----END ') + output.write(type_name) + output.write(b'-----\n') + + return output.getvalue() + + +def _unarmor(pem_bytes): + """ + Convert a PEM-encoded byte string into one or more DER-encoded byte strings + + :param pem_bytes: + A byte string of the PEM-encoded data + + :raises: + ValueError - when the pem_bytes do not appear to be PEM-encoded bytes + + :return: + A generator of 3-element tuples in the format: (type_name, headers, + der_bytes). The type_name is a unicode string of what is between + "-----BEGIN " and "-----". Examples include: "CERTIFICATE", + "PUBLIC KEY", "PRIVATE KEY". The headers is a dict containing any lines + in the form "Name: Value" that are right after the begin line. + """ + + if not isinstance(pem_bytes, byte_cls): + raise ValueError('pem_bytes must be a byte string, not %s' % pem_bytes.__class__.__name__) + + # Valid states include: "trash", "headers", "body" + state = 'trash' + headers = {} + base64_data = b'' + type_name = None + + found_start = False + found_end = False + + for line in pem_bytes.splitlines(False): + if line == b'': + continue + + if state == "trash": + # Look for a starting line since some CA cert bundle show the cert + # into in a parsed format above each PEM block + type_name_match = re.match(b'^(?:---- |-----)BEGIN ([A-Z0-9 ]+)(?: ----|-----)', line) + if not type_name_match: + continue + type_name = type_name_match.group(1).decode('ascii') + + found_start = True + state = 'headers' + continue + + if state == 'headers': + if line.find(b':') == -1: + state = 'body' + else: + decoded_line = line.decode('ascii') + name, value = decoded_line.split(':', 1) + headers[name] = value.strip() + continue + + if state == 'body': + if line[0:5] in (b'-----', b'---- '): + der_bytes = base64.b64decode(base64_data) + + yield (type_name, headers, der_bytes) + + state = 'trash' + headers = {} + base64_data = b'' + type_name = None + found_end = True + continue + + base64_data += line + + if not found_start or not found_end: + raise ValueError('pem_bytes does not appear to contain PEM-encoded data - no BEGIN/END combination found') + + +def unarmor(pem_bytes, multiple=False): + """ + Convert a PEM-encoded byte string into a DER-encoded byte string + + :param pem_bytes: + A byte string of the PEM-encoded data + + :param multiple: + If True, function will return a generator + + :raises: + ValueError - when the pem_bytes do not appear to be PEM-encoded bytes + + :return: + A 3-element tuple (type_name, headers, der_bytes). The type_name is a + unicode string of what is between "-----BEGIN " and "-----". Examples + include: "CERTIFICATE", "PUBLIC KEY", "PRIVATE KEY". The headers is a + dict containing any lines in the form "Name: Value" that are right + after the begin line. + """ + + generator = _unarmor(pem_bytes) + + if not multiple: + return next(generator) + + return generator diff --git a/dev/tests.py b/dev/tests.py index 39f5d8b..553e27f 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -9,12 +9,13 @@ from tests.test_crl import CRLTests from tests.test_csr import CSRTests from tests.test_keys import KeysTests from tests.test_ocsp import OCSPTests +from tests.test_pem import PEMTests from tests.test_tsp import TSPTests from tests.test_x509 import X509Tests from tests.test_core import CoreTests -test_classes = [CMSTests, CRLTests, CSRTests, KeysTests, OCSPTests, TSPTests, X509Tests, CoreTests] +test_classes = [CMSTests, CRLTests, CSRTests, KeysTests, OCSPTests, PEMTests, TSPTests, X509Tests, CoreTests] def run(matcher=None): diff --git a/docs/pem.md b/docs/pem.md new file mode 100644 index 0000000..4f4bca7 --- /dev/null +++ b/docs/pem.md @@ -0,0 +1,79 @@ +# PEM Decoder and Encoder + +Often times DER-encoded data is wrapped in PEM encoding. This allows the binary +DER data to be identified and reliably sent over various communication channels. + +The `asn1crypto.pem` module includes three functions: + + - `detect(byte_string)` + - `unarmor(pem_bytes, multiple=False)` + - `armor(type_name, der_bytes, headers=None)` + +## detect() + +The `detect()` function accepts a byte string and looks for a `BEGIN` block +line. This is useful to determine in a byte string needs to be PEM-decoded +before parsing. + +```python +from asn1crypto import pem, x509 + +with open('/path/to/cert', 'rb') as f: + der_bytes = f.read() + if pem.detect(der_bytes): + _, _, der_bytes = pem.unarmor(der_bytes) +``` + +## unarmor() + +The `unarmor()` function accepts a byte string and the flag to indicates if +more than one PEM block may be contained in the byte string. The result is +a three-element tuple. + + - The first element is a unicode string of the type of PEM block. Examples + include: `CERTIFICATE`, `PRIVATE KEY`, `PUBLIC KEY`. + - The second element is a `dict` of PEM block headers. Headers are typically + only used by encrypted OpenSSL private keys, and are in the format + `Name: Value`. + - The third element is a byte string of the decoded block contents. + +```python +from asn1crypto import pem, x509 + +with open('/path/to/cert', 'rb') as f: + der_bytes = f.read() + if pem.detect(der_bytes): + type_name, headers, der_bytes = pem.unarmor(der_bytes) + +cert = x509.Certificate.load(der_bytes) +``` + +If the `multiple` keyword argument is set to `True`, a generator will be +returned. + +```python +from asn1crypto import pem, x509 + +certs = [] +with open('/path/to/ca_certs', 'rb') as f: + for type_name, headers, der_bytes in pem.unarmor(f.read(), multiple=True): + certs.append(x509.Certificate.load(der_bytes)) +``` + +## armor() + +The `armor()` function accepts three parameters: a unicode string of the block +type name, a byte string to encode and an optional keyword argument `headers`, +that should be a `dict` of headers to add after the `BEGIN` line. Headers are +typically only used by encrypted OpenSSL private keys. + +```python +from asn1crypto import pem, x509 + +# cert is an instance of x509.Certificate + +with open('/path/to/cert', 'wb') as f: + der_bytes = cert.dump() + pem_bytes = pem.armor('CERTIFICATE', der_bytes) + f.write(pem_bytes) +``` diff --git a/docs/readme.md b/docs/readme.md index bb8ab62..563d2b9 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,10 +1,12 @@ -# asn1crypto docs +# asn1crypto Documentation -The documentation for *asn1crypto* is composed of a tutorial on basic usage that -covers all of the universal data types, and links to the source for the various -pre-defined data types. +The documentation for *asn1crypto* is composed of tutorials on basic usage and +links to the source for the various pre-defined type classes. - - [Tutorial](tutorial.md) +## Tutorials + + - [Universal Types with BER/DER Decoder and DER Encoder](universal_types.md) + - [PEM Decoder and Encoder](pem.md) ## Reference diff --git a/docs/tutorial.md b/docs/tutorial.md deleted file mode 100644 index b29d14f..0000000 --- a/docs/tutorial.md +++ /dev/null @@ -1,549 +0,0 @@ -# asn1crypto Tutorial - -The *asn1crypto* library is a combination of a general-purpose BER/DER parser -and DER serializer, along with a number of pre-built cryptographic types -definitions. - -For a general overview of ASN.1 as used in cryptography, please see -[A Layman's Guide to a Subset of ASN.1, BER, and DER](http://luca.ntop.org/Teaching/Appunti/asn1.html). - -## Universal Types - -For general purpose ASN.1 parsing, the `asn1crypto.core` module is used. It -contains the following classes, that parse, represent and serialize all of the -ASN.1 universal types: - -| Class | Native Type | Implementation Notes | -| ------------------ | ------------------------------------ | ------------------------------------ | -| `Boolean` | `bool` | | -| `Integer` | `int` | may be `long` on Python 2 | -| `BitString` | `tuple` of `int` or `OrderedDict` | `OrderedDict` used if `_map` present | -| `OctetString` | `bytes` (`str`) | | -| `Null` | `None` | | -| `ObjectIdentifier` | `str` (`unicode`) | string is dotted integer format | -| `ObjectDescriptor` | | no native conversion | -| `InstanceOf` | | no native conversion | -| `Real` | | no native conversion | -| `Enumerated` | `str` (`unicode`) | `_map` must be set | -| `UTF8String` | `str` (`unicode`) | | -| `RelativeOid` | `str` (`unicode`) | string is dotted integer format | -| `Sequence` | `OrderedDict` | | -| `SequenceOf` | `list` | | -| `Set` | `OrderedDict` | | -| `SetOf` | `list` | | -| `EmbeddedPdv` | `OrderedDict` | no named field parsing | -| `NumericString` | `str` (`unicode`) | no charset limitations | -| `PrintableString` | `str` (`unicode`) | no charset limitations | -| `TeletexString` | `str` (`unicode`) | | -| `VideotexString` | `bytes` (`str`) | no unicode conversion | -| `IA5String` | `str` (`unicode`) | | -| `UTCTime` | `datetime.datetime` | | -| `GeneralizedTime` | `datetime.datetime` | treated as UTC when no timezone | -| `GraphicString` | `str` (`unicode`) | unicode conversion as latin1 | -| `VisibleString` | `str` (`unicode`) | no charset limitations | -| `GeneralString` | `str` (`unicode`) | unicode conversion as latin1 | -| `UniversalString` | `str` (`unicode`) | | -| `CharacterString` | `str` (`unicode`) | unicode conversion as latin1 | -| `BMPString` | `str` (`unicode`) | | - -For *Native Type*, the Python 3 type is listed first, with the Python 2 type -in parentheses. - -As mentioned next to some of the types, value parsing may not be implemented -for types not currently used in cryptography (such as `ObjectDescriptor`, -`InstanceOf` and `Real`). Additionally some of the string classes don't -enforce character set limitations, and for some string types that accept all -different encodings, the default encoding is set to latin1. - -In addition, there are a few overridden types where various specifications use -a `BitString` or `OctetString` type to represent a different type. These -include: - -| Class | Native Type | Implementation Notes | -| -------------------- | ------------------- | ------------------------------- | -| `OctetBitString` | `bytes` (`str`) | | -| `IntegerBitString` | `int` | may be `long` on Python 2 | -| `IntegerOctetString` | `int` | may be `long` on Python 2 | - -## Basic Usage - -All of the universal types implement two methods, the class method `.load()` for -parsing and the instance method `.dump()` for serialization. - -```python -from asn1crypto.core import Sequence - -parsed = Sequence.load(der_byte_string) -serialized = parsed.dump() -``` - -In addition to the two primary methods, every instance has a `.native` property -that converts the data into a native Python data type. - -```python -import pprint -from asn1crypto.core import Sequence - -parsed = Sequence.load(der_byte_string) -pprint(parsed.native) -``` - -## Sequence - -One of the core structures when dealing with ASN.1 is the Sequence type. The -`Sequence` class can handle field with universal data types, however in most -situations the `_fields` property will need to be set with the expected -definition of each field in the Sequence. - -### Configuration - -The `_fields` property must be set to a `list` of 2-3 element `tuple`s. The -first element in the tuple must be a unicode string of the field name. The -second must be a type class - either a universal type, or a custom type. The -third, and optional, element is a `dict` with parameters to pass to the type -class for things like default values, marking the field as optional, or -implicit/explicit tagging. - -```python -from asn1crypto.core import Sequence, Integer, OctetString, IA5String - -class MySequence(Sequence): - _fields = [ - ('field_one', Integer), - ('field_two', OctetString), - ('field_three', IA5String, {'optional': True}), - ] -``` - -Implicit and explicit tagging will be covered in more detail later, however -the following are options that can be set for each field type class: - - - `{'default: 1}` sets the field's default value to `1`, allowing it to be - omitted from the serialized form - - `{'optional': True}` set the field to be optional, allowing it to be - omitted - -### Usage - -To access values of the sequence, use dict-like access via `[]` and use the -name of the field: - -```python -seq = MySequence.load(der_byte_string) -print(seq['field_two'].native) -``` - -The values of fields can be set by assigning via `[]`. If the value assigned is -of the correct type class, it will be used as-is. If the value is not of the -correct type class, a new instance of that type class will be created and the -value will be passed to the constructor. - -```python -seq = MySequence.load(der_byte_string) -# These statements will result in the same state -seq['field_one'] = Integer(5) -seq['field_one'] = 5 -``` - -When fields are complex types such as `Sequence` or `SequenceOf`, there is no -way to construct the value out of a native Python data type. - -### Optional Fields - -When a field is configured via the `optional` parameter, not present in the -`Sequence`, but accessed, an instance of the `NoValue` class will be returned. -This class is serialized to an empty byte string and returns `None` when -`.native` is accessed. - -## Set - -The `Set` class is configured in the same was as `Sequence`, however it allows -serialized fields to be in any order, per the ASN.1 standard. - -```python -from asn1crypto.core import Set, Integer, OctetString, IA5String - -class MySet(Set): - _fields = [ - ('field_one', Integer), - ('field_two', OctetString), - ('field_three', IA5String, {'optional': True}), - ] -``` - -## SequenceOf - -The `SequenceOf` class is used to allow for zero or more instances of a type. -The class uses the `_child_spec` property to define the instance class type. - -```python -from asn1crypto.core import SequenceOf, Integer - -class Integers(SequenceOf): - _child_spec = Integer -``` - -Values in the `SequenceOf` can be accessed via `[]` with an integer key. The -length of the `SequenceOf` is determined via `len()`. - -```python -values = Integers.load(der_byte_string) -for i in range(0, len(values)): - print(values[i].native) -``` - -## SetOf - -The `SetOf` class is an exact duplicate of `SequenceOf`. According to the ASN.1 -standard, the difference is that a `SequenceOf` is explicitly ordered, however -`SetOf` may be in any order. This is an equivalent comparison of a Python `list` -and `set`. - -```python -from asn1crypto.core import SetOf, Integer - -class Integers(SetOf): - _child_spec = Integer -``` - -## Integer - -The `Integer` class allows values to be *named*. An `Integer` with named values -may contain any integer, however special values with named will be represented -as those names when `.native` is called. - -Named values are configured via the `_map` property, which must be a `dict` -with the keys being integers and the values being unicode strings. - -```python -from asn1crypto.core import Integer - -class Version(Integer): - _map = { - 1: 'v1', - 2: 'v2', - } - -# Will print: "v1" -print(Version(1).native) - -# Will print: 4 -print(Version(4).native) -``` - -## Enumerated - -The `Enumerated` class is almost identical to `Integer`, however only values in -the `_map` property are valid. - -```python -from asn1crypto.core import Enumerated - -class Version(Enumerated): - _map = { - 1: 'v1', - 2: 'v2', - } - -# Will print: "v1" -print(Version(1).native) - -# Will raise a ValueError exception -print(Version(4).native) -``` - -## ObjectIdentifier - -The `ObjectIdentifier` class represents values of the ASN.1 type of the same -name. `ObjectIdentifier` instances are converted to a unicode string in a -dotted-integer format when `.native` is accessed. - -While this standard conversion is a reasonable baseline, in most situations -it will be more maintainable to map the OID strings to a unicode string -containing a description of what the OID repesents. - -The mapping of OID strings to name strings is configured via the `_map` -property, which is a `dict` object with keys being unicode OID string and the -values being a unicode string. - -```python -from asn1crypto.core import ObjectIdentifier - -class MyType(ObjectIdentifier): - _map = { - '1.8.2.1.23': 'value_name', - '1.8.2.1.24': 'other_value', - } - -# Will print: "value_name" -print(MyType('1.8.2.1.23').native) - -# Will print: "1.8.2.1.25" -print(MyType('1.8.2.1.25').native) -``` - -## BitString - -When no `_map` is set for a `BitString` class, the native representation is a -`tuple` of `int`s (being either `1` or `0`). In addition, it is possible to set -the value of a `BitString` by passing an integer. - -```python -from asn1crypto.core import BitString - -# These are equivalent -b1 = BitString((1, 0, 1)) -b2 = BitString(5) -``` - -Additionally, it is possible to set the `_map` property to a dict where the -keys are bit indexes and the values are unicode string names. This allows -checking the value of a given bit by item access, and the native representation -becomes an `OrderedDict` where keys are the unicode strings and values are -either `True` or `False`. - -```python -from asn1crypto.core import BitString - -class MyFlags(BitString): - _map = { - 0: 'edit', - 1: 'delete', - 2: 'manage_users', - } - -permissions = MyFlags(3) - -# This will be printed -if permissions['edit'] and permissions['delete']: - print('Can edit and delete') - -# This will not -if permissions['manage_users']: - print('Is admin') -``` - -## Strings - -ASN.1 contains quite a number of string types: - -| Type | Standard Encoding | Implementation Encoding | Notes | -| ----------------- | --------------------------------- | ----------------------- | ------------------------------------------------------------------------- | -| `UTF8String` | UTF-8 | UTF-8 | | -| `NumericString` | ASCII `[0-9 ]` | ISO 8859-1 | The implementation is a superset of supported characters | -| `PrintableString` | ASCII `[a-zA-Z0-9 '()+,\\-./:=?]` | ISO 8859-1 | The implementation is a superset of supported characters | -| `TeletexString` | ITU T.61 | Custom | The implementation is based off of https://en.wikipedia.org/wiki/ITU_T.61 | -| `VideotexString` | *?* | *None* | This has no set encoding, and it not used in cryptography | -| `IA5String` | ITU T.50 (very similar to ASCII) | ISO 8859-1 | The implementation is a superset of supported characters | -| `GraphicString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | -| `VisibleString` | ASCII (printable) | ISO 8859-1 | The implementation is a superset of supported characters | -| `GeneralString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | -| `UniversalString` | UTF-32 | UTF-32 | | -| `CharacterString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | -| `BMPString` | UTF-16 | UTF-16 | | - -As noted in the table above, many of the implementations are supersets of the -supported characters. This simplifies parsing, but puts the onus of using valid -characters on the developer. However, in general `UTF8String`, `BMPString` or -`UniversalString` should be preferred when a choice is given. - -All string types other than `VideotexString` are created from unicode strings. - -```python -from asn1crypto.core import IA5String - -print(IA5String('Testing!').native) -``` - -## UTCTime - -The class `UTCTime` accepts a unicode string in one of the formats: - - - `%y%m%d%H%MZ` - - `%y%m%d%H%M%SZ` - - `%y%m%d%H%M%z` - - `%y%m%d%H%M%S%z` - -or a `datetime.datetime` instance. See the -[Python datetime strptime() reference](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) -for details of the formats. - -When `.native` is accessed, it returns a `datetime.datetime` object with a -`tzinfo` of `asn1crypto.core.timezone.utc`. - -## GeneralizedTime - -The class `GeneralizedTime` accepts a unicode string in one of the formats: - - - `%Y%m%d%H` - - `%Y%m%d%H%M` - - `%Y%m%d%H%M%S` - - `%Y%m%d%H%M%S.%f` - - `%Y%m%d%HZ` - - `%Y%m%d%H%MZ` - - `%Y%m%d%H%M%SZ` - - `%Y%m%d%H%M%S.%fZ` - - `%Y%m%d%H%z` - - `%Y%m%d%H%M%z` - - `%Y%m%d%H%M%S%z` - - `%Y%m%d%H%M%S.%f%z` - -or a `datetime.datetime` instance. See the -[Python datetime strptime() reference](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) -for details of the formats. - -When `.native` is accessed, it returns a `datetime.datetime` object with a -`tzinfo` of `asn1crypto.core.timezone.utc`. For formats where the time has a -timezone offset is specified (`[+-]\d{4}`), the time is converted to UTC. For -times without a timezone, the time is assumed to be in UTC. - -## Choice - -The `Choice` class allows handling ASN.1 Choice structures. The `_alternatives` -property must be set to a `list` containing 2-3 element `tuple`s. The first -element in the tuple is the alternative name. The second element is the type -class for the alternative. The, optional, third element is a `dict` of -parameters to pass to the type class constructor. This is used primarily for -implicit and explicit tagging. - -```python -from asn1crypto.core import Choice, Integer, OctetString, IA5String - -class MyChoice(Choice): - _alternatives = [ - ('option_one', Integer), - ('option_two', OctetString), - ('option_three', IA5String), - ] -``` - -`Choice` objects has two extra properties, `.name` and `.chosen`. The `.name` -property contains the name of the chosen alternative. The `.chosen` property -contains the instance of the chosen type class. - -```python -parsed = MyChoice.load(der_bytes) -print(parsed.name) -print(type(parsed.chosen)) -``` - -The `.native` property and `.dump()` method work as with the universal type -classes. Under the hood they just proxy the calls to the `.chosen` object. - -## Any - -The `Any` class implements the ASN.1 Any type, which allows any data type. By -default objects of this class do not perform any parsing. However, the -`.parse()` instance method allows parsing the contents of the `Any` object, -either into a universal type, or to a specification pass in via the `spec` -parameter. - -This type is not used as a top-level structure, but instead allows `Sequence` -and `Set` objects to accept varying contents, usually based on some sort of -`ObjectIdentifier`. - -```python -from asn1crypto.core import Sequence, ObjectIdentifier, Any, Integer, OctetString - -class MySequence(Sequence): - _fields = [ - ('type', ObjectIdentifier), - ('value', Any), - ] -``` - -## Specification via OID - -Throughout the usage of ASN.1 in cryptography, a pattern is present where an -`ObjectIdenfitier` is used to determine what specification should be used to -interpret another field in a `Sequence`. Usually the other field is an instance -of `Any`, however ocassionally it is an `OctetString`. - -*asn1crypto* provides the `_oid_pair` and `_oid_specs` properties of the -`Sequence` class to allow handling these situations. - -The `_oid_pair` is a tuple with two unicode string elements. The first is the -name of the field that is an `ObjectIdentifier` and the second if the name of -the field that has a variable specification based on the first field. - -The `_oid_specs` property is a `dict` object with `ObjectIdentifier` values as -the keys (either dotted or mapped notation) and a type class as the value. When -the first field in `_oid_pair` has a value equal to one of the keys in -`_oid_specs`, then the corresponding type class will be used as the -specification for the second field of `_oid_pair`. - -```python -from asn1crypto.core import Sequence, ObjectIdentifier, Any, OctetString, Integer - -class MyId(ObjectIdentifier): - _map = { - '1.2.3.4': 'initialization_vector', - '1.2.3.5': 'iterations', - } - -class MySequence(Sequence): - _fields = [ - ('type', MyId), - ('value', Any), - ] - - _oid_pair = ('type', 'value') - _oid_specs = { - 'initialization_vector': OctetString, - 'iterations': Integer, - } -``` - -In some situations, the second field in `_oid_pair` is not an instance of `Any`, -but instead is an instance of `OctetString`. This is dictated by the ASN.1 -specification of the data structures being worked with. - -## Explicit and Implicit Tagging - -When working with `Sequence`, `Set` and `Choice` it is often necessary to -disambiguate between fields because of a number of factors: - - - In `Sequence` the presence of an optional field must be determined by tag number - - In `Set`, each field must have a different tag number since they can be in any order - - In `Choice`, each alternative must have a different tag number to determine which is present - -The universal types all have unique tag numbers. However, if a `Sequence`, `Set` -or `Choice` has more than one field with the same universal type, tagging allows -a way to keep the semantics of the original type, but with a different tag -number. - -Implicit tagging simply changes the tag number of a type to a different value. -However, Explicit tagging wraps the existing type in another tag with the -specified tag number. - -In general, most situations allow for implicit tagging, with the notable -exception than a field that is a `Choice` type must always be explicitly tagged. -Otherwise, using implicit tagging would modify the tag of the chosen -alternative, breaking the mechanism by which `Choice` works. - -Here is an example of implicit and explicit tagging where explicit tagging on -the `Sequence` allows a `Choice` type field to be optional, and where implicit -tagging in the `Choice` structure allows disambiguating between two string of -the same type. - -```python -from asn1crypto.core import Sequence, Choice, IA5String, UTCTime, ObjectIdentifier - -class Person(Choice): - _alternatives = [ - ('name', IA5String), - ('email', IA5String, {'tag_type': 'implicit', 'tag': 0}), - ] - -class Record(Sequence): - _fields = [ - ('id', ObjectIdentifier), - ('created', UTCTime), - ('creator', Person, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ] -``` - -As is shown above, the keys `tag_type` and `tag` are used for tagging, and are -passed to a type class constructor via the optional third element of a field -or alternative tuple. The `tag_type` may be the unicode strings `'implicit'` or -`'explicit'` and the `tag` may be any integer. diff --git a/docs/universal_types.md b/docs/universal_types.md new file mode 100644 index 0000000..ae9124f --- /dev/null +++ b/docs/universal_types.md @@ -0,0 +1,550 @@ +# Universal Types with BER/DER Decoder and DER Encoder + +The *asn1crypto* library is a combination of universal type classes that +implement BER/DER decoding and DER encoding, a PEM encoder and decoder, and a +number of pre-built cryptographic type classes. This document covers the +universal type classes. + +For a general overview of ASN.1 as used in cryptography, please see +[A Layman's Guide to a Subset of ASN.1, BER, and DER](http://luca.ntop.org/Teaching/Appunti/asn1.html). + +## Universal Types + +For general purpose ASN.1 parsing, the `asn1crypto.core` module is used. It +contains the following classes, that parse, represent and serialize all of the +ASN.1 universal types: + +| Class | Native Type | Implementation Notes | +| ------------------ | ------------------------------------ | ------------------------------------ | +| `Boolean` | `bool` | | +| `Integer` | `int` | may be `long` on Python 2 | +| `BitString` | `tuple` of `int` or `OrderedDict` | `OrderedDict` used if `_map` present | +| `OctetString` | `bytes` (`str`) | | +| `Null` | `None` | | +| `ObjectIdentifier` | `str` (`unicode`) | string is dotted integer format | +| `ObjectDescriptor` | | no native conversion | +| `InstanceOf` | | no native conversion | +| `Real` | | no native conversion | +| `Enumerated` | `str` (`unicode`) | `_map` must be set | +| `UTF8String` | `str` (`unicode`) | | +| `RelativeOid` | `str` (`unicode`) | string is dotted integer format | +| `Sequence` | `OrderedDict` | | +| `SequenceOf` | `list` | | +| `Set` | `OrderedDict` | | +| `SetOf` | `list` | | +| `EmbeddedPdv` | `OrderedDict` | no named field parsing | +| `NumericString` | `str` (`unicode`) | no charset limitations | +| `PrintableString` | `str` (`unicode`) | no charset limitations | +| `TeletexString` | `str` (`unicode`) | | +| `VideotexString` | `bytes` (`str`) | no unicode conversion | +| `IA5String` | `str` (`unicode`) | | +| `UTCTime` | `datetime.datetime` | | +| `GeneralizedTime` | `datetime.datetime` | treated as UTC when no timezone | +| `GraphicString` | `str` (`unicode`) | unicode conversion as latin1 | +| `VisibleString` | `str` (`unicode`) | no charset limitations | +| `GeneralString` | `str` (`unicode`) | unicode conversion as latin1 | +| `UniversalString` | `str` (`unicode`) | | +| `CharacterString` | `str` (`unicode`) | unicode conversion as latin1 | +| `BMPString` | `str` (`unicode`) | | + +For *Native Type*, the Python 3 type is listed first, with the Python 2 type +in parentheses. + +As mentioned next to some of the types, value parsing may not be implemented +for types not currently used in cryptography (such as `ObjectDescriptor`, +`InstanceOf` and `Real`). Additionally some of the string classes don't +enforce character set limitations, and for some string types that accept all +different encodings, the default encoding is set to latin1. + +In addition, there are a few overridden types where various specifications use +a `BitString` or `OctetString` type to represent a different type. These +include: + +| Class | Native Type | Implementation Notes | +| -------------------- | ------------------- | ------------------------------- | +| `OctetBitString` | `bytes` (`str`) | | +| `IntegerBitString` | `int` | may be `long` on Python 2 | +| `IntegerOctetString` | `int` | may be `long` on Python 2 | + +## Basic Usage + +All of the universal types implement two methods, the class method `.load()` for +parsing and the instance method `.dump()` for serialization. + +```python +from asn1crypto.core import Sequence + +parsed = Sequence.load(der_byte_string) +serialized = parsed.dump() +``` + +In addition to the two primary methods, every instance has a `.native` property +that converts the data into a native Python data type. + +```python +import pprint +from asn1crypto.core import Sequence + +parsed = Sequence.load(der_byte_string) +pprint(parsed.native) +``` + +## Sequence + +One of the core structures when dealing with ASN.1 is the Sequence type. The +`Sequence` class can handle field with universal data types, however in most +situations the `_fields` property will need to be set with the expected +definition of each field in the Sequence. + +### Configuration + +The `_fields` property must be set to a `list` of 2-3 element `tuple`s. The +first element in the tuple must be a unicode string of the field name. The +second must be a type class - either a universal type, or a custom type. The +third, and optional, element is a `dict` with parameters to pass to the type +class for things like default values, marking the field as optional, or +implicit/explicit tagging. + +```python +from asn1crypto.core import Sequence, Integer, OctetString, IA5String + +class MySequence(Sequence): + _fields = [ + ('field_one', Integer), + ('field_two', OctetString), + ('field_three', IA5String, {'optional': True}), + ] +``` + +Implicit and explicit tagging will be covered in more detail later, however +the following are options that can be set for each field type class: + + - `{'default: 1}` sets the field's default value to `1`, allowing it to be + omitted from the serialized form + - `{'optional': True}` set the field to be optional, allowing it to be + omitted + +### Usage + +To access values of the sequence, use dict-like access via `[]` and use the +name of the field: + +```python +seq = MySequence.load(der_byte_string) +print(seq['field_two'].native) +``` + +The values of fields can be set by assigning via `[]`. If the value assigned is +of the correct type class, it will be used as-is. If the value is not of the +correct type class, a new instance of that type class will be created and the +value will be passed to the constructor. + +```python +seq = MySequence.load(der_byte_string) +# These statements will result in the same state +seq['field_one'] = Integer(5) +seq['field_one'] = 5 +``` + +When fields are complex types such as `Sequence` or `SequenceOf`, there is no +way to construct the value out of a native Python data type. + +### Optional Fields + +When a field is configured via the `optional` parameter, not present in the +`Sequence`, but accessed, an instance of the `NoValue` class will be returned. +This class is serialized to an empty byte string and returns `None` when +`.native` is accessed. + +## Set + +The `Set` class is configured in the same was as `Sequence`, however it allows +serialized fields to be in any order, per the ASN.1 standard. + +```python +from asn1crypto.core import Set, Integer, OctetString, IA5String + +class MySet(Set): + _fields = [ + ('field_one', Integer), + ('field_two', OctetString), + ('field_three', IA5String, {'optional': True}), + ] +``` + +## SequenceOf + +The `SequenceOf` class is used to allow for zero or more instances of a type. +The class uses the `_child_spec` property to define the instance class type. + +```python +from asn1crypto.core import SequenceOf, Integer + +class Integers(SequenceOf): + _child_spec = Integer +``` + +Values in the `SequenceOf` can be accessed via `[]` with an integer key. The +length of the `SequenceOf` is determined via `len()`. + +```python +values = Integers.load(der_byte_string) +for i in range(0, len(values)): + print(values[i].native) +``` + +## SetOf + +The `SetOf` class is an exact duplicate of `SequenceOf`. According to the ASN.1 +standard, the difference is that a `SequenceOf` is explicitly ordered, however +`SetOf` may be in any order. This is an equivalent comparison of a Python `list` +and `set`. + +```python +from asn1crypto.core import SetOf, Integer + +class Integers(SetOf): + _child_spec = Integer +``` + +## Integer + +The `Integer` class allows values to be *named*. An `Integer` with named values +may contain any integer, however special values with named will be represented +as those names when `.native` is called. + +Named values are configured via the `_map` property, which must be a `dict` +with the keys being integers and the values being unicode strings. + +```python +from asn1crypto.core import Integer + +class Version(Integer): + _map = { + 1: 'v1', + 2: 'v2', + } + +# Will print: "v1" +print(Version(1).native) + +# Will print: 4 +print(Version(4).native) +``` + +## Enumerated + +The `Enumerated` class is almost identical to `Integer`, however only values in +the `_map` property are valid. + +```python +from asn1crypto.core import Enumerated + +class Version(Enumerated): + _map = { + 1: 'v1', + 2: 'v2', + } + +# Will print: "v1" +print(Version(1).native) + +# Will raise a ValueError exception +print(Version(4).native) +``` + +## ObjectIdentifier + +The `ObjectIdentifier` class represents values of the ASN.1 type of the same +name. `ObjectIdentifier` instances are converted to a unicode string in a +dotted-integer format when `.native` is accessed. + +While this standard conversion is a reasonable baseline, in most situations +it will be more maintainable to map the OID strings to a unicode string +containing a description of what the OID repesents. + +The mapping of OID strings to name strings is configured via the `_map` +property, which is a `dict` object with keys being unicode OID string and the +values being a unicode string. + +```python +from asn1crypto.core import ObjectIdentifier + +class MyType(ObjectIdentifier): + _map = { + '1.8.2.1.23': 'value_name', + '1.8.2.1.24': 'other_value', + } + +# Will print: "value_name" +print(MyType('1.8.2.1.23').native) + +# Will print: "1.8.2.1.25" +print(MyType('1.8.2.1.25').native) +``` + +## BitString + +When no `_map` is set for a `BitString` class, the native representation is a +`tuple` of `int`s (being either `1` or `0`). In addition, it is possible to set +the value of a `BitString` by passing an integer. + +```python +from asn1crypto.core import BitString + +# These are equivalent +b1 = BitString((1, 0, 1)) +b2 = BitString(5) +``` + +Additionally, it is possible to set the `_map` property to a dict where the +keys are bit indexes and the values are unicode string names. This allows +checking the value of a given bit by item access, and the native representation +becomes an `OrderedDict` where keys are the unicode strings and values are +either `True` or `False`. + +```python +from asn1crypto.core import BitString + +class MyFlags(BitString): + _map = { + 0: 'edit', + 1: 'delete', + 2: 'manage_users', + } + +permissions = MyFlags(3) + +# This will be printed +if permissions['edit'] and permissions['delete']: + print('Can edit and delete') + +# This will not +if permissions['manage_users']: + print('Is admin') +``` + +## Strings + +ASN.1 contains quite a number of string types: + +| Type | Standard Encoding | Implementation Encoding | Notes | +| ----------------- | --------------------------------- | ----------------------- | ------------------------------------------------------------------------- | +| `UTF8String` | UTF-8 | UTF-8 | | +| `NumericString` | ASCII `[0-9 ]` | ISO 8859-1 | The implementation is a superset of supported characters | +| `PrintableString` | ASCII `[a-zA-Z0-9 '()+,\\-./:=?]` | ISO 8859-1 | The implementation is a superset of supported characters | +| `TeletexString` | ITU T.61 | Custom | The implementation is based off of https://en.wikipedia.org/wiki/ITU_T.61 | +| `VideotexString` | *?* | *None* | This has no set encoding, and it not used in cryptography | +| `IA5String` | ITU T.50 (very similar to ASCII) | ISO 8859-1 | The implementation is a superset of supported characters | +| `GraphicString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | +| `VisibleString` | ASCII (printable) | ISO 8859-1 | The implementation is a superset of supported characters | +| `GeneralString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | +| `UniversalString` | UTF-32 | UTF-32 | | +| `CharacterString` | * | ISO 8859-1 | This has not set encoding, but seems to often contain ISO 8859-1 | +| `BMPString` | UTF-16 | UTF-16 | | + +As noted in the table above, many of the implementations are supersets of the +supported characters. This simplifies parsing, but puts the onus of using valid +characters on the developer. However, in general `UTF8String`, `BMPString` or +`UniversalString` should be preferred when a choice is given. + +All string types other than `VideotexString` are created from unicode strings. + +```python +from asn1crypto.core import IA5String + +print(IA5String('Testing!').native) +``` + +## UTCTime + +The class `UTCTime` accepts a unicode string in one of the formats: + + - `%y%m%d%H%MZ` + - `%y%m%d%H%M%SZ` + - `%y%m%d%H%M%z` + - `%y%m%d%H%M%S%z` + +or a `datetime.datetime` instance. See the +[Python datetime strptime() reference](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) +for details of the formats. + +When `.native` is accessed, it returns a `datetime.datetime` object with a +`tzinfo` of `asn1crypto.core.timezone.utc`. + +## GeneralizedTime + +The class `GeneralizedTime` accepts a unicode string in one of the formats: + + - `%Y%m%d%H` + - `%Y%m%d%H%M` + - `%Y%m%d%H%M%S` + - `%Y%m%d%H%M%S.%f` + - `%Y%m%d%HZ` + - `%Y%m%d%H%MZ` + - `%Y%m%d%H%M%SZ` + - `%Y%m%d%H%M%S.%fZ` + - `%Y%m%d%H%z` + - `%Y%m%d%H%M%z` + - `%Y%m%d%H%M%S%z` + - `%Y%m%d%H%M%S.%f%z` + +or a `datetime.datetime` instance. See the +[Python datetime strptime() reference](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior) +for details of the formats. + +When `.native` is accessed, it returns a `datetime.datetime` object with a +`tzinfo` of `asn1crypto.core.timezone.utc`. For formats where the time has a +timezone offset is specified (`[+-]\d{4}`), the time is converted to UTC. For +times without a timezone, the time is assumed to be in UTC. + +## Choice + +The `Choice` class allows handling ASN.1 Choice structures. The `_alternatives` +property must be set to a `list` containing 2-3 element `tuple`s. The first +element in the tuple is the alternative name. The second element is the type +class for the alternative. The, optional, third element is a `dict` of +parameters to pass to the type class constructor. This is used primarily for +implicit and explicit tagging. + +```python +from asn1crypto.core import Choice, Integer, OctetString, IA5String + +class MyChoice(Choice): + _alternatives = [ + ('option_one', Integer), + ('option_two', OctetString), + ('option_three', IA5String), + ] +``` + +`Choice` objects has two extra properties, `.name` and `.chosen`. The `.name` +property contains the name of the chosen alternative. The `.chosen` property +contains the instance of the chosen type class. + +```python +parsed = MyChoice.load(der_bytes) +print(parsed.name) +print(type(parsed.chosen)) +``` + +The `.native` property and `.dump()` method work as with the universal type +classes. Under the hood they just proxy the calls to the `.chosen` object. + +## Any + +The `Any` class implements the ASN.1 Any type, which allows any data type. By +default objects of this class do not perform any parsing. However, the +`.parse()` instance method allows parsing the contents of the `Any` object, +either into a universal type, or to a specification pass in via the `spec` +parameter. + +This type is not used as a top-level structure, but instead allows `Sequence` +and `Set` objects to accept varying contents, usually based on some sort of +`ObjectIdentifier`. + +```python +from asn1crypto.core import Sequence, ObjectIdentifier, Any, Integer, OctetString + +class MySequence(Sequence): + _fields = [ + ('type', ObjectIdentifier), + ('value', Any), + ] +``` + +## Specification via OID + +Throughout the usage of ASN.1 in cryptography, a pattern is present where an +`ObjectIdenfitier` is used to determine what specification should be used to +interpret another field in a `Sequence`. Usually the other field is an instance +of `Any`, however ocassionally it is an `OctetString`. + +*asn1crypto* provides the `_oid_pair` and `_oid_specs` properties of the +`Sequence` class to allow handling these situations. + +The `_oid_pair` is a tuple with two unicode string elements. The first is the +name of the field that is an `ObjectIdentifier` and the second if the name of +the field that has a variable specification based on the first field. + +The `_oid_specs` property is a `dict` object with `ObjectIdentifier` values as +the keys (either dotted or mapped notation) and a type class as the value. When +the first field in `_oid_pair` has a value equal to one of the keys in +`_oid_specs`, then the corresponding type class will be used as the +specification for the second field of `_oid_pair`. + +```python +from asn1crypto.core import Sequence, ObjectIdentifier, Any, OctetString, Integer + +class MyId(ObjectIdentifier): + _map = { + '1.2.3.4': 'initialization_vector', + '1.2.3.5': 'iterations', + } + +class MySequence(Sequence): + _fields = [ + ('type', MyId), + ('value', Any), + ] + + _oid_pair = ('type', 'value') + _oid_specs = { + 'initialization_vector': OctetString, + 'iterations': Integer, + } +``` + +In some situations, the second field in `_oid_pair` is not an instance of `Any`, +but instead is an instance of `OctetString`. This is dictated by the ASN.1 +specification of the data structures being worked with. + +## Explicit and Implicit Tagging + +When working with `Sequence`, `Set` and `Choice` it is often necessary to +disambiguate between fields because of a number of factors: + + - In `Sequence` the presence of an optional field must be determined by tag number + - In `Set`, each field must have a different tag number since they can be in any order + - In `Choice`, each alternative must have a different tag number to determine which is present + +The universal types all have unique tag numbers. However, if a `Sequence`, `Set` +or `Choice` has more than one field with the same universal type, tagging allows +a way to keep the semantics of the original type, but with a different tag +number. + +Implicit tagging simply changes the tag number of a type to a different value. +However, Explicit tagging wraps the existing type in another tag with the +specified tag number. + +In general, most situations allow for implicit tagging, with the notable +exception than a field that is a `Choice` type must always be explicitly tagged. +Otherwise, using implicit tagging would modify the tag of the chosen +alternative, breaking the mechanism by which `Choice` works. + +Here is an example of implicit and explicit tagging where explicit tagging on +the `Sequence` allows a `Choice` type field to be optional, and where implicit +tagging in the `Choice` structure allows disambiguating between two string of +the same type. + +```python +from asn1crypto.core import Sequence, Choice, IA5String, UTCTime, ObjectIdentifier + +class Person(Choice): + _alternatives = [ + ('name', IA5String), + ('email', IA5String, {'tag_type': 'implicit', 'tag': 0}), + ] + +class Record(Sequence): + _fields = [ + ('id', ObjectIdentifier), + ('created', UTCTime), + ('creator', Person, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ] +``` + +As is shown above, the keys `tag_type` and `tag` are used for tagging, and are +passed to a type class constructor via the optional third element of a field +or alternative tuple. The `tag_type` may be the unicode strings `'implicit'` or +`'explicit'` and the `tag` may be any integer. diff --git a/readme.md b/readme.md index de112ab..085ecc6 100644 --- a/readme.md +++ b/readme.md @@ -41,11 +41,13 @@ pip install asn1crypto ## Documentation -The documentation for *asn1crypto* is composed of a tutorial on basic usage that -covers all of the universal data types, and links to the source for the various -pre-defined data types. +The documentation for *asn1crypto* is composed of tutorials on basic usage and +links to the source for the various pre-defined type classes. - - [Tutorial](docs/tutorial.md) +### Tutorials + + - [Universal Types with BER/DER Decoder and DER Encoder](docs/universal_types.md) + - [PEM Encoder and Decoder](docs/pem.md) ### Reference diff --git a/tests/fixtures/keys/test-aes128-der.key b/tests/fixtures/keys/test-aes128-der.key new file mode 100644 index 0000000..8609b25 Binary files /dev/null and b/tests/fixtures/keys/test-aes128-der.key differ diff --git a/tests/fixtures/keys/test-aes128.key b/tests/fixtures/keys/test-aes128.key new file mode 100644 index 0000000..a01e55a --- /dev/null +++ b/tests/fixtures/keys/test-aes128.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,01F6EE04516C912788B11BD7377626C2 + +73aqVra5MCRJyYTjaqnm8d8OTBSdKvs8hTamsyIcHKe+hy8c/A5XaxQy6GZhGqze +gpu1E8sRpvCHI5WpNflnzuiySamR5iC2OEoMtxHi4j9K0xw/jSdWn2H+uj4grPOo +xIyMj8X9jyLHTa9N12Msvevdd6Zp6HCB6v8K9yi89Q6jl8B929sldQEftcHzLALi +iI9QBs2uH47WyRY4PfZC3HHSynfZSE2Q5hm/Si3b7OGx14VZx78fPfVfBlwRGne6 +7ZPQdpQm8HP1FR9XmfotZh6PjkcNEkfi6k8c3LPEcpOsRb0LdlHB3/H2+a6YAKOu +ipA6HpDD3C6f8fq7n6S1pih681iTlVxaUMJkHEmTJPdKdc4tThAQ4V9rutqNIfhv +y/FrmN/1klnulvwyGIbwaPatcrZyvPBbFt5hJYh55sGLzPq7pumaDSd+8jlTRr70 +KC/ivwGXxFNRah76S52v/tSVuiBxCc5q5tYE2dKHdrqNZGUjE80PUGQQEunCNjY9 +CFq84wDXZ4VZsPGMBQlhZmx/nyC9dQ8UGBwrLgfyFfp/W9j7o4xbAyremfgWVFvs +ChYifyw5BErF0cfbfF1nGmMDH26mLcSLD18/dipzL1vUGLH5mJegPeD7KHScDUpy +4t/3wddEnbU4w9BUZhI4zqlSiAnxARO4maEFFgsuJNuwYwd7CSfIVQpYQS+2qVSZ +Qp6pYzfmJFJ0CSqOtkK88clb7pKjOCBYHhfdcm84K8v0hhgDviVQ92HG9p7VmwDo +gzNhUFwJ/mSkZQKz/CLy7dGoW0fD3xiseGTlsR658wcJcR7l8qkUBGF3D3hi4t83 +fiTCR9j8MBhezgqO6ATSxgN3rqKQUBVphYMgNsVBlSKr2h8TwJAADo8xVGS5TRR4 +OrY3TOminoG2Bn0KyavMiwIVQI/6U0Y5OrLDgl666NHVtjWMfK5NXD4pMTPRuLVF +Snnsi1b0fxGMHtXLLA96iLNtyy5H91xqek1vFou2bsN9PW9TTihNMyTLO/BAi3oy +kpRv99swSE6LaEBXKD1HvGJ3pyLarTRQvLE1YilBz8aHVM06Hh/MZIsQ1x6O9lZI +5mQSeFgcD/SAOrgxIE3tNnPSHHcBiRbUFxgfZQDlo4+WgYZl5WOTBuoqJKb0Zeeh +ubH2I+EGW2kCQo87t4kt7fKEU+k+DfCMoLF+WoEo/s9jU/k5kuCC8iEnITUC8ew8 +9ppGkcEf086WDVI4lG/Q7LNHyIZuzPNVf+ZpVDRYL8I/j6dgq5OWCv2uXFjZCE+q +H664h31DV2TbaSZUsETzvVbzKmvYjzD1QayFrJIKtrgxlWxlw0HmucCm+K+Kbjk7 +SeJE21KbKkxl4dxjhrtsxXp9uhL4IgebeXBV88UetkdqvYtDTRHUtyPCnf8VQjJp +DelBSgLahhZuuHcXBBrk5KUKzKMZElsYkS+RdR+P5nESseFtXqJH2kjsAJKGLra5 +3dDeOMPluHKQKHtwYUaO9QpWwAwJu3V2l+Tmo20JyQAZJujDfQ1WECCES+Kb8VIN +khp52v9TVhFKSTSLKNFyt8ytEX4wCR2T56Vv3Gwe9/6e6lDljx3eRgmGFhw1Fa6h +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/keys/test-inter.crt b/tests/fixtures/keys/test-inter.crt new file mode 100644 index 0000000..da64fe5 --- /dev/null +++ b/tests/fixtures/keys/test-inter.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEBDCCAuygAwIBAgIDGEN5MA0GCSqGSIb3DQEBCwUAMIGdMQswCQYDVQQGEwJV +UzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEQMA4GA1UEBxMHTmV3YnVyeTEeMBwG +A1UEChMVQ29kZXggTm9uIFN1ZmZpY2l0IExDMRAwDgYDVQQLEwdUZXN0aW5nMRIw +EAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5p +bzAeFw0xNTA1MDkwMDQxMzlaFw0yNTA1MDYwMDQxMzlaMIGYMQswCQYDVQQGEwJV +UzEWMBQGA1UECBMNTWFzc2FjaHVzZXR0czEeMBwGA1UEChMVQ29kZXggTm9uIFN1 +ZmZpY2l0IExDMR0wGwYDVQQLExRUZXN0aW5nIEludGVybWVkaWF0ZTESMBAGA1UE +AxMJV2lsbCBCb25kMR4wHAYJKoZIhvcNAQkBFg93aWxsQGNvZGV4bnMuaW8wggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/PVOdogtqe2gqXZ20dWB5NFxj +JX6z6A57zGqT11OfiNmckIqxMRK8eToR69/DJmVNbQTJy/cFexZ33db+UrtEItxO +klFFbG/6TjmqsOYF87TkJqGX7zXs/Z7D1Xl0/2CNh6vPPPbi/o1FHtKnk4HYOnyU +LRMxxtt06O/u//wdnWXiSAOKOasLT7eP6Fuok28+BeQ29GB3UNL8oC7biyhv89yN +VhDlHAkk+zfrGP4dNqxOY8SkwI0GYki9skEckwf7Nyz+vA8zVJaIymte/eRicCFF +YFxh+QtQhyxF7DwsB5/XIgEWKRXnd/ivyORMlIVc83CHCM4ryvXgG0+I7WuZAgMB +AAGjUDBOMB0GA1UdDgQWBBTSCv0uJdG3IddQfrukfb8071JeAjAfBgNVHSMEGDAW +gBS+QoU9zP/j+SgCj35YVrT9A1zqSzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQAKnbphxDozjob2qobtO5QRfZNwIANr/Pyz6SoD8UnJDfZAg9y1jb/I +OutcURjYfV4vyLKI5oyhepyBMkuwvnB/lRpLERAwv5lJgq4sIGDn5Sp56yYcxazC +QXLyG5YJ2Q29kVuq9//IE/dkLVaP444iJZZ7IgrGN4EFQwvFRVa6toeVpwzp8kkn +5eQaiKQYt/1dVsQ5mv8ksT/gNJRMD4xJ7xvlats+Jc6cyygJk18h+JqMYDoFTTu6 +fJmjtnjPFg+XU0q9GnJfrW+FjxYGMv+5tv0pJrujEq7+8gz5A4viSCzHHoc4O/Qs +0Jt6dQ/xmGYswxjrR/ZgsSjPx3lib7XC +-----END CERTIFICATE----- diff --git a/tests/fixtures/keys/test-third.crt b/tests/fixtures/keys/test-third.crt new file mode 100644 index 0000000..e664008 --- /dev/null +++ b/tests/fixtures/keys/test-third.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgIFAJOEAykwDQYJKoZIhvcNAQELBQAwgZgxCzAJBgNVBAYT +AlVTMRYwFAYDVQQIEw1NYXNzYWNodXNldHRzMR4wHAYDVQQKExVDb2RleCBOb24g +U3VmZmljaXQgTEMxHTAbBgNVBAsTFFRlc3RpbmcgSW50ZXJtZWRpYXRlMRIwEAYD +VQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5pbzAe +Fw0xNTA1MTEyMTQyNTZaFw0yNTA1MDgyMTQyNTZaMIGgMQswCQYDVQQGEwJVUzEW +MBQGA1UECBMNTWFzc2FjaHVzZXR0czEeMBwGA1UEChMVQ29kZXggTm9uIFN1ZmZp +Y2l0IExDMSUwIwYDVQQLExxUZXN0IFRoaXJkLUxldmVsIENlcnRpZmljYXRlMRIw +EAYDVQQDEwlXaWxsIEJvbmQxHjAcBgkqhkiG9w0BCQEWD3dpbGxAY29kZXhucy5p +bzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMAKLNVdAlEUbBeAKfSz +3Ymd78tt6l2jjEdz4QMqFkYDSbl5pMB0RP+yRKHVSr+gL99F3wAXHoMb81bFVItL +dyx3vK1qK2FhMvmuw8yNUy3o4aIwE/Neobp4eWLWNWwdqmPyNpvc8g8HdVsnk97u +9ZUGoiHEblvLZ8uijvbxK5hbCkKHjOhHCDT5egtPKOyFqOWJ1WzINICRfEeIjZM0 +cuLUWVJmzb9QJ8CmN4O6R1rYK131eIZ6FWlJTpx1n7MvygTOmDiyVE0NMfJFTzdj +a8kQxiwdyqN9I9OfHqRefh8TCQBuAguuwPUyAu9Ve45eDqzfeWrgoyinZ0KXiqrH +5DUCAwEAAaNCMEAwHQYDVR0OBBYEFEQ44OAmhb+Yhtwb4R31MjC+q6wNMB8GA1Ud +IwQYMBaAFNIK/S4l0bch11B+u6R9vzTvUl4CMA0GCSqGSIb3DQEBCwUAA4IBAQAb +Wq6ueTRDgY5hDAcVn3j5Eaco88rzhhzgfnH4GSES/QOQlOEFbj0/zzAk7LCgcXq3 +7ud/cbA0tuoFmra9Q4D3YEcL0xHiDlXkvzcy2MJ6MysRiQftvHE9o1f8lxWqEfHK +EtL3espMfVE8zisiA7kS07Jm4t7OSBte21JVwqC+dlqYca2T0coJ47Fw/yVvlaQU +O5h6wvHuUS4L0myNdW+/V2yoUex8C9OK3qs8H61ULcgoVnvn/DJJerad53LBfJR3 +jmmamq6Pu7COWU07N7MOGkrG8bwal64ncdzwvTtBj9NdoDataw9LvjyAGxYDSY6X +KiqBZoOHm108hjAfW+XC +-----END CERTIFICATE----- diff --git a/tests/test_pem.py b/tests/test_pem.py new file mode 100644 index 0000000..6494963 --- /dev/null +++ b/tests/test_pem.py @@ -0,0 +1,77 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import unittest +import sys +import os + +from asn1crypto import pem + +from .unittest_data import DataDecorator, data + +if sys.version_info < (3,): + byte_cls = str + num_cls = long #pylint: disable=E0602 +else: + byte_cls = bytes + num_cls = int + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + +@DataDecorator +class PEMTests(unittest.TestCase): + + #pylint: disable=C0326 + @staticmethod + def detect_files(): + return ( + ('keys/test-der.crt', False), + ('keys/test-inter-der.crt', False), + ('keys/test-third-der.crt', False), + ('keys/test.crt', True), + ('keys/test-inter.crt', True), + ('keys/test-third.crt', True), + ) + + @data('detect_files') + def detect(self, relative_path, is_pem): + with open(os.path.join(fixtures_dir, relative_path), 'rb') as f: + byte_string = f.read() + self.assertEqual(is_pem, pem.detect(byte_string)) + + #pylint: disable=C0326 + @staticmethod + def unarmor_armor_files(): + return ( + ('keys/test.crt', 'keys/test-der.crt', 'CERTIFICATE', {}), + ('keys/test-inter.crt', 'keys/test-inter-der.crt', 'CERTIFICATE', {}), + ('keys/test-third.crt', 'keys/test-third-der.crt', 'CERTIFICATE', {}), + ('keys/test-pkcs8.key', 'keys/test-pkcs8-der.key', 'PRIVATE KEY', {}), + ('test-third.csr', 'test-third-der.csr', 'CERTIFICATE REQUEST', {}), + ('keys/test-aes128.key', 'keys/test-aes128-der.key', 'RSA PRIVATE KEY', {'Proc-Type': '4,ENCRYPTED', 'DEK-Info': 'AES-128-CBC,01F6EE04516C912788B11BD7377626C2'}), + ) + + @data('unarmor_armor_files') + def unarmor(self, relative_path, expected_bytes_filename, expected_type_name, expected_headers): + with open(os.path.join(fixtures_dir, relative_path), 'rb') as f: + byte_string = f.read() + + type_name, headers, decoded_bytes = pem.unarmor(byte_string) + self.assertEqual(expected_type_name, type_name) + self.assertEqual(expected_headers, headers) + with open(os.path.join(fixtures_dir, expected_bytes_filename), 'rb') as f: + expected_bytes = f.read() + self.assertEqual(expected_bytes, decoded_bytes) + + @data('unarmor_armor_files') + def armor(self, expected_bytes_filename, relative_path, type_name, headers): + with open(os.path.join(fixtures_dir, relative_path), 'rb') as f: + byte_string = f.read() + + encoded_bytes = pem.armor(type_name, byte_string, headers=headers) + with open(os.path.join(fixtures_dir, expected_bytes_filename), 'rb') as f: + expected_bytes = f.read() + self.assertEqual(expected_bytes, encoded_bytes) -- cgit v1.2.3 From 5af38cd870e06d9b092eb459ced9ba619f9baa43 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 15 Jul 2015 13:19:22 -0400 Subject: Explicitly mention pypy3 support --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 085ecc6..64fa298 100644 --- a/readme.md +++ b/readme.md @@ -27,7 +27,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: ## Dependencies -Python 2.7, 3.3, 3.4 or pypy. *No third-party packages required.* +Python 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Version -- cgit v1.2.3 From 39ea6ab45ae742d3288a56d913eb94e34a01b78c Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 11:07:37 -0400 Subject: Make NoValue() iterate 0 times --- asn1crypto/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d737f1f..8e7916a 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -397,6 +397,9 @@ class NoValue(Asn1Value): def __len__(self): return 0 + def __iter__(self): + return iter(()) + @property def native(self): """ -- cgit v1.2.3 From d556acc211d0e9b9ad3cb05a4b71c4f2dc772c01 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 11:13:52 -0400 Subject: Extracted field definition determination and value creation from Sequence __setitem__() and _parse_children() --- asn1crypto/core.py | 194 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 116 insertions(+), 78 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8e7916a..56dd33f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1757,62 +1757,12 @@ class Sequence(Asn1Value): raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) key = self._field_map[key] - field_info = self._fields[key] - field_spec = field_info[1] - field_params = field_info[2] if len(field_info) > 2 else {} - value_spec = field_spec - - if self._spec_callbacks is not None and field_info[0] in self._spec_callbacks: - callback = self._spec_callbacks[field_info[0]] - spec_override = callback(self) - # Allow a spec callback to specify both the base spec and - # the override, for situations such as OctetString and parse_as - if isinstance(spec_override, tuple) and len(spec_override) == 2: - field_spec, value_spec = spec_override - else: - value_spec = spec_override - - elif self._oid_nums is not None and key == self._oid_nums[1]: - oid = self._lazy_child(self._oid_nums[0]).native - if oid in self._oid_specs: - value_spec = self._oid_specs[oid] - - if issubclass(value_spec, Choice): - if not isinstance(value, Asn1Value): - raise ValueError('Can not set a native python value to %s, which has the choice type of %s – value must be an instance of Asn1Value' % (field_info[0], value_spec.__name__)) - if not isinstance(value, value_spec): - wrapper = value_spec() - wrapper.validate(value.class_, value.tag) - wrapper._parsed = value #pylint: disable=W0212 - new_value = wrapper - else: - new_value = value - - elif isinstance(value, field_spec): - new_value = value - if value_spec != field_spec: - new_value.parse(value_spec) - - else: - if isinstance(value, value_spec): - new_value = value - else: - new_value = value_spec(value) + field_name, field_spec, value_spec, field_params, _ = self._determine_spec(key) - # For when the field is OctetString or OctetBitString with embedded - # values we need to wrap the value in the field spec to get the - # appropriate encoded value. - if field_spec != value_spec and not issubclass(field_spec, Any): - wrapper = field_spec(value=new_value.dump()) - wrapper._parsed = new_value #pylint: disable=W0212 - new_value = wrapper - - if field_params and 'tag_type' in field_params and 'tag' in field_params: - if field_params['tag_type'] != new_value.tag_type or field_params['tag'] != new_value.tag: - new_value = new_value.retag(tag_type=field_params['tag_type'], tag=field_params['tag']) + new_value = self._make_value(field_name, field_spec, value_spec, field_params, value) if new_value.contents is None: - raise ValueError('Value for field "%s" of %s is not set' % (field_info[0], self.__class__.__name__)) + raise ValueError('Value for field "%s" of %s is not set' % (field_name, self.__class__.__name__)) self.children[key] = new_value @@ -1913,6 +1863,113 @@ class Sequence(Asn1Value): if cls._oid_pair is not None: cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) + def _determine_spec(self, index): + """ + Determine how a value for a field should be constructed + + :param index: + The field number + + :return: + A tuple containing the following elements: + - unicode string of the field name + - Ans1Value class of the field spec + - Asn1Value class of the value spec + - None or dict of params to pass to the field spec + - None or Asn1Value class indicating the value spec was derived fomr an OID or a spec callback + """ + + info = self._fields[index] + field_params = info[2] if len(info) > 2 else {} + + field_spec = info[1] + value_spec = field_spec + spec_override = None + + if self._spec_callbacks is not None and info[0] in self._spec_callbacks: + callback = self._spec_callbacks[info[0]] + spec_override = callback(self) + if spec_override: + # Allow a spec callback to specify both the base spec and + # the override, for situations such as OctetString and parse_as + if isinstance(spec_override, tuple) and len(spec_override) == 2: + field_spec, value_spec = spec_override #pylint: disable=W0633 + else: + value_spec = spec_override + + elif self._oid_nums is not None and self._oid_nums[1] == index: + oid = self._lazy_child(self._oid_nums[0]).native + if oid in self._oid_specs: + spec_override = self._oid_specs[oid] + value_spec = spec_override + + return (info[0], field_spec, value_spec, field_params, spec_override) + + def _make_value(self, field_name, field_spec, value_spec, field_params, value): + """ + Contructs an appropriate Asn1Value object for a field + + :param field_name: + A unicode string of the field name + + :param field_spec: + An Asn1Value class that is the field spec + + :param value_spec: + An Asn1Value class that is the vaue spec + + :param field_params: + None or a dict of params for the field spec + + :param value: + The value to construct an Asn1Value object from + + :return: + An instance of a child class of Asn1Value + """ + + specs_different = field_spec != value_spec + is_any = issubclass(field_spec, Any) + + if issubclass(value_spec, Choice): + if not isinstance(value, Asn1Value): + raise ValueError('Can not set a native python value to %s, which has the choice type of %s – value must be an instance of Asn1Value' % (field_name, value_spec.__name__)) + if not isinstance(value, value_spec): + wrapper = value_spec() + wrapper.validate(value.class_, value.tag) + wrapper._parsed = value #pylint: disable=W0212 + new_value = wrapper + else: + new_value = value + + elif isinstance(value, field_spec): + new_value = value + if specs_different: + new_value.parse(value_spec) + + elif (not specs_different or is_any) and not isinstance(value, value_spec): + new_value = value_spec(value, **field_params) + + else: + if isinstance(value, value_spec): + new_value = value + else: + new_value = value_spec(value) + + # For when the field is OctetString or OctetBitString with embedded + # values we need to wrap the value in the field spec to get the + # appropriate encoded value. + if specs_different and not is_any: + wrapper = field_spec(value=new_value.dump(), **field_params) + wrapper._parsed = new_value #pylint: disable=W0212 + new_value = wrapper + + if field_params and 'tag_type' in field_params and 'tag' in field_params: + if field_params['tag_type'] != new_value.tag_type or field_params['tag'] != new_value.tag: + new_value = new_value.retag(tag_type=field_params['tag_type'], tag=field_params['tag']) + + return new_value + def _parse_children(self, recurse=False): """ Parses the contents and generates Asn1Value objects based on the @@ -1940,37 +1997,18 @@ class Sequence(Asn1Value): parts, num_bytes = _parse(self.contents, pointer=child_pointer) if field < len(self._fields): - field_info = self._fields[field] - field_params = field_info[2] if len(field_info) > 2 else {} - - field_spec = field_info[1] - value_spec = field_spec - spec_override = None - - if self._spec_callbacks is not None and field_info[0] in self._spec_callbacks: - callback = self._spec_callbacks[field_info[0]] - spec_override = callback(self) - if spec_override: - # Allow a spec callback to specify both the base spec and - # the override, for situations such as OctetString and parse_as - if isinstance(spec_override, tuple) and len(spec_override) == 2: - field_spec, value_spec = spec_override #pylint: disable=W0633 - else: - value_spec = spec_override - - elif self._oid_nums is not None and self._oid_nums[1] == field: - oid = self._lazy_child(self._oid_nums[0]).native - if oid in self._oid_specs: - spec_override = self._oid_specs[oid] - value_spec = spec_override - - # If the next value is optional or default, allow it to not be present + _, field_spec, value_spec, field_params, spec_override = self._determine_spec(field) + + # If the next value is optional or default, allow it to be absent if 'optional' in field_params or 'default' in field_params: id_ = (parts[0], parts[2]) no_id_match = self._field_ids[field] != id_ not_any = field_spec != Any if no_id_match and not_any: + + # See if the value is a valid choice before assuming + # that we have a missing optional or default value choice_match = False if issubclass(field_spec, Choice): try: -- cgit v1.2.3 From 417eae5f9a756aae85cc98f22f1d0bb9c24b1b55 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 11:14:37 -0400 Subject: When creating a Sequence from scratch, make sure the default values are populated --- asn1crypto/core.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 56dd33f..12e4362 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1984,7 +1984,12 @@ class Sequence(Asn1Value): """ if self.contents is None: - self.children = [NoValue()] * len(self._fields) + if self._fields: + self.children = [NoValue()] * len(self._fields) + for index, info in enumerate(self._fields): + if len(info) > 2 and 'default' in info[2]: + field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) + self.children[index] = self._make_value(field_name, field_spec, value_spec, field_params, None) return try: -- cgit v1.2.3 From 2ddd95f50fcf5b2b7d21c2f540212af56fcbcf3b Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 11:16:02 -0400 Subject: Removed extra unnecessary code from SequenceOf._set_contents() that had been pulled from Sequence --- asn1crypto/core.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 12e4362..072a440 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2333,22 +2333,8 @@ class SequenceOf(Asn1Value): self._parse_children() self.contents = b'' - for index, info in enumerate(self._fields): - child = self.children[index] - if child is None: - child_dump = b'' - elif isinstance(child, tuple): - if force: - child_dump = self._lazy_child(index).dump(force=force) - else: - child_dump = child[3] + child[4] + child[5] - else: - child_dump = child.dump(force=force) - # Skip values that are the same as the default - if len(info) > 2 and 'default' in info[2]: - default_value = info[1](**info[2]) - if default_value.dump() == child_dump: - continue + for child in self: + child_dump = child.dump(force=force) self.contents += child_dump self.header = None if self.trailer != b'': @@ -2369,6 +2355,8 @@ class SequenceOf(Asn1Value): try: self.children = [] + if self.contents is None: + return contents_length = len(self.contents) child_pointer = 0 while child_pointer < contents_length: -- cgit v1.2.3 From 77b0ccdee3e4ddf7ef0ac293d5e080064c03d698 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 11:17:02 -0400 Subject: Fixed typos in OCSP code --- asn1crypto/ocsp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index e5bd5db..efd7283 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -89,7 +89,7 @@ class RequestExtensions(SequenceOf): class TBSRequestExtensionId(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.2': 'ocsp_noonce', + '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', '1.3.6.1.5.5.7.48.1.4': 'ocsp_response', '1.3.6.1.5.5.7.48.1.8': 'ocsp_preferred_signature_algorithms', } @@ -104,7 +104,7 @@ class TBSRequestExtension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'ocsp_noonce': OctetString, + 'ocsp_nonce': OctetString, 'ocsp_response': AcceptableResponses, 'ocsp_preferred_signature_algorithms': PreferredSignatureAlgorithms, } @@ -116,7 +116,7 @@ class TBSRequestExtensions(SequenceOf): class ResponseDataExtensionId(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.2': 'ocsp_noonce', + '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', '1.3.6.1.5.5.7.48.1.9': 'ocsp_extended_revoke', } @@ -130,7 +130,7 @@ class ResponseDataExtension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'ocsp_noonce': OctetString, + 'ocsp_nonce': OctetString, 'ocsp_extended_revoke': Null, } @@ -239,7 +239,7 @@ class OCSPResponseStatus(Enumerated): 2: 'internal_error', 3: 'try_later', 5: 'sign_required', - 6: 'unauthoried', + 6: 'unauthorized', } -- cgit v1.2.3 From 66babf687be79c75d7d97d4ce233446f5a7774c2 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 11:51:57 -0400 Subject: Add .sha1 and .sha256 properties to PublicKeyInfo that calculate over the DER-encoded contents of the public_key field --- asn1crypto/keys.py | 24 ++++++++++++++++++++++++ tests/test_keys.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 07b9576..11d7fd3 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -828,6 +828,8 @@ class PublicKeyInfo(Sequence): _algorithm = None _bit_size = None _fingerprint = None + _sha1 = None + _sha256 = None @classmethod def wrap(cls, public_key, algorithm): @@ -966,6 +968,28 @@ class PublicKeyInfo(Sequence): return int(math.ceil(self.bit_size / 8)) + @property + def sha1(self): + """ + :return: + The SHA1 hash of the DER-encoded bytes of this public key info + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(byte_cls(self['public_key'])).digest() + return self._sha1 + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this public key info + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(byte_cls(self['public_key'])).digest() + return self._sha256 + @property def fingerprint(self): """ diff --git a/tests/test_keys.py b/tests/test_keys.py index 6b4bf8e..fc59ff8 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -284,6 +284,40 @@ class KeysTests(unittest.TestCase): key_info['attributes'].native ) + #pylint: disable=C0326 + @staticmethod + def key_sha1_hashes(): + return ( + ('keys/test-public-der.key', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ('keys/test-public-dsa-der.key', b'\x81\xa37\x86\xf9\x99(\xf2tp`\x87\xf2\xd3~\x8d\x19a\xa8\xbe'), + ('keys/test-public-ec-named-der.key', b'#\x8d\xee\xeeGH*\xe45T\xb8\xfdVh\x16_\xe2\xaa\xcd\x81'), + ('keys/test-public-ec-der.key', b'T\xaaTpl4\x1am\xeb]\x97\xd7\x1e\xfc\xd5$<\x8a\x0e\xd7'), + ) + + @data('key_sha1_hashes') + def sha1(self, relative_path, sha1): + with open(os.path.join(fixtures_dir, relative_path), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(sha1, public_key.sha1) + + #pylint: disable=C0326 + @staticmethod + def key_sha256_hashes(): + return ( + ('keys/test-public-der.key', b'\xd9\x80\xdf\x94J\x8e\x1e\xf5z\xd2o\x8eS\xa8\x03qX\x9a[\x17g\x12\x89\xc5\xcc\xca\x04\x94\xf2R|F'), + ('keys/test-public-dsa-der.key', b'<\x10X\xbf=\xe4\xec3\xb9\xb2 \x11\xce9\xca\xd4\x95\xcf\xf9\xbc\x91q]O\x8f4\xbf\xdb\xdc\xe2\xd6\x82'), + ('keys/test-public-ec-named-der.key', b'\x87e \xb4\x13\x8cu\xdd\x11\x92\xa4\xd9;\x8e\xe5"p\xb2\xb7\xa7\xcb8\x88\x16;f\xb9\xf8I\x86J\x1c'), + ('keys/test-public-ec-der.key', b'\xf3\xa3k\xe0\xbf\xa9\xd9sl\xaa\x99\xe7\x9c-\xec\xb9\x0e\xe2d\xe9\xc3$\xb9\x893\x99A\xc19ec_'), + ) + + @data('key_sha256_hashes') + def sha256(self, relative_path, sha256): + with open(os.path.join(fixtures_dir, relative_path), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(sha256, public_key.sha256) + #pylint: disable=C0326 @staticmethod def key_pairs(): -- cgit v1.2.3 From fc9bf822b991797e42955bb044e98cb87589ef74 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 11:58:26 -0400 Subject: Change the PEM test with headers to use an OrderedDict for header order consistency --- tests/test_pem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_pem.py b/tests/test_pem.py index 6494963..744a746 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import unittest import sys import os +from collections import OrderedDict from asn1crypto import pem @@ -51,7 +52,7 @@ class PEMTests(unittest.TestCase): ('keys/test-third.crt', 'keys/test-third-der.crt', 'CERTIFICATE', {}), ('keys/test-pkcs8.key', 'keys/test-pkcs8-der.key', 'PRIVATE KEY', {}), ('test-third.csr', 'test-third-der.csr', 'CERTIFICATE REQUEST', {}), - ('keys/test-aes128.key', 'keys/test-aes128-der.key', 'RSA PRIVATE KEY', {'Proc-Type': '4,ENCRYPTED', 'DEK-Info': 'AES-128-CBC,01F6EE04516C912788B11BD7377626C2'}), + ('keys/test-aes128.key', 'keys/test-aes128-der.key', 'RSA PRIVATE KEY', OrderedDict([('Proc-Type', '4,ENCRYPTED'), ('DEK-Info', 'AES-128-CBC,01F6EE04516C912788B11BD7377626C2')])), ) @data('unarmor_armor_files') -- cgit v1.2.3 From 2972012d4fd8011ddddd16147067babb2f6bac0b Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 12:01:24 -0400 Subject: Added business_category OID --- asn1crypto/x509.py | 1 + 1 file changed, 1 insertion(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 951e19f..7b55184 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -109,6 +109,7 @@ class NameType(ObjectIdentifier): '2.5.4.10': 'organization_name', '2.5.4.11': 'organizational_unit_name', '2.5.4.12': 'title', + '2.5.4.15': 'business_category', '2.5.4.41': 'name', '2.5.4.42': 'given_name', '2.5.4.43': 'initials', -- cgit v1.2.3 From 91d70326de922b15083df7c3b0d8567b67c2486e Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 12:04:55 -0400 Subject: Added .sha1 and .sha256 to x509.Name class --- asn1crypto/x509.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 7b55184..a80eafd 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -20,6 +20,7 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function +import hashlib from collections import OrderedDict from .core import ( @@ -168,6 +169,9 @@ class Name(Choice): ('', RDNSequence), ] + _sha1 = None + _sha256 = None + @property def native(self): if self.contents is None: @@ -184,6 +188,28 @@ class Name(Choice): self._native[type_val_type] = type_val['value'] return self._native + @property + def sha1(self): + """ + :return: + The SHA1 hash of the DER-encoded bytes of this name + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(self.dump()).digest() + return self._sha1 + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this name + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(self.dump()).digest() + return self._sha256 + class AnotherName(Sequence): _fields = [ -- cgit v1.2.3 From 47f78fd6621237ab45bc7a01d0934f9cde9c84ec Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 12:08:51 -0400 Subject: Improve variable name --- asn1crypto/x509.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index a80eafd..c11242b 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -180,12 +180,12 @@ class Name(Choice): self._native = OrderedDict() for rdn in self.chosen.native: for type_val in rdn: - type_val_type = type_val['type'] - if type_val_type in self._native: - self._native[type_val_type] = [self._native[type_val_type]] - self._native[type_val_type].append(type_val['value']) + field_name = type_val['type'] + if field_name in self._native: + self._native[field_name] = [self._native[field_name]] + self._native[field_name].append(type_val['value']) else: - self._native[type_val_type] = type_val['value'] + self._native[field_name] = type_val['value'] return self._native @property -- cgit v1.2.3 From 045db0e0c1fcb179d0289d972c3fde812bd50cb6 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 12:09:37 -0400 Subject: Add timestamping access method OID --- asn1crypto/x509.py | 1 + 1 file changed, 1 insertion(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index c11242b..5730aae 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -666,6 +666,7 @@ class AccessMethod(ObjectIdentifier): _map = { '1.3.6.1.5.5.7.48.1': 'ocsp', '1.3.6.1.5.5.7.48.2': 'ca_issuers', + '1.3.6.1.5.5.7.48.3': 'timestamping', } -- cgit v1.2.3 From fce1338d8837024c8de3e28ec71444d225de80f6 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 12:10:43 -0400 Subject: Added x509.Name.human_friendly for end-user display --- asn1crypto/x509.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5730aae..b98144a 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -124,6 +124,35 @@ class NameType(ObjectIdentifier): '1.3.6.1.4.1.311.60.2.1.3': 'incorporation_country', } + @property + def human_friendly(self): + """ + :return: + A human-friendly unicode string to display to users + """ + + return { + 'common_name': 'Common Name', + 'surname': 'Surname', + 'serial_number': 'Serial Number', + 'country_name': 'Country', + 'locality_name': 'Locality', + 'state_or_province_name': 'State/Province', + 'organization_name': 'Organization', + 'organizational_unit_name': 'Organizational Unit', + 'title': 'Title', + 'business_category': 'Business Category', + 'name': 'Name', + 'given_name': 'Given Name', + 'initials': 'Initials', + 'generation_qualifier': 'Generation Qualifier', + 'dn_qualifier': 'DN Qualifier', + 'email_address': 'Email Address', + 'incorporation_locality': 'Incorporation Locality', + 'incorporation_state_or_province': 'Incorporation State/Province', + 'incorporation_country': 'Incorporation Country', + }[self.native] + class NameTypeAndValue(Sequence): _fields = [ @@ -169,6 +198,7 @@ class Name(Choice): ('', RDNSequence), ] + _human_friendly = None _sha1 = None _sha256 = None @@ -188,6 +218,41 @@ class Name(Choice): self._native[field_name] = type_val['value'] return self._native + @property + def human_friendly(self): + """ + :return: + A human-friendly unicode string containing the parts of the name + """ + + if self._human_friendly is None: + data = OrderedDict() + for rdn in self.chosen: + for type_val in rdn: + field_name = type_val['type'].human_friendly + if field_name in data: + data[field_name] = [data[field_name]] + data[field_name].append(type_val['value']) + else: + data[field_name] = type_val['value'] + to_join = [] + for key in data: + value = data[key] + if isinstance(value, list): + value = ', '.join(value) + to_join.append('%s: %s' % (key, value.native)) + + has_comma = False + for element in to_join: + if element.find(',') != -1: + has_comma = True + break + + separator = ', ' if not has_comma else '; ' + self._human_friendly = separator.join(to_join[::-1]) + + return self._human_friendly + @property def sha1(self): """ -- cgit v1.2.3 From af1f5a8d7f6c62cb4223485a1214ccfecb392ee0 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 12:13:15 -0400 Subject: Added various convenience attributes to x509.Certificate --- asn1crypto/x509.py | 195 +++ tests/fixtures/geotrust_certs/codex.crt | 37 + .../globalsign_example_keys/IssuingCA-der.cer | Bin 0 -> 1262 bytes .../fixtures/globalsign_example_keys/IssuingCA.cer | 29 + .../fixtures/globalsign_example_keys/IssuingCA.key | 27 + .../fixtures/globalsign_example_keys/SSL1-der.cer | Bin 0 -> 1126 bytes tests/fixtures/globalsign_example_keys/SSL1.cer | 26 + tests/fixtures/globalsign_example_keys/SSL1.key | 27 + .../fixtures/globalsign_example_keys/SSL2-der.cer | Bin 0 -> 1214 bytes tests/fixtures/globalsign_example_keys/SSL2.cer | 28 + tests/fixtures/globalsign_example_keys/SSL2.key | 27 + .../fixtures/globalsign_example_keys/SSL3-der.cer | Bin 0 -> 1198 bytes tests/fixtures/globalsign_example_keys/SSL3.cer | 27 + tests/fixtures/globalsign_example_keys/SSL3.key | 27 + .../globalsign_example_keys/rootCA-der.cer | Bin 0 -> 991 bytes tests/fixtures/globalsign_example_keys/rootCA.cer | 23 + tests/fixtures/globalsign_example_keys/rootCA.key | 27 + tests/fixtures/lets_encrypt/isrgrootx1.pem | 31 + .../lets_encrypt/letsencryptauthorityx1.pem | 32 + .../lets_encrypt/letsencryptauthorityx2.pem | 32 + tests/test_x509.py | 1376 +++++++++++++++++--- 21 files changed, 1781 insertions(+), 190 deletions(-) create mode 100644 tests/fixtures/geotrust_certs/codex.crt create mode 100644 tests/fixtures/globalsign_example_keys/IssuingCA-der.cer create mode 100755 tests/fixtures/globalsign_example_keys/IssuingCA.cer create mode 100755 tests/fixtures/globalsign_example_keys/IssuingCA.key create mode 100644 tests/fixtures/globalsign_example_keys/SSL1-der.cer create mode 100755 tests/fixtures/globalsign_example_keys/SSL1.cer create mode 100755 tests/fixtures/globalsign_example_keys/SSL1.key create mode 100644 tests/fixtures/globalsign_example_keys/SSL2-der.cer create mode 100755 tests/fixtures/globalsign_example_keys/SSL2.cer create mode 100755 tests/fixtures/globalsign_example_keys/SSL2.key create mode 100644 tests/fixtures/globalsign_example_keys/SSL3-der.cer create mode 100755 tests/fixtures/globalsign_example_keys/SSL3.cer create mode 100755 tests/fixtures/globalsign_example_keys/SSL3.key create mode 100644 tests/fixtures/globalsign_example_keys/rootCA-der.cer create mode 100755 tests/fixtures/globalsign_example_keys/rootCA.cer create mode 100755 tests/fixtures/globalsign_example_keys/rootCA.key create mode 100644 tests/fixtures/lets_encrypt/isrgrootx1.pem create mode 100644 tests/fixtures/lets_encrypt/letsencryptauthorityx1.pem create mode 100644 tests/fixtures/lets_encrypt/letsencryptauthorityx2.pem diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index b98144a..f87700a 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -20,6 +20,8 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function +import sys +import re import hashlib from collections import OrderedDict @@ -52,6 +54,12 @@ from .core import ( from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo +if sys.version_info < (3,): + str_cls = unicode #pylint: disable=E0602 +else: + str_cls = str + + # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 # and a few other supplementary sources, mostly due to extra supported @@ -870,6 +878,10 @@ class Certificate(Sequence): _extended_key_usage_value = None _authority_information_access_value = None _ocsp_no_check_value = None + _issuer_serial = None + _authority_issuer_serial = False + _valid_domains = None + _valid_ips = None def _set_extensions(self): """ @@ -1047,3 +1059,186 @@ class Certificate(Sequence): if not self._processed_extensions: self._set_extensions() return self._ocsp_no_check_value + + @property + def public_key(self): + """ + :return: + The PublicKeyInfo object for this certificate + """ + + return self['tbs_certificate']['subject_public_key_info'] + + @property + def subject(self): + """ + :return: + The Name object for the subject of this certificate + """ + + return self['tbs_certificate']['subject'] + + @property + def issuer(self): + """ + :return: + The Name object for the issuer of this certificate + """ + + return self['tbs_certificate']['issuer'] + + @property + def serial_number(self): + """ + :return: + An integer of the certificate's serial number + """ + + return self['tbs_certificate']['serial_number'].native + + @property + def key_identifier(self): + """ + :return: + None or a byte string of the certificate's key identifier from the + key identifier extension + """ + + if not self.key_identifier_value: + return None + + return self.key_identifier_value.native + + @property + def issuer_serial(self): + """ + :return: + A byte string of the SHA-256 hash of the issuer concatenated with + the ascii character ":", concatenated with the serial number as + an ascii string + """ + + if self._issuer_serial is None: + self._issuer_serial = self.issuer.sha256 + b':' + str_cls(self.serial_number).encode('ascii') + return self._issuer_serial + + @property + def authority_key_identifier(self): + """ + :return: + None or a byte string of the key_identifier from the authority key + identifier extension + """ + + if not self.authority_key_identifier_value: + return None + + return self.authority_key_identifier_value['key_identifier'].native + + @property + def authority_issuer_serial(self): + """ + :return: + None or a byte string of the SHA-256 hash of the isser from the + authority key identifier extension concatenated with the ascii + character ":", concatenated with the serial number from the + authority key identifier extension as an ascii string + """ + + if self._authority_issuer_serial is False: + if self.authority_key_identifier_value and self.authority_key_identifier_value['authority_cert_issuer'].native: + authority_issuer = self.authority_key_identifier_value['authority_cert_issuer'][0].chosen + # We untag the element since it is tagged via being a choice from GeneralName + authority_issuer = authority_issuer.untag() + authority_serial = self.authority_key_identifier_value['authority_cert_serial_number'].native + self._authority_issuer_serial = authority_issuer.sha256 + b':' + str_cls(authority_serial).encode('ascii') + else: + self._authority_issuer_serial = None + return self._authority_issuer_serial + + @property + def crl_urls(self): + """ + :return: + A list of zero or more unicode strings of the CRL URLs for this cert + """ + + if not self.crl_distribution_points_value: + return [] + + output = [] + for entry in self.crl_distribution_points_value: + distribution_point_name = entry['distribution_point'] + # RFC5280 indicates conforming CA should not use the relative form + if distribution_point_name.name == 'name_relative_to_crl_issuer': + continue + for general_name in distribution_point_name.chosen: + if general_name.name == 'uniform_resource_identifier': + output.append(general_name.native) + + return output + + @property + def ocsp_urls(self): + """ + :return: + A list of zero or more unicode strings of the OCSP URLs for this + cert + """ + + if not self.authority_information_access_value: + return [] + + output = [] + for entry in self.authority_information_access_value: + if entry['access_method'].native == 'ocsp': + output.append(entry['access_location'].native) + + return output + + @property + def valid_domains(self): + """ + :return: + A list of unicode strings of valid domain names for the certificate. + Wildcard certificates will have a domain in the form: *.example.com + """ + + if self._valid_domains is None: + self._valid_domains = [] + + # If the common name in the subject looks like a domain, add it + pattern = re.compile('^(\\*\\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$') + for rdn in self.subject.chosen: + for name_type_value in rdn: + if name_type_value['type'].native == 'common_name': + value = name_type_value['value'].native + if pattern.match(value): + self._valid_domains.append(value) + + # For the subject alt name extension, we can look at the name of + # the choice selected since it distinguishes between domain names, + # email addresses, IPs, etc + if self.subject_alt_name_value: + for general_name in self.subject_alt_name_value: + if general_name.name == 'dns_name' and general_name.native not in self._valid_domains: + self._valid_domains.append(general_name.native) + + return self._valid_domains + + @property + def valid_ips(self): + """ + :return: + A list of unicode strings of valid IP addresses for the certificate + """ + + if self._valid_ips is None: + self._valid_ips = [] + + if self.subject_alt_name_value: + for general_name in self.subject_alt_name_value: + if general_name.name == 'ip_address': + self._valid_ips.append(general_name.native) + + return self._valid_ips diff --git a/tests/fixtures/geotrust_certs/codex.crt b/tests/fixtures/geotrust_certs/codex.crt new file mode 100644 index 0000000..bd3a52c --- /dev/null +++ b/tests/fixtures/geotrust_certs/codex.crt @@ -0,0 +1,37 @@ +-----BEGIN CERTIFICATE----- +MIIGizCCBXOgAwIBAgIQYg46WU6Gb9G2jdI/8qVAIjANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMX +R2VvVHJ1c3QgRVYgU1NMIENBIC0gRzQwHhcNMTQxMjE3MDAwMDAwWhcNMTUxMDI5 +MjM1OTU5WjCB1DETMBEGCysGAQQBgjc8AgEDEwJVUzEeMBwGCysGAQQBgjc8AgEC +FA1NYXNzYWNodXNldHRzMR0wGwYDVQQPExRQcml2YXRlIE9yZ2FuaXphdGlvbjES +MBAGA1UEBRMJNDcxNzE0NjM5MQswCQYDVQQGEwJVUzEWMBQGA1UECBQNTWFzc2Fj +aHVzZXR0czEQMA4GA1UEBxQHTmV3YnVyeTEeMBwGA1UEChQVQ29kZXggTm9uIFN1 +ZmZpY2l0IExDMRMwEQYDVQQDFApjb2RleG5zLmlvMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAn1IWBm9rvLbYyxu10iBprifB5hVJEWa7mQaXqU54v3up +llfcZzqpL2Bk0MHIfPTbxwg1n3B9m4+Ey1HOgWeiVFxOHA8dI6SgXCSAOvxPVLsT +cy82iNBVpBvJ+Ci1u3lLUh8UzS0j2eci8U+3rVrwrMjiaboexCIV1bXQteiOihWh +YnEpsqHivPXX8DQ6rvG9PMBXCVPkwqJLA0GAzGdwvddyqMa5760Yv2170XpwNg7v +wih3AH3pMKX39Cgd+Jn8zhzuB0RgYdkQuEcqI8V3O24DwBW6hLyVL3ajFDr1VQb1 +kLT219G6rMND8PzSfaNiYvitPSjsHbDNFmD8hHppBwIDAQABo4IC4zCCAt8wUgYD +VR0RBEswSYIOZGV2LmNvZGV4bnMuaW+CDXJjLmNvZGV4bnMuaW+CEXBhY2thZ2Vj +b250cm9sLmlvggl3Ym9uZC5uZXSCCmNvZGV4bnMuaW8wCQYDVR0TBAIwADAOBgNV +HQ8BAf8EBAMCBaAwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2dtLnN5bWNiLmNv +bS9nbS5jcmwwgaAGA1UdIASBmDCBlTCBkgYJKwYBBAHwIgEGMIGEMD8GCCsGAQUF +BwIBFjNodHRwczovL3d3dy5nZW90cnVzdC5jb20vcmVzb3VyY2VzL3JlcG9zaXRv +cnkvbGVnYWwwQQYIKwYBBQUHAgIwNQwzaHR0cHM6Ly93d3cuZ2VvdHJ1c3QuY29t +L3Jlc291cmNlcy9yZXBvc2l0b3J5L2xlZ2FsMB0GA1UdJQQWMBQGCCsGAQUFBwMB +BggrBgEFBQcDAjAfBgNVHSMEGDAWgBTez1xQt64CHxUXqhboDbUonWpa8zBXBggr +BgEFBQcBAQRLMEkwHwYIKwYBBQUHMAGGE2h0dHA6Ly9nbS5zeW1jZC5jb20wJgYI +KwYBBQUHMAKGGmh0dHA6Ly9nbS5zeW1jYi5jb20vZ20uY3J0MIIBAwYKKwYBBAHW +eQIEAgSB9ASB8QDvAHUApLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BAA +AAFKVctwzgAABAMARjBEAiBhrEBQ6LfRQnQ2EQvOlsFtTu906MGMT+eXpr8RiJQ5 +bAIgesSFdYX7Qqs5AmV4/A6UQswos0iUc9NrvkUjnCe5+FMAdgBo9pj4H2SCvjqM +7rkoHUz8cVFdZ5PURNEKZ6y7T0/7xAAAAUpVy3D0AAAEAwBHMEUCIGJv8m9tSDUh +b8Cx/+GKLZbgqhoR5KvmwqnUkomve8BUAiEAjDx9OdDiIpoFNTrabIAqFy4l9/z8 +AcGY8ZYJNLjl5YIwDQYJKoZIhvcNAQELBQADggEBAFk2w3U4Rxa3pHBjuuq26ZZx +S38UV2MSGon2Vr0pNtsRs4A0lcfqwX63CljuEWVCWQcLap00CgnfL4Lk0Nvt3HNB +Vh3/rXzxyOUXvPzWhatyMmjl19y/iO6JGnEfPkk9Xzv6iLfCB0aZhm9V0pswbbKg +bST1kUrftEBMeAqOQYgK0wvFu2TB5wD1dnI5fziLsvBndq2jScvBr20XVpMrORQ/ +K7+RlP1jWhlfoE3WMm9DlShpq+b67vP8JxySbeY8JJtG61W5re8TMFrDZXbnWJOt +4QVPZgEO87IX3vyrTu9XBmJ/vknv8NY1khUXpE/F/ONWZ4wDltEGOOr8jDOWCp8= +-----END CERTIFICATE----- diff --git a/tests/fixtures/globalsign_example_keys/IssuingCA-der.cer b/tests/fixtures/globalsign_example_keys/IssuingCA-der.cer new file mode 100644 index 0000000..159b855 Binary files /dev/null and b/tests/fixtures/globalsign_example_keys/IssuingCA-der.cer differ diff --git a/tests/fixtures/globalsign_example_keys/IssuingCA.cer b/tests/fixtures/globalsign_example_keys/IssuingCA.cer new file mode 100755 index 0000000..7e6ebad --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/IssuingCA.cer @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE6jCCA9KgAwIBAgIGJ5o5Cj9oMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNVBAYT +AlVTMQswCQYDVQQIDAJNQTEPMA0GA1UEBwwGQm9zdG9uMRQwEgYDVQQKDAtFeGFt +cGxlIExMQzEQMA4GA1UECwwHUm9vdCBDQTEVMBMGA1UEAwwMVGVzdCBSb290IENB +MB4XDTEzMDcxNzE0NDUzMVoXDTE4MDcxODE0NDUzMVowcDELMAkGA1UEBhMCVVMx +CzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xFDASBgNVBAoMC0V4YW1wbGUg +TExDMRMwEQYDVQQLDApJc3N1aW5nIENBMRgwFgYDVQQDDA9UZXN0IElzc3Vpbmcg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxf7DkFf5wnKo9v9Rs +fVJ0Vw6F2rNhkN5PzYg44Hw+Ned1Hp+N2CJhAVgleTecCcsEMcCzuvOUorvXqJI+ +QRnGC51APNIPqowA2H3VMpl7MxlLdhCS/e5dEWbTK74CDqUJfHqlALX4AWmVwUD6 +Rh6SX7Y+Hx4OVHEDStM4K1YBJPj5mRl0SvAK3UNHxAezISmLOf8mYaVlYzXMqXgP +LqZZGATIKfwEJg5fOzDq59++EOWCDHxmHQEPt0UYVfpZhTfsTe9WhLkyvtFGGxDZ +lCSnxWQHYQmcU5+12xNFqD/3GU1ayIrpcl4KSW/v1h8aiIs7zWJWPvCHGekylr8R +aiOfAgMBAAGjggGOMIIBijAdBgNVHQ4EFgQUJ/gv6V3XDfSo6oeZPf2Os55A0JEw +HwYDVR0jBBgwFoAUZHxc4eBgOE5InwW8VWN+P65N9x4wDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAQYwTAYDVR0gBEUwQzBBBgkrBgEEAaAyATwwNDAyBggr +BgEFBQcCARYmaHR0cHM6Ly93d3cuZ2xvYmFsc2lnbi5jb20vcmVwb3NpdG9yeS8w +QAYDVR0fBDkwNzA1oDOgMYYvaHR0cDovL2NybC5nbG9iYWxzaWduLmNvbS9ncy90 +cnVzdHJvb3RjYXRnMi5jcmwwgZYGA1UdHgSBjjCBi6BXMA6CDG9ubHl0aGlzLmNv +bTBFpEMwQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1BMQ8wDQYDVQQHEwZCb3N0 +b24xFDASBgNVBAoTC0V4YW1wbGUgTExDoTAwCocIAAAAAAAAAAAwIocgAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwDQYJKoZIhvcNAQEFBQADggEBALDg +An3XGGE8zPgRZT7enszDZZdw0+8l5ZKoEWqr2xjkYRXGipcI79KOX4ncxzsnc5f5 +dSPqJVEF2ezn0qQsn+c8irbe4OIb/1cYWvRZbk1s5mWHzbr+ODvzUOTec0cUffC5 +SbPVdrCWxkatBktLV2ByEJBjLMkPR0pvs6+S1Vf/X8wACaTuIvSzoELUAYAuRnOO +FQZbDHKlRJ989nunrcVeZlGwucEm44CAq8/9JO+dqk+dBxL5Yud26A3Xs3v5Jmlp +yZdglK8H7yQRg3j2j55t/RqjGEuuqEaZ/BZORbtkpyOHMoELkoVosd65nczUQn0i +Q6ri1isyJNSKOUO/irI= +-----END CERTIFICATE----- diff --git a/tests/fixtures/globalsign_example_keys/IssuingCA.key b/tests/fixtures/globalsign_example_keys/IssuingCA.key new file mode 100755 index 0000000..22d5f19 --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/IssuingCA.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAsX+w5BX+cJyqPb/UbH1SdFcOhdqzYZDeT82IOOB8PjXndR6f +jdgiYQFYJXk3nAnLBDHAs7rzlKK716iSPkEZxgudQDzSD6qMANh91TKZezMZS3YQ +kv3uXRFm0yu+Ag6lCXx6pQC1+AFplcFA+kYekl+2Ph8eDlRxA0rTOCtWAST4+ZkZ +dErwCt1DR8QHsyEpizn/JmGlZWM1zKl4Dy6mWRgEyCn8BCYOXzsw6uffvhDlggx8 +Zh0BD7dFGFX6WYU37E3vVoS5Mr7RRhsQ2ZQkp8VkB2EJnFOftdsTRag/9xlNWsiK +6XJeCklv79YfGoiLO81iVj7whxnpMpa/EWojnwIDAQABAoIBAAiyU+1o8nV8B49M +9dB293JBzbFbPMy791h7noAC57N4mqWPYYvmmhCcqz/yx3m6tRq4gVONBmAy9Pcl +CD1KnUOp0AOUt0oTNhbYhJnMh96Ua1naKAe7r1EaCCqyivW41/c2BSBOf5vuHck7 +lb5tbxQG4nv6tFNJadwab2ziGq2lmDV9qxS0nXcLdijhM2I+5GmIWj80ydFpU6J0 +QraB1NqGD+YtLI+8BaksUlkAZmDjShOCgASylAcsmc4E9WmFyGAiemsLPKftb5Px +8Ra8wVqibbylPif5JSXBH7JbJN/eGEmKh6KfYjdPo/yBufPf9kybzPGGCqBwtzKy +ea/pBiECgYEA15ADtdv2rT4TmegwHy9w+jnd5MCixeL/mgH+2wKwq4K4Enyp/qaK +05WLw/U9qrjzCIykdpYSl78oD+Ww7QrQ6raZG+Org/anCaqrYKDZF/1dRYDEt5mi +dPCHNugLL5HDIPcuSLcmL4BQm0X9IgvaHQ9bBYxLou8JXq8Hvb4/4u8CgYEA0su8 +aL9W4NgI8p1BxQAyuTW2sHoSqcIUHj2jvmSLvmB3gvp+GODeU85j9p7dCr4HEHpD +d1Oe4goce92EK6UILnExjZEr/MaRzMJNFaiZPQc5nAIv/RXNYWQeujRobTInrkqp +thsYiEFF+1GdKEz0QaQbfzRiv3vk4U1zY60KClECgYEA02iIWwkZShrBeoX++/a5 +JI8wEbLjcJQJ/e7LFdvzjKGtCWR+DCMlsBDQfCS+j/rHT7EvcqYIIg71qXGpLTEY +Z7khO/rzMX7rn01kumXFxANWQF3jj/T7IRjsY2r73XFlH6WMHQCSUK/VXhMsCQH6 +rdlreWt4mpk4ZUXfn7VATr0CgYAlOiHeBdyb/Msnvan91pkeqGPJKuXc4Q+Yf55J +Y4xiZLr2gLKARkY9WrfAuDGlUgYBXPZJPpVSqiJ5pZdP9edJ/GeZ7sdr7s2U8cOX +TZ0yb/I2oRREh/MrffkHPXYrwq3LVBhAtuxQM+beCX3Nvjls1kSc5G2ED6dOOtVk +Bw084QKBgQCmrqzaRmIt7ojggcGoNSc246q45bHpNJRZ+xEl1WLLW37KH2beO3ya +VNoZVfTQR4fUv0r5gtKRC8McIxvyUu6de5PZP3NPeaUfO+mipwFe6Z2P1G01jgP8 +BgBFq13mv1H3fbFcFyhdkw5KWKkveFfUi5Cm9c2LC6MebaxUU8Fm0g== +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/globalsign_example_keys/SSL1-der.cer b/tests/fixtures/globalsign_example_keys/SSL1-der.cer new file mode 100644 index 0000000..32d72c5 Binary files /dev/null and b/tests/fixtures/globalsign_example_keys/SSL1-der.cer differ diff --git a/tests/fixtures/globalsign_example_keys/SSL1.cer b/tests/fixtures/globalsign_example_keys/SSL1.cer new file mode 100755 index 0000000..25b5bc3 --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/SSL1.cer @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEYjCCA0qgAwIBAgIFYv0+d6owDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMC +VVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xFDASBgNVBAoMC0V4YW1w +bGUgTExDMRMwEQYDVQQLDApJc3N1aW5nIENBMRgwFgYDVQQDDA9UZXN0IElzc3Vp +bmcgQ0EwHhcNMTMwNzE3MTQyNTQwWhcNMTMwODMxMTQyNTQwWjAAMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwe6NSvHnvvL06kAvamgbnHdcdvZInPoz +qZ+E7T1PmBAe/HNAWtkbdb45PNV5sNG9b25QkJCHZkYuzNVDnZMDzU+ecevnu8H2 +i7qOgxrik5SjnhvjUKpqrLT+n8ypTKu1gYrt0eVrHVVOzCGWCyQsIX/x2oIzc12V +dRGYZYtr68+x9SXRkuGkZINjDx7nwRGajCRwCJ6vzB0QV/nnQ0461KhNyBmdQSvU +INXLOIyjRHtWa0zND7FGWgdmoaHftptHkEbrZvQT0EMzsVOGv7ZHNNfP1KRGxvWZ +1rQlrfko8UxMDJR9rG3BzrArSIUo1qztnkXhbCqcMc1kIRLF5Tq3DQIDAQABo4IB +cTCCAW0wDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggr +BgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSUYQSSBEzm/2iolq950vMy +hK5bzzAfBgNVHSMEGDAWgBQn+C/pXdcN9Kjqh5k9/Y6znkDQkTAfBgNVHREEGDAW +ghRhbnl0aGluZy5leGFtcGxlLmNvbTB8BggrBgEFBQcBAQRwMG4wKAYIKwYBBQUH +MAGGHGh0dHA6Ly9vY3NwLmV4YW1wbGVvdmNhLmNvbS8wQgYIKwYBBQUHMAKGNmh0 +dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L3RydXN0cm9vdGNhdGcy +LmNydDBMBgNVHSAERTBDMEEGCSsGAQQBoDIBPDA0MDIGCCsGAQUFBwIBFiZodHRw +czovL3d3dy5nbG9iYWxzaWduLmNvbS9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQUF +AAOCAQEAZpyLnjpPpgMJY5eD9+3vNhqptmSS6+m84DI7+iq88vtKyT5sbY2o9WU7 +m3J0ZfpIz3cuMDpvTsR4B0P7RKUIE+qgBmu3lreYk5r72WOopXCYH19ZuQsEYo+S +kU3sDM8vfpVOX/oOEJ+XwRbxwrIGL21s8yKuTKSdY1OmbeTvlAaN5cqaBxO/jiP0 +hVAgFPSfufvg8LBFmsjILqFzBPwZ0hWTCiJzRzu2PFGTYYQMZD+s+VX1Wbcpxiz3 +LkmWj/GGgMAZtWh/g7mKQe/tsL3APebsfXbMMs2uy3TU2zAzbI5s/rYAaEOH7s65 +aUGW6khL6LspncsQ36eg+pusb7LF/g== +-----END CERTIFICATE----- diff --git a/tests/fixtures/globalsign_example_keys/SSL1.key b/tests/fixtures/globalsign_example_keys/SSL1.key new file mode 100755 index 0000000..d15cece --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/SSL1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwe6NSvHnvvL06kAvamgbnHdcdvZInPozqZ+E7T1PmBAe/HNA +Wtkbdb45PNV5sNG9b25QkJCHZkYuzNVDnZMDzU+ecevnu8H2i7qOgxrik5Sjnhvj +UKpqrLT+n8ypTKu1gYrt0eVrHVVOzCGWCyQsIX/x2oIzc12VdRGYZYtr68+x9SXR +kuGkZINjDx7nwRGajCRwCJ6vzB0QV/nnQ0461KhNyBmdQSvUINXLOIyjRHtWa0zN +D7FGWgdmoaHftptHkEbrZvQT0EMzsVOGv7ZHNNfP1KRGxvWZ1rQlrfko8UxMDJR9 +rG3BzrArSIUo1qztnkXhbCqcMc1kIRLF5Tq3DQIDAQABAoIBAAgXGZHc0Zwnqovz +LYc03KIEYLkdwR27WlhjLTpwalefpItHi5G+qOSakOy2wyLbPRne8kF1phBgMSee +Zfm23lu8TJHYE4zDpLNjjvptLrKVatX3t93vng+iZVTpRs7KAwJqd01gUr2gh28A +n6/LTIQBQGerMtZHOyrtFvx1eoUVzHCWPg0Xfbd4cwr0FnNvvzd13XtHXC11j1oF +7RFs8NzvwO/wYvMMge6qkF+A4rWAmw0jZnBhjJ0i0LbnHaEp4eqpoTlyITht7lOZ +2jBXaXYMFs4KRueNdSk4n8kDQyPD5+fpRA3Lzy+3fIvALeExx38a3WkiWrnnWnCT +2+0VNwECgYEA41NK4gBt6ilhqTkbfKUq5TcZ5xw466mCQSBDNnHsilI4L1FfqCkc +Qtkz7jL00Gj2IH+3UJRY442hsb5VL39n6WHuLv3FYV2NogVn0UQteGnNN5oOuq74 +7q84Hax3tOCuNv4acC3nevk0sozT0YRx0v+XXVQi4vZRDIrEa49CFi0CgYEA2mTt +mm0KZ73U5YaxEyLPzNC564Iixh5Scl671QWhJcC92TO/HijHYErE+Rsy3U7xlsN3 +MBh3kS11E6xx588fmm5vl/rAtNSLXLezvATXevI5JTtrUp4KFdXaskUDN9rF0IhH +3gPf4z/6CuBUfTVsSqpyKpQGnO4im2LHSHTzkGECgYEAuATpDWJDl9a/0/kCozgh +LUQZl9hky4CAjK/NOPmn/aDpEoTQ5pPA6Oxi+WQOgdc1xsEcaAJuomY4imYFF1oP +iAFainernFHbIVk23VRParZbBbOUUNLreGwnBP5kOOvYm3O/eyftxsKNQix2G5kX +ezKkGUzOoOO8YGbE8j0ZxlECgYALEtEFYoADkJmJ5dF2se4taWvz6A5RU1pE2E7X +10g7fNFjgP8wzUqGtGPWaa2jkQwo49JYSvVNFCv6imTgJx1oHC9mWl2JDbnfQqVH +ZEt0vXFuVNv1PXQvdT94iI1IOLyM/Uv/kty4ThcklAlUq+/IvWm6hPTs4ho5HMIU +B3IOIQKBgDx81STXvlCHW5K4Xp7WXMeztabKEn1anEQFt4c3Y4aWkFpCxBJ7SfNL +MXeK26YTUCjk+DA6lSG6P/wIChYDAooc4/CMkabmLaOGp06ff3XirdaGJ4/K5jvY +31o8fBPEDJTlwMrc+5BRIp0ZufT2NCXEHRvsWpB3dYXrO4BsAIaP +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/globalsign_example_keys/SSL2-der.cer b/tests/fixtures/globalsign_example_keys/SSL2-der.cer new file mode 100644 index 0000000..3fa12a7 Binary files /dev/null and b/tests/fixtures/globalsign_example_keys/SSL2-der.cer differ diff --git a/tests/fixtures/globalsign_example_keys/SSL2.cer b/tests/fixtures/globalsign_example_keys/SSL2.cer new file mode 100755 index 0000000..46a9e6a --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/SSL2.cer @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEujCCA6KgAwIBAgIFYv0+d6owDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMC +VVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xFDASBgNVBAoMC0V4YW1w +bGUgTExDMRMwEQYDVQQLDApJc3N1aW5nIENBMRgwFgYDVQQDDA9UZXN0IElzc3Vp +bmcgQ0EwHhcNMTMwNzE3MTQyNTQ1WhcNMTMwODMxMTQyNTQ1WjBYMQswCQYDVQQG +EwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjEUMBIGA1UECgwLRXhh +bXBsZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAK5070sRytJr8Og0m64H5QILBNaHSZMr+1laKaMl340l +0sui22+1bqD65QVDdyioofkeAFcSZBMGPQVlcbhOCNfu11FvTpAnnD0nQUA8XhCg ++BPHVpyt6Ec7NnQRbBl3+OxXqOsiOHWxcYV7m6jVoCLTRXwBEnbY+hmsTEDtZpqM +eqkikQKSLiKHRlFQ7LKIQDdiQrEgK9jO3sPX8SR7iTOflSHL8Gx7pM4ea7Hsqtvx +lq2WX/RaNiBxBPOO6Wjy0K8muOCCXlVqREy9YjbLB5PRXJYm4vOeosEzhhjjn58p +j6Qks5BywY8XG+ycDmZX0fcYDt/ZbZA2ixdaFDz2+KMCAwEAAaOCAXEwggFtMA4G +A1UdDwEB/wQEAwIFoDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw +DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0rcVf2QwByhwg8oo+oiW3p78ij0wHwYD +VR0jBBgwFoAUJ/gv6V3XDfSo6oeZPf2Os55A0JEwHwYDVR0RBBgwFoIUYW55dGhp +bmcuZXhhbXBsZS5jb20wfAYIKwYBBQUHAQEEcDBuMCgGCCsGAQUFBzABhhxodHRw +Oi8vb2NzcC5leGFtcGxlb3ZjYS5jb20vMEIGCCsGAQUFBzAChjZodHRwOi8vc2Vj +dXJlLmdsb2JhbHNpZ24uY29tL2NhY2VydC90cnVzdHJvb3RjYXRnMi5jcnQwTAYD +VR0gBEUwQzBBBgkrBgEEAaAyATwwNDAyBggrBgEFBQcCARYmaHR0cHM6Ly93d3cu +Z2xvYmFsc2lnbi5jb20vcmVwb3NpdG9yeS8wDQYJKoZIhvcNAQEFBQADggEBAEGv +7ndOdEaASijk6kFupDEakCWDLm1HWGwEQ1+mjuatqGcA1MII8BbEjHb7irnvfIOT +OS/cU7CbgsZVQNEPtFmQVeuRAHX869nDTjSbLmQSwJXT1IP9ApGCGKeMYnzXSAAT +9qO8B5jcMu83xkT6UZV5K/xkKtIcCUk3ToaN1H+lc0PGDYtOxQ5He/n7Dlx9mGtb +M8uZyuuOja4Fyk+q5RI2HYVcW6w35aqEMIIsFv4P232PVy5acTmRhZZ4XenAhHzS +F3Y/Hs1WJK98UUjlWEyrkKo7l51D0p8dHvoIHKAnhNswuIbZgJRlsg/tESNK9jAg +L6Di/FIgN3DqzNOPmfI= +-----END CERTIFICATE----- diff --git a/tests/fixtures/globalsign_example_keys/SSL2.key b/tests/fixtures/globalsign_example_keys/SSL2.key new file mode 100755 index 0000000..bc71109 --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/SSL2.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEArnTvSxHK0mvw6DSbrgflAgsE1odJkyv7WVopoyXfjSXSy6Lb +b7VuoPrlBUN3KKih+R4AVxJkEwY9BWVxuE4I1+7XUW9OkCecPSdBQDxeEKD4E8dW +nK3oRzs2dBFsGXf47Feo6yI4dbFxhXubqNWgItNFfAESdtj6GaxMQO1mmox6qSKR +ApIuIodGUVDssohAN2JCsSAr2M7ew9fxJHuJM5+VIcvwbHukzh5rseyq2/GWrZZf +9Fo2IHEE847paPLQrya44IJeVWpETL1iNssHk9Fclibi856iwTOGGOOfnymPpCSz +kHLBjxcb7JwOZlfR9xgO39ltkDaLF1oUPPb4owIDAQABAoIBABR/7yJ+G7wwLOXM +UMLZcKKV0uK2kQG3OFjejGf8alF2sVd2cpyk0DQgZ0sAC39+mVHhoZ6ZraLCp+b7 +bap/mPBuw2RxVOUBko1pEHTQ4yjHEX+Ze+b7VIESRyrKZU50145GGrZOlh3WVQWf +acIkICYXd2HD6nyGsJTVtzwl6VmdmJSQQlRREEFtqXA2BYJkyAbzjR7NRJ6erK/b +ZaB1oamoRiOQNh2DUHJl6QkbaPsrnNouEgMuoXih7u7fAqGFNgsVV5yAKaLvjkE9 +MyOeOARGvSkTZM1WscUxSr+2JBC9298w6wGgnvuFhCDDlNGR/IPbzLetJeAc7rMD +tKa60dECgYEA24MQRprbrh/zSWiwLVwzZHqeqB29kclyqTDDvuGixjux9tgSsUmg +/0zMeYSRJgYoF14zvAVl5/YV9BYZ2xgqdbkEuQ1wjdQfRruwk1bqa5Ao0Q20WImu +SXMvCQDxRvKZIZsr/dQkZx/SQd2uOR44fZZZQP1NroUD3o6HaRmXwSkCgYEAy3Sg +7//zP4lg/5sGWHipR7e5qXTjkYU6ZDqvORShkzkE3I51E7sOkpEIKqfOzPwyE8cY +CbaeyLi4OcJ/4FLZUs6pRd8cYYnc86JJqZUi8LMWOwamiTFX/sGQ8JxjYMoE/ZuC +2+E3seQIdkFVkDtuJQM5URO3Rof/FoNbkmywaOsCgYAcoz6uV2mtj9GHlDbX1B2I +UE7+k9K1gFiLJieDcaBwyDzxfUMDCh4M8JIEkHz3PvpgAhQxxWqEFqDKlU+OO9re +POMW2WADwNbLvZTNxBsVKVuJ2oXavyuTvYk3XX4cyW2c6seUd+a/5XDi0u712LF6 +APFn/yPxTr0wfdvApGwd8QKBgBEzoivIgyN7FQVncQjn4sAai4sFQ/xYvFAfGhOE +aAjPiFaxgLqTVS8VLhCVMYnpRL6hVan0k8Y6v/C6Ph+UQaWbrXon2/lvM4wxy3KY +FmUtbxK8hDYTQvJaIUwGnOxhCDz8+fpnN1NGCWUeLwLL04szk5QES7md4/ZeUs61 +e9DTAoGASlUCBD7hlknR/JYplouCxkL/h9169kIcGIkRVmvmZNy4u00kYGXjnZMk +Moma8Q66+9qGmTtoSVrtzAhMtA/zwzcjPIizeSn2R7WegPXTKXkSXMULHLO7Zkln +fc59/oYiv0ArEa5O/y+f+/C2dVEobKslx1feQ+8vcz8Kz89O38Y= +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/globalsign_example_keys/SSL3-der.cer b/tests/fixtures/globalsign_example_keys/SSL3-der.cer new file mode 100644 index 0000000..b1cc2ff Binary files /dev/null and b/tests/fixtures/globalsign_example_keys/SSL3-der.cer differ diff --git a/tests/fixtures/globalsign_example_keys/SSL3.cer b/tests/fixtures/globalsign_example_keys/SSL3.cer new file mode 100755 index 0000000..d5bf8e5 --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/SSL3.cer @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEqjCCA5KgAwIBAgIFYv0+d6owDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMC +VVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xFDASBgNVBAoMC0V4YW1w +bGUgTExDMRMwEQYDVQQLDApJc3N1aW5nIENBMRgwFgYDVQQDDA9UZXN0IElzc3Vp +bmcgQ0EwHhcNMTMwNzE3MTQyODE0WhcNMTMwODMxMTQyODE0WjBpMQswCQYDVQQG +EwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjEUMBIGA1UECgwLRXhh +bXBsZSBMTEMxDzANBgNVBAoMBkdvb2dsZTEVMBMGA1UEAwwMKi5nb29nbGUuY29t +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwAowZIZZpk8P4YJkpiS2 +IcRrY2XjfklqbEz7JxjvB3qP0QvWYoru5vYcVEMjnFPZgbdUuxUhmh8atLp9Xj40 +t3J4st3YhMvmpTN0LCAG4yMDigxdBSkrkR8e3m8EFowmKNwo0XwGBYFVVXQwKrWu +cb28GEst4LCBH+TVPjJUgdfTowdPRr/k30mvw22J5aJRI53GnZ8V+hn3izb5zAZw +NhbAjjoHFTDnowEkhRyochmXv/2aSsoacqiaZnmH+WsWYR2BgqSrwOlPEuIw6QOM +XYGOB200mRlXjifR+fPOfmM71+0PqAnwkb+t10IJzbARoeqrEaWgRqlu9jZ8W8IE +2QIDAQABo4IBUDCCAUwwDgYDVR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRH3qTn6mDn +7jbI8dWwRgcHnkJozjAfBgNVHSMEGDAWgBQn+C/pXdcN9Kjqh5k9/Y6znkDQkTB8 +BggrBgEFBQcBAQRwMG4wKAYIKwYBBQUHMAGGHGh0dHA6Ly9vY3NwLmV4YW1wbGVv +dmNhLmNvbS8wQgYIKwYBBQUHMAKGNmh0dHA6Ly9zZWN1cmUuZ2xvYmFsc2lnbi5j +b20vY2FjZXJ0L3RydXN0cm9vdGNhdGcyLmNydDBMBgNVHSAERTBDMEEGCSsGAQQB +oDIBPDA0MDIGCCsGAQUFBwIBFiZodHRwczovL3d3dy5nbG9iYWxzaWduLmNvbS9y +ZXBvc2l0b3J5LzANBgkqhkiG9w0BAQUFAAOCAQEArh/qtAvF8NIBg+274/SLrqPA +QUttAKGYX22hunJdwctE57Kvh8XK2bq1ZuL+egEPvQ/18KzxRUxdt4OLcUA0hh6z +CoMalBT2t0ogILUZSLYXKwe6Ywk/UPbSGB225YHDeFLIK35T9AFaf45s5IQmOemi +2O2mIOADO7BnyoNOcx6aL42yuj4bn4/8bQ9zcE/6TdGi0IhaAeydS49Ir9Y9Iu4d +SARdBFm62hggIgOpw8d5UNnjNXYMfWwj5oSCmfWZcwTtwFUipnJhyfzZK+1kTT2w +NjgKxZ3QJ8bUAp7nfPtfh8424oablZ3kbOpusTVv0JN9YwvMxBkIrU7oEe2C1A== +-----END CERTIFICATE----- diff --git a/tests/fixtures/globalsign_example_keys/SSL3.key b/tests/fixtures/globalsign_example_keys/SSL3.key new file mode 100755 index 0000000..83d3090 --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/SSL3.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAwAowZIZZpk8P4YJkpiS2IcRrY2XjfklqbEz7JxjvB3qP0QvW +Yoru5vYcVEMjnFPZgbdUuxUhmh8atLp9Xj40t3J4st3YhMvmpTN0LCAG4yMDigxd +BSkrkR8e3m8EFowmKNwo0XwGBYFVVXQwKrWucb28GEst4LCBH+TVPjJUgdfTowdP +Rr/k30mvw22J5aJRI53GnZ8V+hn3izb5zAZwNhbAjjoHFTDnowEkhRyochmXv/2a +SsoacqiaZnmH+WsWYR2BgqSrwOlPEuIw6QOMXYGOB200mRlXjifR+fPOfmM71+0P +qAnwkb+t10IJzbARoeqrEaWgRqlu9jZ8W8IE2QIDAQABAoIBAB5RvYAxgffy5ZP6 +DT/57dN4+mdwD7HBj47XvJNYqWxp2kjr8IYQX1WRp7lZ/EZTKrUDJ9p9pJd7r7C2 +/NIjShlodkvvIJ8eviR48i+BQvUbcxSZjRoifOFlo28E4gVZTTEISV2BkkXOPJXI +SU6E7qzAgvDm9bBSzaAmddBjC9qP3amvSZRik2ywLUNVEf7GJxxPCMRyzRiKAGHI +hS7nhv8VVLlUYP1v8UvNM2z9qrwWu/Nq+vJqhM0dk/o2JxeXg6DD+I7te9ZnCy9u +h+3ptIeHbThoopuKgnQOCCQjA2333SJB+GiKGVX9fRZV3L7McpBOSR1VumP3/6hL +DMUJdT0CgYEA5UES0DkTHQcKJv3tkNJqCIooVg8Kzp4C/xe57eoe0JzrnmRM8azL +O5ykgExj2iu27u/kS8HLgEJNxJ/kvquFGJD20cXnEmTFhvHuP47V6UPBcPkVFtHh +IpVIyngz1mlHNTHrrWoWYkhcuBP18FTqbTX3u7IUhXZzOwrm3hHKSF8CgYEA1nGq +yPCGj03OBYe2UmVInaYycBmDZD/VIqpXvLMAimE4TPmUJ6WVoKZtSXC7+r3NBaOd +inlSWMgPZWFipxGDo3/y/lfrB7wEEslmXPd4BhXd+49mN/NH/fuOV7CU56eSXKWV +DJMMq+ZmeKyipKajUR+NEGNILHYkpUcCmLhPHccCgYA0Zr9qIOGhjO5hI0GeDLp3 +4Tx/D0klGTEOJdo164HHpVamCb8crqZ1pcRkHxHj2IIj82l3d4CQfJdSDko22vW9 +O8VvBZFfvvD3e2090eRLQVWCAS003hxbz0uoG/mdVMsV+acpKEqdhHTNDqL0oDRF +akSJ/pZ6OyzznfZPZDmceQKBgQC6QTvPD2owKanZj8hBxIrPsrx4NRC0D+U1GLLf +yLGdf1eBM/0EeoN9Z0/gy7PZ0uSyEywQS9PEHO+SZIVlCodFiSoq033l193J23e3 +I5Hx5yhJCIIF8p4C8WzuqQaMNjWflongxA/rdlBmW7tgOwP6v+ar5y+Wvn6Rtx2A +PAUrnQKBgGxXbN7rik57gXwrKGz98LmTGrEc6vfdqyI5t9pq3sDqWsrIh7wxbgCK +zJJWqHwFnTFPNkj71i+KcHdob6HQI/m8+ouuNg+m2Vy7Z7O6rPP7iqJ6JlQzsuPW +bebeeBKqRk5ZnFYPbVekd7d2O69xT02zMTbMRlqi3YzCv0DVs2ki +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/globalsign_example_keys/rootCA-der.cer b/tests/fixtures/globalsign_example_keys/rootCA-der.cer new file mode 100644 index 0000000..ff7bf04 Binary files /dev/null and b/tests/fixtures/globalsign_example_keys/rootCA-der.cer differ diff --git a/tests/fixtures/globalsign_example_keys/rootCA.cer b/tests/fixtures/globalsign_example_keys/rootCA.cer new file mode 100755 index 0000000..f46c97a --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/rootCA.cer @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID2zCCAsOgAwIBAgIHATeD1RoXvDANBgkqhkiG9w0BAQUFADBqMQswCQYDVQQG +EwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJvc3RvbjEUMBIGA1UECgwLRXhh +bXBsZSBMTEMxEDAOBgNVBAsMB1Jvb3QgQ0ExFTATBgNVBAMMDFRlc3QgUm9vdCBD +QTAeFw0xMzA3MTcxNDE4MzZaFw0yMzA3MTkxNDE4MzZaMGoxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJNQTEPMA0GA1UEBwwGQm9zdG9uMRQwEgYDVQQKDAtFeGFtcGxl +IExMQzEQMA4GA1UECwwHUm9vdCBDQTEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt7Oj0dJlpWQyy6mTH1hfqMSs +V30jqyjPktzbEQ5VnVbEHLQejGHGk7LVwx9UgYrnreV2qFJAMoBguIcXDNZN7tj1 +xEFsSaG8DA+44BMUNzJQSjDchnKCcgdbux8InrC9GLgJrbgKuZBRXplrycdsfhkA +7YlL7DnecKVwMTuNCywDtppHghQhKqBjxcUqgb2tgeF/F6c91PJ53q9AfrK2u5rs +tXALxuqqrf/yKfOyLQP2h3KAhXHf5xfEf/kuW/8SLiieZ1fLv0amp/t/uLjSgfq6 +3BDPeh+OCg0V2txczfJOzlGa4vYWJYij+U6jJG7qEAodKvOONHzXp5CVGmBIcQID +AQABo4GFMIGCMB0GA1UdDgQWBBRkfFzh4GA4TkifBbxVY34/rk33HjAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjBABgNVHR8EOTA3MDWgM6Axhi9odHRw +Oi8vY3JsLmdsb2JhbHNpZ24uY29tL2dzL3RydXN0cm9vdGNhdGcyLmNybDANBgkq +hkiG9w0BAQUFAAOCAQEAj18a0Ld9HnMKxxMZ4elz00W1LDcx8j5UD13+6C4W/NnP +o+sIoKJnUbzcxe8XVzjcTHt1RHx2niVq1BCLR6kJjEjY2c3QhXDficYB88VnctWd +CW3sTFlcl5WYJcbXwQ0k1DPMyNY0sz3s8QlFrn46l4WGq9YxMaboP4m6ZDkCrNC1 +Jq+QJdrKRPSint/Trwq++9PPENBJIrSckWwNGloATWCTgbBPfMxS1VpLciekzEKT +7Nc8t/akcqJjN2QFRNVj8GgcyPgGAN3RWStFjygI4IBQfH62GAcigDVRaF4+ieN4 +KnnYVcdSLlj3MQ3/grcy2h98h1GJD+STvYjB116/PA== +-----END CERTIFICATE----- diff --git a/tests/fixtures/globalsign_example_keys/rootCA.key b/tests/fixtures/globalsign_example_keys/rootCA.key new file mode 100755 index 0000000..850ae22 --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/rootCA.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAt7Oj0dJlpWQyy6mTH1hfqMSsV30jqyjPktzbEQ5VnVbEHLQe +jGHGk7LVwx9UgYrnreV2qFJAMoBguIcXDNZN7tj1xEFsSaG8DA+44BMUNzJQSjDc +hnKCcgdbux8InrC9GLgJrbgKuZBRXplrycdsfhkA7YlL7DnecKVwMTuNCywDtppH +ghQhKqBjxcUqgb2tgeF/F6c91PJ53q9AfrK2u5rstXALxuqqrf/yKfOyLQP2h3KA +hXHf5xfEf/kuW/8SLiieZ1fLv0amp/t/uLjSgfq63BDPeh+OCg0V2txczfJOzlGa +4vYWJYij+U6jJG7qEAodKvOONHzXp5CVGmBIcQIDAQABAoIBABwcjGw2g0GNFMzf +1VjNoE3mUu1MhCHUK/ewfoGcrPNX7Mjrs2UOLWI60sV6TOdKB2wwGjll5NcVmDeE +zL01KlXrs6hlzplx+6Ho4gTARq6vr2O7GHQmn9mtUJdRB3OpXjajKy//YvzEnf8Z +AUqujua5EtBG22x56pVYa9PM5ieYQPjujwjBg54RRh4Qu8Q6UNAszolbSAX1Gugn +URZgSib4Y/QYNibmD6KZ/ObQt300kWSDejj85Qd/orEaOgU9WIrxqhKwajRJvFsz +HPTicEvhXqYdCkHVf+fJErSYiA2N/NGS+OyPJS8JW7tx0Vmt+snehdIaPhjupVo8 +K+2No9UCgYEA8Ma9nbx1i0yWoay9EWXAVilgEyvauBI4D0IAwP2L2vUqNk7++TIm +qOqxjrkoxMCeFIHG3wleCqx/cMvtUCnJL0UT+6Ar7WunAj9UZJAQOxXG5znANy8C +Mn1ySOFV1JJU38siu8UADMuxZV6KMl7hHyzx7bgVMdqVKEfcjN64TNcCgYEAw1ET +rCnSCUfsMcPoBCdLp2H1uatGHllNeZ1dePPA+IlKrkVA8yfkVaznbelI94ig1Ses +og9VdbNd8bc9O2e0Hiykf/OZgVfWQFk/pTQO/0ESc2OHY8OhBvlyr9UekuaJLpiX +inWXCRDFiRGgtZpA3u3IK1k9KLuzmtQWm/wBY/cCgYBHD4C64voWCJ6UTLToQ42G +YGO4hMLifI4LAsHSM4JpNt4kdSAPT9vVEp8grkj3+JkvGDYncU5N/CcIlUcO16ZG +yy5gnx8XzSPXJ/WyUEpaBc1URNkT8E3HtPpbxBVezWk7O2qe3D9th1htwH8s6o+q +cctdC21F72sCHmNbOAhQtQKBgB+ODbuW1hQhxosTt3xUTOix7t0cSqvEibvILL3J +w7djlukozyF5pG4jDRC4y80SCcnmKwHTsF7fp6HRlNbwHi1x0PHLDVXUNw0WXi32 +hyW+AZkaz1jS1kUmL90wdUwOasNYa8M21DvmtcM7UdeFIE3j5J78P+FA0feFpFF3 +GVJpAoGBAIiCTK+yZKjc+dRgnP2Hr4h5nlSMkQkVRAweGbNUT+YjaWRdolzBsf97 +ze3i9Nxj7VfnnZDPGMfHOa1EK11a92lFNLgcbDanG+R018L+Jkxng5Xffigoqn9b +xZQe0aZ7j//i8nOpTYi9Peoeie+wxq9GherAb5AtqublOuGju5s6 +-----END RSA PRIVATE KEY----- diff --git a/tests/fixtures/lets_encrypt/isrgrootx1.pem b/tests/fixtures/lets_encrypt/isrgrootx1.pem new file mode 100644 index 0000000..57d4a37 --- /dev/null +++ b/tests/fixtures/lets_encrypt/isrgrootx1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/lets_encrypt/letsencryptauthorityx1.pem b/tests/fixtures/lets_encrypt/letsencryptauthorityx1.pem new file mode 100644 index 0000000..98e88ab --- /dev/null +++ b/tests/fixtures/lets_encrypt/letsencryptauthorityx1.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIRAOeTkL6SBwNJGF95dYHlyoMwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTIwMDIw +WhcNMjAwNjA0MTIwMDIwWjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDEwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX +NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf +89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl +Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc +Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz +uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB +AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU +BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB +FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo +SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js +LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF +BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG +AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD +VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB +AGvM/XGv8yafGRGMPP6hnggoI9DGWGf4l0mzjBhuCkDVqoG/7rsH1ytzteePxiA3 +7kqSBo0fXu5GmbWOw09GpwPYyAAY0iWOMU6ybrTJHS466Urzoe/4IwLQoQc219EK +lh+4Ugu1q4KxNY1qMDA/1YX2Qm9M6AcAs1UvZKHSpJQAbsYrbN6obNoUGOeG6ONH +Yr8KRQz5FMfZYcA49fmdDTwKn/pyLOkJFeA/dm/oP99UmKCFoeOa5w9YJr2Vi7ic +Xd59CU8mprWhxFXnma1oU3T8ZNovjib3UHocjlEJfNbDy9zgKTYURcMVweo1dkbH +NbLc5mIjIk/kJ+RPD+chR+gJjy3Gh9xMNkDrZQKfsIO93hxTsZMmgZQ4c+vujC1M +jSak+Ai87YZeYQPh1fCGMSTno5III37DUCtIn8BJxJixuPeOMKsjLLD5AtMVy0fp +d19lcUek4bjDY8/Ujb5/wfn2+Kk7z72SxWdekjtHOWBmKxqq8jDuuMw4ymg1g5n7 +R7TZ/Y3y4bTpWUDkBHFo03xNM21wBFDIrCZZeVhvDW4MtT6+Ass2bcpoHwYcGol2 +gaLDa5k2dkG41OGtXa0fY+TjdryY4cOcstJUKjv2MJku4yaTtjjECX1rJvFLnqYe +wC+FmxjgWPuyRNuLDAWK30mmpcJZ3CmD6dFtAi4h7H37 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/fixtures/lets_encrypt/letsencryptauthorityx2.pem b/tests/fixtures/lets_encrypt/letsencryptauthorityx2.pem new file mode 100644 index 0000000..4cc5585 --- /dev/null +++ b/tests/fixtures/lets_encrypt/letsencryptauthorityx2.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIRAJY2TKc4C+SL3JDGzeC33mgwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTIwMDMx +WhcNMjAwNjA0MTIwMDMxWjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDhJHRCe7eRMdlz/ziq2M5EXLc5 +CtxErg29RbmXN2evvVBPX9MQVGv3QdqOY+ZtW8DoQKmMQfzRA4n/YmEJYNYHBXia +kL0aZD5P3M93L4lry2evQU3FjQDAa/6NhNy18pUxqOj2kKBDSpN0XLM+Q2lLiSJH +dFE+mWTDzSQB+YQvKHcXIqfdw2wITGYvN3TFb5OOsEY3FmHRUJjIsA9PWFN8rPba +LZZhUK1D3AqmT561Urmcju9O30azMdwg/GnCoyB1Puw4GzZOZmbS3/VmpJMve6YO +lD5gPUpLHG+6tE0cPJFYbi9NxNpw2+0BOXbASefpNbUUBpDB5ZLiEP1rubSFAgMB +AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU +BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB +FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBTF +satOTLHNZDCTfsGEmQWr5gPiJTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js +LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF +BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG +AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD +VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB +AA4eqMjSEJKCF6XRR5pEutkS/e7xgy2vCYYbw1ospQiGQ4FO5TtbvO+5K4v7WR3b +1peMQ03rX0Dr+ylmGNypZahNxTqDiO0X2sHBwJWj/k61+MYq3bRYxKwI6cduTDXb +YQxilGTDNGZUIFKKIloz4zGAl68sj+8pLg534EqKgl8+rWSxclToS1KrydJezokE +dQRXfxu79iscWA3PIj1vbaUBB16lnWJxA3LhTGhUrhZrCnFuOZ93KO8kCKPM7EVo +7c4FCYKI8eWDsf0FF49A4xMUmxPJAPIyZkwQ8KkjpzcTHOmT4CEXUhNu9eMI9qBK +VSFDDMifJ8HzCaVLyMvY1Kf7iR+840EkX1EGC+Z39EaK1hjm314LYpLoYGvYYLJO +/J76XAx8ZgpofqHz1gAEfiMLMLxLQkOjKLXqoUEd5KdnzaO3aLH91gnasy8aD4D5 +9RfEO2xcaozD2rbYsoAMVzcZZHw0Smdmobaz2YazMBjFRcqGntg6s5Xqwusaleiy +snjMCC/9mvIPqGyuVnBPTBaUDFDEhX6qD2MX4dzODL91Z0ogYDWcFLN+uLnZKHje +4JoNuzkJ2FXWOREcsW93KXb+3T8COjhTDKvK4H6ufdrZxxusx60ajJAMBzW0XTf5 +nm2yGEDtyVoMgJLp0rkiPlormgHxSkFDOJbY94J7yxRK +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/test_x509.py b/tests/test_x509.py index a1868b8..915f7b5 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -7,7 +7,9 @@ import os from collections import OrderedDict from datetime import datetime -from asn1crypto import x509, core +from asn1crypto import x509, core, pem + +from .unittest_data import DataDecorator, data if sys.version_info < (3,): byte_cls = str @@ -19,215 +21,1211 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') - +@DataDecorator class X509Tests(unittest.TestCase): - def test_extensions(self): - with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) - - self.assertEqual([], cert.critical_extensions) - self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.key_identifier_value.native) - self.assertEqual(None, cert.key_usage_value) - self.assertEqual(None, cert.subject_alt_name_value) - self.assertEqual(True, cert.basic_constraints_value['ca'].native) - self.assertEqual(None, cert.basic_constraints_value['path_len_constraint'].native) - self.assertEqual(None, cert.name_constraints_value) - self.assertEqual(None, cert.crl_distribution_points_value) - self.assertEqual(None, cert.certificate_policies_value) - self.assertEqual(None, cert.policy_mappings_value) - self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.authority_key_identifier_value['key_identifier'].native) - self.assertEqual(None, cert.policy_constraints_value) - self.assertEqual(None, cert.extended_key_usage_value) - self.assertEqual(None, cert.authority_information_access_value) - self.assertEqual(None, cert.ocsp_no_check_value) - - def test_extensions2(self): - with open(os.path.join(fixtures_dir, 'keys/test-inter-der.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) - - self.assertEqual([], cert.critical_extensions) - self.assertEqual(b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02', cert.key_identifier_value.native) - self.assertEqual(None, cert.key_usage_value) - self.assertEqual(None, cert.subject_alt_name_value) - self.assertEqual(True, cert.basic_constraints_value['ca'].native) - self.assertEqual(None, cert.basic_constraints_value['path_len_constraint'].native) - self.assertEqual(None, cert.name_constraints_value) - self.assertEqual(None, cert.crl_distribution_points_value) - self.assertEqual(None, cert.certificate_policies_value) - self.assertEqual(None, cert.policy_mappings_value) - self.assertEqual(b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK', cert.authority_key_identifier_value['key_identifier'].native) - self.assertEqual(None, cert.policy_constraints_value) - self.assertEqual(None, cert.extended_key_usage_value) - self.assertEqual(None, cert.authority_information_access_value) - self.assertEqual(None, cert.ocsp_no_check_value) - - def test_extensions3(self): - with open(os.path.join(fixtures_dir, 'keys/test-third-der.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) - - self.assertEqual([], cert.critical_extensions) - self.assertEqual(b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r', cert.key_identifier_value.native) - self.assertEqual(None, cert.key_usage_value) - self.assertEqual(None, cert.subject_alt_name_value) - self.assertEqual(None, cert.basic_constraints_value) - self.assertEqual(None, cert.name_constraints_value) - self.assertEqual(None, cert.crl_distribution_points_value) - self.assertEqual(None, cert.certificate_policies_value) - self.assertEqual(None, cert.policy_mappings_value) - self.assertEqual(b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02', cert.authority_key_identifier_value['key_identifier'].native) - self.assertEqual(None, cert.policy_constraints_value) - self.assertEqual(None, cert.extended_key_usage_value) - self.assertEqual(None, cert.authority_information_access_value) - self.assertEqual(None, cert.ocsp_no_check_value) - - def test_extensions4(self): - with open(os.path.join(fixtures_dir, 'geotrust_certs/GeoTrust_Universal_CA.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) - - self.assertEqual(['basic_constraints', 'key_usage'], cert.critical_extensions) - self.assertEqual(b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6', cert.key_identifier_value.native) - self.assertEqual( - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', True), - ('crl_sign', True), - ('encipher_only', False), - ('decipher_only', False), - ]), - cert.key_usage_value.native - ) - self.assertEqual(None, cert.subject_alt_name_value) - self.assertEqual( - OrderedDict([ - ('ca', True), - ('path_len_constraint', None), - ]), - cert.basic_constraints_value.native + def _load_cert(self, relative_path): + with open(os.path.join(fixtures_dir, relative_path), 'rb') as f: + cert_bytes = f.read() + if pem.detect(cert_bytes): + _, _, cert_bytes = pem.unarmor(cert_bytes) + return x509.Certificate.load(cert_bytes) + + #pylint: disable=C0326 + @staticmethod + def critical_extensions_info(): + return ( + ('keys/test-der.crt', []), + ('keys/test-inter-der.crt', []), + ('keys/test-third-der.crt', []), + ('geotrust_certs/GeoTrust_Universal_CA.crt', ['basic_constraints', 'key_usage']), + ('geotrust_certs/GeoTrust_Primary_CA.crt', ['basic_constraints', 'key_usage']), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', ['basic_constraints', 'key_usage']), + ('geotrust_certs/codex.crt', ['key_usage']), + ('lets_encrypt/isrgrootx1.pem', ['key_usage', 'basic_constraints']), + ('lets_encrypt/letsencryptauthorityx1.pem', ['key_usage', 'basic_constraints']), + ('lets_encrypt/letsencryptauthorityx2.pem', ['key_usage', 'basic_constraints']), + ('globalsign_example_keys/IssuingCA-der.cer', ['basic_constraints', 'key_usage']), + ('globalsign_example_keys/rootCA.cer', ['basic_constraints', 'key_usage']), + ('globalsign_example_keys/SSL1.cer', ['key_usage', 'extended_key_usage', 'basic_constraints']), + ('globalsign_example_keys/SSL2.cer', ['key_usage', 'extended_key_usage', 'basic_constraints']), + ('globalsign_example_keys/SSL3.cer', ['key_usage', 'extended_key_usage', 'basic_constraints']), ) - self.assertEqual(None, cert.name_constraints_value) - self.assertEqual(None, cert.crl_distribution_points_value) - self.assertEqual(None, cert.certificate_policies_value) - self.assertEqual(None, cert.policy_mappings_value) - self.assertEqual(b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6', cert.authority_key_identifier_value['key_identifier'].native) - self.assertEqual(None, cert.policy_constraints_value) - self.assertEqual(None, cert.extended_key_usage_value) - self.assertEqual(None, cert.authority_information_access_value) - self.assertEqual(None, cert.ocsp_no_check_value) - def test_extensions5(self): - with open(os.path.join(fixtures_dir, 'geotrust_certs/GeoTrust_Primary_CA.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) + @data('critical_extensions_info') + def critical_extensions(self, relative_path, critical_extensions): + cert = self._load_cert(relative_path) + self.assertEqual(critical_extensions, cert.critical_extensions) - self.assertEqual(['basic_constraints', 'key_usage'], cert.critical_extensions) - self.assertEqual(b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92', cert.key_identifier_value.native) - self.assertEqual( - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]), - cert.key_usage_value.native - ) - self.assertEqual(None, cert.subject_alt_name_value) - self.assertEqual(True, cert.basic_constraints_value['ca'].native) - self.assertEqual(None, cert.basic_constraints_value['path_len_constraint'].native) - self.assertEqual(None, cert.name_constraints_value) - self.assertEqual(None, cert.crl_distribution_points_value) - self.assertEqual(None, cert.certificate_policies_value) - self.assertEqual(None, cert.policy_mappings_value) - self.assertEqual(None, cert.authority_key_identifier_value) - self.assertEqual(None, cert.policy_constraints_value) - self.assertEqual(None, cert.extended_key_usage_value) - self.assertEqual(None, cert.authority_information_access_value) - self.assertEqual(None, cert.ocsp_no_check_value) - - def test_extensions6(self): - with open(os.path.join(fixtures_dir, 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) - - self.assertEqual(['basic_constraints', 'key_usage'], cert.critical_extensions) - self.assertEqual(b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3', cert.key_identifier_value.native) - self.assertEqual( - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]), - cert.key_usage_value.native + #pylint: disable=C0326 + @staticmethod + def key_identifier_value_info(): + return ( + ('keys/test-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ('keys/test-inter-der.crt', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), + ('keys/test-third-der.crt', b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r'), + ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), + ('geotrust_certs/GeoTrust_Primary_CA.crt', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), + ('geotrust_certs/codex.crt', None), + ('lets_encrypt/isrgrootx1.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), + ('lets_encrypt/letsencryptauthorityx1.pem', b'\xa8Jjc\x04}\xdd\xba\xe6\xd19\xb7\xa6Ee\xef\xf3\xa8\xec\xa1'), + ('lets_encrypt/letsencryptauthorityx2.pem', b'\xc5\xb1\xabNL\xb1\xcdd0\x93~\xc1\x84\x99\x05\xab\xe6\x03\xe2%'), + ('globalsign_example_keys/IssuingCA-der.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ('globalsign_example_keys/rootCA.cer', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), + ('globalsign_example_keys/SSL1.cer', b'\x94a\x04\x92\x04L\xe6\xffh\xa8\x96\xafy\xd2\xf32\x84\xae[\xcf'), + ('globalsign_example_keys/SSL2.cer', b'\xd2\xb7\x15\x7fd0\x07(p\x83\xca(\xfa\x88\x96\xde\x9e\xfc\x8a='), + ('globalsign_example_keys/SSL3.cer', b'G\xde\xa4\xe7\xea`\xe7\xee6\xc8\xf1\xd5\xb0F\x07\x07\x9eBh\xce'), ) - self.assertEqual( - [ + + @data('key_identifier_value_info') + def key_identifier_value(self, relative_path, key_identifier_value): + cert = self._load_cert(relative_path) + value = cert.key_identifier_value + self.assertEqual(key_identifier_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def key_usage_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', OrderedDict([ - ('common_name', 'SymantecPKI-1-538') + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', True), + ('crl_sign', True), + ('encipher_only', False), + ('decipher_only', False), ]) - ], - cert.subject_alt_name_value.native - ) - self.assertEqual(True, cert.basic_constraints_value['ca'].native) - self.assertEqual(0, cert.basic_constraints_value['path_len_constraint'].native) - self.assertEqual(None, cert.name_constraints_value) - self.assertEqual( - [ + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', OrderedDict([ - ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']), - ('reasons', None), - ('crl_issuer', None) + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), ]) - ], - cert.crl_distribution_points_value.native + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'geotrust_certs/codex.crt', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', True), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'lets_encrypt/isrgrootx1.pem', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', True), + ('crl_sign', True), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', True), + ('crl_sign', True), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'globalsign_example_keys/rootCA.cer', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', True), + ('key_encipherment', False), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'globalsign_example_keys/SSL1.cer', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', True), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'globalsign_example_keys/SSL2.cer', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', True), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), + ( + 'globalsign_example_keys/SSL3.cer', + OrderedDict([ + ('digital_signature', True), + ('non_repudiation', False), + ('key_encipherment', True), + ('data_encipherment', False), + ('key_agreement', False), + ('key_cert_sign', False), + ('crl_sign', False), + ('encipher_only', False), + ('decipher_only', False), + ]) + ), ) - self.assertEqual( - [ + + @data('key_usage_value_info') + def key_usage_value(self, relative_path, key_usage_value): + cert = self._load_cert(relative_path) + value = cert.key_usage_value + self.assertEqual(key_usage_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def subject_alt_name_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [OrderedDict([('common_name', 'SymantecPKI-1-538')])]), + ('geotrust_certs/codex.crt', ['dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net', 'codexns.io']), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', None), + ('lets_encrypt/letsencryptauthorityx2.pem', None), + ('globalsign_example_keys/IssuingCA-der.cer', None), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', ['anything.example.com']), + ('globalsign_example_keys/SSL2.cer', ['anything.example.com']), + ('globalsign_example_keys/SSL3.cer', None), + ) + + @data('subject_alt_name_value_info') + def subject_alt_name_value(self, relative_path, subject_alt_name_value): + cert = self._load_cert(relative_path) + value = cert.subject_alt_name_value + self.assertEqual(subject_alt_name_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def basic_constraints_value_info(): + return ( + ('keys/test-der.crt', {'ca': True, 'path_len_constraint': None}), + ('keys/test-inter-der.crt', {'ca': True, 'path_len_constraint': None}), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', {'ca': True, 'path_len_constraint': None}), + ('geotrust_certs/GeoTrust_Primary_CA.crt', {'ca': True, 'path_len_constraint': None}), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', {'ca': True, 'path_len_constraint': 0}), + ('geotrust_certs/codex.crt', {'ca': False, 'path_len_constraint': None}), + ('lets_encrypt/isrgrootx1.pem', {'ca': True, 'path_len_constraint': None}), + ('lets_encrypt/letsencryptauthorityx1.pem', {'ca': True, 'path_len_constraint': 0}), + ('lets_encrypt/letsencryptauthorityx2.pem', {'ca': True, 'path_len_constraint': 0}), + ('globalsign_example_keys/IssuingCA-der.cer', {'ca': True, 'path_len_constraint': None}), + ('globalsign_example_keys/rootCA.cer', {'ca': True, 'path_len_constraint': None}), + ('globalsign_example_keys/SSL1.cer', {'ca': False, 'path_len_constraint': None}), + ('globalsign_example_keys/SSL2.cer', {'ca': False, 'path_len_constraint': None}), + ('globalsign_example_keys/SSL3.cer', {'ca': False, 'path_len_constraint': None}), + ) + + @data('basic_constraints_value_info') + def basic_constraints_value(self, relative_path, basic_constraints_value): + cert = self._load_cert(relative_path) + value = cert.basic_constraints_value + self.assertEqual(basic_constraints_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def name_constraints_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), + ('geotrust_certs/codex.crt', None), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', None), + ('lets_encrypt/letsencryptauthorityx2.pem', None), + ( + 'globalsign_example_keys/IssuingCA-der.cer', OrderedDict([ - ('policy_identifier', 'any_policy'), ( - 'policy_qualifiers', + 'permitted_subtrees', + [ + OrderedDict([ + ('base', 'onlythis.com'), + ('minimum', 0), + ('maximum', None) + ]), + OrderedDict([ + ( + 'base', + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'MA'), + ('locality_name', 'Boston'), + ('organization_name', 'Example LLC') + ]) + ), + ('minimum', 0), + ('maximum', None) + ]) + ] + ), + ( + 'excluded_subtrees', [ OrderedDict([ - ('policy_qualifier_id', 'certification_practice_statement'), - ('qualifier', 'https://www.geotrust.com/resources/cps') + ('base', b'\x00\x00\x00\x00\x00\x00\x00\x00'), + ('minimum', 0), + ('maximum', None) + ]), + OrderedDict([ + ('base', b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), + ('minimum', 0), + ('maximum', None) ]) ] - ) + ), ]) - ], - cert.certificate_policies_value.native + ), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', None), + ('globalsign_example_keys/SSL2.cer', None), + ('globalsign_example_keys/SSL3.cer', None), ) - self.assertEqual(None, cert.policy_mappings_value) - self.assertEqual(b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92', cert.authority_key_identifier_value['key_identifier'].native) - self.assertEqual(None, cert.policy_constraints_value) - self.assertEqual(None, cert.extended_key_usage_value) - self.assertEqual( - [ + + @data('name_constraints_value_info') + def name_constraints_value(self, relative_path, name_constraints_value): + cert = self._load_cert(relative_path) + value = cert.name_constraints_value + self.assertEqual(name_constraints_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def crl_distribution_points_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + [ + OrderedDict([ + ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ( + 'geotrust_certs/codex.crt', + [ + OrderedDict([ + ('distribution_point', ['http://gm.symcb.com/gm.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ('lets_encrypt/isrgrootx1.pem', None), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + [ + OrderedDict([ + ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + [ + OrderedDict([ + ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + [ + OrderedDict([ + ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ]), + ( + 'globalsign_example_keys/rootCA.cer', + [ + OrderedDict([ + ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ]), + ('globalsign_example_keys/SSL1.cer', None), + ('globalsign_example_keys/SSL2.cer', None), + ('globalsign_example_keys/SSL3.cer', None), + ) + + @data('crl_distribution_points_value_info') + def crl_distribution_points_value(self, relative_path, crl_distribution_points_value): + cert = self._load_cert(relative_path) + value = cert.crl_distribution_points_value + self.assertEqual(crl_distribution_points_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def certificate_policies_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + [ + OrderedDict([ + ('policy_identifier', 'any_policy'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'https://www.geotrust.com/resources/cps') + ]) + ] + ) + ]) + ] + ), + ( + 'geotrust_certs/codex.crt', + [ + OrderedDict([ + ('policy_identifier', '1.3.6.1.4.1.14370.1.6'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'https://www.geotrust.com/resources/repository/legal') + ]), + OrderedDict([ + ('policy_qualifier_id', 'user_notice'), + ( + 'qualifier', + OrderedDict([ + ('notice_ref', None), + ('explicit_text', 'https://www.geotrust.com/resources/repository/legal') + ]) + ) + ]) + ] + ) + ]) + ] + ), + ('lets_encrypt/isrgrootx1.pem', None), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + [ + OrderedDict([ + ('policy_identifier', '2.23.140.1.2.1'), + ('policy_qualifiers', None) + ]), + OrderedDict([ + ('policy_identifier', '1.3.6.1.4.1.44947.1.1.1'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'http://cps.root-x1.letsencrypt.org') + ]) + ] + ) + ]) + ] + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + [ + OrderedDict([ + ('policy_identifier', '2.23.140.1.2.1'), + ('policy_qualifiers', None) + ]), + OrderedDict([ + ('policy_identifier', '1.3.6.1.4.1.44947.1.1.1'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'http://cps.root-x1.letsencrypt.org') + ]) + ] + ) + ]) + ] + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + [ + OrderedDict([ + ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'https://www.globalsign.com/repository/') + ]) + ] + ) + ]) + ] + ), + ('globalsign_example_keys/rootCA.cer', None), + ( + 'globalsign_example_keys/SSL1.cer', + [ + OrderedDict([ + ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'https://www.globalsign.com/repository/') + ]) + ] + ) + ]) + ] + ), + ( + 'globalsign_example_keys/SSL2.cer', + [ + OrderedDict([ + ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'https://www.globalsign.com/repository/') + ]) + ] + ) + ]) + ] + ), + ( + 'globalsign_example_keys/SSL3.cer', + [ + OrderedDict([ + ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), + ( + 'policy_qualifiers', + [ + OrderedDict([ + ('policy_qualifier_id', 'certification_practice_statement'), + ('qualifier', 'https://www.globalsign.com/repository/') + ]) + ] + ) + ]) + ] + ), + ) + + @data('certificate_policies_value_info') + def certificate_policies_value(self, relative_path, certificate_policies_value): + cert = self._load_cert(relative_path) + value = cert.certificate_policies_value + self.assertEqual(certificate_policies_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def policy_mappings_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), + ('geotrust_certs/codex.crt', None), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', None), + ('lets_encrypt/letsencryptauthorityx2.pem', None), + ('globalsign_example_keys/IssuingCA-der.cer', None), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', None), + ('globalsign_example_keys/SSL2.cer', None), + ('globalsign_example_keys/SSL3.cer', None), + ) + + @data('policy_mappings_value_info') + def policy_mappings_value(self, relative_path, policy_mappings_value): + cert = self._load_cert(relative_path) + value = cert.policy_mappings_value + self.assertEqual(policy_mappings_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def authority_key_identifier_value_info(): + return ( + ( + 'keys/test-der.crt', OrderedDict([ - ('access_method', 'ocsp'), - ('access_location', 'http://g2.symcb.com') + ('key_identifier', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ( + 'authority_cert_issuer', + [ + OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'Massachusetts'), + ('locality_name', 'Newbury'), + ('organization_name', 'Codex Non Sufficit LC'), + ('organizational_unit_name', 'Testing'), + ('common_name', 'Will Bond'), + ('email_address', 'will@codexns.io') + ]) + ] + ), + ('authority_cert_serial_number', 13683582341504654466) ]) - ], - cert.authority_information_access_value.native + ), + ( + 'keys/test-inter-der.crt', + OrderedDict([ + ('key_identifier', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'keys/test-third-der.crt', + OrderedDict([ + ('key_identifier', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + OrderedDict([ + ('key_identifier', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + OrderedDict([ + ('key_identifier', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'geotrust_certs/codex.crt', + OrderedDict([ + ('key_identifier', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + OrderedDict([ + ('key_identifier', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + OrderedDict([ + ('key_identifier', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + OrderedDict([ + ('key_identifier', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + OrderedDict([ + ('key_identifier', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'globalsign_example_keys/SSL2.cer', + OrderedDict([ + ('key_identifier', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ( + 'globalsign_example_keys/SSL3.cer', + OrderedDict([ + ('key_identifier', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ('authority_cert_issuer', None), + ('authority_cert_serial_number', None) + ]) + ), + ) + + @data('authority_key_identifier_value_info') + def authority_key_identifier_value(self, relative_path, authority_key_identifier_value): + cert = self._load_cert(relative_path) + value = cert.authority_key_identifier_value + self.assertEqual(authority_key_identifier_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def policy_constraints_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), + ('geotrust_certs/codex.crt', None), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', None), + ('lets_encrypt/letsencryptauthorityx2.pem', None), + ('globalsign_example_keys/IssuingCA-der.cer', None), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', None), + ('globalsign_example_keys/SSL2.cer', None), + ('globalsign_example_keys/SSL3.cer', None), + ) + + @data('policy_constraints_value_info') + def policy_constraints_value(self, relative_path, policy_constraints_value): + cert = self._load_cert(relative_path) + value = cert.policy_constraints_value + self.assertEqual(policy_constraints_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def extended_key_usage_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), + ('geotrust_certs/codex.crt', ['server_auth', 'client_auth']), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', None), + ('lets_encrypt/letsencryptauthorityx2.pem', None), + ('globalsign_example_keys/IssuingCA-der.cer', None), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', ['server_auth', 'client_auth']), + ('globalsign_example_keys/SSL2.cer', ['server_auth', 'client_auth']), + ('globalsign_example_keys/SSL3.cer', ['server_auth', 'client_auth']), + ) + + @data('extended_key_usage_value_info') + def extended_key_usage_value(self, relative_path, extended_key_usage_value): + cert = self._load_cert(relative_path) + value = cert.extended_key_usage_value + self.assertEqual(extended_key_usage_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def authority_information_access_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://g2.symcb.com') + ]) + ] + ), + ( + 'geotrust_certs/codex.crt', + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://gm.symcd.com') + ]), + OrderedDict([ + ('access_method', 'ca_issuers'), + ('access_location', 'http://gm.symcb.com/gm.crt') + ]), + ] + ), + ('lets_encrypt/isrgrootx1.pem', None), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://ocsp.root-x1.letsencrypt.org/') + ]), + OrderedDict([ + ('access_method', 'ca_issuers'), + ('access_location', 'http://cert.root-x1.letsencrypt.org/') + ]) + ] + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://ocsp.root-x1.letsencrypt.org/') + ]), + OrderedDict([ + ('access_method', 'ca_issuers'), + ('access_location', 'http://cert.root-x1.letsencrypt.org/') + ]) + ] + ), + ('globalsign_example_keys/IssuingCA-der.cer', None), + ('globalsign_example_keys/rootCA.cer', None), + ( + 'globalsign_example_keys/SSL1.cer', + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://ocsp.exampleovca.com/') + ]), + OrderedDict([ + ('access_method', 'ca_issuers'), + ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt') + ]) + ] + ), + ( + 'globalsign_example_keys/SSL2.cer', + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://ocsp.exampleovca.com/') + ]), + OrderedDict([ + ('access_method', 'ca_issuers'), + ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt') + ]) + ] + ), + ( + 'globalsign_example_keys/SSL3.cer', + [ + OrderedDict([ + ('access_method', 'ocsp'), + ('access_location', 'http://ocsp.exampleovca.com/') + ]), + OrderedDict([ + ('access_method', 'ca_issuers'), + ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt') + ]) + ] + ), + ) + + @data('authority_information_access_value_info') + def authority_information_access_value(self, relative_path, authority_information_access_value): + cert = self._load_cert(relative_path) + value = cert.authority_information_access_value + self.assertEqual(authority_information_access_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def ocsp_no_check_value_info(): + return ( + ('keys/test-der.crt', None), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), + ('geotrust_certs/codex.crt', None), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', None), + ('lets_encrypt/letsencryptauthorityx2.pem', None), + ('globalsign_example_keys/IssuingCA-der.cer', None), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', None), + ('globalsign_example_keys/SSL2.cer', None), + ('globalsign_example_keys/SSL3.cer', None), + ) + + @data('ocsp_no_check_value_info') + def ocsp_no_check_value(self, relative_path, ocsp_no_check_value): + cert = self._load_cert(relative_path) + value = cert.ocsp_no_check_value + self.assertEqual(ocsp_no_check_value, value.native if value else None) + + #pylint: disable=C0326 + @staticmethod + def serial_number_info(): + return ( + ('keys/test-der.crt', 13683582341504654466), + ('keys/test-inter-der.crt', 1590137), + ('keys/test-third-der.crt', 2474902313), + ('geotrust_certs/GeoTrust_Universal_CA.crt', 1), + ('geotrust_certs/GeoTrust_Primary_CA.crt', 32798226551256963324313806436981982369), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', 146934555852773531829332059263122711876), + ('geotrust_certs/codex.crt', 130338219198307073574879940486642352162), + ('lets_encrypt/isrgrootx1.pem', 172886928669790476064670243504169061120), + ('lets_encrypt/letsencryptauthorityx1.pem', 307817870430047279283060309415759825539), + ('lets_encrypt/letsencryptauthorityx2.pem', 199666138109676817050168330923544141416), + ('globalsign_example_keys/IssuingCA-der.cer', 43543335419752), + ('globalsign_example_keys/rootCA.cer', 342514332211132), + ('globalsign_example_keys/SSL1.cer', 425155524522), + ('globalsign_example_keys/SSL2.cer', 425155524522), + ('globalsign_example_keys/SSL3.cer', 425155524522), + ) + + @data('serial_number_info') + def serial_number(self, relative_path, serial_number): + cert = self._load_cert(relative_path) + self.assertEqual(serial_number, cert.serial_number) + + #pylint: disable=C0326 + @staticmethod + def key_identifier_info(): + return ( + ('keys/test-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ('keys/test-inter-der.crt', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), + ('keys/test-third-der.crt', b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r'), + ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), + ('geotrust_certs/GeoTrust_Primary_CA.crt', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), + ('geotrust_certs/codex.crt', None), + ('lets_encrypt/isrgrootx1.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), + ('lets_encrypt/letsencryptauthorityx1.pem', b'\xa8Jjc\x04}\xdd\xba\xe6\xd19\xb7\xa6Ee\xef\xf3\xa8\xec\xa1'), + ('lets_encrypt/letsencryptauthorityx2.pem', b'\xc5\xb1\xabNL\xb1\xcdd0\x93~\xc1\x84\x99\x05\xab\xe6\x03\xe2%'), + ('globalsign_example_keys/IssuingCA-der.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ('globalsign_example_keys/rootCA.cer', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), + ('globalsign_example_keys/SSL1.cer', b'\x94a\x04\x92\x04L\xe6\xffh\xa8\x96\xafy\xd2\xf32\x84\xae[\xcf'), + ('globalsign_example_keys/SSL2.cer', b'\xd2\xb7\x15\x7fd0\x07(p\x83\xca(\xfa\x88\x96\xde\x9e\xfc\x8a='), + ('globalsign_example_keys/SSL3.cer', b'G\xde\xa4\xe7\xea`\xe7\xee6\xc8\xf1\xd5\xb0F\x07\x07\x9eBh\xce'), + ) + + @data('key_identifier_info') + def key_identifier(self, relative_path, key_identifier): + cert = self._load_cert(relative_path) + self.assertEqual(key_identifier, cert.key_identifier) + + #pylint: disable=C0326 + @staticmethod + def issuer_serial_info(): + return ( + ('keys/test-der.crt', b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:13683582341504654466'), + ('keys/test-inter-der.crt', b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:1590137'), + ('keys/test-third-der.crt', b'\xed{\x9b\xbf\x9b\xdbd\xa4\xea\xf2#+H\x96\xcd\x80\x99\xf6\xecCM\x94\x07\x02\xe2\x18\xf3\x83\x8c8%\x01:2474902313'), + ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xa1\x848\xf2\xe5w\xee\xec\xce\xfefJC+\xdf\x97\x7f\xd2Y\xe3\xdc\xa0D7~\x07\xd9\x9dzL@g:1'), + ('geotrust_certs/GeoTrust_Primary_CA.crt', b'\xdcg\x0c\x80\x03\xb3D\xa0v\xe2\xee\xec\x8b\xd6\x82\x01\xf0\x13\x0cwT\xb4\x8f\x80\x0eT\x9d\xbf\xbf\xa4\x11\x80:32798226551256963324313806436981982369'), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b'\xdcg\x0c\x80\x03\xb3D\xa0v\xe2\xee\xec\x8b\xd6\x82\x01\xf0\x13\x0cwT\xb4\x8f\x80\x0eT\x9d\xbf\xbf\xa4\x11\x80:146934555852773531829332059263122711876'), + ('geotrust_certs/codex.crt', b'x\x12\xe0\x15\x00d;\xc3\xb9/\xf6\x13\n\xd8\xe2\xddY\xf7\xaf*=C\x01<\x86\xf5\x9f_\xab;e\xd1:130338219198307073574879940486642352162'), + ('lets_encrypt/isrgrootx1.pem', b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-0\xf8:172886928669790476064670243504169061120'), + ('lets_encrypt/letsencryptauthorityx1.pem', b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-0\xf8:307817870430047279283060309415759825539'), + ('lets_encrypt/letsencryptauthorityx2.pem', b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-0\xf8:199666138109676817050168330923544141416'), + ('globalsign_example_keys/IssuingCA-der.cer', b'\xd2\xe7\xca\x10\xc1\x91\x92Y^A\x11\xd3Rz\xd5\x93\x19wk\x11\xef\xaa\x9c\xad\x10\x8ak\x8a\x08-\x0c\xff:43543335419752'), + ('globalsign_example_keys/rootCA.cer', b'\xd2\xe7\xca\x10\xc1\x91\x92Y^A\x11\xd3Rz\xd5\x93\x19wk\x11\xef\xaa\x9c\xad\x10\x8ak\x8a\x08-\x0c\xff:342514332211132'), + ('globalsign_example_keys/SSL1.cer', b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f\xf0\x1d\xbc\x9f\xe4:425155524522'), + ('globalsign_example_keys/SSL2.cer', b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f\xf0\x1d\xbc\x9f\xe4:425155524522'), + ('globalsign_example_keys/SSL3.cer', b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f\xf0\x1d\xbc\x9f\xe4:425155524522'), ) - self.assertEqual(None, cert.ocsp_no_check_value) + + @data('issuer_serial_info') + def issuer_serial(self, relative_path, issuer_serial): + cert = self._load_cert(relative_path) + self.assertEqual(issuer_serial, cert.issuer_serial) + + #pylint: disable=C0326 + @staticmethod + def authority_key_identifier_info(): + return ( + ('keys/test-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ('keys/test-inter-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ('keys/test-third-der.crt', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), + ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), + ('geotrust_certs/codex.crt', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), + ('lets_encrypt/letsencryptauthorityx2.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), + ('globalsign_example_keys/IssuingCA-der.cer', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ('globalsign_example_keys/SSL2.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ('globalsign_example_keys/SSL3.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ) + + @data('authority_key_identifier_info') + def authority_key_identifier(self, relative_path, authority_key_identifier): + cert = self._load_cert(relative_path) + self.assertEqual(authority_key_identifier, cert.authority_key_identifier) + + #pylint: disable=C0326 + @staticmethod + def authority_issuer_serial_info(): + return ( + ('keys/test-der.crt', b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:13683582341504654466'), + ('keys/test-inter-der.crt', None), + ('keys/test-third-der.crt', None), + ('geotrust_certs/GeoTrust_Universal_CA.crt', None), + ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), + ('geotrust_certs/codex.crt', None), + ('lets_encrypt/isrgrootx1.pem', None), + ('lets_encrypt/letsencryptauthorityx1.pem', None), + ('lets_encrypt/letsencryptauthorityx2.pem', None), + ('globalsign_example_keys/IssuingCA-der.cer', None), + ('globalsign_example_keys/rootCA.cer', None), + ('globalsign_example_keys/SSL1.cer', None), + ('globalsign_example_keys/SSL2.cer', None), + ('globalsign_example_keys/SSL3.cer', None), + ) + + @data('authority_issuer_serial_info') + def authority_issuer_serial(self, relative_path, authority_issuer_serial): + cert = self._load_cert(relative_path) + self.assertEqual(authority_issuer_serial, cert.authority_issuer_serial) + + #pylint: disable=C0326 + @staticmethod + def ocsp_urls_info(): + return ( + ('keys/test-der.crt', []), + ('keys/test-inter-der.crt', []), + ('keys/test-third-der.crt', []), + ('geotrust_certs/GeoTrust_Universal_CA.crt', []), + ('geotrust_certs/GeoTrust_Primary_CA.crt', []), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', ['http://g2.symcb.com']), + ('geotrust_certs/codex.crt', ['http://gm.symcd.com']), + ('lets_encrypt/isrgrootx1.pem', []), + ('lets_encrypt/letsencryptauthorityx1.pem', ['http://ocsp.root-x1.letsencrypt.org/']), + ('lets_encrypt/letsencryptauthorityx2.pem', ['http://ocsp.root-x1.letsencrypt.org/']), + ('globalsign_example_keys/IssuingCA-der.cer', []), + ('globalsign_example_keys/rootCA.cer', []), + ('globalsign_example_keys/SSL1.cer', ['http://ocsp.exampleovca.com/']), + ('globalsign_example_keys/SSL2.cer', ['http://ocsp.exampleovca.com/']), + ('globalsign_example_keys/SSL3.cer', ['http://ocsp.exampleovca.com/']), + ) + + @data('ocsp_urls_info') + def ocsp_urls(self, relative_path, ocsp_url): + cert = self._load_cert(relative_path) + self.assertEqual(ocsp_url, cert.ocsp_urls) + + #pylint: disable=C0326 + @staticmethod + def crl_urls_info(): + return ( + ('keys/test-der.crt', []), + ('keys/test-inter-der.crt', []), + ('keys/test-third-der.crt', []), + ('geotrust_certs/GeoTrust_Universal_CA.crt', []), + ('geotrust_certs/GeoTrust_Primary_CA.crt', []), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', ['http://g1.symcb.com/GeoTrustPCA.crl']), + ('geotrust_certs/codex.crt', ['http://gm.symcb.com/gm.crl']), + ('lets_encrypt/isrgrootx1.pem', []), + ('lets_encrypt/letsencryptauthorityx1.pem', ['http://crl.root-x1.letsencrypt.org']), + ('lets_encrypt/letsencryptauthorityx2.pem', ['http://crl.root-x1.letsencrypt.org']), + ('globalsign_example_keys/IssuingCA-der.cer', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), + ('globalsign_example_keys/rootCA.cer', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), + ('globalsign_example_keys/SSL1.cer', []), + ('globalsign_example_keys/SSL2.cer', []), + ('globalsign_example_keys/SSL3.cer', []), + ) + + @data('crl_urls_info') + def crl_urls(self, relative_path, crl_url): + cert = self._load_cert(relative_path) + self.assertEqual(crl_url, cert.crl_urls) + + #pylint: disable=C0326 + @staticmethod + def valid_domains_info(): + return ( + ('keys/test-der.crt', []), + ('keys/test-inter-der.crt', []), + ('keys/test-third-der.crt', []), + ('geotrust_certs/GeoTrust_Universal_CA.crt', []), + ('geotrust_certs/GeoTrust_Primary_CA.crt', []), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', []), + ('geotrust_certs/codex.crt', ['codexns.io', 'dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net']), + ('lets_encrypt/isrgrootx1.pem', []), + ('lets_encrypt/letsencryptauthorityx1.pem', []), + ('lets_encrypt/letsencryptauthorityx2.pem', []), + ('globalsign_example_keys/IssuingCA-der.cer', []), + ('globalsign_example_keys/rootCA.cer', []), + ('globalsign_example_keys/SSL1.cer', ['anything.example.com']), + ('globalsign_example_keys/SSL2.cer', ['*.google.com', 'anything.example.com']), + ('globalsign_example_keys/SSL3.cer', ['*.google.com']), + ) + + @data('valid_domains_info') + def valid_domains(self, relative_path, valid_domains): + cert = self._load_cert(relative_path) + self.assertEqual(valid_domains, cert.valid_domains) + + #pylint: disable=C0326 + @staticmethod + def valid_ips_info(): + return ( + ('keys/test-der.crt', []), + ('keys/test-inter-der.crt', []), + ('keys/test-third-der.crt', []), + ('geotrust_certs/GeoTrust_Universal_CA.crt', []), + ('geotrust_certs/GeoTrust_Primary_CA.crt', []), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', []), + ('geotrust_certs/codex.crt', []), + ('lets_encrypt/isrgrootx1.pem', []), + ('lets_encrypt/letsencryptauthorityx1.pem', []), + ('lets_encrypt/letsencryptauthorityx2.pem', []), + ('globalsign_example_keys/IssuingCA-der.cer', []), + ('globalsign_example_keys/rootCA.cer', []), + ('globalsign_example_keys/SSL1.cer', []), + ('globalsign_example_keys/SSL2.cer', []), + ('globalsign_example_keys/SSL3.cer', []), + ) + + @data('valid_ips_info') + def valid_ips(self, relative_path, crl_url): + cert = self._load_cert(relative_path) + self.assertEqual(crl_url, cert.valid_ips) def test_parse_certificate(self): - with open(os.path.join(fixtures_dir, 'keys/test-der.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) + cert = self._load_cert('keys/test-der.crt') tbs_certificate = cert['tbs_certificate'] signature = tbs_certificate['signature'] @@ -374,8 +1372,7 @@ class X509Tests(unittest.TestCase): ) def test_parse_dsa_certificate(self): - with open(os.path.join(fixtures_dir, 'keys/test-dsa-der.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) + cert = self._load_cert('keys/test-dsa-der.crt') tbs_certificate = cert['tbs_certificate'] signature = tbs_certificate['signature'] @@ -509,8 +1506,7 @@ class X509Tests(unittest.TestCase): ) def test_parse_ec_certificate(self): - with open(os.path.join(fixtures_dir, 'keys/test-ec-der.crt'), 'rb') as f: - cert = x509.Certificate.load(f.read()) + cert = self._load_cert('keys/test-ec-der.crt') tbs_certificate = cert['tbs_certificate'] signature = tbs_certificate['signature'] -- cgit v1.2.3 From 55908ac8cdcf6d0f225412e10876c0575151c464 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 17 Jul 2015 16:24:30 -0400 Subject: Make "setup.py clean" actually clean stuff up --- setup.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8652410..5568ac4 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,29 @@ -from setuptools import setup, find_packages +import os +import shutil + +from setuptools import setup, find_packages, Command + import asn1crypto + +class CleanCommand(Command): + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + folder = os.path.dirname(os.path.abspath(__file__)) + for sub_folder in ['build', 'dist', 'asn1crypto.egg-info']: + full_path = os.path.join(folder, sub_folder) + if os.path.exists(full_path): + shutil.rmtree(full_path) + + setup( name='asn1crypto', version=asn1crypto.__version__, @@ -30,5 +52,9 @@ setup( keywords='asn1 crypto', - packages=find_packages(exclude=['tests*', 'dev*']) + packages=find_packages(exclude=['tests*', 'dev*']), + + cmdclass={ + 'clean': CleanCommand, + } ) -- cgit v1.2.3 From 85abe2cdea32efbb8c5e7af69f613481c5ac266c Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:45:49 -0400 Subject: Corrected some docstrings --- asn1crypto/pem.py | 7 ++++++- asn1crypto/x509.py | 5 ----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py index c812d03..e4cd64d 100644 --- a/asn1crypto/pem.py +++ b/asn1crypto/pem.py @@ -1,7 +1,12 @@ # coding: utf-8 """ -Encoding DER to PEM and decoding PEM to DER +Encoding DER to PEM and decoding PEM to DER. Exports the following items: + + - armor() + - detect() + - unarmor() + """ from __future__ import unicode_literals, division, absolute_import, print_function diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index f87700a..0678189 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -10,11 +10,6 @@ ASN.1 type classes for X509 certificates. Exports the following items: - GeneralNames() - Name() -Also adds TimeStampedData() support to asn1crypto.cms.ContentInfo(), -TimeStampedData() and TSTInfo() support to -asn1crypto.cms.EncapsulatedContentInfo() and some oids and value parsers to -asn1crypto.cms.CMSAttribute(). - Other type classes are defined that help compose the types listed above. """ -- cgit v1.2.3 From 1cfca230e42c945b431a32aaf92388347018a661 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:51:58 -0400 Subject: Added .signature_algo and .hash_algo to SignedDigestAlgorithm --- asn1crypto/algos.py | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_x509.py | 17 +++++++++++++ 2 files changed, 88 insertions(+) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 0b619c4..a003d7e 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -191,6 +191,77 @@ class SignedDigestAlgorithm(Sequence): 'rsa_pss': RSASSAPSSParams, } + @property + def signature_algo(self): + """ + :return: + A unicode string of "rsassa_pkcs1v15", "rsassa_pss", "dsa" or + "ecdsa" + """ + + algorithm = self['algorithm'].native + + algo_map = { + 'md2_rsa': 'rsassa_pkcs1v15', + 'md5_rsa': 'rsassa_pkcs1v15', + 'sha1_rsa': 'rsassa_pkcs1v15', + 'sha224_rsa': 'rsassa_pkcs1v15', + 'sha256_rsa': 'rsassa_pkcs1v15', + 'sha384_rsa': 'rsassa_pkcs1v15', + 'sha512_rsa': 'rsassa_pkcs1v15', + 'rsassa_pkcs1v15': 'rsassa_pkcs1v15', + 'rsassa_pss': 'rsassa_pss', + 'sha1_dsa': 'dsa', + 'sha224_dsa': 'dsa', + 'sha256_dsa': 'dsa', + 'dsa': 'dsa', + 'sha1_ecdsa': 'ecdsa', + 'sha224_ecdsa': 'ecdsa', + 'sha256_ecdsa': 'ecdsa', + 'sha384_ecdsa': 'ecdsa', + 'sha512_ecdsa': 'ecdsa', + 'ecdsa': 'ecdsa', + } + if algorithm in algo_map: + return algo_map[algorithm] + + raise ValueError('Signature algorithm not known for %s' % algorithm) + + @property + def hash_algo(self): + """ + :return: + A unicode string of "md2", "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", "sha512_224", "sha512_256" + """ + + algorithm = self['algorithm'].native + + algo_map = { + 'md2_rsa': 'md2', + 'md5_rsa': 'md5', + 'sha1_rsa': 'sha1', + 'sha224_rsa': 'sha224', + 'sha256_rsa': 'sha256', + 'sha384_rsa': 'sha384', + 'sha512_rsa': 'sha512', + 'sha1_dsa': 'sha1', + 'sha224_dsa': 'sha224', + 'sha256_dsa': 'sha256', + 'sha1_ecdsa': 'sha1', + 'sha224_ecdsa': 'sha224', + 'sha256_ecdsa': 'sha256', + 'sha384_ecdsa': 'sha384', + 'sha512_ecdsa': 'sha512', + } + if algorithm in algo_map: + return algo_map[algorithm] + + if algorithm == 'rsassa_pss': + return self['parameters']['hash_algorithm']['algorithm'].native + + raise ValueError('Hash algorithm not known for %s' % algorithm) + class Pbkdf2Salt(Choice): _alternatives = [ diff --git a/tests/test_x509.py b/tests/test_x509.py index 915f7b5..1a7c4bc 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -31,6 +31,23 @@ class X509Tests(unittest.TestCase): _, _, cert_bytes = pem.unarmor(cert_bytes) return x509.Certificate.load(cert_bytes) + #pylint: disable=C0326 + @staticmethod + def signature_algo_info(): + return ( + ('keys/test-der.crt', 'rsassa_pkcs1v15', 'sha256'), + ('keys/test-inter-der.crt', 'rsassa_pkcs1v15', 'sha256'), + ('keys/test-dsa-der.crt', 'dsa', 'sha256'), + ('keys/test-third-der.crt', 'rsassa_pkcs1v15', 'sha256'), + ('keys/test-ec-der.crt', 'ecdsa', 'sha256'), + ) + + @data('signature_algo_info') + def signature_algo(self, relative_path, signature_algo, hash_algo): + cert = self._load_cert(relative_path) + self.assertEqual(signature_algo, cert['signature_algorithm'].signature_algo) + self.assertEqual(hash_algo, cert['signature_algorithm'].hash_algo) + #pylint: disable=C0326 @staticmethod def critical_extensions_info(): -- cgit v1.2.3 From 2f1eb268c4277cdb165317f3de69a571a857f38c Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:52:34 -0400 Subject: Added .human_friendly to CRLReason --- asn1crypto/crl.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 63c376c..2721302 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -107,6 +107,29 @@ class CRLReason(Enumerated): 10: 'aa_compromise', } + @property + def human_friendly(self): + """ + :return: + A unicode string with revocation description that is suitable to + show to end-users. Starts with a lower case letter and phrased in + such a way that it makes sense after the phrase "because of" or + "due to". + """ + + return { + 'unspecified': 'an unspecified reason', + 'key_compromise': 'a compromised key', + 'ca_compromise': 'the CA being compromised', + 'affiliation_changed': 'an affiliation change', + 'superseded': 'certificate supersession', + 'cessation_of_operation': 'a cessation of operation', + 'certificate_hold': 'a certificate hold', + 'remove_from_crl': 'removal from the CRL', + 'privilege_withdrawn': 'privilege withdrawl', + 'aa_compromise': 'the AA being compromised', + }[self.native] + class CRLEntryExtensionId(ObjectIdentifier): _map = { -- cgit v1.2.3 From 598c22899b53d4e7a1470298255017d4dc85ec10 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:54:22 -0400 Subject: Added high-level extension attributes to crl.CertificateList and crl.RevokedCertificate --- asn1crypto/crl.py | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 2721302..9d90e60 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -165,6 +165,88 @@ class RevokedCertificate(Sequence): ('crl_entry_extensions', CRLEntryExtensions, {'optional': True}), ] + _processed_extensions = False + _critical_extensions = None + _crl_reason_value = None + _invalidity_date_value = None + _certificate_issuer_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = [] + + for extension in self['crl_entry_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.append(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a list of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A list of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def crl_reason_value(self): + """ + This extension indicates the reason that a certificate was revoked. + + :return: + None or a CRLReason object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._crl_reason_value + + @property + def invalidity_date_value(self): + """ + This extension indicates the suspected date/time the private key was + compromised or the certificate became invalid. This would usually be + before the revocation date, which is when the CA processed the + revocation. + + :return: + None or a GeneralizedTime object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._invalidity_date_value + + @property + def certificate_issuer_value(self): + """ + This extension indicates the issuer of the certificate in question, + and is used in indirect CRLs. CRL entries without this extension are + for certificates issued from the last seen issuer. + + :return: + None or an x509.GeneralNames object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._certificate_issuer_value + class RevokedCertificates(SequenceOf): _child_spec = RevokedCertificate @@ -188,3 +270,143 @@ class CertificateList(Sequence): ('signature_algorith', SignedDigestAlgorithm), ('signature', OctetBitString), ] + + _processed_extensions = False + _critical_extensions = None + _issuer_alt_name = None + _crl_number = None + _delta_crl_indicator = None + _issuing_distribution_point = None + _authority_key_identifier = None + _freshest_crl = None + _authority_information_access = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = [] + + for extension in self['tbs_cert_list']['crl_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.append(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a list of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A list of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def issuer_alt_name_value(self): + """ + This extension allows associating one or more alternative names with + the issuer of the CRL. + + :return: + None or an x509.GeneralNames object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._issuer_alt_name_value + + @property + def crl_number_value(self): + """ + This extension adds a monotonically increasing number to the CRL and is + used to distinguish different versions of the CRL. + + :return: + None or an Integer object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._crl_number_value + + @property + def delta_crl_indicator_value(self): + """ + This extension indicates a CRL is a delta CRL, and contains the CRL + number of the base CRL that it is a delta from. + + :return: + None or an Integer object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._delta_crl_indicator_value + + @property + def issuing_distribution_point_value(self): + """ + This extension includes information about what types of revocations + and certificates are part of the CRL. + + :return: + None or an IssuingDistributionPoint object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._issuing_distribution_point_value + + @property + def authority_key_identifier_value(self): + """ + This extension helps in identifying the public key with which to + validate the authenticity of the CRL. + + :return: + None or an AuthorityKeyIdentifier object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._authority_key_identifier_value + + @property + def freshest_crl_value(self): + """ + This extension is used in complete CRLs to indicate where a delta CRL + may be located. + + :return: + None or a CRLDistributionPoints object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._freshest_crl_value + + @property + def authority_information_access_value(self): + """ + This extension is used to provide a URL with which to download the + certificate used to sign this CRL. + + :return: + None or an AuthorityInfoAccessSyntax object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._authority_information_access_value -- cgit v1.2.3 From 35f81356710b828085462d1e97074a3de3b0c7b4 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:56:03 -0400 Subject: Added Subject Information Access extension support to x509, fixed timestamping OID name to time_stamping --- asn1crypto/x509.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 0678189..572ca5f 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -734,7 +734,8 @@ class AccessMethod(ObjectIdentifier): _map = { '1.3.6.1.5.5.7.48.1': 'ocsp', '1.3.6.1.5.5.7.48.2': 'ca_issuers', - '1.3.6.1.5.5.7.48.3': 'timestamping', + '1.3.6.1.5.5.7.48.3': 'time_stamping', + '1.3.6.1.5.5.7.48.5': 'ca_repository', } @@ -749,6 +750,10 @@ class AuthorityInfoAccessSyntax(SequenceOf): _child_spec = AccessDescription +class SubjectInfoAccessSyntax(SequenceOf): + _child_spec = AccessDescription + + class EntrustVersionInfo(Sequence): _fields = [ ('entrust_vers', GeneralString), @@ -787,6 +792,7 @@ class ExtensionId(ObjectIdentifier): '2.5.29.37': 'extended_key_usage', '2.5.29.54': 'inhibit_any_policy', '1.3.6.1.5.5.7.1.1': 'authority_information_access', + '1.3.6.1.5.5.7.1.11': 'subject_information_access', '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', '1.2.840.113533.7.65.0': 'entrust_version_extension', '2.16.840.1.113730.1.1': 'netscape_certificate_type', @@ -818,6 +824,7 @@ class Extension(Sequence): 'extended_key_usage': ExtKeyUsageSyntax, 'inhibit_any_policy': Integer, 'authority_information_access': AuthorityInfoAccessSyntax, + 'subject_information_access': SubjectInfoAccessSyntax, 'ocsp_no_check': Null, 'entrust_version_extension': EntrustVersionInfo, 'netscape_certificate_type': NetscapeCertificateType, @@ -872,6 +879,7 @@ class Certificate(Sequence): _policy_constraints_value = None _extended_key_usage_value = None _authority_information_access_value = None + _subject_information_access_value = None _ocsp_no_check_value = None _issuer_serial = None _authority_issuer_serial = False @@ -1044,6 +1052,20 @@ class Certificate(Sequence): self._set_extensions() return self._authority_information_access_value + @property + def subject_information_access_value(self): + """ + This extension is used to access information about the subject of this + certificate. + + :return: + None or a SubjectInfoAccessSyntax object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._subject_information_access_value + @property def ocsp_no_check_value(self): """ -- cgit v1.2.3 From ada58e75fcb8ed6ee6e1c4ff8b3a5f373d1bc0bf Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:57:00 -0400 Subject: Added support for the issuer alt name extension to x509 --- asn1crypto/x509.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 572ca5f..557a2dd 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -781,6 +781,7 @@ class ExtensionId(ObjectIdentifier): '2.5.29.15': 'key_usage', '2.5.29.16': 'private_key_usage_period', '2.5.29.17': 'subject_alt_name', + '2.5.29.18': 'issuer_alt_name', '2.5.29.19': 'basic_constraints', '2.5.29.23': 'hold_instruction_code', '2.5.29.30': 'name_constraints', @@ -813,6 +814,7 @@ class Extension(Sequence): 'key_usage': KeyUsage, 'private_key_usage_period': PrivateKeyUsagePeriod, 'subject_alt_name': GeneralNames, + 'issuer_alt_name': GeneralNames, 'basic_constraints': BasicConstraints, 'hold_instruction_code': ObjectIdentifier, 'name_constraints': NameConstraints, @@ -870,6 +872,7 @@ class Certificate(Sequence): _key_identifier_value = None _key_usage_value = None _subject_alt_name_value = None + _issuer_alt_name_value = None _basic_constraints_value = None _name_constraints_value = None _crl_distribution_points_value = None @@ -951,6 +954,20 @@ class Certificate(Sequence): self._set_extensions() return self._subject_alt_name_value + @property + def issuer_alt_name_value(self): + """ + This extension allows associating one or more alternative names with + the issuer of the certificate. + + :return: + None or an x509.GeneralNames object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._issuer_alt_name_value + @property def basic_constraints_value(self): """ -- cgit v1.2.3 From fb88b8c1c72d49aa29b739ba41fe156233504783 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:58:16 -0400 Subject: Added support for the freshest crl extension to x509 --- asn1crypto/x509.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 557a2dd..5793f40 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -791,6 +791,7 @@ class ExtensionId(ObjectIdentifier): '2.5.29.35': 'authority_key_identifier', '2.5.29.36': 'policy_constraints', '2.5.29.37': 'extended_key_usage', + '2.5.29.46': 'freshest_crl', '2.5.29.54': 'inhibit_any_policy', '1.3.6.1.5.5.7.1.1': 'authority_information_access', '1.3.6.1.5.5.7.1.11': 'subject_information_access', @@ -824,6 +825,7 @@ class Extension(Sequence): 'authority_key_identifier': AuthorityKeyIdentifier, 'policy_constraints': PolicyConstraints, 'extended_key_usage': ExtKeyUsageSyntax, + 'freshest_crl': CRLDistributionPoints, 'inhibit_any_policy': Integer, 'authority_information_access': AuthorityInfoAccessSyntax, 'subject_information_access': SubjectInfoAccessSyntax, @@ -880,6 +882,7 @@ class Certificate(Sequence): _policy_mappings_value = None _authority_key_identifier_value = None _policy_constraints_value = None + _freshest_crl_value = None _extended_key_usage_value = None _authority_information_access_value = None _subject_information_access_value = None @@ -1047,6 +1050,19 @@ class Certificate(Sequence): self._set_extensions() return self._policy_constraints_value + @property + def freshest_crl_value(self): + """ + This extension is used to help locate any available delta CRLs + + :return: + None or an CRLDistributionPoints object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._freshest_crl_value + @property def extended_key_usage_value(self): """ -- cgit v1.2.3 From 433adbb6062a1eed207f1954109d14facc940134 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 08:59:57 -0400 Subject: Added high-level attribute access for the subject directory attributes and inhibit anypolicy extensions to x509.Certificate --- asn1crypto/x509.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5793f40..34a005c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -871,6 +871,7 @@ class Certificate(Sequence): _processed_extensions = False _critical_extensions = None + _subject_directory_attributes = None _key_identifier_value = None _key_usage_value = None _subject_alt_name_value = None @@ -883,6 +884,7 @@ class Certificate(Sequence): _authority_key_identifier_value = None _policy_constraints_value = None _freshest_crl_value = None + _inhibit_any_policy_value = None _extended_key_usage_value = None _authority_information_access_value = None _subject_information_access_value = None @@ -924,6 +926,20 @@ class Certificate(Sequence): self._set_extensions() return self._critical_extensions + @property + def subject_directory_attributes_value(self): + """ + This extension is used to contain additional identification attributes + about the subject. + + :return: + None or an Attributes object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._key_identifier_value + @property def key_identifier_value(self): """ @@ -1063,6 +1079,20 @@ class Certificate(Sequence): self._set_extensions() return self._freshest_crl_value + @property + def inhibit_any_policy_value(self): + """ + This extension is used to prevent mapping of the any policy to + specific requirements + + :return: + None or a Integer object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._inhibit_any_policy_value + @property def extended_key_usage_value(self): """ -- cgit v1.2.3 From ea86001c948c0108b03f6ea26d49694d964b8b22 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 09:00:29 -0400 Subject: Improved docstrings for the x509.Certificate extension attributes --- asn1crypto/x509.py | 73 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 34a005c..4b152d3 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -943,8 +943,12 @@ class Certificate(Sequence): @property def key_identifier_value(self): """ + This extension is used to help in creating certificate validation paths. + It contains an identifier that should generally, but is not guaranteed + to, be unique. + :return: - None or the parsed value of the key identifier extension + None or an OctetString object """ if not self._processed_extensions: @@ -954,8 +958,11 @@ class Certificate(Sequence): @property def key_usage_value(self): """ + This extension is used to define the purpose of the public key + contained within the certificate. + :return: - None or the parsed value of the key usage extension + None or a KeyUsage """ if not self._processed_extensions: @@ -965,8 +972,13 @@ class Certificate(Sequence): @property def subject_alt_name_value(self): """ + This extension allows for additional names to be associate with the + subject of the certificate. While it may contain a whole host of + possible names, it is usually used to allow certificates to be used + with multiple different domain names. + :return: - None or the parsed value of the subject alt name extension + None or a GeneralNames object """ if not self._processed_extensions: @@ -990,8 +1002,12 @@ class Certificate(Sequence): @property def basic_constraints_value(self): """ + This extension is used to determine if the subject of the certificate + is a CA, and if so, what the maximum number of intermediate CA certs + after this are, before an end-entity certificate is found. + :return: - None or the parsed value of the basic constraints extension + None or a BasicConstraints object """ if not self._processed_extensions: @@ -1001,8 +1017,11 @@ class Certificate(Sequence): @property def name_constraints_value(self): """ + This extension is used in CA certificates, and is used to limit the + possible names of certificates issued. + :return: - None or the parsed value of the name constraints extension + None or a NameConstraints object """ if not self._processed_extensions: @@ -1012,8 +1031,10 @@ class Certificate(Sequence): @property def crl_distribution_points_value(self): """ + This extension is used to help in locating the CRL for this certificate. + :return: - None or the parsed value of the CRL distribution points + None or a CRLDistributionPoints object extension """ @@ -1024,8 +1045,13 @@ class Certificate(Sequence): @property def certificate_policies_value(self): """ + This extension defines policies in CA certificates under which + certificates may be issued. In end-entity certificates, the inclusion + of a policy indicates the issuance of the certificate follows the + policy. + :return: - None or the parsed value of the certificate policies extension + None or a CertificatePolicies object """ if not self._processed_extensions: @@ -1035,8 +1061,12 @@ class Certificate(Sequence): @property def policy_mappings_value(self): """ + This extension allows mapping policy OIDs to other OIDs. This is used + to allow different policies to be treated as equivalent in the process + of validation. + :return: - None or the parsed value of the policy mappings extension + None or a PolicyMappings object """ if not self._processed_extensions: @@ -1046,9 +1076,11 @@ class Certificate(Sequence): @property def authority_key_identifier_value(self): """ + This extension helps in identifying the public key with which to + validate the authenticity of the certificate. + :return: - None or the parsed value of the authority key identifier - extension + None or an AuthorityKeyIdentifier object """ if not self._processed_extensions: @@ -1058,8 +1090,11 @@ class Certificate(Sequence): @property def policy_constraints_value(self): """ + This extension is used to control if policy mapping is allowed and + when policies are required. + :return: - None or the parsed value of the policy constraints extension + None or a PolicyConstraints object """ if not self._processed_extensions: @@ -1096,8 +1131,11 @@ class Certificate(Sequence): @property def extended_key_usage_value(self): """ + This extension is used to define additional purposes for the public key + beyond what is contained in the basic constraints. + :return: - None or the parsed value of the extended key usage extension + None or an ExtKeyUsageSyntax object """ if not self._processed_extensions: @@ -1107,8 +1145,11 @@ class Certificate(Sequence): @property def authority_information_access_value(self): """ + This extension is used to locate the CA certificate used to sign this + certificate, or the OCSP responder for this certificate. + :return: - None or the parsed value of the authority information access extension + None or an AuthorityInfoAccessSyntax object """ if not self._processed_extensions: @@ -1132,8 +1173,12 @@ class Certificate(Sequence): @property def ocsp_no_check_value(self): """ + This extension is used on certificates of OCSP responders, indicating + that revocation information for the certificate should never need to + be verified, thus preventing possible loops in path validation. + :return: - None or the parsed value of the OCSP no check extension + None or Null (if present) """ if not self._processed_extensions: -- cgit v1.2.3 From 5d1e851134ad7c8d607dc4748cd53e2d2bdaabd0 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 09:01:55 -0400 Subject: Ignore coverage file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2ad65e9..2dc6a99 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ tests/output/ .DS_Store tmp/ +.coverage -- cgit v1.2.3 From 90ec130d2e74486370de62372e02d2425e1fc8e6 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 09:10:50 -0400 Subject: Restructured ocsp.py so classes are in more of a dependency order --- asn1crypto/ocsp.py | 201 +++++++++++++++++++++++++++-------------------------- 1 file changed, 101 insertions(+), 100 deletions(-) diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index efd7283..846f40a 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -36,14 +36,19 @@ from .x509 import Certificate, GeneralName, GeneralNames, Name # The structures in this file are taken from https://tools.ietf.org/html/rfc6960 -class ResponseType(ObjectIdentifier): +class Version(Integer): _map = { - '1.3.6.1.5.5.7.48.1.1': 'basic_ocsp_response', + 0: 'v1' } -class AcceptableResponses(SequenceOf): - _child_spec = ResponseType +class CertId(Sequence): + _fields = [ + ('hash_algorithm', DigestAlgorithm), + ('issuer_name_hash', OctetString), + ('issuer_key_hash', OctetString), + ('serial_number', Integer), + ] class ServiceLocator(Sequence): @@ -53,17 +58,6 @@ class ServiceLocator(Sequence): ] -class PreferredSignatureAlgorithm(Sequence): - _fields = [ - ('sig_identifier', SignedDigestAlgorithm), - ('cert_identifier', PublicKeyAlgorithm, {'optional': True}), - ] - - -class PreferredSignatureAlgorithms(SequenceOf): - _child_spec = PreferredSignatureAlgorithm - - class RequestExtensionId(ObjectIdentifier): _map = { '1.3.6.1.5.5.7.48.1.7': 'ocsp_service_locator', @@ -87,121 +81,63 @@ class RequestExtensions(SequenceOf): _child_spec = RequestExtension -class TBSRequestExtensionId(ObjectIdentifier): - _map = { - '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', - '1.3.6.1.5.5.7.48.1.4': 'ocsp_response', - '1.3.6.1.5.5.7.48.1.8': 'ocsp_preferred_signature_algorithms', - } - - -class TBSRequestExtension(Sequence): +class Request(Sequence): _fields = [ - ('extn_id', TBSRequestExtensionId), - ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('req_cert', CertId), + ('single_request_extensions', RequestExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), ] - _oid_pair = ('extn_id', 'extn_value') - _oid_specs = { - 'ocsp_nonce': OctetString, - 'ocsp_response': AcceptableResponses, - 'ocsp_preferred_signature_algorithms': PreferredSignatureAlgorithms, - } - -class TBSRequestExtensions(SequenceOf): - _child_spec = TBSRequestExtension +class Requests(SequenceOf): + _child_spec = Request -class ResponseDataExtensionId(ObjectIdentifier): +class ResponseType(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', - '1.3.6.1.5.5.7.48.1.9': 'ocsp_extended_revoke', - } - - -class ResponseDataExtension(Sequence): - _fields = [ - ('extn_id', ResponseDataExtensionId), - ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), - ] - - _oid_pair = ('extn_id', 'extn_value') - _oid_specs = { - 'ocsp_nonce': OctetString, - 'ocsp_extended_revoke': Null, + '1.3.6.1.5.5.7.48.1.1': 'basic_ocsp_response', } -class ResponseDataExtensions(SequenceOf): - _child_spec = ResponseDataExtension +class AcceptableResponses(SequenceOf): + _child_spec = ResponseType -class CrlId(Sequence): +class PreferredSignatureAlgorithm(Sequence): _fields = [ - ('crl_url', IA5String, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('crl_num', Integer, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), - ('crl_time', GeneralizedTime, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ('sig_identifier', SignedDigestAlgorithm), + ('cert_identifier', PublicKeyAlgorithm, {'optional': True}), ] -class SingleResponseExtensionId(ObjectIdentifier): +class PreferredSignatureAlgorithms(SequenceOf): + _child_spec = PreferredSignatureAlgorithm + + +class TBSRequestExtensionId(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.3': 'ocsp_crl', - '1.3.6.1.5.5.7.48.1.6': 'ocsp_archive_cutoff', - # These are CRLEntryExtension values from https://tools.ietf.org/html/rfc5280 - '2.5.29.21': 'crl_reason', - '2.5.29.24': 'invalidity_date', - '2.5.29.29': 'certificate_issuer', + '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', + '1.3.6.1.5.5.7.48.1.4': 'ocsp_response', + '1.3.6.1.5.5.7.48.1.8': 'ocsp_preferred_signature_algorithms', } -class SingleResponseExtension(Sequence): +class TBSRequestExtension(Sequence): _fields = [ - ('extn_id', SingleResponseExtensionId), + ('extn_id', TBSRequestExtensionId), ('critical', Boolean, {'default': False}), ('extn_value', OctetString), ] _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'ocsp_crl': CrlId, - 'ocsp_archive_cutoff': GeneralizedTime, - 'crl_reason': CRLReason, - 'invalidity_date': GeneralizedTime, - 'certificate_issuer': GeneralNames, - } - - -class SingleResponseExtensions(SequenceOf): - _child_spec = SingleResponseExtension - - -class Version(Integer): - _map = { - 0: 'v1' + 'ocsp_nonce': OctetString, + 'ocsp_response': AcceptableResponses, + 'ocsp_preferred_signature_algorithms': PreferredSignatureAlgorithms, } -class CertId(Sequence): - _fields = [ - ('hash_algorithm', DigestAlgorithm), - ('issuer_name_hash', OctetString), - ('issuer_key_hash', OctetString), - ('serial_number', Integer), - ] - - -class Request(Sequence): - _fields = [ - ('req_cert', CertId), - ('single_request_extensions', RequestExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ] - -class Requests(SequenceOf): - _child_spec = Request +class TBSRequestExtensions(SequenceOf): + _child_spec = TBSRequestExtension class TBSRequest(Sequence): @@ -265,6 +201,46 @@ class CertStatus(Choice): ] +class CrlId(Sequence): + _fields = [ + ('crl_url', IA5String, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('crl_num', Integer, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('crl_time', GeneralizedTime, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ] + + +class SingleResponseExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.3': 'ocsp_crl', + '1.3.6.1.5.5.7.48.1.6': 'ocsp_archive_cutoff', + # These are CRLEntryExtension values from https://tools.ietf.org/html/rfc5280 + '2.5.29.21': 'crl_reason', + '2.5.29.24': 'invalidity_date', + '2.5.29.29': 'certificate_issuer', + } + + +class SingleResponseExtension(Sequence): + _fields = [ + ('extn_id', SingleResponseExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'ocsp_crl': CrlId, + 'ocsp_archive_cutoff': GeneralizedTime, + 'crl_reason': CRLReason, + 'invalidity_date': GeneralizedTime, + 'certificate_issuer': GeneralNames, + } + + +class SingleResponseExtensions(SequenceOf): + _child_spec = SingleResponseExtension + + class SingleResponse(Sequence): _fields = [ ('cert_id', CertId), @@ -279,6 +255,31 @@ class Responses(SequenceOf): _child_spec = SingleResponse +class ResponseDataExtensionId(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', + '1.3.6.1.5.5.7.48.1.9': 'ocsp_extended_revoke', + } + + +class ResponseDataExtension(Sequence): + _fields = [ + ('extn_id', ResponseDataExtensionId), + ('critical', Boolean, {'default': False}), + ('extn_value', OctetString), + ] + + _oid_pair = ('extn_id', 'extn_value') + _oid_specs = { + 'ocsp_nonce': OctetString, + 'ocsp_extended_revoke': Null, + } + + +class ResponseDataExtensions(SequenceOf): + _child_spec = ResponseDataExtension + + class ResponseData(Sequence): _fields = [ ('version', Version, {'tag_type': 'explicit', 'tag': 0, 'default': 'v1'}), -- cgit v1.2.3 From a613f810125729047521e4d146fd47b41f389052 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 10:13:47 -0400 Subject: Fixed extension attributes in crl.CertificateList --- asn1crypto/crl.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 9d90e60..435a86c 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -273,13 +273,13 @@ class CertificateList(Sequence): _processed_extensions = False _critical_extensions = None - _issuer_alt_name = None - _crl_number = None - _delta_crl_indicator = None - _issuing_distribution_point = None - _authority_key_identifier = None - _freshest_crl = None - _authority_information_access = None + _issuer_alt_name_value = None + _crl_number_value = None + _delta_crl_indicator_value = None + _issuing_distribution_point_value = None + _authority_key_identifier_value = None + _freshest_crl_value = None + _authority_information_access_value = None def _set_extensions(self): """ -- cgit v1.2.3 From 65593fe8ae30faf63eea9a874732d95bbf245e3e Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 10:14:50 -0400 Subject: Removed the "ocsp_" prefix from various OCSP OIDs --- asn1crypto/ocsp.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 846f40a..147cd54 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -60,7 +60,7 @@ class ServiceLocator(Sequence): class RequestExtensionId(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.7': 'ocsp_service_locator', + '1.3.6.1.5.5.7.48.1.7': 'service_locator', } @@ -73,7 +73,7 @@ class RequestExtension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'ocsp_service_locator': ServiceLocator, + 'service_locator': ServiceLocator, } @@ -115,9 +115,9 @@ class PreferredSignatureAlgorithms(SequenceOf): class TBSRequestExtensionId(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', - '1.3.6.1.5.5.7.48.1.4': 'ocsp_response', - '1.3.6.1.5.5.7.48.1.8': 'ocsp_preferred_signature_algorithms', + '1.3.6.1.5.5.7.48.1.2': 'nonce', + '1.3.6.1.5.5.7.48.1.4': 'acceptable_responses', + '1.3.6.1.5.5.7.48.1.8': 'preferred_signature_algorithms', } @@ -130,9 +130,9 @@ class TBSRequestExtension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'ocsp_nonce': OctetString, - 'ocsp_response': AcceptableResponses, - 'ocsp_preferred_signature_algorithms': PreferredSignatureAlgorithms, + 'nonce': OctetString, + 'acceptable_responses': AcceptableResponses, + 'preferred_signature_algorithms': PreferredSignatureAlgorithms, } @@ -211,9 +211,10 @@ class CrlId(Sequence): class SingleResponseExtensionId(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.3': 'ocsp_crl', - '1.3.6.1.5.5.7.48.1.6': 'ocsp_archive_cutoff', - # These are CRLEntryExtension values from https://tools.ietf.org/html/rfc5280 + '1.3.6.1.5.5.7.48.1.3': 'crl', + '1.3.6.1.5.5.7.48.1.6': 'archive_cutoff', + # These are CRLEntryExtension values from + # https://tools.ietf.org/html/rfc5280 '2.5.29.21': 'crl_reason', '2.5.29.24': 'invalidity_date', '2.5.29.29': 'certificate_issuer', @@ -229,8 +230,8 @@ class SingleResponseExtension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'ocsp_crl': CrlId, - 'ocsp_archive_cutoff': GeneralizedTime, + 'crl': CrlId, + 'archive_cutoff': GeneralizedTime, 'crl_reason': CRLReason, 'invalidity_date': GeneralizedTime, 'certificate_issuer': GeneralNames, @@ -257,8 +258,8 @@ class Responses(SequenceOf): class ResponseDataExtensionId(ObjectIdentifier): _map = { - '1.3.6.1.5.5.7.48.1.2': 'ocsp_nonce', - '1.3.6.1.5.5.7.48.1.9': 'ocsp_extended_revoke', + '1.3.6.1.5.5.7.48.1.2': 'nonce', + '1.3.6.1.5.5.7.48.1.9': 'extended_revoke', } @@ -271,8 +272,8 @@ class ResponseDataExtension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'ocsp_nonce': OctetString, - 'ocsp_extended_revoke': Null, + 'nonce': OctetString, + 'extended_revoke': Null, } -- cgit v1.2.3 From bcb6264030e3e5d7be9d97a905ae7786d94d7ddb Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 10:16:27 -0400 Subject: Added high-level extension attributes to ocsp.Request, ocsp.OCSPRequest, ocsp.SingleResponse and ocsp.OCSPResponse --- asn1crypto/ocsp.py | 308 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 147cd54..e940f90 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -87,6 +87,56 @@ class Request(Sequence): ('single_request_extensions', RequestExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), ] + _processed_extensions = False + _critical_extensions = None + _service_locator_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = [] + + for extension in self['single_request_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.append(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a list of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A list of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def service_locator_value(self): + """ + This extension is used when communicating with an OCSP responder that + acts as a proxy for OCSP requests + + :return: + None or a ServiceLocator object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._service_locator_value + class Requests(SequenceOf): _child_spec = Request @@ -167,6 +217,89 @@ class OCSPRequest(Sequence): ('optional_signature', Signature, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), ] + _processed_extensions = False + _critical_extensions = None + _nonce_value = None + _acceptable_responses_value = None + _preferred_signature_algorithms_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = [] + + for extension in self['tbs_request']['request_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.append(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a list of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A list of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def nonce_value(self): + """ + This extension is used to prevent replay attacks by including a unique, + random value with each request/response pair + + :return: + None or an OctetString object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._nonce_value + + @property + def acceptable_responses_value(self): + """ + This extension is used to allow the client and server to communicate + with alternative response formats other than just basic_ocsp_response, + although no other formats are defined in the standard. + + :return: + None or an AcceptableResponses object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._acceptable_responses_value + + @property + def preferred_signature_algorithms_value(self): + """ + This extension is used by the client to define what signature algorithms + are preferred, including both the hash algorithm and the public key + algorithm, with a level of detail down to even the public key algorithm + parameters, such as curve name. + + :return: + None or a PreferredSignatureAlgorithms object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._preferred_signature_algorithms_value + class OCSPResponseStatus(Enumerated): _map = { @@ -251,6 +384,116 @@ class SingleResponse(Sequence): ('single_extensions', SingleResponseExtensions, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), ] + _processed_extensions = False + _critical_extensions = None + _crl_value = None + _archive_cutoff_value = None + _crl_reason_value = None + _invalidity_date_value = None + _certificate_issuer_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = [] + + for extension in self['single_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.append(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a list of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A list of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def crl_value(self): + """ + This extension is used to locate the CRL that a certificate's revocation + is contained within. + + :return: + None or a CrlId object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._crl_value + + @property + def archive_cutoff_value(self): + """ + This extension is used to indicate the date at which an archived + (historical) certificate status entry will no longer be available. + + :return: + None or a GeneralizedTime object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._archive_cutoff_value + + @property + def crl_reason_value(self): + """ + This extension indicates the reason that a certificate was revoked. + + :return: + None or a CRLReason object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._crl_reason_value + + @property + def invalidity_date_value(self): + """ + This extension indicates the suspected date/time the private key was + compromised or the certificate became invalid. This would usually be + before the revocation date, which is when the CA processed the + revocation. + + :return: + None or a GeneralizedTime object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._invalidity_date_value + + @property + def certificate_issuer_value(self): + """ + This extension indicates the issuer of the certificate in question. + + :return: + None or an x509.GeneralNames object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._certificate_issuer_value + class Responses(SequenceOf): _child_spec = SingleResponse @@ -317,3 +560,68 @@ class OCSPResponse(Sequence): ('response_status', OCSPResponseStatus), ('response_bytes', ResponseBytes, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), ] + + _processed_extensions = False + _critical_extensions = None + _nonce_value = None + _extended_revoke_value = None + + def _set_extensions(self): + """ + Sets common named extensions to private attributes and creates a list + of critical extensions + """ + + self._critical_extensions = [] + + for extension in self['response_bytes']['response'].parsed['tbs_response_data']['response_extensions']: + name = extension['extn_id'].native + attribute_name = '_%s_value' % name + if hasattr(self, attribute_name): + setattr(self, attribute_name, extension['extn_value'].parsed) + if extension['critical'].native: + self._critical_extensions.append(name) + + self._processed_extensions = True + + @property + def critical_extensions(self): + """ + Returns a list of the names (or OID if not a known extension) of the + extensions marked as critical + + :return: + A list of unicode strings + """ + + if not self._processed_extensions: + self._set_extensions() + return self._critical_extensions + + @property + def nonce_value(self): + """ + This extension is used to prevent replay attacks on the request/response + exchange + + :return: + None or an OctetString object + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._nonce_value + + @property + def extended_revoke_value(self): + """ + This extension is used to signal that the responder will return a + "revoked" status for non-issued certificates. + + :return: + None or a Null object (if present) + """ + + if self._processed_extensions is False: + self._processed_extensions() + return self._extended_revoke_value -- cgit v1.2.3 From d0f71af1ed72d72adb959cfeacec178df11737d7 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 10:17:01 -0400 Subject: Make the value of x509.Certificate.ocsp_no_check_value more clear --- asn1crypto/x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 4b152d3..05f29dc 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1178,7 +1178,7 @@ class Certificate(Sequence): be verified, thus preventing possible loops in path validation. :return: - None or Null (if present) + None or a Null object (if present) """ if not self._processed_extensions: -- cgit v1.2.3 From ad218f99ce4ac1353264eb90d72b9b0e15714ad0 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 10:43:16 -0400 Subject: Fix method calls to process OCSP and CRL extensions --- asn1crypto/crl.py | 20 ++++++++++---------- asn1crypto/ocsp.py | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 435a86c..1e3ee75 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -213,7 +213,7 @@ class RevokedCertificate(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._crl_reason_value @property @@ -229,7 +229,7 @@ class RevokedCertificate(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._invalidity_date_value @property @@ -244,7 +244,7 @@ class RevokedCertificate(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._certificate_issuer_value @@ -324,7 +324,7 @@ class CertificateList(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._issuer_alt_name_value @property @@ -338,7 +338,7 @@ class CertificateList(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._crl_number_value @property @@ -352,7 +352,7 @@ class CertificateList(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._delta_crl_indicator_value @property @@ -366,7 +366,7 @@ class CertificateList(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._issuing_distribution_point_value @property @@ -380,7 +380,7 @@ class CertificateList(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._authority_key_identifier_value @property @@ -394,7 +394,7 @@ class CertificateList(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._freshest_crl_value @property @@ -408,5 +408,5 @@ class CertificateList(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._authority_information_access_value diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index e940f90..6f599d2 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -134,7 +134,7 @@ class Request(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._service_locator_value @@ -266,7 +266,7 @@ class OCSPRequest(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._nonce_value @property @@ -281,7 +281,7 @@ class OCSPRequest(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._acceptable_responses_value @property @@ -297,7 +297,7 @@ class OCSPRequest(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._preferred_signature_algorithms_value @@ -435,7 +435,7 @@ class SingleResponse(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._crl_value @property @@ -449,7 +449,7 @@ class SingleResponse(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._archive_cutoff_value @property @@ -462,7 +462,7 @@ class SingleResponse(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._crl_reason_value @property @@ -478,7 +478,7 @@ class SingleResponse(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._invalidity_date_value @property @@ -491,7 +491,7 @@ class SingleResponse(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._certificate_issuer_value @@ -609,7 +609,7 @@ class OCSPResponse(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._nonce_value @property @@ -623,5 +623,5 @@ class OCSPResponse(Sequence): """ if self._processed_extensions is False: - self._processed_extensions() + self._set_extensions() return self._extended_revoke_value -- cgit v1.2.3 From fe20ba757078b8f91098ff4d913a3b134c762a58 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Jul 2015 12:55:08 -0400 Subject: Improve consistency of exception error messages --- asn1crypto/core.py | 6 +++--- asn1crypto/keys.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 072a440..3d89ec6 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -221,13 +221,13 @@ class Asn1Value(): if tag_type is not None: if tag_type not in ('implicit', 'explicit'): - raise ValueError('tag_type must be one of "implicit", "explicit" - is %s' % repr(tag_type)) + raise ValueError('tag_type must be one of "implicit", "explicit", not %s' % repr(tag_type)) self.tag_type = tag_type if class_ is None: class_ = 'context' if class_ not in CLASS_NAME_TO_NUM_MAP: - raise ValueError('class_ must be one of "universal", "application", "context", "private" - is %s' % repr(class_)) + raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) class_ = CLASS_NAME_TO_NUM_MAP[class_] if tag is not None: @@ -243,7 +243,7 @@ class Asn1Value(): else: if class_ is not None: if class_ not in CLASS_NUM_TO_NAME_MAP: - raise ValueError('class_ must be one of "universal", "application", "context", "private" - is %s' % repr(class_)) + raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) self.class_ = CLASS_NAME_TO_NUM_MAP[class_] if tag is not None: diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 11d7fd3..1fcefbe 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -481,7 +481,7 @@ class PrivateKeyInfo(Sequence): params = private_key['parameters'] del private_key['parameters'] else: - raise ValueError('algorithm must be one of "rsa", "dsa", "ec" - is %s' % repr(algorithm)) + raise ValueError('algorithm must be one of "rsa", "dsa", "ec", not %s' % repr(algorithm)) private_key_algo = PrivateKeyAlgorithm() private_key_algo['algorithm'] = PrivateKeyAlgorithmId(algorithm) @@ -850,7 +850,7 @@ class PublicKeyInfo(Sequence): raise ValueError('public_key must be a byte string or Asn1Value, not %s' % public_key.__class__.__name__) if algorithm != 'rsa': - raise ValueError('algorithm must be one of "rsa" - is %s' % repr(algorithm)) + raise ValueError('algorithm must "rsa", not %s' % repr(algorithm)) algo = PublicKeyAlgorithm() algo['algorithm'] = PublicKeyAlgorithmId(algorithm) -- cgit v1.2.3 From 4a2627ff1ca2ddc8f6ad529e25984f26f99d3b15 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jul 2015 14:56:48 -0400 Subject: Fix typo in crl.Certificate --- asn1crypto/crl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 1e3ee75..60d0dde 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -267,7 +267,7 @@ class TbsCertList(Sequence): class CertificateList(Sequence): _fields = [ ('tbs_cert_list', TbsCertList), - ('signature_algorith', SignedDigestAlgorithm), + ('signature_algorithm', SignedDigestAlgorithm), ('signature', OctetBitString), ] -- cgit v1.2.3 From 5fb733ff0a149290a06744b496118648ca56210f Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jul 2015 14:57:59 -0400 Subject: Added high-level extension attributes to crl.CertificateList and crl.RevokedCertificate --- asn1crypto/crl.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 60d0dde..5cac004 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -170,6 +170,7 @@ class RevokedCertificate(Sequence): _crl_reason_value = None _invalidity_date_value = None _certificate_issuer_value = None + _issuer_name = False def _set_extensions(self): """ @@ -247,6 +248,22 @@ class RevokedCertificate(Sequence): self._set_extensions() return self._certificate_issuer_value + @property + def issuer_name(self): + """ + :return: + None, or an asn1crypto.x509.Name object for the issuer of the cert + """ + + if self._issuer_name is False: + self._issuer_name = None + if self.certificate_issuer_value: + for general_name in self.certificate_issuer_value: + if general_name.name == 'directory_name': + self._issuer_name = general_name.chosen + break + return self._issuer_name + class RevokedCertificates(SequenceOf): _child_spec = RevokedCertificate @@ -280,6 +297,8 @@ class CertificateList(Sequence): _authority_key_identifier_value = None _freshest_crl_value = None _authority_information_access_value = None + _issuer_cert_urls = None + _delta_crl_distribution_points = None def _set_extensions(self): """ @@ -410,3 +429,63 @@ class CertificateList(Sequence): if self._processed_extensions is False: self._set_extensions() return self._authority_information_access_value + + @property + def authority_key_identifier(self): + """ + :return: + None or a byte string of the key_identifier from the authority key + identifier extension + """ + + if not self.authority_key_identifier_value: + return None + + return self.authority_key_identifier_value['key_identifier'].native + + @property + def issuer_cert_urls(self): + """ + :return: + A list of unicode strings that are URLs that should contain either + an individual DER-encoded X509 certificate, or a DER-encoded CMS + message containing multiple certificates + """ + + if self._issuer_cert_urls is None: + self._issuer_cert_urls = [] + if self.authority_information_access_value: + for entry in self.authority_information_access_value: + if entry['access_method'].native == 'ca_issuers': + location = entry['access_location'] + if location.name != 'uniform_resource_identifier': + continue + url = location.native + if url.lower()[0:7] == 'http://': + self._issuer_cert_urls.append(url) + return self._issuer_cert_urls + + @property + def delta_crl_distribution_points(self): + """ + Returns delta CRL URLs - only applies to complete CRLs + + :return: + A list of zero or more DistributionPoint objects + """ + + if self._delta_crl_distribution_points is None: + self._delta_crl_distribution_points = [] + + if self.freshest_crl_value is not None: + for distribution_point in self.freshest_crl_value: + distribution_point_name = distribution_point['distribution_point'] + # RFC5280 indicates conforming CA should not use the relative form + if distribution_point_name.name == 'name_relative_to_crl_issuer': + continue + # This library is currently only concerned with HTTP-based CRLs + for general_name in distribution_point_name.chosen: + if general_name.name == 'uniform_resource_identifier': + self._delta_crl_distribution_points.append(distribution_point) + + return self._delta_crl_distribution_points -- cgit v1.2.3 From 6888bc66020caa3259b4e88c77e4f63d58e69d79 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jul 2015 15:05:59 -0400 Subject: Instead of returning CRL URLs, we return the whole DistributionPoint object for CRL validation --- asn1crypto/x509.py | 49 ++++++++++++++++++++++++++++++----- tests/test_x509.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 107 insertions(+), 17 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 05f29dc..ccd3786 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -891,6 +891,8 @@ class Certificate(Sequence): _ocsp_no_check_value = None _issuer_serial = None _authority_issuer_serial = False + _crl_distribution_points = None + _delta_crl_distribution_points = None _valid_domains = None _valid_ips = None @@ -1282,24 +1284,57 @@ class Certificate(Sequence): return self._authority_issuer_serial @property - def crl_urls(self): + def crl_distribution_points(self): """ + Returns complete CRL URLs - does not include delta CRLs + :return: - A list of zero or more unicode strings of the CRL URLs for this cert + A list of zero or more DistributionPoint objects """ - if not self.crl_distribution_points_value: - return [] + if self._crl_distribution_points is None: + self._crl_distribution_points = self._get_http_crl_distribution_points(self.crl_distribution_points_value) + return self._crl_distribution_points + + @property + def delta_crl_distribution_points(self): + """ + Returns delta CRL URLs - does not include complete CRLs + + :return: + A list of zero or more DistributionPoint objects + """ + + if self._delta_crl_distribution_points is None: + self._delta_crl_distribution_points = self._get_http_crl_distribution_points(self.freshest_crl_value) + return self._delta_crl_distribution_points + + def _get_http_crl_distribution_points(self, crl_distribution_points): + """ + Fetches the DistributionPoint object for non-relative, HTTP CRLs + referenced by the certificate + + :param crl_distribution_points: + A CRLDistributionPoints object to grab the DistributionPoints from + + :return: + A list of zero or more DistributionPoint objects + """ output = [] - for entry in self.crl_distribution_points_value: - distribution_point_name = entry['distribution_point'] + + if crl_distribution_points is None: + return [] + + for distribution_point in crl_distribution_points: + distribution_point_name = distribution_point['distribution_point'] # RFC5280 indicates conforming CA should not use the relative form if distribution_point_name.name == 'name_relative_to_crl_issuer': continue + # This library is currently only concerned with HTTP-based CRLs for general_name in distribution_point_name.chosen: if general_name.name == 'uniform_resource_identifier': - output.append(general_name.native) + output.append(distribution_point) return output diff --git a/tests/test_x509.py b/tests/test_x509.py index 1a7c4bc..13b3d2a 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -1165,29 +1165,84 @@ class X509Tests(unittest.TestCase): #pylint: disable=C0326 @staticmethod - def crl_urls_info(): + def crl_distribution_points_info(): return ( ('keys/test-der.crt', []), ('keys/test-inter-der.crt', []), ('keys/test-third-der.crt', []), ('geotrust_certs/GeoTrust_Universal_CA.crt', []), ('geotrust_certs/GeoTrust_Primary_CA.crt', []), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', ['http://g1.symcb.com/GeoTrustPCA.crl']), - ('geotrust_certs/codex.crt', ['http://gm.symcb.com/gm.crl']), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + [ + OrderedDict([ + ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ( + 'geotrust_certs/codex.crt', + [ + OrderedDict([ + ('distribution_point', ['http://gm.symcb.com/gm.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), ('lets_encrypt/isrgrootx1.pem', []), - ('lets_encrypt/letsencryptauthorityx1.pem', ['http://crl.root-x1.letsencrypt.org']), - ('lets_encrypt/letsencryptauthorityx2.pem', ['http://crl.root-x1.letsencrypt.org']), - ('globalsign_example_keys/IssuingCA-der.cer', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), - ('globalsign_example_keys/rootCA.cer', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + [ + OrderedDict([ + ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + [ + OrderedDict([ + ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + [ + OrderedDict([ + ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), + ( + 'globalsign_example_keys/rootCA.cer', + [ + OrderedDict([ + ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ), ('globalsign_example_keys/SSL1.cer', []), ('globalsign_example_keys/SSL2.cer', []), ('globalsign_example_keys/SSL3.cer', []), ) - @data('crl_urls_info') - def crl_urls(self, relative_path, crl_url): + @data('crl_distribution_points_info') + def crl_distribution_points(self, relative_path, crl_distribution_point): cert = self._load_cert(relative_path) - self.assertEqual(crl_url, cert.crl_urls) + points = [point.native for point in cert.crl_distribution_points] + self.assertEqual(crl_distribution_point, points) #pylint: disable=C0326 @staticmethod -- cgit v1.2.3 From 4ff78f2ac49db485936cf31626de432cbbb8dab9 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jul 2015 15:26:51 -0400 Subject: Added core.BitString.named_bits --- asn1crypto/core.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 3d89ec6..dc3ddc5 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1052,6 +1052,22 @@ class BitString(Primitive, ValueMap, object): self._native[key] = bool(value) self.set(self._native) + @property + def named_bits(self): + """ + :return: + A set of unicode strings for the bits that are set to 1 + """ + + if not isinstance(self._map, dict): + raise ValueError('%s bit map has not been defined' % self.__class__.__name__) + + output = set() + for key, value in self.native.items(): + if value: + output.add(key) + return output + @property def native(self): """ -- cgit v1.2.3 From 3b22a4c8038cfd5f98ad586936e4751fb8a5c586 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jul 2015 15:27:40 -0400 Subject: Added x509.DistributionPoint.url --- asn1crypto/x509.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index ccd3786..afb0bc8 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -620,6 +620,30 @@ class DistributionPoint(Sequence): ('crl_issuer', GeneralNames, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), ] + _url = False + + @property + def url(self): + """ + :return: + None or a unicode string of the distribution point's URL + """ + + if self._url is False: + self._url = None + name = self['distribution_point'] + if name.name != 'full_name': + raise ValueError('CRL distribution points that are relative to the issuer are not supported') + + for general_name in name.chosen: + if general_name.name == 'uniform_resource_identifier': + url = general_name.native + if url[0:7] == 'http://': + self._url = url + break + + return self._url + class CRLDistributionPoints(SequenceOf): _child_spec = DistributionPoint -- cgit v1.2.3 From 4c248d59809d89fea78074fad4278d38f2d61b04 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jul 2015 15:28:28 -0400 Subject: Ensure that OCSP URIs are http:// --- asn1crypto/x509.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index afb0bc8..546d631 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1376,8 +1376,12 @@ class Certificate(Sequence): output = [] for entry in self.authority_information_access_value: if entry['access_method'].native == 'ocsp': - output.append(entry['access_location'].native) - + location = entry['access_location'] + if location.name != 'uniform_resource_identifier': + continue + url = location.native + if url.lower()[0:7] == 'http://': + output.append(url) return output @property -- cgit v1.2.3 From f46457278a0a86fec89e1b6f5b4b2d2539c3f918 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 22 Jul 2015 12:36:37 -0400 Subject: Added proper IP address parsing to x509.GeneralName --- asn1crypto/_ffi.py | 87 +++++++++++++++++++++++++++++++++ asn1crypto/_int.py | 6 +-- asn1crypto/_perf/_big_num_cffi.py | 14 +----- asn1crypto/_perf/_big_num_ctypes.py | 16 +------ asn1crypto/_perf/_ffi.py | 26 ---------- asn1crypto/_win/__init__.py | 0 asn1crypto/_win/_ws2_32.py | 72 ++++++++++++++++++++++++++++ asn1crypto/_win/_ws2_32_cffi.py | 36 ++++++++++++++ asn1crypto/_win/_ws2_32_ctypes.py | 33 +++++++++++++ asn1crypto/x509.py | 96 ++++++++++++++++++++++++++++++++++++- tests/test_x509.py | 22 ++++++++- 11 files changed, 349 insertions(+), 59 deletions(-) create mode 100644 asn1crypto/_ffi.py delete mode 100644 asn1crypto/_perf/_ffi.py create mode 100644 asn1crypto/_win/__init__.py create mode 100644 asn1crypto/_win/_ws2_32.py create mode 100644 asn1crypto/_win/_ws2_32_cffi.py create mode 100644 asn1crypto/_win/_ws2_32_ctypes.py diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py new file mode 100644 index 0000000..375aeb3 --- /dev/null +++ b/asn1crypto/_ffi.py @@ -0,0 +1,87 @@ +# coding: utf-8 + +""" +Exceptions for help trying to use cffi, then ctypes for shared library access. +Also includes 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 + + +try: + import cffi + + ffi = cffi.FFI() + + def buffer_from_bytes(initializer): + return ffi.new('char[]', initializer) + + def unicode_buffer(initializer): + return ffi.new('wchar_t[]', initializer) + + def bytes_from_buffer(buffer, maxlen=None): + return ffi.buffer(buffer, maxlen)[:] + + def null(): + return ffi.NULL + + def cast_void_p(value): + return ffi.cast('void *', value) + + def is_null(point): + if point == ffi.NULL: + return True + if point[0] == ffi.NULL: + return True + return False + + def string_from_buffer(buffer): + return ffi.string(buffer) + +except (ImportError): + from ctypes import create_string_buffer, create_unicode_buffer, cast, c_void_p + + def buffer_from_bytes(initializer): + return create_string_buffer(initializer) + + def unicode_buffer(initializer): + return create_unicode_buffer(initializer) + + def bytes_from_buffer(buffer, maxlen=None): #pylint: disable=W0613 + return buffer.raw + + def null(): + return None + + def cast_void_p(value): + return cast(value, c_void_p) + + def is_null(point): + return not bool(point) + + def string_from_buffer(buffer): + return buffer.value + + +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 902ce81..d63c799 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -39,7 +39,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import math -from ._perf._ffi import LibraryNotFoundError, FFIEngineError +from ._ffi import LibraryNotFoundError, FFIEngineError, buffer_from_bytes, bytes_from_buffer, null @@ -140,9 +140,9 @@ else: # First try to use ctypes or cffi with OpenSSL for better performance try: try: - from ._perf._big_num_cffi import libcrypto, buffer_from_bytes, bytes_from_buffer, null + from ._perf._big_num_cffi import libcrypto except (FFIEngineError) as e: - from ._perf._big_num_ctypes import libcrypto, buffer_from_bytes, bytes_from_buffer, null + from ._perf._big_num_ctypes import libcrypto def inverse_mod(a, p): """ diff --git a/asn1crypto/_perf/_big_num_cffi.py b/asn1crypto/_perf/_big_num_cffi.py index 3b47200..2b7b217 100644 --- a/asn1crypto/_perf/_big_num_cffi.py +++ b/asn1crypto/_perf/_big_num_cffi.py @@ -4,8 +4,6 @@ cffi interface for BN_mod_inverse() function from OpenSSL. Exports the following items: - - buffer_from_bytes() - - bytes_from_buffer() - libcrypto - BN_bin2bin() - BN_CTX_free() @@ -15,7 +13,6 @@ following items: - BN_new() - BN_num_bits() - BN_set_negative() - - null() Will raise asn1crypto._ffi.LibraryNotFoundError() if libcrypto can not be found. Will raise asn1crypto._ffi.FFIEngineError() if cffi is not instaled @@ -26,7 +23,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi from ctypes.util import find_library -from ._ffi import LibraryNotFoundError, FFIEngineError +from .._ffi import LibraryNotFoundError, FFIEngineError try: from cffi import FFI @@ -64,14 +61,5 @@ try: libcrypto = ffi.dlopen(libcrypto_path) - def buffer_from_bytes(initializer): - return ffi.new('char[]', initializer) - - def bytes_from_buffer(buffer, maxlen=None): - return ffi.buffer(buffer, maxlen)[:] - - def null(): - return ffi.NULL - except (AttributeError): raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_perf/_big_num_ctypes.py b/asn1crypto/_perf/_big_num_ctypes.py index d764223..a99b658 100644 --- a/asn1crypto/_perf/_big_num_ctypes.py +++ b/asn1crypto/_perf/_big_num_ctypes.py @@ -4,8 +4,6 @@ ctypes interface for BN_mod_inverse() function from OpenSSL. Exports the following items: - - buffer_from_bytes() - - bytes_from_buffer() - libcrypto - BN_bin2bin() - BN_CTX_free() @@ -15,7 +13,6 @@ following items: - BN_new() - BN_num_bits() - BN_set_negative() - - null() Will raise asn1crypto._ffi.LibraryNotFoundError() if libcrypto can not be found. Will raise asn1crypto._ffi.FFIEngineError() if there is an error @@ -25,9 +22,9 @@ interfacing with libcrypto. from __future__ import unicode_literals, division, absolute_import, print_function from ctypes.util import find_library -from ctypes import CDLL, c_int, c_char_p, c_void_p, create_string_buffer +from ctypes import CDLL, c_int, c_char_p, c_void_p -from ._ffi import LibraryNotFoundError, FFIEngineError +from .._ffi import LibraryNotFoundError, FFIEngineError try: @@ -64,14 +61,5 @@ try: 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 - def buffer_from_bytes(initializer): - return create_string_buffer(initializer) - - def bytes_from_buffer(buffer, maxlen=None): #pylint: disable=W0613 - return buffer.raw - - def null(): - return None - except (AttributeError): raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_perf/_ffi.py b/asn1crypto/_perf/_ffi.py deleted file mode 100644 index 64032f3..0000000 --- a/asn1crypto/_perf/_ffi.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding: utf-8 - -""" -Exceptions for help trying to use cffi, then ctypes for shared library access -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - - - -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/_win/__init__.py b/asn1crypto/_win/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/asn1crypto/_win/_ws2_32.py b/asn1crypto/_win/_ws2_32.py new file mode 100644 index 0000000..9e8e4a8 --- /dev/null +++ b/asn1crypto/_win/_ws2_32.py @@ -0,0 +1,72 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import socket + +from .._ffi import FFIEngineError, buffer_from_bytes, bytes_from_buffer, cast_void_p, is_null, unicode_buffer, string_from_buffer + +try: + from ._ws2_32_cffi import ws2_32 #pylint: disable=W0611 +except (FFIEngineError): + from ._ws2_32_ctypes import ws2_32 + + +class ws2_32_const(): + AF_INET = 2 + AF_INET6 = 23 + + +def inet_ntop(address_family, packed_ip): + """ + Windows compatiblity shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param packed_ip: + A byte string of the network form of an IP address + + :return: + A unicode string of the IP address + """ + + family = { + socket.AF_INET: ws2_32_const.AF_INET, + socket.AF_INET6: ws2_32_const.AF_INET6, + }[address_family] + + buffer_size = 45 if family == ws2_32_const.AF_INET6 else 15 + buffer = unicode_buffer(buffer_size) + result = ws2_32.InetNtop(family, cast_void_p(packed_ip), buffer, buffer_size) + if is_null(result): + raise OSError('Windows error %s calling InetNtop' % ws2_32.WSAGetLastError()) + + return string_from_buffer(buffer) + + +def inet_pton(address_family, ip_string): + """ + Windows compatiblity shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param ip_string: + A unicode string of an IP address + + :return: + A byte string of the network form of the IP address + """ + + family = { + socket.AF_INET: ws2_32_const.AF_INET, + socket.AF_INET6: ws2_32_const.AF_INET6, + }[address_family] + + buffer_size = 16 if family == ws2_32_const.AF_INET6 else 4 + buffer = buffer_from_bytes(buffer_size) + result = ws2_32.InetPton(family, ip_string, buffer) + if result != 1: + raise OSError('Windows error %s calling InetPtob' % ws2_32.WSAGetLastError()) + + return bytes_from_buffer(buffer_size) diff --git a/asn1crypto/_win/_ws2_32_cffi.py b/asn1crypto/_win/_ws2_32_cffi.py new file mode 100644 index 0000000..0d22046 --- /dev/null +++ b/asn1crypto/_win/_ws2_32_cffi.py @@ -0,0 +1,36 @@ +# coding: utf-8 + +""" +cffi interface for IP translation functions in Windows. Exports the +following items: + + - ws2_32 + - InetNtop() + - InetPton() + - WSAGetLastError() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from .._ffi import FFIEngineError + +try: + from cffi import FFI + +except (ImportError): + raise FFIEngineError('Error importing cffi') + + +try: + ffi = FFI() + ffi.set_unicode(True) + ffi.cdef(""" + LPCWSTR InetNtop(INT Family, void *pAddr, LPWSTR pStringBuf, size_t StringBufSize); + INT InetPton(INT Family, LPCWSTR pszAddrString, void *pAddrBuf); + int WSAGetLastError(void); + """) + + ws2_32 = ffi.dlopen('ws2_32.dll') + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_win/_ws2_32_ctypes.py b/asn1crypto/_win/_ws2_32_ctypes.py new file mode 100644 index 0000000..2745818 --- /dev/null +++ b/asn1crypto/_win/_ws2_32_ctypes.py @@ -0,0 +1,33 @@ +# coding: utf-8 + +""" +ctypes interface for IP translation functions in Windows. Exports the +following items: + + - ws2_32 + - InetNtop() + - InetPton() + - WSAGetLastError() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from ctypes import windll, wintypes, c_size_t, c_void_p, c_int + +from .._ffi import FFIEngineError + + +try: + ws2_32 = windll.ws2_32 + + ws2_32.InetNtopW.argtypes = [wintypes.INT, c_void_p, wintypes.LPWSTR, c_size_t] + ws2_32.InetNtopW.restype = wintypes.LPCWSTR + + ws2_32.InetPtonW.argtypes = [wintypes.INT, wintypes.LPCWSTR, c_void_p] + ws2_32.InetPtonW.restype = wintypes.INT + + ws2_32.WSAGetLastError.argtypes = [] + ws2_32.WSAGetLastError.restype = c_int + +except (AttributeError): + raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 546d631..c16a1de 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -18,6 +18,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import re import hashlib +import socket from collections import OrderedDict from .core import ( @@ -48,12 +49,18 @@ from .core import ( ) from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo +from ._int import int_to_bytes, int_from_bytes if sys.version_info < (3,): str_cls = unicode #pylint: disable=E0602 else: str_cls = str +if sys.platform == 'win32': + from ._win._ws2_32 import inet_ntop, inet_pton +else: + from socket import inet_ntop, inet_pton + # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 @@ -526,6 +533,93 @@ class EDIPartyName(Sequence): ] +class IPAddress(OctetString): + def parse(self, spec=None, spec_params=None): + """ + This method is not applicable to IP addresses + """ + + raise ValueError('IP address values can not be parsed') + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string containing an IPv4 address, IPv4 address with CIDR, + an IPv6 address or IPv6 address with CIDR + """ + + if not isinstance(value, str_cls): + raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + + original_value = value + + has_cidr = value.find('/') != -1 + cidr = 0 + if has_cidr: + parts = value.split('/', 1) + value = parts[0] + cidr = int(parts[1]) + if cidr < 0: + raise ValueError('%s value contains a CIDR range less than 0' % self.__class__.__name__) + + if value.find(':') != -1: + family = socket.AF_INET6 + if cidr > 128: + raise ValueError('%s value contains a CIDR range bigger than 128, the maximum value for an IPv6 address' % self.__class__.__name__) + cidr_size = 128 + else: + family = socket.AF_INET + if cidr > 32: + raise ValueError('%s value contains a CIDR range bigger than 32, the maximum value for an IPv4 address' % self.__class__.__name__) + cidr_size = 32 + + cidr_bytes = b'' + if has_cidr: + cidr_mask = '1' * cidr + cidr_mask += '0' * (cidr_size - len(cidr_mask)) + cidr_bytes = int_to_bytes(int(cidr_mask, 2)) + cidr_bytes = (b'\x00' * ((cidr_size // 8) - len(cidr_bytes))) + cidr_bytes + + self._native = original_value + self.contents = inet_pton(family, value) + cidr_bytes + self.header = None + if self.trailer != b'': + self.trailer = b'' + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A unicode string or None + """ + + if self.contents is None: + return None + + if self._native is None: + byte_string = self.__bytes__() + byte_len = len(byte_string) + cidr_int = None + if byte_len in {32, 16}: + value = inet_ntop(socket.AF_INET6, byte_string[0:16]) + if byte_len > 16: + cidr_int = int_from_bytes(byte_string[16:]) + elif byte_len in {8, 4}: + value = inet_ntop(socket.AF_INET, byte_string[0:4]) + if byte_len > 4: + cidr_int = int_from_bytes(byte_string[4:]) + if cidr_int is not None: + cidr_bits = '{0:b}'.format(cidr_int) + cidr = len(cidr_bits.rstrip('0')) + value = value + '/' + str_cls(cidr) + self._native = value + return self._native + + class GeneralName(Choice): _alternatives = [ ('other_name', AnotherName, {'tag_type': 'implicit', 'tag': 0}), @@ -535,7 +629,7 @@ class GeneralName(Choice): ('directory_name', Name, {'tag_type': 'explicit', 'tag': 4}), ('edi_party_name', EDIPartyName, {'tag_type': 'implicit', 'tag': 5}), ('uniform_resource_identifier', IA5String, {'tag_type': 'implicit', 'tag': 6}), - ('ip_address', OctetString, {'tag_type': 'implicit', 'tag': 7}), + ('ip_address', IPAddress, {'tag_type': 'implicit', 'tag': 7}), ('registered_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 8}), ] diff --git a/tests/test_x509.py b/tests/test_x509.py index 13b3d2a..c3f2f19 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -31,6 +31,24 @@ class X509Tests(unittest.TestCase): _, _, cert_bytes = pem.unarmor(cert_bytes) return x509.Certificate.load(cert_bytes) + #pylint: disable=C0326 + @staticmethod + def ip_address_info(): + return ( + ('127.0.0.1', b'\x04\x04\x7F\x00\x00\x01'), + ('255.255.255.255', b'\x04\x04\xFF\xFF\xFF\xFF'), + ('127.0.0.1/28', b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xF0'), + ('255.255.255.255/0', b'\x04\x08\xFF\xFF\xFF\xFF\x00\x00\x00\x00'), + ('af::ed', b'\x04\x10\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xED'), + ('af::ed/128', b'\x04\x20\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xED\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'), + ('af::ed/0', b'\x04\x20\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), + ) + + @data('ip_address_info') + def ip_address(self, unicode_string, der_bytes): + self.assertEqual(der_bytes, x509.IPAddress(unicode_string).dump()) + self.assertEqual(unicode_string, x509.IPAddress.load(der_bytes).native) + #pylint: disable=C0326 @staticmethod def signature_algo_info(): @@ -382,12 +400,12 @@ class X509Tests(unittest.TestCase): 'excluded_subtrees', [ OrderedDict([ - ('base', b'\x00\x00\x00\x00\x00\x00\x00\x00'), + ('base', '0.0.0.0/0'), ('minimum', 0), ('maximum', None) ]), OrderedDict([ - ('base', b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), + ('base', '::/0'), ('minimum', 0), ('maximum', None) ]) -- cgit v1.2.3 From 1a5a2ac3cd908d863cd5b81fbcb79c2603740469 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 22 Jul 2015 12:52:39 -0400 Subject: Fixed some windows IP address conversion issues --- asn1crypto/_win/_ws2_32.py | 9 +++++---- asn1crypto/_win/_ws2_32_cffi.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/asn1crypto/_win/_ws2_32.py b/asn1crypto/_win/_ws2_32.py index 9e8e4a8..51de74a 100644 --- a/asn1crypto/_win/_ws2_32.py +++ b/asn1crypto/_win/_ws2_32.py @@ -35,9 +35,10 @@ def inet_ntop(address_family, packed_ip): socket.AF_INET6: ws2_32_const.AF_INET6, }[address_family] - buffer_size = 45 if family == ws2_32_const.AF_INET6 else 15 + buffer_size = 46 if family == ws2_32_const.AF_INET6 else 16 buffer = unicode_buffer(buffer_size) - result = ws2_32.InetNtop(family, cast_void_p(packed_ip), buffer, buffer_size) + packed_ip_buffer = buffer_from_bytes(packed_ip) + result = ws2_32.InetNtopW(family, cast_void_p(packed_ip_buffer), buffer, buffer_size) if is_null(result): raise OSError('Windows error %s calling InetNtop' % ws2_32.WSAGetLastError()) @@ -65,8 +66,8 @@ def inet_pton(address_family, ip_string): buffer_size = 16 if family == ws2_32_const.AF_INET6 else 4 buffer = buffer_from_bytes(buffer_size) - result = ws2_32.InetPton(family, ip_string, buffer) + result = ws2_32.InetPtonW(family, ip_string, buffer) if result != 1: raise OSError('Windows error %s calling InetPtob' % ws2_32.WSAGetLastError()) - return bytes_from_buffer(buffer_size) + return bytes_from_buffer(buffer, buffer_size) diff --git a/asn1crypto/_win/_ws2_32_cffi.py b/asn1crypto/_win/_ws2_32_cffi.py index 0d22046..55c9565 100644 --- a/asn1crypto/_win/_ws2_32_cffi.py +++ b/asn1crypto/_win/_ws2_32_cffi.py @@ -25,8 +25,8 @@ try: ffi = FFI() ffi.set_unicode(True) ffi.cdef(""" - LPCWSTR InetNtop(INT Family, void *pAddr, LPWSTR pStringBuf, size_t StringBufSize); - INT InetPton(INT Family, LPCWSTR pszAddrString, void *pAddrBuf); + LPCWSTR InetNtopW(INT Family, void *pAddr, LPWSTR pStringBuf, size_t StringBufSize); + INT InetPtonW(INT Family, LPCWSTR pszAddrString, void *pAddrBuf); int WSAGetLastError(void); """) -- cgit v1.2.3 From 06080f7df51f0a8a6268945aeb06848637ec4b0b Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 23 Jul 2015 07:11:41 -0400 Subject: Set initial Sequence values in field order because of OID-based specs --- asn1crypto/core.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index dc3ddc5..52af514 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1696,8 +1696,16 @@ class Sequence(Asn1Value): value = default if value is not None: - for key, child in value.items(): - self.__setitem__(key, child) + # Fields are iterated in definition order to allow things like + # OID-based specs. Otherwise sometimes the value would be processed + # before the OID field, resulting in invalid value object creation. + if self._fields: + keys = [info[0] for info in self._fields] + else: + keys = value.keys() + for key in keys: + if key in value: + self.__setitem__(key, value[key]) def _lazy_child(self, index): """ -- cgit v1.2.3 From 3086cabc7c7e3e5f0aae5d3aeb042d86154770e8 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 23 Jul 2015 07:12:07 -0400 Subject: Properly handle Choice types when setting a Sequence field value --- asn1crypto/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 52af514..67ce8f8 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1785,7 +1785,8 @@ class Sequence(Asn1Value): new_value = self._make_value(field_name, field_spec, value_spec, field_params, value) - if new_value.contents is None: + is_choice = isinstance(new_value, Choice) + if (is_choice and new_value.chosen.contents is None) or (not is_choice and new_value.contents is None): raise ValueError('Value for field "%s" of %s is not set' % (field_name, self.__class__.__name__)) self.children[key] = new_value -- cgit v1.2.3 From 50959e9c7a8e5768468234ddfd7f5aa7af210623 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 23 Jul 2015 07:13:45 -0400 Subject: Add some more x509.Name OID seen in various OS trust roots and EV certs --- asn1crypto/x509.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index c16a1de..77feae6 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -117,21 +117,26 @@ class NameType(ObjectIdentifier): '2.5.4.6': 'country_name', '2.5.4.7': 'locality_name', '2.5.4.8': 'state_or_province_name', + '2.5.4.9': 'street_address', '2.5.4.10': 'organization_name', '2.5.4.11': 'organizational_unit_name', '2.5.4.12': 'title', '2.5.4.15': 'business_category', + '2.5.4.17': 'postal_code', '2.5.4.41': 'name', '2.5.4.42': 'given_name', '2.5.4.43': 'initials', '2.5.4.44': 'generation_qualifier', '2.5.4.46': 'dn_qualifier', + '2.5.4.65': 'pseudonym', # https://tools.ietf.org/html/rfc2985#page-26 '1.2.840.113549.1.9.1': 'email_address', # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf '1.3.6.1.4.1.311.60.2.1.1': 'incorporation_locality', '1.3.6.1.4.1.311.60.2.1.2': 'incorporation_state_or_province', '1.3.6.1.4.1.311.60.2.1.3': 'incorporation_country', + # https://tools.ietf.org/html/rfc2247#section-4 + '0.9.2342.19200300.100.1.25': 'domain_component', } @property @@ -148,19 +153,23 @@ class NameType(ObjectIdentifier): 'country_name': 'Country', 'locality_name': 'Locality', 'state_or_province_name': 'State/Province', + 'street_address': 'Street Address', 'organization_name': 'Organization', 'organizational_unit_name': 'Organizational Unit', 'title': 'Title', 'business_category': 'Business Category', + 'postal_code': 'Postal Code', 'name': 'Name', 'given_name': 'Given Name', 'initials': 'Initials', 'generation_qualifier': 'Generation Qualifier', 'dn_qualifier': 'DN Qualifier', + 'pseudonym': 'Pseudonym', 'email_address': 'Email Address', 'incorporation_locality': 'Incorporation Locality', 'incorporation_state_or_province': 'Incorporation State/Province', 'incorporation_country': 'Incorporation Country', + 'domain_component': 'Domain Component', }[self.native] @@ -178,20 +187,25 @@ class NameTypeAndValue(Sequence): 'country_name': DirectoryString, 'locality_name': DirectoryString, 'state_or_province_name': DirectoryString, + 'street_address': DirectoryString, 'organization_name': DirectoryString, 'organizational_unit_name': DirectoryString, 'title': DirectoryString, + 'business_category': DirectoryString, + 'postal_code': DirectoryString, 'name': DirectoryString, 'given_name': DirectoryString, 'initials': DirectoryString, 'generation_qualifier': DirectoryString, 'dn_qualifier': DirectoryString, + 'pseudonym': DirectoryString, # https://tools.ietf.org/html/rfc2985#page-26 'email_address': IA5String, # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf 'incorporation_locality': DirectoryString, 'incorporation_state_or_province': DirectoryString, 'incorporation_country': DirectoryString, + 'domain_component': IA5String, } -- cgit v1.2.3 From 0c8f2ca7a9a2cb83298cbf7163031512d41c00ed Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 23 Jul 2015 07:15:06 -0400 Subject: Add a preferred order for x509.RelativeDistinguishedName attribute names --- asn1crypto/x509.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 77feae6..69b8318 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -139,6 +139,35 @@ class NameType(ObjectIdentifier): '0.9.2342.19200300.100.1.25': 'domain_component', } + # This order is largely based on observed order seen in EV certs from + # Symantec and DigiCert. Some of the uncommon name-related fields are + # just placed in what seems like a reasonable order. + preferred_order = [ + 'incorporation_country', + 'incorporation_state_or_province', + 'incorporation_locality', + 'business_category', + 'serial_number', + 'country_name', + 'postal_code', + 'state_or_province_name', + 'locality_name', + 'street_address', + 'organization_name', + 'organizational_unit_name', + 'title', + 'common_name', + 'initials', + 'generation_qualifier', + 'surname', + 'given_name', + 'name', + 'pseudonym', + 'dn_qualifier', + 'email_address', + 'domain_component', + ] + @property def human_friendly(self): """ -- cgit v1.2.3 From fd65d600d783e13217c8e1d9d5dbb9d26640d3e9 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 23 Jul 2015 07:16:44 -0400 Subject: Add RFC5280-based equality rules for x509.Name and x509.RelativeDistinguishedName --- asn1crypto/x509.py | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_x509.py | 43 +++++++++ 2 files changed, 304 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 69b8318..1686057 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -18,6 +18,8 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import re import hashlib +import stringprep +import unicodedata import socket from collections import OrderedDict @@ -237,14 +239,198 @@ class NameTypeAndValue(Sequence): 'domain_component': IA5String, } + _prepped = None + + @property + def prepped_value(self): + """ + Returns the value after being processed by the internationalized string + preparation as specified by RFC5280 + + :return: + A unicode string + """ + + if self._prepped is None: + self._prepped = self._ldap_string_prep(self['value'].native) + return self._prepped + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another NameTypeAndValue object + + :return: + A boolean + """ + + if not isinstance(other, NameTypeAndValue): + return False + + if other['type'].native != self['type'].native: + return False + + return other.prepped_value == self.prepped_value + + def _ldap_string_prep(self, string): + """ + Implements the internationalized string preparation algorithm from + RFC 4518. https://tools.ietf.org/html/rfc4518#section-2 + + :param string: + A unicode string to prepare + + :return: + A prepared unicode string, ready for comparison + """ + + # Map step + string = re.sub('[\u00ad\u1806\u034f\u180b-\u180d\ufe0f-\uff00\ufffc]+', '', string) + string = re.sub('[\u0009\u000a\u000b\u000c\u000d\u0085]', ' ', string) + string = re.sub('[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f]+', '', string) + string = string.replace('\u200b', '') + string = re.sub('[\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000]', ' ', string) + + string = ''.join(map(stringprep.map_table_b2, string)) + + # Normalize step + string = unicodedata.normalize('NFKC', string) + + # Prohibit step + for char in string: + if stringprep.in_table_a1(char): + raise ValueError('X509 Name objects may not contain unassigned code points') + + if stringprep.in_table_c8(char): + raise ValueError('X509 Name objects may not contain change display or deprecated characters') + + if stringprep.in_table_c3(char): + raise ValueError('X509 Name objects may not contain private use characters') + + if stringprep.in_table_c4(char): + raise ValueError('X509 Name objects may not contain non-character code points') + + if stringprep.in_table_c5(char): + raise ValueError('X509 Name objects may not contain surrogate code points') + + if char == '\ufffd': + raise ValueError('X509 Name objects may not contain the replacement character') + + # Check bidirectional step - here we ensure that we are not mixing + # left-to-right and right-to-left text in the string + has_r_and_al_cat = False + has_l_cat = False + for char in string: + if stringprep.in_table_d1(char): + has_r_and_al_cat = True + elif stringprep.in_table_d2(char): + has_l_cat = True + + if has_r_and_al_cat: + first_is_r_and_al = stringprep.in_table_d1(string[0]) + last_is_r_and_al = stringprep.in_table_d1(string[-1]) + + if has_l_cat or not first_is_r_and_al or not last_is_r_and_al: + raise ValueError('X509 Name object contains a malformed bidirectional sequence') + + # Insignificant space handling step + string = ' ' + re.sub(' +', ' ', string).strip() + ' ' + + return string + class RelativeDistinguishedName(SetOf): _child_spec = NameTypeAndValue + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another RelativeDistinguishedName object + + :return: + A boolean + """ + + if not isinstance(other, RelativeDistinguishedName): + return False + + if len(self) != len(other): + return False + + self_types = self._get_types(self) + other_types = self._get_types(other) + + if self_types != other_types: + return False + + self_values = self._get_values(self) + other_values = self._get_values(other) + + for type_name in self_types: + if self_values[type_name] != other_values[type_name]: + return False + + return True + + def _get_types(self, rdn): + """ + Returns a set of types contained in an RDN + + :param rdn: + A RelativeDistinguishedName object + + :return: + A set object with unicode strings of NameTypeAndValue type field + values + """ + + return set([ntv['type'].native for ntv in rdn]) + + def _get_values(self, rdn): + """ + Returns a dict of prepped values contained in an RDN + + :param rdn: + A RelativeDistinguishedName object + + :return: + A dict object with unicode strings of NameTypeAndValue value field + values that have been prepped for comparison + """ + + return {ntv['type'].native: ntv.prepped_value for ntv in rdn} + class RDNSequence(SequenceOf): _child_spec = RelativeDistinguishedName + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another RDNSequence object + + :return: + A boolean + """ + + if not isinstance(other, RDNSequence): + return False + + if len(self) != len(other): + return False + + for index, self_rdn in enumerate(self): + if other[index] != self_rdn: + return False + + return True + class Name(Choice): _alternatives = [ @@ -255,6 +441,56 @@ class Name(Choice): _sha1 = None _sha256 = None + @classmethod + def build(cls, name_dict): + """ + Creates a Name object from a dict of unicode string keys and values. + The keys should be from NameType._map, or a dotted-integer OID unicode + string. + + :param name_dict: + A dict of name information, e.g. {"common_name": "Will Bond", + "country_name": "US", "organization": "Codex Non Sufficit LC"} + + :return: + An x509.Name object + """ + + attributes = [] + + for attribute_name in NameType.preferred_order: + if attribute_name not in name_dict: + continue + + if attribute_name in {'email_address', 'domain_component'}: + value = IA5String(name_dict[attribute_name]) + else: + value = DirectoryString(name='utf8_string', value=UTF8String(name_dict[attribute_name])) + + attributes.append(NameTypeAndValue({ + 'type': attribute_name, + 'value': value + })) + + rdn = RelativeDistinguishedName(attributes) + sequence = RDNSequence([rdn]) + return cls(name='', value=sequence) + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 + + :param other: + Another Name object + + :return: + A boolean + """ + + if not isinstance(other, Name): + return False + return self.chosen == other.chosen + @property def native(self): if self.contents is None: @@ -676,6 +912,31 @@ class GeneralName(Choice): ('registered_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 8}), ] + def __eq__(self, other): + """ + Does not support other_name, x400_address or edi_party_name + + :param other: + The other GeneralName to compare to + + :return: + A boolean + """ + + if self.name in ('other_name', 'x400_address', 'edi_party_name'): + raise ValueError('comparison is not supported for GeneralName objects of choice %s' % self.name) + + if other.name in ('other_name', 'x400_address', 'edi_party_name'): + raise ValueError('comparison is not supported for GeneralName objects of choice %s' % other.name) + + if self.name != other.name: + return False + + if self.name == 'directory_name': + return self.chosen == other.chosen + + return self.native == other.native + class GeneralNames(SequenceOf): _child_spec = GeneralName diff --git a/tests/test_x509.py b/tests/test_x509.py index c3f2f19..7a16d02 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -49,6 +49,49 @@ class X509Tests(unittest.TestCase): self.assertEqual(der_bytes, x509.IPAddress(unicode_string).dump()) self.assertEqual(unicode_string, x509.IPAddress.load(der_bytes).native) + #pylint: disable=C0326 + @staticmethod + def compare_name_info(): + return ( + ( + True, + x509.Name.build({ + 'common_name': 'Will Bond' + }), + x509.Name.build({ + 'common_name': 'will bond' + }) + ), + ( + True, + x509.Name.build({ + 'common_name': 'Will Bond' + }), + x509.Name.build({ + 'common_name': 'will\tbond' + }) + ), + ( + False, + x509.Name.build({ + 'country_name': 'US', + 'common_name': 'Will Bond' + }), + x509.Name.build({ + 'country_name': 'US', + 'state_or_province_name': 'Massachusetts', + 'common_name': 'Will Bond' + }) + ), + ) + + @data('compare_name_info') + def compare_name(self, are_equal, general_name_1, general_name_2): + if are_equal: + self.assertEqual(general_name_1, general_name_2) + else: + self.assertNotEqual(general_name_1, general_name_2) + #pylint: disable=C0326 @staticmethod def signature_algo_info(): -- cgit v1.2.3 From 9a7a09921cbd8760cae3fbc7bdb669aa58ffcfa6 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 23 Jul 2015 09:59:06 -0400 Subject: Added x509.Certificate.self_issued and x509.Certificate.self_signed --- asn1crypto/x509.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_x509.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 1686057..738fce2 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1317,6 +1317,8 @@ class Certificate(Sequence): _delta_crl_distribution_points = None _valid_domains = None _valid_ips = None + _self_issued = None + _self_signed = None def _set_extensions(self): """ @@ -1828,3 +1830,39 @@ class Certificate(Sequence): self._valid_ips.append(general_name.native) return self._valid_ips + + @property + def self_issued(self): + """ + :return: + A boolean - if the certificate is self-issued, as defined by RFC5280 + """ + + if self._self_issued is None: + self._self_issued = False + if self.basic_constraints_value and self.basic_constraints_value['ca'].native: + self._self_issued = self.subject == self.issuer + return self._self_issued + + @property + def self_signed(self): + """ + :return: + A unicode string of "yes", "no" or "maybe". The "maybe" result will + be returned if the certificate does not contain a key identifier + extension, but is issued by the subject. In this case the + certificate signature will need to be verified using the subject + public key to determine a "yes" or "no" answer. + """ + + if self._self_signed is None: + self._self_signed = 'no' + if self.self_issued: + if self.key_identifier: + if not self.authority_key_identifier: + self._self_signed = 'yes' + elif self.authority_key_identifier == self.key_identifier: + self._self_signed = 'yes' + else: + self._self_signed = 'maybe' + return self._self_signed diff --git a/tests/test_x509.py b/tests/test_x509.py index 7a16d02..feefbac 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -1357,6 +1357,58 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(crl_url, cert.valid_ips) + #pylint: disable=C0326 + @staticmethod + def self_issued_info(): + return ( + ('keys/test-der.crt', True), + ('keys/test-inter-der.crt', False), + ('keys/test-third-der.crt', False), + ('geotrust_certs/GeoTrust_Universal_CA.crt', True), + ('geotrust_certs/GeoTrust_Primary_CA.crt', True), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', False), + ('geotrust_certs/codex.crt', False), + ('lets_encrypt/isrgrootx1.pem', True), + ('lets_encrypt/letsencryptauthorityx1.pem', False), + ('lets_encrypt/letsencryptauthorityx2.pem', False), + ('globalsign_example_keys/IssuingCA-der.cer', False), + ('globalsign_example_keys/rootCA.cer', True), + ('globalsign_example_keys/SSL1.cer', False), + ('globalsign_example_keys/SSL2.cer', False), + ('globalsign_example_keys/SSL3.cer', False), + ) + + @data('self_issued_info') + def self_issued(self, relative_path, self_issued): + cert = self._load_cert(relative_path) + self.assertEqual(self_issued, cert.self_issued) + + #pylint: disable=C0326 + @staticmethod + def self_signed_info(): + return ( + ('keys/test-der.crt', 'yes'), + ('keys/test-inter-der.crt', 'no'), + ('keys/test-third-der.crt', 'no'), + ('geotrust_certs/GeoTrust_Universal_CA.crt', 'yes'), + ('geotrust_certs/GeoTrust_Primary_CA.crt', 'yes'), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', 'no'), + ('geotrust_certs/codex.crt', 'no'), + ('lets_encrypt/isrgrootx1.pem', 'yes'), + ('lets_encrypt/letsencryptauthorityx1.pem', 'no'), + ('lets_encrypt/letsencryptauthorityx2.pem', 'no'), + ('globalsign_example_keys/IssuingCA-der.cer', 'no'), + ('globalsign_example_keys/rootCA.cer', 'yes'), + ('globalsign_example_keys/SSL1.cer', 'no'), + ('globalsign_example_keys/SSL2.cer', 'no'), + ('globalsign_example_keys/SSL3.cer', 'no'), + ) + + @data('self_signed_info') + def self_signed(self, relative_path, self_signed): + cert = self._load_cert(relative_path) + self.assertEqual(self_signed, cert.self_signed) + def test_parse_certificate(self): cert = self._load_cert('keys/test-der.crt') -- cgit v1.2.3 From 2fde645613cc5bbfff251ed4ff04c039e64bc0be Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 23 Jul 2015 10:54:13 -0400 Subject: Change critical extension tracking to use sets instead of lists --- asn1crypto/crl.py | 16 ++++++++-------- asn1crypto/ocsp.py | 32 ++++++++++++++++---------------- asn1crypto/x509.py | 8 ++++---- tests/test_x509.py | 30 +++++++++++++++--------------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 5cac004..4f73648 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -178,7 +178,7 @@ class RevokedCertificate(Sequence): of critical extensions """ - self._critical_extensions = [] + self._critical_extensions = set() for extension in self['crl_entry_extensions']: name = extension['extn_id'].native @@ -186,18 +186,18 @@ class RevokedCertificate(Sequence): if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: - self._critical_extensions.append(name) + self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ - Returns a list of the names (or OID if not a known extension) of the + Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: - A list of unicode strings + A set of unicode strings """ if not self._processed_extensions: @@ -306,7 +306,7 @@ class CertificateList(Sequence): of critical extensions """ - self._critical_extensions = [] + self._critical_extensions = set() for extension in self['tbs_cert_list']['crl_extensions']: name = extension['extn_id'].native @@ -314,18 +314,18 @@ class CertificateList(Sequence): if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: - self._critical_extensions.append(name) + self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ - Returns a list of the names (or OID if not a known extension) of the + Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: - A list of unicode strings + A set of unicode strings """ if not self._processed_extensions: diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 6f599d2..98e8ad4 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -97,7 +97,7 @@ class Request(Sequence): of critical extensions """ - self._critical_extensions = [] + self._critical_extensions = set() for extension in self['single_request_extensions']: name = extension['extn_id'].native @@ -105,18 +105,18 @@ class Request(Sequence): if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: - self._critical_extensions.append(name) + self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ - Returns a list of the names (or OID if not a known extension) of the + Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: - A list of unicode strings + A set of unicode strings """ if not self._processed_extensions: @@ -229,7 +229,7 @@ class OCSPRequest(Sequence): of critical extensions """ - self._critical_extensions = [] + self._critical_extensions = set() for extension in self['tbs_request']['request_extensions']: name = extension['extn_id'].native @@ -237,18 +237,18 @@ class OCSPRequest(Sequence): if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: - self._critical_extensions.append(name) + self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ - Returns a list of the names (or OID if not a known extension) of the + Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: - A list of unicode strings + A set of unicode strings """ if not self._processed_extensions: @@ -398,7 +398,7 @@ class SingleResponse(Sequence): of critical extensions """ - self._critical_extensions = [] + self._critical_extensions = set() for extension in self['single_extensions']: name = extension['extn_id'].native @@ -406,18 +406,18 @@ class SingleResponse(Sequence): if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: - self._critical_extensions.append(name) + self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ - Returns a list of the names (or OID if not a known extension) of the + Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: - A list of unicode strings + A set of unicode strings """ if not self._processed_extensions: @@ -572,7 +572,7 @@ class OCSPResponse(Sequence): of critical extensions """ - self._critical_extensions = [] + self._critical_extensions = set() for extension in self['response_bytes']['response'].parsed['tbs_response_data']['response_extensions']: name = extension['extn_id'].native @@ -580,18 +580,18 @@ class OCSPResponse(Sequence): if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: - self._critical_extensions.append(name) + self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ - Returns a list of the names (or OID if not a known extension) of the + Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: - A list of unicode strings + A set of unicode strings """ if not self._processed_extensions: diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 738fce2..f4fc461 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1326,7 +1326,7 @@ class Certificate(Sequence): of critical extensions """ - self._critical_extensions = [] + self._critical_extensions = set() for extension in self['tbs_certificate']['extensions']: name = extension['extn_id'].native @@ -1334,18 +1334,18 @@ class Certificate(Sequence): if hasattr(self, attribute_name): setattr(self, attribute_name, extension['extn_value'].parsed) if extension['critical'].native: - self._critical_extensions.append(name) + self._critical_extensions.add(name) self._processed_extensions = True @property def critical_extensions(self): """ - Returns a list of the names (or OID if not a known extension) of the + Returns a set of the names (or OID if not a known extension) of the extensions marked as critical :return: - A list of unicode strings + A set of unicode strings """ if not self._processed_extensions: diff --git a/tests/test_x509.py b/tests/test_x509.py index feefbac..7de7a62 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -113,21 +113,21 @@ class X509Tests(unittest.TestCase): @staticmethod def critical_extensions_info(): return ( - ('keys/test-der.crt', []), - ('keys/test-inter-der.crt', []), - ('keys/test-third-der.crt', []), - ('geotrust_certs/GeoTrust_Universal_CA.crt', ['basic_constraints', 'key_usage']), - ('geotrust_certs/GeoTrust_Primary_CA.crt', ['basic_constraints', 'key_usage']), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', ['basic_constraints', 'key_usage']), - ('geotrust_certs/codex.crt', ['key_usage']), - ('lets_encrypt/isrgrootx1.pem', ['key_usage', 'basic_constraints']), - ('lets_encrypt/letsencryptauthorityx1.pem', ['key_usage', 'basic_constraints']), - ('lets_encrypt/letsencryptauthorityx2.pem', ['key_usage', 'basic_constraints']), - ('globalsign_example_keys/IssuingCA-der.cer', ['basic_constraints', 'key_usage']), - ('globalsign_example_keys/rootCA.cer', ['basic_constraints', 'key_usage']), - ('globalsign_example_keys/SSL1.cer', ['key_usage', 'extended_key_usage', 'basic_constraints']), - ('globalsign_example_keys/SSL2.cer', ['key_usage', 'extended_key_usage', 'basic_constraints']), - ('globalsign_example_keys/SSL3.cer', ['key_usage', 'extended_key_usage', 'basic_constraints']), + ('keys/test-der.crt', set()), + ('keys/test-inter-der.crt', set()), + ('keys/test-third-der.crt', set()), + ('geotrust_certs/GeoTrust_Universal_CA.crt', {'basic_constraints', 'key_usage'}), + ('geotrust_certs/GeoTrust_Primary_CA.crt', {'basic_constraints', 'key_usage'}), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', {'basic_constraints', 'key_usage'}), + ('geotrust_certs/codex.crt', {'key_usage'}), + ('lets_encrypt/isrgrootx1.pem', {'key_usage', 'basic_constraints'}), + ('lets_encrypt/letsencryptauthorityx1.pem', {'key_usage', 'basic_constraints'}), + ('lets_encrypt/letsencryptauthorityx2.pem', {'key_usage', 'basic_constraints'}), + ('globalsign_example_keys/IssuingCA-der.cer', {'basic_constraints', 'key_usage'}), + ('globalsign_example_keys/rootCA.cer', {'basic_constraints', 'key_usage'}), + ('globalsign_example_keys/SSL1.cer', {'key_usage', 'extended_key_usage', 'basic_constraints'}), + ('globalsign_example_keys/SSL2.cer', {'key_usage', 'extended_key_usage', 'basic_constraints'}), + ('globalsign_example_keys/SSL3.cer', {'key_usage', 'extended_key_usage', 'basic_constraints'}), ) @data('critical_extensions_info') -- cgit v1.2.3 From 7a5c01555913be1bfdb9c312cdb7b94efce8dfd6 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 24 Jul 2015 08:37:53 -0400 Subject: Added crl.CertificateList.issuer --- asn1crypto/crl.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 4f73648..b83d99e 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -430,6 +430,15 @@ class CertificateList(Sequence): self._set_extensions() return self._authority_information_access_value + @property + def issuer(self): + """ + :return: + An asn1crypto.x509.Name object for the issuer of the CRL + """ + + return self['tbs_cert_list']['issuer'] + @property def authority_key_identifier(self): """ -- cgit v1.2.3 From 936b971a83e371f3a3d59124e33a1e90f653dbc2 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 24 Jul 2015 08:38:55 -0400 Subject: Added x509.Name.hashable and x509.RelativeDistinguishedName.hashable for use as dict keys, or presence in sets --- asn1crypto/x509.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index f4fc461..cc671db 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -344,6 +344,22 @@ class NameTypeAndValue(Sequence): class RelativeDistinguishedName(SetOf): _child_spec = NameTypeAndValue + @property + def hashable(self): + """ + :return: + A unicode string that can be used as a dict key or in a set + """ + + output = [] + values = self._get_values() + for key in sorted(values.keys()): + output.append('%s: %s' % (key, values[key])) + # Unit separator is used here since the normalization process for + # values moves any such character, and the keys are all dotted integers + # or under_score_words + return '\x1F'.join(output) + def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 @@ -408,6 +424,18 @@ class RelativeDistinguishedName(SetOf): class RDNSequence(SequenceOf): _child_spec = RelativeDistinguishedName + @property + def hashable(self): + """ + :return: + A unicode string that can be used as a dict key or in a set + """ + + # Record separator is used here since the normalization process for + # values moves any such character, and the keys are all dotted integers + # or under_score_words + return '\x1E'.join(rdn.hashable for rdn in self) + def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 @@ -476,6 +504,15 @@ class Name(Choice): sequence = RDNSequence([rdn]) return cls(name='', value=sequence) + @property + def hashable(self): + """ + :return: + A unicode string that can be used as a dict key or in a set + """ + + return self.chosen.hashable + def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 -- cgit v1.2.3 From 06921bb687752e858c3b793efccfeb5a74fdd240 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 24 Jul 2015 08:39:59 -0400 Subject: Added x509.Certificate.signature, x509.Certificate.signature_algo and x509.Certificate.hash_algo --- asn1crypto/x509.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index cc671db..83850aa 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1648,6 +1648,34 @@ class Certificate(Sequence): self._set_extensions() return self._ocsp_no_check_value + @property + def signature(self): + """ + :return: + A byte string of the signature + """ + + return self['signature_value'].native + + @property + def signature_algo(self): + """ + :return: + A unicode string of "rsassa_pkcs1v15", "rsassa_pss", "dsa", "ecdsa" + """ + + return self['signature_algorithm'].signature_algo + + @property + def hash_algo(self): + """ + :return: + A unicode string of "md2", "md5", "sha1", "sha224", "sha256", + "sha384", "sha512", "sha512_224", "sha512_256" + """ + + return self['signature_algorithm'].hash_algo + @property def public_key(self): """ -- cgit v1.2.3 From b5e193e14dbe0a317b7379df11bfed802ee2f0c2 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 24 Jul 2015 08:40:24 -0400 Subject: Added x509.Certificate.ca and x509.Certificate.max_path_length --- asn1crypto/x509.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 83850aa..11877e4 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1896,6 +1896,26 @@ class Certificate(Sequence): return self._valid_ips + @property + def ca(self): + """ + :return; + A boolean - if the certificate is marked as a CA + """ + + return self.basic_constraints_value and self.basic_constraints_value['ca'].native + + @property + def max_path_length(self): + """ + :return; + None or an integer of the maximum path length + """ + + if not self.ca: + return None + return self.basic_constraints_value['path_len_constraint'].native + @property def self_issued(self): """ @@ -1905,7 +1925,7 @@ class Certificate(Sequence): if self._self_issued is None: self._self_issued = False - if self.basic_constraints_value and self.basic_constraints_value['ca'].native: + if self.ca: self._self_issued = self.subject == self.issuer return self._self_issued -- cgit v1.2.3 From 7d7cccb6130175c45424a7210a3c13b1a1ac7706 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 24 Jul 2015 14:33:53 -0400 Subject: Overhauled BitString - fixed bugs, changed native representation to set(), added item access for non-mapped versions --- asn1crypto/core.py | 168 +++++++++++++++++++++++++++++++++++------------- docs/universal_types.md | 76 +++++++++++----------- tests/test_core.py | 79 +++++++++++++++++++++++ tests/test_csr.py | 12 +--- tests/test_x509.py | 144 ++++------------------------------------- 5 files changed, 252 insertions(+), 227 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 67ce8f8..3aa9624 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -46,6 +46,7 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function +import math import sys import re from collections import OrderedDict @@ -968,6 +969,20 @@ class BitString(Primitive, ValueMap, object): tag = 3 + _size = None + + #pylint: disable=W0212 + def _setup(self): + """ + Generates _reverse_map from _map + """ + + ValueMap._setup(self) + + cls = self.__class__ + if cls._map is not None: + cls._size = max(self._map.keys()) + 1 + def set(self, value): """ Sets the value of the object @@ -979,26 +994,64 @@ class BitString(Primitive, ValueMap, object): ValueError - when an invalid value is passed """ - if not isinstance(value, int_types) and not isinstance(value, tuple): - raise ValueError('%s value must be an integer or a tuple of ones and zeros, not %s' % (self.__class__.__name__, value.__class__.__name__)) + if isinstance(value, set): + if self._map is None: + raise ValueError('%s _map has not been defined' % self.__class__.__name__) - if isinstance(value, tuple): + bits = [0] * self._size self._native = value + for index in range(0, self._size): + key = self._map.get(index) + if key is None: + continue + if key in value: + bits[index] = 1 + + value = ''.join(map(str_cls, bits)) + + elif isinstance(value, tuple): + if self._map is None: + self._native = value + else: + self._native = set() + for index, bit in enumerate(value): + if bit: + name = self._map.get(index, index) + self._native.add(name) value = ''.join(map(str_cls, value)) - elif isinstance(value, int_types): - value = '{0:b}'.format(value) - self._native = tuple(map(int, tuple(value))) + else: + raise ValueError('%s value must be a tuple of ones and zeros or a set of unicode strings, not %s' % (self.__class__.__name__, value.__class__.__name__)) - size = max(self._map.keys()) + 1 - if len(value) != size: - raise ValueError('%s value must be %s bits long, specified was only %s long' % (self.__class__.__name__, size, len(value))) + if self._map is not None: + size = self._size + if len(value) > size: + raise ValueError('%s value must be at most %s bits long, specified was %s long' % (self.__class__.__name__, size, len(value))) + value += '0' * (size - len(value)) + else: + size = len(value) - extra_bits = (size % 8) - if extra_bits != 0: + size_mod = size % 8 + extra_bits = 0 + if size_mod != 0: + extra_bits = 8 - size_mod value += '0' * extra_bits - self.contents = int_to_bytes(extra_bits) + int_to_bytes(int(value, 2)) + size_in_bytes = int(math.ceil(size / 8)) + + if extra_bits: + extra_bits_byte = int_to_bytes(extra_bits) + else: + extra_bits_byte = b'\x00' + + if value == '': + value_bytes = b'' + else: + value_bytes = int_to_bytes(int(value, 2)) + if len(value_bytes) != size_in_bytes: + value_bytes = (b'\x00' * (size_in_bytes - len(value_bytes))) + value_bytes + + self.contents = extra_bits_byte + value_bytes self.header = None if self.trailer != b'': self.trailer = b'' @@ -1018,16 +1071,26 @@ class BitString(Primitive, ValueMap, object): A boolean if the bit is set """ - if not isinstance(self._map, dict): - raise ValueError('%s bit map has not been defined' % self.__class__.__name__) + is_int = isinstance(key, int_types) + if not is_int: + if not isinstance(self._map, dict): + raise ValueError('%s _map has not been defined' % self.__class__.__name__) - if key not in self._map: - raise ValueError('%s map does not contain an entry for "%s"' % (self.__class__.__name__, key)) + if key not in self._reverse_map: + raise ValueError('%s _map does not contain an entry for "%s"' % (self.__class__.__name__, key)) if self._native is None: _ = self.native - return self._native[key] + if self._map is None: + if len(self._native) >= key + 1: + return bool(self._native[key]) + return False + + if is_int: + key = self._map.get(key, key) + + return key in self._native def __setitem__(self, key, value): """ @@ -1043,30 +1106,37 @@ class BitString(Primitive, ValueMap, object): ValueError - when _map is not set or the key name is invalid """ - if not isinstance(self._map, dict) or key not in self._map: - return super(BitString, self).__setattr__(key, value) + is_int = isinstance(key, int_types) + if not is_int: + if self._map is None: + raise ValueError('%s _map has not been defined' % self.__class__.__name__) + + if key not in self._reverse_map: + raise ValueError('%s _map does not contain an entry for "%s"' % (self.__class__.__name__, key)) if self._native is None: _ = self.native - self._native[key] = bool(value) - self.set(self._native) - - @property - def named_bits(self): - """ - :return: - A set of unicode strings for the bits that are set to 1 - """ + if self._map is None: + new_native = list(self._native) + max_key = len(new_native) - 1 + if key > max_key: + new_native.extend([0] * (key - max_key)) + new_native[key] = 1 if value else 0 + self._native = tuple(new_native) - if not isinstance(self._map, dict): - raise ValueError('%s bit map has not been defined' % self.__class__.__name__) + else: + if is_int: + key = self._map.get(key, key) - output = set() - for key, value in self.native.items(): if value: - output.add(key) - return output + if key not in self._native: + self._native.add(key) + else: + if key in self._native: + self._native.remove(key) + + self.set(self._native) @property def native(self): @@ -1074,28 +1144,38 @@ class BitString(Primitive, ValueMap, object): The a native Python datatype representation of this value :return: - If a _map is set, an OrdredDict of names as keys and boolean values - or if no _map is set, a tuple of integers 1 and 0. None if no value. + If a _map is set, a set of names, or if no _map is set, a tuple of + integers 1 and 0. None if no value. """ # For BitString we default the value to be all zeros if self.contents is None: - size = max(self._map.keys()) + 1 - self.set((0,) * size) + if self._map is None: + self.set(()) + else: + self.set(set()) if self._native is None: extra_bits = int_from_bytes(self.contents[0:1]) bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) + + # Left-pad the bit string to a byte multiple to ensure we didn't + # lose any zero bits on the left + mod_bit_len = len(bit_string) % 8 + if mod_bit_len != 0: + bit_string = ('0' * (8 - mod_bit_len)) + 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] + bits = tuple(map(int, tuple(bit_string))) if self._map: - self._native = OrderedDict() - for i, bit in enumerate(bits): - self._native[self._map.get(i, i)] = bool(bit) - for i, name in self._map.items(): - if name not in self._native: - self._native[name] = False + self._native = set() + for index, bit in enumerate(bits): + if bit: + name = self._map.get(index, index) + self._native.add(name) else: self._native = bits return self._native diff --git a/docs/universal_types.md b/docs/universal_types.md index ae9124f..807a97c 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -14,38 +14,38 @@ For general purpose ASN.1 parsing, the `asn1crypto.core` module is used. It contains the following classes, that parse, represent and serialize all of the ASN.1 universal types: -| Class | Native Type | Implementation Notes | -| ------------------ | ------------------------------------ | ------------------------------------ | -| `Boolean` | `bool` | | -| `Integer` | `int` | may be `long` on Python 2 | -| `BitString` | `tuple` of `int` or `OrderedDict` | `OrderedDict` used if `_map` present | -| `OctetString` | `bytes` (`str`) | | -| `Null` | `None` | | -| `ObjectIdentifier` | `str` (`unicode`) | string is dotted integer format | -| `ObjectDescriptor` | | no native conversion | -| `InstanceOf` | | no native conversion | -| `Real` | | no native conversion | -| `Enumerated` | `str` (`unicode`) | `_map` must be set | -| `UTF8String` | `str` (`unicode`) | | -| `RelativeOid` | `str` (`unicode`) | string is dotted integer format | -| `Sequence` | `OrderedDict` | | -| `SequenceOf` | `list` | | -| `Set` | `OrderedDict` | | -| `SetOf` | `list` | | -| `EmbeddedPdv` | `OrderedDict` | no named field parsing | -| `NumericString` | `str` (`unicode`) | no charset limitations | -| `PrintableString` | `str` (`unicode`) | no charset limitations | -| `TeletexString` | `str` (`unicode`) | | -| `VideotexString` | `bytes` (`str`) | no unicode conversion | -| `IA5String` | `str` (`unicode`) | | -| `UTCTime` | `datetime.datetime` | | -| `GeneralizedTime` | `datetime.datetime` | treated as UTC when no timezone | -| `GraphicString` | `str` (`unicode`) | unicode conversion as latin1 | -| `VisibleString` | `str` (`unicode`) | no charset limitations | -| `GeneralString` | `str` (`unicode`) | unicode conversion as latin1 | -| `UniversalString` | `str` (`unicode`) | | -| `CharacterString` | `str` (`unicode`) | unicode conversion as latin1 | -| `BMPString` | `str` (`unicode`) | | +| Class | Native Type | Implementation Notes | +| ------------------ | -------------------------------------- | ------------------------------------ | +| `Boolean` | `bool` | | +| `Integer` | `int` | may be `long` on Python 2 | +| `BitString` | `tuple` of `int` or `set` of `unicode` | `set` used if `_map` present | +| `OctetString` | `bytes` (`str`) | | +| `Null` | `None` | | +| `ObjectIdentifier` | `str` (`unicode`) | string is dotted integer format | +| `ObjectDescriptor` | | no native conversion | +| `InstanceOf` | | no native conversion | +| `Real` | | no native conversion | +| `Enumerated` | `str` (`unicode`) | `_map` must be set | +| `UTF8String` | `str` (`unicode`) | | +| `RelativeOid` | `str` (`unicode`) | string is dotted integer format | +| `Sequence` | `OrderedDict` | | +| `SequenceOf` | `list` | | +| `Set` | `OrderedDict` | | +| `SetOf` | `list` | | +| `EmbeddedPdv` | `OrderedDict` | no named field parsing | +| `NumericString` | `str` (`unicode`) | no charset limitations | +| `PrintableString` | `str` (`unicode`) | no charset limitations | +| `TeletexString` | `str` (`unicode`) | | +| `VideotexString` | `bytes` (`str`) | no unicode conversion | +| `IA5String` | `str` (`unicode`) | | +| `UTCTime` | `datetime.datetime` | | +| `GeneralizedTime` | `datetime.datetime` | treated as UTC when no timezone | +| `GraphicString` | `str` (`unicode`) | unicode conversion as latin1 | +| `VisibleString` | `str` (`unicode`) | no charset limitations | +| `GeneralString` | `str` (`unicode`) | unicode conversion as latin1 | +| `UniversalString` | `str` (`unicode`) | | +| `CharacterString` | `str` (`unicode`) | unicode conversion as latin1 | +| `BMPString` | `str` (`unicode`) | | For *Native Type*, the Python 3 type is listed first, with the Python 2 type in parentheses. @@ -286,22 +286,18 @@ print(MyType('1.8.2.1.25').native) ## BitString When no `_map` is set for a `BitString` class, the native representation is a -`tuple` of `int`s (being either `1` or `0`). In addition, it is possible to set -the value of a `BitString` by passing an integer. +`tuple` of `int`s (being either `1` or `0`). ```python from asn1crypto.core import BitString -# These are equivalent b1 = BitString((1, 0, 1)) -b2 = BitString(5) ``` Additionally, it is possible to set the `_map` property to a dict where the keys are bit indexes and the values are unicode string names. This allows checking the value of a given bit by item access, and the native representation -becomes an `OrderedDict` where keys are the unicode strings and values are -either `True` or `False`. +becomes a `set` of unicode strings. ```python from asn1crypto.core import BitString @@ -313,14 +309,14 @@ class MyFlags(BitString): 2: 'manage_users', } -permissions = MyFlags(3) +permissions = MyFlags({'edit', 'delete'}) # This will be printed if permissions['edit'] and permissions['delete']: print('Can edit and delete') # This will not -if permissions['manage_users']: +if 'manage_users' in permissions.native: print('Is admin') ``` diff --git a/tests/test_core.py b/tests/test_core.py index af575e8..7d7033e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -13,6 +13,18 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') +class NamedBits(core.BitString): + _map = { + 0: 'zero', + 1: 'one', + 2: 'two', + 3: 'three', + 4: 'four', + 6: 'six', + 7: 'seven', + } + + @DataDecorator class CoreTests(unittest.TestCase): @@ -31,3 +43,70 @@ class CoreTests(unittest.TestCase): self.assertEqual(native, parsed.native) self.assertEqual(der, parsed.dump(force=True)) + + #pylint: disable=C0326 + @staticmethod + def bit_string_info(): + return ( + ((0, 1, 1), b'\x03\x02\x05\x60'), + ((0, 1, 1, 0, 0, 0, 0, 0), b'\x03\x02\x00\x60'), + ) + + @data('bit_string_info') + def bit_string(self, native, der_bytes): + bs = core.BitString(native) + self.assertEqual(der_bytes, bs.dump()) + self.assertEqual(native, core.BitString.load(der_bytes).native) + + def test_bit_string_item_access(self): + named = core.BitString() + named[0] = True + self.assertEqual(False, named[2]) + self.assertEqual(False, named[1]) + self.assertEqual(True, named[0]) + + #pylint: disable=C0326 + @staticmethod + def mapped_bit_string_info(): + return ( + ( + (0, 1, 1), + b'\x03\x02\x00\x60', + {'one', 'two'} + ), + ( + (0,), + b'\x03\x02\x00\x00', + set() + ), + ( + {'one', 'two'}, + b'\x03\x02\x00\x60', + {'one', 'two'} + ) + ) + + @data('mapped_bit_string_info') + def mapped_bit_string(self, input_native, der_bytes, native): + named = NamedBits(input_native) + self.assertEqual(der_bytes, named.dump()) + self.assertEqual(native, NamedBits.load(der_bytes).native) + + def test_mapped_bit_string_item_access(self): + named = NamedBits() + named['one'] = True + self.assertEqual(False, named['two']) + self.assertEqual(True, named['one']) + self.assertEqual(True, 'one' in named.native) + + def test_mapped_bit_string_sparse(self): + named = NamedBits((0, 0, 0, 0, 0, 1)) + self.assertEqual(False, named['two']) + self.assertEqual(True, named[5]) + self.assertEqual(True, 5 in named.native) + + def test_mapped_bit_string_numeric(self): + named = NamedBits() + named[1] = True + self.assertEqual(True, named['one']) + self.assertEqual({'one'}, named.native) diff --git a/tests/test_csr.py b/tests/test_csr.py index d2d4a70..4ffff69 100644 --- a/tests/test_csr.py +++ b/tests/test_csr.py @@ -128,17 +128,7 @@ class CSRTests(unittest.TestCase): ('critical', False), ( 'extn_value', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', True), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]), + {'digital_signature', 'non_repudiation', 'key_encipherment'}, ), ]) ] diff --git a/tests/test_x509.py b/tests/test_x509.py index 7de7a62..a216dec 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -171,171 +171,51 @@ class X509Tests(unittest.TestCase): ('keys/test-third-der.crt', None), ( 'geotrust_certs/GeoTrust_Universal_CA.crt', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', True), - ('crl_sign', True), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'digital_signature', 'key_cert_sign', 'crl_sign'} ), ( 'geotrust_certs/GeoTrust_Primary_CA.crt', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'key_cert_sign', 'crl_sign'} ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'key_cert_sign', 'crl_sign'} ), ( 'geotrust_certs/codex.crt', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', True), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'digital_signature', 'key_encipherment'} ), ( 'lets_encrypt/isrgrootx1.pem', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'key_cert_sign', 'crl_sign'} ), ( 'lets_encrypt/letsencryptauthorityx1.pem', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', True), - ('crl_sign', True), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'digital_signature', 'key_cert_sign', 'crl_sign'} ), ( 'lets_encrypt/letsencryptauthorityx2.pem', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', True), - ('crl_sign', True), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'digital_signature', 'key_cert_sign', 'crl_sign'} ), ( 'globalsign_example_keys/IssuingCA-der.cer', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'key_cert_sign', 'crl_sign'} ), ( 'globalsign_example_keys/rootCA.cer', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', True), - ('key_encipherment', False), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'key_cert_sign', 'crl_sign'} ), ( 'globalsign_example_keys/SSL1.cer', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', True), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'digital_signature', 'key_encipherment'} ), ( 'globalsign_example_keys/SSL2.cer', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', True), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'digital_signature', 'key_encipherment'} ), ( 'globalsign_example_keys/SSL3.cer', - OrderedDict([ - ('digital_signature', True), - ('non_repudiation', False), - ('key_encipherment', True), - ('data_encipherment', False), - ('key_agreement', False), - ('key_cert_sign', False), - ('crl_sign', False), - ('encipher_only', False), - ('decipher_only', False), - ]) + {'digital_signature', 'key_encipherment'} ), ) -- cgit v1.2.3 From 076c412ec071c2206ca20ff6011853169fa6f4b6 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 25 Jul 2015 10:42:58 -0400 Subject: Added core.SequenceOf.append(), fixed some SequenceOf bugs, added SequenceOf tests --- asn1crypto/core.py | 84 +++++++++++++++++++++++++++++++++++++++--------------- tests/test_core.py | 43 ++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 3aa9624..8d6b695 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2324,6 +2324,40 @@ class SequenceOf(Asn1Value): self.children[index] = child return child + def _make_value(self, value): + """ + Constructs a _child_spec value from a native Python data type, or + an appropriate Asn1Value object + + :param value: + A native Python value, or some child of Asn1Value + + :return: + An object of type _child_spec + """ + + if isinstance(value, self._child_spec): + return value + + elif issubclass(self._child_spec, Any): + if isinstance(value, Asn1Value): + return value + else: + raise ValueError('Can not set a native python value to %s where the _child_spec is Any – value must be an instance of Asn1Value' % self.__class__.__name__) + + elif issubclass(self._child_spec, Choice): + if not isinstance(value, Asn1Value): + raise ValueError('Can not set a native python value to %s where the _child_spec is the choice type %s – value must be an instance of Asn1Value' % (self.__class__.__name__, self._child_spec.__name__)) + if not isinstance(value, self._child_spec): + wrapper = self._child_spec() + wrapper.validate(value.class_, value.tag) + wrapper._parsed = value #pylint: disable=W0212 + value = wrapper + return value + + else: + return self._child_spec(value=value) + def __len__(self): """ :return: @@ -2365,34 +2399,19 @@ class SequenceOf(Asn1Value): if self.children is None: self._parse_children() + new_value = self._make_value(value) + # If adding at the end, create a space for the new value if key == len(self.children): self.children.append(None) + if self._native is not None: + self._native.append(None) - if issubclass(self._child_spec, Any): - if isinstance(value, Asn1Value): - self.chilren[key] = value - else: - raise ValueError('Can not set a native python value to %s where the _child_spec is Any – value must be an instance of Asn1Value' % self.__class__.__name__) - - elif issubclass(self._child_spec, Choice): - if not isinstance(value, Asn1Value): - raise ValueError('Can not set a native python value to %s where the _child_spec is the choice type %s – value must be an instance of Asn1Value' % (self.__class__.__name__, self._child_spec.__name__)) - if not isinstance(value, self._child_spec): - wrapper = self._child_spec() - wrapper.validate(value.class_, value.tag) - wrapper._parsed = value #pylint: disable=W0212 - value = wrapper - self.children[key] = value - - elif isinstance(value, self._child_spec): - self.children[key] = value - - else: - self.children[key] = self._child_spec(value=value) + self.children[key] = new_value if self._native is not None: self._native[key] = self.children[key].native + self._set_contents() def __delitem__(self, key): @@ -2407,9 +2426,9 @@ class SequenceOf(Asn1Value): if self.children is None: self._parse_children() - self.children.remove(key) + self.children.pop(key) if self._native is not None: - self._native.remove(key) + self._native.pop(key) self._set_contents() def __iter__(self): #pylint: disable=W0234 @@ -2425,6 +2444,25 @@ class SequenceOf(Asn1Value): for index in range(0, len(self.children)): yield self._lazy_child(index) + def append(self, value): + """ + Allows adding a child to the end of the sequence + + :param value: + Native python datatype that will be passed to _child_spec to create + new child object + """ + + # We inline this checks to prevent method invocation each time + if self.children is None: + self._parse_children() + + self.children.append(self._make_value(value)) + + if self._native is not None: + self._native.append(self.children[-1].native) + self._set_contents() + def _set_contents(self, force=False): """ Encodes all child objects into the contents for this object diff --git a/tests/test_core.py b/tests/test_core.py index 7d7033e..49cf422 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -25,6 +25,15 @@ class NamedBits(core.BitString): } +class SequenceOfInts(core.SequenceOf): + _child_spec = core.Integer + + +class SequenceAny(core.SequenceOf): + _child_spec = core.Any + + + @DataDecorator class CoreTests(unittest.TestCase): @@ -110,3 +119,37 @@ class CoreTests(unittest.TestCase): named[1] = True self.assertEqual(True, named['one']) self.assertEqual({'one'}, named.native) + + def test_get_sequence_value(self): + seq = SequenceOfInts([1, 2]) + self.assertEqual(2, seq[1].native) + + def test_replace_sequence_value(self): + seq = SequenceOfInts([1, 2]) + self.assertEqual([1, 2], seq.native) + seq[0] = 5 + self.assertEqual([5, 2], seq.native) + + def test_add_to_end_sequence_value(self): + seq = SequenceOfInts([1, 2]) + self.assertEqual([1, 2], seq.native) + seq[2] = 5 + self.assertEqual([1, 2, 5], seq.native) + seq.append(6) + self.assertEqual([1, 2, 5, 6], seq.native) + + def test_delete_sequence_value(self): + seq = SequenceOfInts([1, 2]) + self.assertEqual([1, 2], seq.native) + del seq[0] + self.assertEqual([2], seq.native) + + def test_sequence_any_asn1value(self): + seq = SequenceAny() + seq.append(core.Integer(5)) + self.assertEqual([5], seq.native) + + def test_sequence_any_native_value(self): + seq = SequenceAny() + with self.assertRaises(ValueError): + seq.append(5) -- cgit v1.2.3 From 5912f8a998abfee7c81d9fec79425e9a07d302e3 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 25 Jul 2015 10:45:43 -0400 Subject: Added a test for removing a bit from a mapped BitString --- tests/test_core.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 49cf422..f818f1c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -108,6 +108,12 @@ class CoreTests(unittest.TestCase): self.assertEqual(True, named['one']) self.assertEqual(True, 'one' in named.native) + def test_mapped_bit_string_unset_bit(self): + named = NamedBits({'one', 'two'}) + named['one'] = False + self.assertEqual(True, named['two']) + self.assertEqual({'two'}, named.native) + def test_mapped_bit_string_sparse(self): named = NamedBits((0, 0, 0, 0, 0, 1)) self.assertEqual(False, named['two']) -- cgit v1.2.3 From 22323607920468d77b29817c4bf64e6a723b5476 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 09:48:56 -0400 Subject: Fixed some ibugs in Integer serialization on Python 2.7 --- asn1crypto/_int.py | 43 +++++++++++++++++++++++++++++++++++-------- tests/test_core.py | 21 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index d63c799..e3075a3 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -46,7 +46,7 @@ from ._ffi import LibraryNotFoundError, FFIEngineError, buffer_from_bytes, bytes # Python 2 if sys.version_info <= (3,): - def int_to_bytes(value, signed=False): + def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string @@ -56,20 +56,39 @@ if sys.version_info <= (3,): :param signed: 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 + :return: A byte string """ # Handle negatives in two's complement + is_neg = False if signed and value < 0: - value = (~value) + 1 + is_neg = True + bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) + value = (value + (1 << bits)) % (1 << bits) hex_str = '%x' % value if len(hex_str) & 1: hex_str = '0' + hex_str + output = hex_str.decode('hex') - if signed and ord(output[0:1]) & 0x80: + + if signed and not is_neg and ord(output[0:1]) & 0x80: output = b'\x00' + output + + if width is not None: + if is_neg: + pad_char = b'\xFF' + else: + pad_char = b'\x00' + output = (pad_char * (width - len(output))) + output + elif is_neg and ord(output[0:1]) & 0x80 == 0: + output = b'\xFF' + output + return output def int_from_bytes(value, signed=False): @@ -101,7 +120,7 @@ if sys.version_info <= (3,): # Python 3 else: - def int_to_bytes(value, signed=False): + def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string @@ -111,14 +130,22 @@ else: :param signed: 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 + :return: A byte string """ - result = value.to_bytes((value.bit_length() // 8) + 1, byteorder='big', signed=signed) - if not signed: - return result.lstrip(b'\x00') - return result + if width is None: + width_ = math.ceil(value.bit_length() / 8) or 1 + try: + return value.to_bytes(width_, byteorder='big', signed=signed) + except (OverflowError): + return value.to_bytes(width_ + 1, byteorder='big', signed=signed) + else: + return value.to_bytes(width_, byteorder='big', signed=signed) def int_from_bytes(value, signed=False): """ diff --git a/tests/test_core.py b/tests/test_core.py index f818f1c..c128e1d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -37,6 +37,27 @@ class SequenceAny(core.SequenceOf): @DataDecorator class CoreTests(unittest.TestCase): + #pylint: disable=C0326 + @staticmethod + def integer_info(): + return ( + (0, b'\x02\x01\x00'), + (255, b'\x02\x02\x00\xFF'), + (128, b'\x02\x02\x00\x80'), + (127, b'\x02\x01\x7F'), + (-127, b'\x02\x01\x81'), + (-127, b'\x02\x01\x81'), + (32768, b'\x02\x03\x00\x80\x00'), + (-32768, b'\x02\x02\x80\x00'), + (-32769, b'\x02\x03\xFF\x7F\xFF'), + ) + + @data('integer_info') + def integer(self, native, der_bytes): + i = core.Integer(native) + self.assertEqual(der_bytes, i.dump()) + self.assertEqual(native, core.Integer.load(der_bytes).native) + #pylint: disable=C0326 @staticmethod def type_info(): -- cgit v1.2.3 From 2b648f5b5fc68a09eeab9c5a6916c08fca82126a Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 09:55:44 -0400 Subject: Exposed int_to_bytes() and int_from_bytes() as public functions --- asn1crypto/_elliptic_curve.py | 3 +- asn1crypto/_int.py | 130 +--------------------------------------- asn1crypto/int.py | 136 ++++++++++++++++++++++++++++++++++++++++++ asn1crypto/keys.py | 2 +- asn1crypto/x509.py | 2 +- 5 files changed, 143 insertions(+), 130 deletions(-) create mode 100644 asn1crypto/int.py diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index 0b000b8..62e77ca 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -52,7 +52,8 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import math -from ._int import int_to_bytes, int_from_bytes, inverse_mod +from ._int import inverse_mod +from .int import int_to_bytes, int_from_bytes if sys.version_info < (3,): byte_cls = str diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index e3075a3..2a8ff09 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -1,14 +1,11 @@ # coding: utf-8 """ -Functions for converting integers to and from bytes, and calculating the modular -inverse. Exports the following items: +Function for calculating the modular inverse. Exports the following items: - - int_from_bytes() - - int_to_bytes() - inverse_mod() -Some of the following source code is derived from +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: @@ -36,134 +33,13 @@ THE SOFTWARE. from __future__ import unicode_literals, division, absolute_import, print_function -import sys import math from ._ffi import LibraryNotFoundError, FFIEngineError, buffer_from_bytes, bytes_from_buffer, null +from .int import int_to_bytes, int_from_bytes -# Python 2 -if sys.version_info <= (3,): - - def int_to_bytes(value, signed=False, width=None): - """ - Converts an integer to a byte string - - :param value: - The integer to convert - - :param signed: - 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 - - :return: - A byte string - """ - - # Handle negatives in two's complement - is_neg = False - if signed and value < 0: - is_neg = True - bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) - value = (value + (1 << bits)) % (1 << bits) - - hex_str = '%x' % value - if len(hex_str) & 1: - hex_str = '0' + hex_str - - output = hex_str.decode('hex') - - if signed and not is_neg and ord(output[0:1]) & 0x80: - output = b'\x00' + output - - if width is not None: - if is_neg: - pad_char = b'\xFF' - else: - pad_char = b'\x00' - output = (pad_char * (width - len(output))) + output - elif is_neg and ord(output[0:1]) & 0x80 == 0: - output = b'\xFF' + output - - return output - - def int_from_bytes(value, signed=False): - """ - Converts a byte string to an integer - - :param value: - The byte string to convert - - :param signed: - If the byte string should be interpreted using two's complement - - :return: - An integer - """ - - num = long(value.encode("hex"), 16) #pylint: disable=E0602 - - if not signed: - return num - - # Check for sign bit and handle two's complement - if ord(value[0:1]) & 0x80: - bit_len = len(value) * 8 - return num - (1 << bit_len) - - return num - -# Python 3 -else: - - def int_to_bytes(value, signed=False, width=None): - """ - Converts an integer to a byte string - - :param value: - The integer to convert - - :param signed: - 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 - - :return: - A byte string - """ - - if width is None: - width_ = math.ceil(value.bit_length() / 8) or 1 - try: - return value.to_bytes(width_, byteorder='big', signed=signed) - except (OverflowError): - return value.to_bytes(width_ + 1, byteorder='big', signed=signed) - else: - return value.to_bytes(width_, byteorder='big', signed=signed) - - def int_from_bytes(value, signed=False): - """ - Converts a byte string to an integer - - :param value: - The byte string to convert - - :param signed: - If the byte string should be interpreted using two's complement - - :return: - An integer - """ - - return int.from_bytes(value, 'big', signed=signed) - - # First try to use ctypes or cffi with OpenSSL for better performance try: try: diff --git a/asn1crypto/int.py b/asn1crypto/int.py new file mode 100644 index 0000000..550347e --- /dev/null +++ b/asn1crypto/int.py @@ -0,0 +1,136 @@ +# coding: utf-8 + +""" +Functions for converting integers to and from bytes. Exports the following +items: + + - int_from_bytes() + - int_to_bytes() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import math + + + +# Python 2 +if sys.version_info <= (3,): + + def int_to_bytes(value, signed=False, width=None): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + 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 + + :return: + A byte string + """ + + # Handle negatives in two's complement + is_neg = False + if signed and value < 0: + is_neg = True + bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) + value = (value + (1 << bits)) % (1 << bits) + + hex_str = '%x' % value + if len(hex_str) & 1: + hex_str = '0' + hex_str + + output = hex_str.decode('hex') + + if signed and not is_neg and ord(output[0:1]) & 0x80: + output = b'\x00' + output + + if width is not None: + if is_neg: + pad_char = b'\xFF' + else: + pad_char = b'\x00' + output = (pad_char * (width - len(output))) + output + elif is_neg and ord(output[0:1]) & 0x80 == 0: + output = b'\xFF' + output + + return output + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + num = long(value.encode("hex"), 16) #pylint: disable=E0602 + + if not signed: + return num + + # Check for sign bit and handle two's complement + if ord(value[0:1]) & 0x80: + bit_len = len(value) * 8 + return num - (1 << bit_len) + + return num + +# Python 3 +else: + + def int_to_bytes(value, signed=False, width=None): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + 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 + + :return: + A byte string + """ + + if width is None: + width_ = math.ceil(value.bit_length() / 8) or 1 + try: + return value.to_bytes(width_, byteorder='big', signed=signed) + except (OverflowError): + return value.to_bytes(width_ + 1, byteorder='big', signed=signed) + else: + return value.to_bytes(width_, byteorder='big', signed=signed) + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + return int.from_bytes(value, 'big', signed=signed) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 1fcefbe..83c740a 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -43,7 +43,7 @@ from ._elliptic_curve import ( PrimeCurve, PrimePoint, ) -from ._int import int_from_bytes +from .int import int_from_bytes try: # Python 2 diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 11877e4..15c482c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -51,7 +51,7 @@ from .core import ( ) from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo -from ._int import int_to_bytes, int_from_bytes +from .int import int_to_bytes, int_from_bytes if sys.version_info < (3,): str_cls = unicode #pylint: disable=E0602 -- cgit v1.2.3 From 3ce3aecede6ddcb30147831a3365121946ded3ee Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 10:23:19 -0400 Subject: Fixed handling of extended unicode characters on some Python 2.7 installs --- asn1crypto/x509.py | 6 +++++- tests/test_x509.py | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 15c482c..d12b66a 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -289,7 +289,11 @@ class NameTypeAndValue(Sequence): # Map step string = re.sub('[\u00ad\u1806\u034f\u180b-\u180d\ufe0f-\uff00\ufffc]+', '', string) string = re.sub('[\u0009\u000a\u000b\u000c\u000d\u0085]', ' ', string) - string = re.sub('[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb\U0001d173-\U0001d17a\U000e0001\U000e0020-\U000e007f]+', '', string) + # Some installs of Python 2.7 don't support 8-digit unicode escape + # ranges, so we have to break them into pieces + # Rriginal was: \U0001D173-\U0001D17A and \U000E0020-\U000E007F + string = re.sub('\ud834[\udd73-\udd7a]|\udb40[\udc20-\udc7f]|\U000e0001', '', string) + string = re.sub('[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb]+', '', string) string = string.replace('\u200b', '') string = re.sub('[\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000]', ' ', string) diff --git a/tests/test_x509.py b/tests/test_x509.py index a216dec..872fa4f 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -71,6 +71,15 @@ class X509Tests(unittest.TestCase): 'common_name': 'will\tbond' }) ), + ( + True, + x509.Name.build({ + 'common_name': 'Will Bond' + }), + x509.Name.build({ + 'common_name': 'Will Bond \U0001D173\U000E007F' + }) + ), ( False, x509.Name.build({ -- cgit v1.2.3 From 15da2ac9b8709e22bc0bb2504b9a6f13d4a8486b Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 10:23:40 -0400 Subject: Correct __eq__ functionality by adding __ne__ for 2.7 --- asn1crypto/x509.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index d12b66a..e74f523 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -255,6 +255,9 @@ class NameTypeAndValue(Sequence): self._prepped = self._ldap_string_prep(self['value'].native) return self._prepped + def __ne__(self, other): + return not self == other + def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 @@ -364,6 +367,9 @@ class RelativeDistinguishedName(SetOf): # or under_score_words return '\x1F'.join(output) + def __ne__(self, other): + return not self == other + def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 @@ -440,6 +446,9 @@ class RDNSequence(SequenceOf): # or under_score_words return '\x1E'.join(rdn.hashable for rdn in self) + def __ne__(self, other): + return not self == other + def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 @@ -517,6 +526,9 @@ class Name(Choice): return self.chosen.hashable + def __ne__(self, other): + return not self == other + def __eq__(self, other): """ Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.1 @@ -953,6 +965,9 @@ class GeneralName(Choice): ('registered_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 8}), ] + def __ne__(self, other): + return not self == other + def __eq__(self, other): """ Does not support other_name, x400_address or edi_party_name -- cgit v1.2.3 From 93ad1436a20267ec493554159dbbc63c28b81b9e Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 10:26:24 -0400 Subject: Only use the long unicode compat code when needed --- asn1crypto/x509.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index e74f523..f858ed6 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -292,10 +292,13 @@ class NameTypeAndValue(Sequence): # Map step string = re.sub('[\u00ad\u1806\u034f\u180b-\u180d\ufe0f-\uff00\ufffc]+', '', string) string = re.sub('[\u0009\u000a\u000b\u000c\u000d\u0085]', ' ', string) - # Some installs of Python 2.7 don't support 8-digit unicode escape - # ranges, so we have to break them into pieces - # Rriginal was: \U0001D173-\U0001D17A and \U000E0020-\U000E007F - string = re.sub('\ud834[\udd73-\udd7a]|\udb40[\udc20-\udc7f]|\U000e0001', '', string) + if sys.maxunicode == 0xffff: + # Some installs of Python 2.7 don't support 8-digit unicode escape + # ranges, so we have to break them into pieces + # Original was: \U0001D173-\U0001D17A and \U000E0020-\U000E007F + string = re.sub('\ud834[\udd73-\udd7a]|\udb40[\udc20-\udc7f]|\U000e0001', '', string) + else: + string = re.sub('[\U0001D173-\U0001D17A\U000E0020-\U000E007F\U000e0001]', '', string) string = re.sub('[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb]+', '', string) string = string.replace('\u200b', '') string = re.sub('[\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000]', ' ', string) -- cgit v1.2.3 From 0b7ffe7bf7ae8a1858cc92e2b0a3c3ab7eeaf19a Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 15:57:44 -0400 Subject: Move hold extension code extension to CRLEntryExtension where it belongs --- asn1crypto/crl.py | 2 ++ asn1crypto/x509.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index b83d99e..9d5e420 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -134,6 +134,7 @@ class CRLReason(Enumerated): class CRLEntryExtensionId(ObjectIdentifier): _map = { '2.5.29.21': 'crl_reason', + '2.5.29.23': 'hold_instruction_code', '2.5.29.24': 'invalidity_date', '2.5.29.29': 'certificate_issuer', } @@ -149,6 +150,7 @@ class CRLEntryExtension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { 'crl_reason': CRLReason, + 'hold_instruction_code': ObjectIdentifier, 'invalidity_date': GeneralizedTime, 'certificate_issuer': GeneralNames, } diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index f858ed6..4fae5f0 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1264,7 +1264,6 @@ class ExtensionId(ObjectIdentifier): '2.5.29.17': 'subject_alt_name', '2.5.29.18': 'issuer_alt_name', '2.5.29.19': 'basic_constraints', - '2.5.29.23': 'hold_instruction_code', '2.5.29.30': 'name_constraints', '2.5.29.31': 'crl_distribution_points', '2.5.29.32': 'certificate_policies', @@ -1298,7 +1297,6 @@ class Extension(Sequence): 'subject_alt_name': GeneralNames, 'issuer_alt_name': GeneralNames, 'basic_constraints': BasicConstraints, - 'hold_instruction_code': ObjectIdentifier, 'name_constraints': NameConstraints, 'crl_distribution_points': CRLDistributionPoints, 'certificate_policies': CertificatePolicies, -- cgit v1.2.3 From adcdebc974b13712580173eb0f787f414215df5a Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 15:58:10 -0400 Subject: Added OID for special extended key usage value any --- asn1crypto/x509.py | 1 + 1 file changed, 1 insertion(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 4fae5f0..7863530 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1194,6 +1194,7 @@ class PolicyConstraints(Sequence): class KeyPurposeId(ObjectIdentifier): _map = { + '2.5.29.37.0': 'any_extended_key_usage', '1.3.6.1.5.5.7.3.1': 'server_auth', '1.3.6.1.5.5.7.3.2': 'client_auth', '1.3.6.1.5.5.7.3.3': 'code_signing', -- cgit v1.2.3 From 956f171312be4a46205fef1da6c6cdf54f950e4a Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 16:03:59 -0400 Subject: Added core.Sequence.spec() and core.SequenceOf.spec() --- asn1crypto/core.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_core.py | 23 +++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8d6b695..e8aeb3b 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2202,6 +2202,33 @@ class Sequence(Asn1Value): e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args raise e + def spec(self, field_name): + """ + Determines the spec to use for the field specified. Depending on how + the spec is determined (_oid_pair or _spec_callbacks), it may be + necessary to set preceeding field values before calling this. Usually + specs, if dynamic, are controlled by a preceeding ObjectIdentifier + field. + + :param field_name: + A unicode string of the field name to get the spec for + + :return: + A child class of asn1crypto.core.Asn1Value that the field must be + encoded using + """ + + if not isinstance(field_name, str_cls): + raise ValueError('field_name must be a unicode string, not %s' % field_name.__class__.__name__) + + if self._fields is None: + raise ValueError('Unable to retrieve spec for field %s in the class %s because _fields has not been set' % (repr(field_name), self.__class__.__name__)) + + index = self._field_map[field_name] + info = self._determine_spec(index) + + return info[2] + @property def native(self): """ @@ -2519,6 +2546,17 @@ class SequenceOf(Asn1Value): e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args raise e + def spec(self): + """ + Determines the spec to use for child values. + + :return: + A child class of asn1crypto.core.Asn1Value that child values must be + encoded using + """ + + return self._child_spec + @property def native(self): """ diff --git a/tests/test_core.py b/tests/test_core.py index c128e1d..c381eae 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -33,10 +33,33 @@ class SequenceAny(core.SequenceOf): _child_spec = core.Any +class Seq(core.Sequence): + _fields = [ + ('id', core.ObjectIdentifier), + ('value', core.Any), + ] + + _oid_pair = ('id', 'value') + _oid_specs = { + '1.2.3': core.Integer, + '2.3.4': core.OctetString, + } + @DataDecorator class CoreTests(unittest.TestCase): + def test_sequence_spec(self): + seq = Seq() + seq['id'] = '1.2.3' + self.assertEqual(core.Integer, seq.spec('value')) + seq['id'] = '2.3.4' + self.assertEqual(core.OctetString, seq.spec('value')) + + def test_sequence_of_spec(self): + seq = SequenceAny() + self.assertEqual(core.Any, seq.spec()) + #pylint: disable=C0326 @staticmethod def integer_info(): -- cgit v1.2.3 From f2a76150b4d26488ddd566bca540ac26655a3091 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Jul 2015 16:49:17 -0400 Subject: Add another reference in CSR comments --- asn1crypto/csr.py | 1 + 1 file changed, 1 insertion(+) diff --git a/asn1crypto/csr.py b/asn1crypto/csr.py index 734c9f1..05085cf 100644 --- a/asn1crypto/csr.py +++ b/asn1crypto/csr.py @@ -19,6 +19,7 @@ from .x509 import DirectoryString, Extensions, Name # The structures in this file are taken from https://tools.ietf.org/html/rfc2986 +# and https://tools.ietf.org/html/rfc2985 class Version(Integer): -- cgit v1.2.3 From a5e572ddd41945d28de81e3db778523919decf5b Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 29 Jul 2015 14:17:44 -0400 Subject: Fixed a bug assigning a nested value inside of an OctetBitString that is part of a Sequence --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index e8aeb3b..21a01e1 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2066,7 +2066,7 @@ class Sequence(Asn1Value): # appropriate encoded value. if specs_different and not is_any: wrapper = field_spec(value=new_value.dump(), **field_params) - wrapper._parsed = new_value #pylint: disable=W0212 + wrapper._parsed = (new_value, new_value.__class__, None) #pylint: disable=W0212 new_value = wrapper if field_params and 'tag_type' in field_params and 'tag' in field_params: -- cgit v1.2.3 From e720d6e370ad489a4111ca8840c97b21112257b6 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 29 Jul 2015 14:18:12 -0400 Subject: Fixed a bug computing the DSA public key when only the private key is available --- asn1crypto/keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 83c740a..cc57a54 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -513,7 +513,7 @@ class PrivateKeyInfo(Sequence): params = self['private_key_algorithm']['parameters'] return Integer(pow( params['g'].native, - self['private_key'].native, + self['private_key'].parsed.native, params['p'].native )) -- cgit v1.2.3 From 78d585ccd40b420fdebb0a17dc7dec01f1cd23b9 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 29 Jul 2015 21:26:12 -0400 Subject: Fixed big num modules comments --- asn1crypto/_perf/_big_num_cffi.py | 2 +- asn1crypto/_perf/_big_num_ctypes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/_perf/_big_num_cffi.py b/asn1crypto/_perf/_big_num_cffi.py index 2b7b217..0c70f46 100644 --- a/asn1crypto/_perf/_big_num_cffi.py +++ b/asn1crypto/_perf/_big_num_cffi.py @@ -5,7 +5,7 @@ cffi interface for BN_mod_inverse() function from OpenSSL. Exports the following items: - libcrypto - - BN_bin2bin() + - BN_bn2bin() - BN_CTX_free() - BN_CTX_new() - BN_free() diff --git a/asn1crypto/_perf/_big_num_ctypes.py b/asn1crypto/_perf/_big_num_ctypes.py index a99b658..6bd4cb7 100644 --- a/asn1crypto/_perf/_big_num_ctypes.py +++ b/asn1crypto/_perf/_big_num_ctypes.py @@ -5,7 +5,7 @@ ctypes interface for BN_mod_inverse() function from OpenSSL. Exports the following items: - libcrypto - - BN_bin2bin() + - BN_bn2bin() - BN_CTX_free() - BN_CTX_new() - BN_free() -- cgit v1.2.3 From b69717344fd4200c5927030d3d1b58e4d952bf33 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 29 Jul 2015 21:32:52 -0400 Subject: Expose ECPoint loading/dumping functionality via keys module --- asn1crypto/_elliptic_curve.py | 60 +-------------------------- asn1crypto/int.py | 2 +- asn1crypto/keys.py | 96 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 87 insertions(+), 71 deletions(-) diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index 62e77ca..bd18c67 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -22,9 +22,8 @@ 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, and to support loading and -serializing to/from ECPrimePoint format. The original project license is listed -below: +modified to fit into this projects lint settings. The original project license +is listed below: Copyright (c) 2014 Peter Pearson @@ -50,10 +49,8 @@ THE SOFTWARE. from __future__ import unicode_literals, division, absolute_import, print_function import sys -import math from ._int import inverse_mod -from .int import int_to_bytes, int_from_bytes if sys.version_info < (3,): byte_cls = str @@ -108,38 +105,6 @@ class PrimePoint(): A point on a prime-field elliptic curve """ - @classmethod - def load(cls, curve, data): - """ - Loads a Point from the ECPoint OctetString representation - - :param curve: - A PrimeCurve object of the curve the point is on - - :param data: - A byte string of the ECPoint representation of the point. This - representation uses the first byte as a compression indicator, and - then lists the X and Y as base256 byte string integers - - :return: - A PrimePoint object - """ - - first_byte = data[0:1] - - # Uncompressed - if first_byte == b'\x04': - remaining = data[1:] - field_len = len(remaining) // 2 - x = int_from_bytes(remaining[0:field_len]) - y = int_from_bytes(remaining[field_len:]) - return cls(curve, x, y) - - if first_byte not in (b'\x02', b'\x03'): - raise ValueError('Invalid ECPoint representation of a point - first byte is incorrect') - - raise ValueError('Compressed ECPoint representations are not supported due to patent US6252960') - def __init__(self, curve, x, y, order=None): """ :param curve: @@ -283,27 +248,6 @@ class PrimePoint(): return PrimePoint(self.curve, x3, y3) - def dump(self): - """ - Dumps a PrimePoint to the ECPoint OctetString representation - - :return: - A byte string of the ECPoint representation - """ - - field_len = int(math.ceil(math.log(self.curve.p, 2) / 8)) - - xb = int_to_bytes(self.x) - yb = int_to_bytes(self.y) - - # Make sure the fields are full width, according to the spec - while len(xb) < field_len: - xb = b'\x00' + xb - while len(yb) < field_len: - yb = b'\x00' + yb - - return b'\x04' + xb + yb - # This one point is the Point At Infinity for all purposes: INFINITY = PrimePoint(None, None, None) diff --git a/asn1crypto/int.py b/asn1crypto/int.py index 550347e..7ef8359 100644 --- a/asn1crypto/int.py +++ b/asn1crypto/int.py @@ -117,7 +117,7 @@ else: except (OverflowError): return value.to_bytes(width_ + 1, byteorder='big', signed=signed) else: - return value.to_bytes(width_, byteorder='big', signed=signed) + return value.to_bytes(width, byteorder='big', signed=signed) def int_from_bytes(value, signed=False): """ diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index cc57a54..3f7af20 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -43,7 +43,7 @@ from ._elliptic_curve import ( PrimeCurve, PrimePoint, ) -from .int import int_from_bytes +from .int import int_from_bytes, int_to_bytes try: # Python 2 @@ -139,6 +139,77 @@ class DSAPrivateKey(Sequence): ] +class _ECPoint(): + """ + In both PublicKeyInfo and PrivateKeyInfo, the EC public key is a byte + string that is encoded as a bit string. This class adds convenience + methods for converting to and from the byte string to a pair of integers + that are the X and Y coordinates. + """ + + @classmethod + def from_coords(cls, x, y): + """ + Creates an ECPoint object from the X and Y integer coordinates of the + point + + :param x: + The X coordinate, as an integer + + :param y: + The Y coordinate, as an integer + + :return: + An ECPoint object + """ + + x_bytes = int(math.ceil(math.log(x, 2) / 8.0)) + y_bytes = int(math.ceil(math.log(y, 2) / 8.0)) + + num_bytes = max(x_bytes, y_bytes) + + byte_string = b'\x04' + byte_string += int_to_bytes(x, width=num_bytes) + byte_string += int_to_bytes(y, width=num_bytes) + + return cls(byte_string) + + def to_coords(self): + """ + Returns the X and Y coordinates for this EC point, as native Python + integers + + :return: + A 2-element tuple containing integers (X, Y) + """ + + data = self.native + first_byte = data[0:1] + + # Uncompressed + if first_byte == b'\x04': + remaining = data[1:] + field_len = len(remaining) // 2 + x = int_from_bytes(remaining[0:field_len]) + y = int_from_bytes(remaining[field_len:]) + return (x, y) + + if first_byte not in {b'\x02', b'\x03'}: + raise ValueError('Invalid EC public key - first byte is incorrect') + + raise ValueError('Compressed representations of EC public keys are not supported due to patent US6252960') + + +class ECPoint(OctetString, _ECPoint): + + pass + + +class ECPointBitString(OctetBitString, _ECPoint): + + pass + + class SpecifiedECDomainVersion(Integer): """ Source: http://www.secg.org/sec1-v2.pdf page 104 @@ -245,7 +316,7 @@ class SpecifiedECDomain(Sequence): ('version', SpecifiedECDomainVersion), ('field_id', FieldID), ('curve', Curve), - ('base', OctetString), + ('base', ECPoint), ('order', Integer), ('cofactor', Integer, {'optional': True}), ('hash', DigestAlgorithm, {'optional': True}), @@ -339,7 +410,7 @@ class ECPrivateKey(Sequence): ('version', ECPrivateKeyVersion), ('private_key', IntegerOctetString), ('parameters', ECDomainParameters, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('public_key', OctetBitString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('public_key', ECPointBitString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), ] @@ -506,7 +577,7 @@ class PrivateKeyInfo(Sequence): :return: For RSA keys, an RSAPublicKey object. For DSA keys, an Integer - object. For EC keys, an OctetString. + object. For EC keys, an ECPointBitString. """ if self.algorithm == 'dsa': @@ -539,7 +610,8 @@ class PrivateKeyInfo(Sequence): int_from_bytes(details['curve']['a']), int_from_bytes(details['curve']['b']) ) - base_point = PrimePoint.load(curve, details['base']) + 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'): @@ -554,7 +626,7 @@ class PrivateKeyInfo(Sequence): }[details] public_point = base_point * self['private_key'].parsed['private_key'].native - return OctetString(public_point.dump()) + return ECPointBitString.from_coords(public_point.x, public_point.y) def unwrap(self): """ @@ -582,7 +654,7 @@ class PrivateKeyInfo(Sequence): if self.algorithm == 'ec': output = self['private_key'].parsed output['parameters'] = self['private_key_algorithm']['parameters'] - output['public_key'] = OctetBitString(self.public_key.native) + output['public_key'] = self.public_key return output @property @@ -676,14 +748,14 @@ class PrivateKeyInfo(Sequence): """ :return: If an RSA key, an RSAPublicKey object. If a DSA key, an Integer - object. If an EC key, an OctetString object. + 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 = OctetString(key['public_key'].native) + self._public_key = key['public_key'].untag() else: self._public_key = self._compute_public_key() else: @@ -816,9 +888,9 @@ class PublicKeyInfo(Sequence): return { 'rsa': RSAPublicKey, 'dsa': Integer, - # ECSDA's public key is an ECPoint, which is an OctetString. Since - # we are using OctetBitString here, we don't need further parsing. - 'ec': None, + # We override the field spec with ECPoint so that users can easily + # decompose the byte string into the constituent X and Y coords + 'ec': (ECPointBitString, None), }[algorithm] _spec_callbacks = { -- cgit v1.2.3 From e3e4bcd5fa2975dd834b4a80c992311b03aa8ac6 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 30 Jul 2015 08:58:20 -0400 Subject: Move asn1crypto.int to asn1crypto.util to prevent import name conflicts with int --- asn1crypto/_int.py | 2 +- asn1crypto/core.py | 2 +- asn1crypto/int.py | 136 ----------------------------------------------------- asn1crypto/keys.py | 2 +- asn1crypto/util.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++ asn1crypto/x509.py | 2 +- 6 files changed, 140 insertions(+), 140 deletions(-) delete mode 100644 asn1crypto/int.py create mode 100644 asn1crypto/util.py diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index 2a8ff09..1011c5f 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -36,7 +36,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math from ._ffi import LibraryNotFoundError, FFIEngineError, buffer_from_bytes, bytes_from_buffer, null -from .int import int_to_bytes, int_from_bytes +from .util import int_to_bytes, int_from_bytes diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 21a01e1..60bd1bb 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -54,7 +54,7 @@ from datetime import datetime, timedelta, tzinfo from pprint import pprint from . import _teletex_codec -from ._int import int_to_bytes, int_from_bytes +from .util import int_to_bytes, int_from_bytes # Python 2 if sys.version_info <= (3,): diff --git a/asn1crypto/int.py b/asn1crypto/int.py deleted file mode 100644 index 7ef8359..0000000 --- a/asn1crypto/int.py +++ /dev/null @@ -1,136 +0,0 @@ -# coding: utf-8 - -""" -Functions for converting integers to and from bytes. Exports the following -items: - - - int_from_bytes() - - int_to_bytes() -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -import sys -import math - - - -# Python 2 -if sys.version_info <= (3,): - - def int_to_bytes(value, signed=False, width=None): - """ - Converts an integer to a byte string - - :param value: - The integer to convert - - :param signed: - 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 - - :return: - A byte string - """ - - # Handle negatives in two's complement - is_neg = False - if signed and value < 0: - is_neg = True - bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) - value = (value + (1 << bits)) % (1 << bits) - - hex_str = '%x' % value - if len(hex_str) & 1: - hex_str = '0' + hex_str - - output = hex_str.decode('hex') - - if signed and not is_neg and ord(output[0:1]) & 0x80: - output = b'\x00' + output - - if width is not None: - if is_neg: - pad_char = b'\xFF' - else: - pad_char = b'\x00' - output = (pad_char * (width - len(output))) + output - elif is_neg and ord(output[0:1]) & 0x80 == 0: - output = b'\xFF' + output - - return output - - def int_from_bytes(value, signed=False): - """ - Converts a byte string to an integer - - :param value: - The byte string to convert - - :param signed: - If the byte string should be interpreted using two's complement - - :return: - An integer - """ - - num = long(value.encode("hex"), 16) #pylint: disable=E0602 - - if not signed: - return num - - # Check for sign bit and handle two's complement - if ord(value[0:1]) & 0x80: - bit_len = len(value) * 8 - return num - (1 << bit_len) - - return num - -# Python 3 -else: - - def int_to_bytes(value, signed=False, width=None): - """ - Converts an integer to a byte string - - :param value: - The integer to convert - - :param signed: - 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 - - :return: - A byte string - """ - - if width is None: - width_ = math.ceil(value.bit_length() / 8) or 1 - try: - return value.to_bytes(width_, byteorder='big', signed=signed) - except (OverflowError): - return value.to_bytes(width_ + 1, byteorder='big', signed=signed) - else: - return value.to_bytes(width, byteorder='big', signed=signed) - - def int_from_bytes(value, signed=False): - """ - Converts a byte string to an integer - - :param value: - The byte string to convert - - :param signed: - If the byte string should be interpreted using two's complement - - :return: - An integer - """ - - return int.from_bytes(value, 'big', signed=signed) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 3f7af20..2ec6a52 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -43,7 +43,7 @@ from ._elliptic_curve import ( PrimeCurve, PrimePoint, ) -from .int import int_from_bytes, int_to_bytes +from .util import int_from_bytes, int_to_bytes try: # Python 2 diff --git a/asn1crypto/util.py b/asn1crypto/util.py new file mode 100644 index 0000000..7ef8359 --- /dev/null +++ b/asn1crypto/util.py @@ -0,0 +1,136 @@ +# coding: utf-8 + +""" +Functions for converting integers to and from bytes. Exports the following +items: + + - int_from_bytes() + - int_to_bytes() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import math + + + +# Python 2 +if sys.version_info <= (3,): + + def int_to_bytes(value, signed=False, width=None): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + 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 + + :return: + A byte string + """ + + # Handle negatives in two's complement + is_neg = False + if signed and value < 0: + is_neg = True + bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) + value = (value + (1 << bits)) % (1 << bits) + + hex_str = '%x' % value + if len(hex_str) & 1: + hex_str = '0' + hex_str + + output = hex_str.decode('hex') + + if signed and not is_neg and ord(output[0:1]) & 0x80: + output = b'\x00' + output + + if width is not None: + if is_neg: + pad_char = b'\xFF' + else: + pad_char = b'\x00' + output = (pad_char * (width - len(output))) + output + elif is_neg and ord(output[0:1]) & 0x80 == 0: + output = b'\xFF' + output + + return output + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + num = long(value.encode("hex"), 16) #pylint: disable=E0602 + + if not signed: + return num + + # Check for sign bit and handle two's complement + if ord(value[0:1]) & 0x80: + bit_len = len(value) * 8 + return num - (1 << bit_len) + + return num + +# Python 3 +else: + + def int_to_bytes(value, signed=False, width=None): + """ + Converts an integer to a byte string + + :param value: + The integer to convert + + :param signed: + 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 + + :return: + A byte string + """ + + if width is None: + width_ = math.ceil(value.bit_length() / 8) or 1 + try: + return value.to_bytes(width_, byteorder='big', signed=signed) + except (OverflowError): + return value.to_bytes(width_ + 1, byteorder='big', signed=signed) + else: + return value.to_bytes(width, byteorder='big', signed=signed) + + def int_from_bytes(value, signed=False): + """ + Converts a byte string to an integer + + :param value: + The byte string to convert + + :param signed: + If the byte string should be interpreted using two's complement + + :return: + An integer + """ + + return int.from_bytes(value, 'big', signed=signed) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 7863530..e8c5700 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -51,7 +51,7 @@ from .core import ( ) from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo -from .int import int_to_bytes, int_from_bytes +from .util import int_to_bytes, int_from_bytes if sys.version_info < (3,): str_cls = unicode #pylint: disable=E0602 -- cgit v1.2.3 From e914215e71acdc421f46572fe782c855326d2a2b Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 30 Jul 2015 09:05:19 -0400 Subject: Moved timezone.utc out of core into util --- asn1crypto/core.py | 22 +++------------------- asn1crypto/util.py | 29 +++++++++++++++++++++++++++-- docs/universal_types.md | 4 ++-- tests/test_cms.py | 6 +++--- tests/test_ocsp.py | 8 ++++---- tests/test_tsp.py | 6 +++--- tests/test_x509.py | 14 +++++++------- 7 files changed, 49 insertions(+), 40 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 60bd1bb..d866d43 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -50,11 +50,11 @@ import math import sys import re from collections import OrderedDict -from datetime import datetime, timedelta, tzinfo +from datetime import datetime from pprint import pprint from . import _teletex_codec -from .util import int_to_bytes, int_from_bytes +from .util import int_to_bytes, int_from_bytes, timezone # Python 2 if sys.version_info <= (3,): @@ -64,21 +64,7 @@ if sys.version_info <= (3,): py2 = True chr_cls = chr range = xrange #pylint: disable=E0602,W0622 - - class utc(tzinfo): - - def tzname(self, _): - return 'UTC+00:00' - - def utcoffset(self, _): - return timedelta(0) - - def dst(self, _): - return None - - class timezone(): - - utc = utc() + from datetime import timedelta # Python 3 else: @@ -90,8 +76,6 @@ else: def chr_cls(num): return bytes([num]) - from datetime import timezone - _teletex_codec.register() diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 7ef8359..a8d916f 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -1,11 +1,12 @@ # coding: utf-8 """ -Functions for converting integers to and from bytes. Exports the following -items: +Miscellaneous data helpers, including functions for converting integers to and +from bytes and UTC timezone. Exports the following items: - int_from_bytes() - int_to_bytes() + - timezone.utc """ from __future__ import unicode_literals, division, absolute_import, print_function @@ -18,6 +19,9 @@ import math # Python 2 if sys.version_info <= (3,): + from datetime import timedelta, tzinfo + + def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string @@ -89,9 +93,30 @@ if sys.version_info <= (3,): return num + + class utc(tzinfo): + + def tzname(self, _): + return 'UTC+00:00' + + def utcoffset(self, _): + return timedelta(0) + + def dst(self, _): + return None + + + class timezone(): + + utc = utc() + + # Python 3 else: + from datetime import timezone #pylint: disable=W0611 + + def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string diff --git a/docs/universal_types.md b/docs/universal_types.md index 807a97c..d048cb1 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -366,7 +366,7 @@ or a `datetime.datetime` instance. See the for details of the formats. When `.native` is accessed, it returns a `datetime.datetime` object with a -`tzinfo` of `asn1crypto.core.timezone.utc`. +`tzinfo` of `asn1crypto.util.timezone.utc`. ## GeneralizedTime @@ -390,7 +390,7 @@ or a `datetime.datetime` instance. See the for details of the formats. When `.native` is accessed, it returns a `datetime.datetime` object with a -`tzinfo` of `asn1crypto.core.timezone.utc`. For formats where the time has a +`tzinfo` of `asn1crypto.util.timezone.utc`. For formats where the time has a timezone offset is specified (`[+-]\d{4}`), the time is converted to UTC. For times without a timezone, the time is assumed to be in UTC. diff --git a/tests/test_cms.py b/tests/test_cms.py index a1475b2..8bfb15a 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -6,7 +6,7 @@ import os from datetime import datetime from collections import OrderedDict -from asn1crypto import cms, core +from asn1crypto import cms, core, util tests_root = os.path.dirname(__file__) @@ -315,7 +315,7 @@ class CMSTests(unittest.TestCase): signed_attrs[1]['type'].native ) self.assertEqual( - datetime(2015, 5, 30, 13, 12, 38, tzinfo=core.timezone.utc), + datetime(2015, 5, 30, 13, 12, 38, tzinfo=util.timezone.utc), signed_attrs[1]['values'][0].native ) self.assertEqual( @@ -441,7 +441,7 @@ class CMSTests(unittest.TestCase): signed_attrs[1]['type'].native ) self.assertEqual( - datetime(2015, 6, 3, 5, 55, 12, tzinfo=core.timezone.utc), + datetime(2015, 6, 3, 5, 55, 12, tzinfo=util.timezone.utc), signed_attrs[1]['values'][0].native ) self.assertEqual( diff --git a/tests/test_ocsp.py b/tests/test_ocsp.py index 62240d1..808a6d4 100644 --- a/tests/test_ocsp.py +++ b/tests/test_ocsp.py @@ -6,7 +6,7 @@ import sys import os from datetime import datetime -from asn1crypto import ocsp, core +from asn1crypto import ocsp, core, util if sys.version_info < (3,): byte_cls = str @@ -100,7 +100,7 @@ class OCSPTests(unittest.TestCase): responder_id.name ) self.assertEqual( - datetime(2015, 5, 22, 16, 24, 8, tzinfo=core.timezone.utc), + datetime(2015, 5, 22, 16, 24, 8, tzinfo=util.timezone.utc), tbs_response_data['produced_at'].native ) self.assertEqual( @@ -124,11 +124,11 @@ class OCSPTests(unittest.TestCase): cert_id['serial_number'].native ) self.assertEqual( - datetime(2015, 5, 22, 16, 24, 8, tzinfo=core.timezone.utc), + datetime(2015, 5, 22, 16, 24, 8, tzinfo=util.timezone.utc), single_response['this_update'].native ) self.assertEqual( - datetime(2015, 5, 29, 16, 24, 8, tzinfo=core.timezone.utc), + datetime(2015, 5, 29, 16, 24, 8, tzinfo=util.timezone.utc), single_response['next_update'].native ) self.assertEqual( diff --git a/tests/test_tsp.py b/tests/test_tsp.py index 0944746..586880f 100644 --- a/tests/test_tsp.py +++ b/tests/test_tsp.py @@ -6,7 +6,7 @@ import os from datetime import datetime from collections import OrderedDict -from asn1crypto import tsp, cms, core +from asn1crypto import tsp, cms, core, util tests_root = os.path.dirname(__file__) @@ -115,7 +115,7 @@ class TSPTests(unittest.TestCase): tst_info['serial_number'].native ) self.assertEqual( - datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), + datetime(2015, 6, 1, 18, 39, 55, tzinfo=util.timezone.utc), tst_info['gen_time'].native ) self.assertEqual( @@ -207,7 +207,7 @@ class TSPTests(unittest.TestCase): signed_attrs[1]['type'].native ) self.assertEqual( - datetime(2015, 6, 1, 18, 39, 55, tzinfo=core.timezone.utc), + datetime(2015, 6, 1, 18, 39, 55, tzinfo=util.timezone.utc), signed_attrs[1]['values'][0].native ) self.assertEqual( diff --git a/tests/test_x509.py b/tests/test_x509.py index 872fa4f..33b88ff 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -7,7 +7,7 @@ import os from collections import OrderedDict from datetime import datetime -from asn1crypto import x509, core, pem +from asn1crypto import x509, core, pem, util from .unittest_data import DataDecorator, data @@ -1340,11 +1340,11 @@ class X509Tests(unittest.TestCase): issuer.native ) self.assertEqual( - datetime(2015, 5, 6, 14, 37, 16, tzinfo=core.timezone.utc), + datetime(2015, 5, 6, 14, 37, 16, tzinfo=util.timezone.utc), validity['not_before'].native ) self.assertEqual( - datetime(2025, 5, 3, 14, 37, 16, tzinfo=core.timezone.utc), + datetime(2025, 5, 3, 14, 37, 16, tzinfo=util.timezone.utc), validity['not_after'].native ) self.assertEqual( @@ -1487,11 +1487,11 @@ class X509Tests(unittest.TestCase): issuer.native ) self.assertEqual( - datetime(2015, 5, 20, 13, 9, 2, tzinfo=core.timezone.utc), + datetime(2015, 5, 20, 13, 9, 2, tzinfo=util.timezone.utc), validity['not_before'].native ) self.assertEqual( - datetime(2025, 5, 17, 13, 9, 2, tzinfo=core.timezone.utc), + datetime(2025, 5, 17, 13, 9, 2, tzinfo=util.timezone.utc), validity['not_after'].native ) self.assertEqual( @@ -1624,11 +1624,11 @@ class X509Tests(unittest.TestCase): issuer.native ) self.assertEqual( - datetime(2015, 5, 20, 12, 56, 46, tzinfo=core.timezone.utc), + datetime(2015, 5, 20, 12, 56, 46, tzinfo=util.timezone.utc), validity['not_before'].native ) self.assertEqual( - datetime(2025, 5, 17, 12, 56, 46, tzinfo=core.timezone.utc), + datetime(2025, 5, 17, 12, 56, 46, tzinfo=util.timezone.utc), validity['not_after'].native ) self.assertEqual( -- cgit v1.2.3 From b7d13b7cf19e80f946af1b676164235b43a374bc Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 30 Jul 2015 09:13:20 -0400 Subject: Update install instructions to work since package not published to pypi yet --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 64fa298..4bb8341 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,7 @@ Python 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Installation ```bash -pip install asn1crypto +pip install git+git://github.com/wbond/asn1crypto.git@0.9.0 ``` ## Documentation -- cgit v1.2.3 From b86376fe4089571d17d4d39eb7a383bffefb89df Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 30 Jul 2015 11:53:55 -0400 Subject: Change "setup.py clean" to also remove .pyc files and __pycache__ dirs --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 5568ac4..0504f1e 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,13 @@ class CleanCommand(Command): full_path = os.path.join(folder, sub_folder) if os.path.exists(full_path): shutil.rmtree(full_path) + for root, dirnames, filenames in os.walk(os.path.join(folder, 'asn1crypto')): + for filename in filenames: + if filename[-4:] == '.pyc': + os.unlink(os.path.join(root, filename)) + for dirname in list(dirnames): + if dirname == '__pycache__': + shutil.rmtree(os.path.join(root, dirname)) setup( -- cgit v1.2.3 From 5b10bc293bef1689c44fa87c5cff1e6884b6deb4 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 30 Jul 2015 14:17:30 -0400 Subject: Add try blocks in constructors to add helpful debugging info about current class --- asn1crypto/core.py | 158 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 93 insertions(+), 65 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d866d43..d91b620 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -198,44 +198,50 @@ class Asn1Value(): ValueError - when tag_type, class_ or tag are invalid values """ - if self.__class__ not in _SETUP_CLASSES: - cls = self.__class__ - if hasattr(cls, '_setup'): - self._setup() - _SETUP_CLASSES[cls] = True - - if tag_type is not None: - if tag_type not in ('implicit', 'explicit'): - raise ValueError('tag_type must be one of "implicit", "explicit", not %s' % repr(tag_type)) - self.tag_type = tag_type - - if class_ is None: - class_ = 'context' - if class_ not in CLASS_NAME_TO_NUM_MAP: - raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) - class_ = CLASS_NAME_TO_NUM_MAP[class_] - - if tag is not None: - if not isinstance(tag, int_types): - raise ValueError('tag must be an integer, not %s' % tag.__class__.__name__) + try: + if self.__class__ not in _SETUP_CLASSES: + cls = self.__class__ + if hasattr(cls, '_setup'): + self._setup() + _SETUP_CLASSES[cls] = True + + if tag_type is not None: + if tag_type not in ('implicit', 'explicit'): + raise ValueError('tag_type must be one of "implicit", "explicit", not %s' % repr(tag_type)) + self.tag_type = tag_type + + if class_ is None: + class_ = 'context' + if class_ not in CLASS_NAME_TO_NUM_MAP: + raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) + class_ = CLASS_NAME_TO_NUM_MAP[class_] - if tag_type == 'implicit': - self.class_ = class_ - self.tag = tag + if tag is not None: + if not isinstance(tag, int_types): + raise ValueError('tag must be an integer, not %s' % tag.__class__.__name__) + + if tag_type == 'implicit': + self.class_ = class_ + self.tag = tag + else: + self.explicit_class = class_ + self.explicit_tag = tag else: - self.explicit_class = class_ - self.explicit_tag = tag - else: - if class_ is not None: - if class_ not in CLASS_NUM_TO_NAME_MAP: - raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) - self.class_ = CLASS_NAME_TO_NUM_MAP[class_] + if class_ is not None: + if class_ not in CLASS_NUM_TO_NAME_MAP: + raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) + self.class_ = CLASS_NAME_TO_NUM_MAP[class_] + + if tag is not None: + self.tag = tag - if tag is not None: - self.tag = tag + if default is not None: + self.set(default) - if default is not None: - self.set(default) + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + raise e def __str__(self): """ @@ -566,23 +572,30 @@ class Choice(Asn1Value): ValueError - when tag_type is "implicit" """ - if tag_type == 'implicit': - raise ValueError('The Choice type can not be implicitly tagged even if in an implicit module - due to its nature any tagging must be explicit') kwargs['tag_type'] = tag_type Asn1Value.__init__(self, **kwargs) - if name is not None: - if name not in self._name_map: - raise ValueError('The name specified, "%s", is not a valid alternative for %s' % (name, self.__class__.__name__)) + try: + if tag_type == 'implicit': + raise ValueError('The Choice type can not be implicitly tagged even if in an implicit module - due to its nature any tagging must be explicit') - self._choice = self._name_map[name] - info = self._alternatives[self._choice] - spec = info[1] - params = {} if len(info) < 3 else info[2] + if name is not None: + if name not in self._name_map: + raise ValueError('The name specified, "%s", is not a valid alternative for %s' % (name, self.__class__.__name__)) + + self._choice = self._name_map[name] + info = self._alternatives[self._choice] + spec = info[1] + params = {} if len(info) < 3 else info[2] - if not isinstance(value, spec): - value = spec(value, **params) - self._parsed = value + if not isinstance(value, spec): + value = spec(value, **params) + self._parsed = value + + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + raise e @property def name(self): @@ -736,11 +749,16 @@ class Primitive(Asn1Value): Asn1Value.__init__(self, **kwargs) - if value is not None: - self.set(value) + try: + if value is not None: + self.set(value) - elif default is not None: - self.set(default) + elif default is not None: + self.set(default) + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + raise e def set(self, value): """ @@ -1760,16 +1778,21 @@ class Sequence(Asn1Value): value = default if value is not None: - # Fields are iterated in definition order to allow things like - # OID-based specs. Otherwise sometimes the value would be processed - # before the OID field, resulting in invalid value object creation. - if self._fields: - keys = [info[0] for info in self._fields] - else: - keys = value.keys() - for key in keys: - if key in value: - self.__setitem__(key, value[key]) + try: + # Fields are iterated in definition order to allow things like + # OID-based specs. Otherwise sometimes the value would be processed + # before the OID field, resulting in invalid value object creation. + if self._fields: + keys = [info[0] for info in self._fields] + else: + keys = value.keys() + for key in keys: + if key in value: + self.__setitem__(key, value[key]) + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + raise e def _lazy_child(self, index): """ @@ -2317,12 +2340,17 @@ class SequenceOf(Asn1Value): Asn1Value.__init__(self, **kwargs) - if value is None and default is not None: - value = default + try: + if value is None and default is not None: + value = default - if value is not None: - for index, child in enumerate(value): - self.__setitem__(index, child) + if value is not None: + for index, child in enumerate(value): + self.__setitem__(index, child) + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + raise e def _lazy_child(self, index): """ -- cgit v1.2.3 From fa06af1a3a54bf830785da0cc4f83f0ab07d2fe0 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 30 Jul 2015 14:43:34 -0400 Subject: Fixed docstring typo --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d91b620..543ab82 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -655,7 +655,7 @@ class Choice(Asn1Value): The integer class_ from the encoded value header :param tag: - The interger tag from the encoded value header + The integer tag from the encoded value header :raises: ValueError - when value is not a valid alternative -- cgit v1.2.3 From 0933715273c84bae6f0d92035a12cc5726da0913 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 30 Jul 2015 14:44:29 -0400 Subject: core.OctetString() and core.OctetBitString() both now have a parsed constructor keyword --- asn1crypto/core.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 543ab82..f211257 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1192,6 +1192,34 @@ class OctetBitString(Primitive): _parsed = None + def __init__(self, value=None, default=None, parsed=None, **kwargs): + """ + Allows providing a parsed object that will be serialized to get the + byte string value + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified + + :param parsed: + If value is None and this is an Asn1Value object, this will be + set as the parsed value, and the value will be obtained by calling + .dump() on this object. + """ + + set_parsed = False + if value is None and parsed is not None and isinstance(parsed, Asn1Value): + value = parsed.dump() + set_parsed = True + + Primitive.__init__(self, value=value, default=default, **kwargs) + + if set_parsed: + self._parsed = (parsed, parsed.__class__, None) + self._native = parsed.native + def set(self, value): """ Sets the value of the object @@ -1364,6 +1392,34 @@ class OctetString(Primitive): _parsed = None + def __init__(self, value=None, default=None, parsed=None, **kwargs): + """ + Allows providing a parsed object that will be serialized to get the + byte string value + + :param value: + A native Python datatype to initialize the object value with + + :param default: + The default value if no value is specified + + :param parsed: + If value is None and this is an Asn1Value object, this will be + set as the parsed value, and the value will be obtained by calling + .dump() on this object. + """ + + set_parsed = False + if value is None and parsed is not None and isinstance(parsed, Asn1Value): + value = parsed.dump() + set_parsed = True + + Primitive.__init__(self, value=value, default=default, **kwargs) + + if set_parsed: + self._parsed = (parsed, parsed.__class__, None) + self._native = parsed.native + def parse(self, spec=None, spec_params=None): """ Parses the contents generically, or using a spec with optional params -- cgit v1.2.3 From e5a1c6e6c25def5c6173d19028f14ab26e47a056 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:42:28 -0400 Subject: Add ParsableOctetString and ParsableOctetBitString These classes explicitly allow sub-parsing of octet values for use with structures that require such functionality. Normal OctetString and OctetBitString classes no longer have a .parse() method or a .parsed attribute. --- asn1crypto/cms.py | 3 +- asn1crypto/core.py | 200 +++++++++++++++++++++++---------------------------- asn1crypto/crl.py | 6 +- asn1crypto/keys.py | 10 +-- asn1crypto/ocsp.py | 11 +-- asn1crypto/pkcs12.py | 3 +- asn1crypto/x509.py | 3 +- tests/test_x509.py | 4 +- 8 files changed, 112 insertions(+), 128 deletions(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 2d6be49..f243504 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -40,6 +40,7 @@ from .core import ( ObjectIdentifier, OctetBitString, OctetString, + ParsableOctetString, Sequence, SequenceOf, SetOf, @@ -290,7 +291,7 @@ class ContentInfo(Sequence): class EncapsulatedContentInfo(Sequence): _fields = [ ('content_type', ContentType), - ('content', OctetString, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('content', ParsableOctetString, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), ] _oid_pair = ('content_type', 'content') diff --git a/asn1crypto/core.py b/asn1crypto/core.py index f211257..aff7bac 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1190,36 +1190,6 @@ class OctetBitString(Primitive): tag = 3 - _parsed = None - - def __init__(self, value=None, default=None, parsed=None, **kwargs): - """ - Allows providing a parsed object that will be serialized to get the - byte string value - - :param value: - A native Python datatype to initialize the object value with - - :param default: - The default value if no value is specified - - :param parsed: - If value is None and this is an Asn1Value object, this will be - set as the parsed value, and the value will be obtained by calling - .dump() on this object. - """ - - set_parsed = False - if value is None and parsed is not None and isinstance(parsed, Asn1Value): - value = parsed.dump() - set_parsed = True - - Primitive.__init__(self, value=value, default=default, **kwargs) - - if set_parsed: - self._parsed = (parsed, parsed.__class__, None) - self._native = parsed.native - def set(self, value): """ Sets the value of the object @@ -1241,29 +1211,6 @@ class OctetBitString(Primitive): if self.trailer != b'': self.trailer = b'' - def parse(self, spec=None, spec_params=None): - """ - Parses the contents generically, or using a spec with optional params - - :param spec: - A class derived from Asn1Value that defines what class_ and tag the - value should have, and the semantics of the encoded value. The - return value will be of this type. If omitted, the encoded value - will be decoded using the standard universal tag based on the - encoded tag number. - - :param spec_params: - A dict of params to pass to the spec object - - :return: - An object of the type spec, or if not present, a child of Asn1Value - """ - - if self._parsed is None or self._parsed[1:3] != (spec, spec_params): - parsed_value, _ = _parse_build(self.__bytes__(), spec=spec, spec_params=spec_params) - self._parsed = (parsed_value, spec, spec_params) - return self._parsed[0] - def __bytes__(self): """ :return: @@ -1289,55 +1236,99 @@ class OctetBitString(Primitive): return None if self._native is None: - if self._parsed is not None: - self._native = self._parsed[0].native - else: - self._native = self.__bytes__() + self._native = self.__bytes__() return self._native + +class IntegerBitString(Primitive): + """ + Represents a bit string in ASN.1 as a Python integer + """ + + tag = 3 + + def set(self, value): + """ + Sets the value of the object + + :param value: + An integer + + :raises: + ValueError - when an invalid value is passed + """ + + if not isinstance(value, int_types): + raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) + + self._native = value + # Set the unused bits to 0 + self.contents = b'\x00' + int_to_bytes(value, signed=True) + self.header = None + if self.trailer != b'': + self.trailer = b'' + @property - def parsed(self): + def native(self): """ - Returns the parsed object from .parse() + The a native Python datatype representation of this value :return: - The object returned by .parse() + An integer or None """ - if self._parsed is None: - self.parse() + if self.contents is None: + return None - return self._parsed[0] + if self._native is None: + extra_bits = int_from_bytes(self.contents[0:1]) + if extra_bits > 0: + bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) + bit_string = bit_string[0:0-extra_bits] + self._native = int(bit_string, 2) + else: + self._native = int_from_bytes(self.contents[1:]) + return self._native - def dump(self, force=False): + +class OctetString(Primitive): + """ + Represents a byte string in both ASN.1 and Python + """ + + tag = 4 + + def __bytes__(self): + """ + :return: + A byte string """ - Encodes the value using DER - :param force: - If the encoded contents already exist, clear them and regenerate - to ensure they are in DER format instead of BER format + return self.contents + + @property + def native(self): + """ + The a native Python datatype representation of this value :return: - A byte string of the DER-encoded value + A byte string or None """ - if force: - if self._parsed is not None: - native = self.parsed.dump(force=force) - else: - native = self.native - self.contents = None - self.set(native) + if self.contents is None: + return None - return Asn1Value.dump(self) + if self._native is None: + self._native = self.__bytes__() + return self._native -class IntegerBitString(Primitive): +class IntegerOctetString(Primitive): """ - Represents a bit string in ASN.1 as a Python integer + Represents a byte string in ASN.1 as a Python integer """ - tag = 3 + tag = 4 def set(self, value): """ @@ -1355,7 +1346,7 @@ class IntegerBitString(Primitive): self._native = value # Set the unused bits to 0 - self.contents = b'\x00' + int_to_bytes(value, signed=True) + self.contents = int_to_bytes(value, signed=True) self.header = None if self.trailer != b'': self.trailer = b'' @@ -1373,20 +1364,11 @@ class IntegerBitString(Primitive): return None if self._native is None: - extra_bits = int_from_bytes(self.contents[0:1]) - if extra_bits > 0: - bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) - bit_string = bit_string[0:0-extra_bits] - self._native = int(bit_string, 2) - else: - self._native = int_from_bytes(self.contents[1:]) + self._native = int_from_bytes(self.contents) return self._native -class OctetString(Primitive): - """ - Represents a byte string in both ASN.1 and Python - """ +class ParsableOctetString(Primitive): tag = 4 @@ -1448,6 +1430,7 @@ class OctetString(Primitive): :return: A byte string """ + return self.contents @property @@ -1506,47 +1489,42 @@ class OctetString(Primitive): return Asn1Value.dump(self) -class IntegerOctetString(OctetString): - """ - Represents a byte string in ASN.1 as a Python integer - """ +class ParsableOctetBitString(ParsableOctetString): + + tag = 3 def set(self, value): """ Sets the value of the object :param value: - An integer + A byte string :raises: ValueError - when an invalid value is passed """ - if not isinstance(value, int_types): - raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) + if not isinstance(value, byte_cls): + raise ValueError('%s value must be a byte string, not %s' % (self.__class__.__name__, value.__class__.__name__)) self._native = value # Set the unused bits to 0 - self.contents = int_to_bytes(value, signed=True) + self.contents = b'\x00' + value self.header = None if self.trailer != b'': self.trailer = b'' - @property - def native(self): + def __bytes__(self): """ - The a native Python datatype representation of this value - :return: - An integer or None + A byte string """ - if self.contents is None: - return None - - if self._native is None: - self._native = int_from_bytes(self.contents) - return self._native + # 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] + return self.contents[1:] class Null(Primitive): diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 9d5e420..c61bc57 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -19,7 +19,7 @@ from .core import ( Integer, ObjectIdentifier, OctetBitString, - OctetString, + ParsableOctetString, Sequence, SequenceOf, ) @@ -74,7 +74,7 @@ class TBSCertListExtension(Sequence): _fields = [ ('extn_id', TBSCertListExtensionId), ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') @@ -144,7 +144,7 @@ class CRLEntryExtension(Sequence): _fields = [ ('extn_id', CRLEntryExtensionId), ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 2ec6a52..c63561e 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -30,6 +30,8 @@ from .core import ( ObjectIdentifier, OctetBitString, OctetString, + ParsableOctetString, + ParsableOctetBitString, Sequence, SequenceOf, SetOf, @@ -494,7 +496,7 @@ class PrivateKeyInfo(Sequence): _fields = [ ('version', Integer), ('private_key_algorithm', PrivateKeyAlgorithm), - ('private_key', OctetString), + ('private_key', ParsableOctetString), ('attributes', Attributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), ] @@ -562,7 +564,7 @@ class PrivateKeyInfo(Sequence): container._algorithm = algorithm #pylint: disable=W0212 container['version'] = Integer(0) container['private_key_algorithm'] = private_key_algo - container['private_key'] = OctetString(private_key.untag().dump()) + container['private_key'] = private_key # Here we save the DSA public key if possible since it is not contained # within the PKCS#8 structure for a DSA key @@ -880,7 +882,7 @@ class PublicKeyInfo(Sequence): _fields = [ ('algorithm', PublicKeyAlgorithm), - ('public_key', OctetBitString), + ('public_key', ParsableOctetBitString), ] def _public_key_spec(self): @@ -932,7 +934,7 @@ class PublicKeyInfo(Sequence): container['algorithm'] = algo if isinstance(public_key, Asn1Value): public_key = public_key.untag().dump() - container['public_key'] = OctetBitString(public_key) + container['public_key'] = ParsableOctetBitString(public_key) return container diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 98e8ad4..8ce5735 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -24,6 +24,7 @@ from .core import ( ObjectIdentifier, OctetBitString, OctetString, + ParsableOctetString, Sequence, SequenceOf, ) @@ -68,7 +69,7 @@ class RequestExtension(Sequence): _fields = [ ('extn_id', RequestExtensionId), ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') @@ -175,7 +176,7 @@ class TBSRequestExtension(Sequence): _fields = [ ('extn_id', TBSRequestExtensionId), ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') @@ -358,7 +359,7 @@ class SingleResponseExtension(Sequence): _fields = [ ('extn_id', SingleResponseExtensionId), ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') @@ -510,7 +511,7 @@ class ResponseDataExtension(Sequence): _fields = [ ('extn_id', ResponseDataExtensionId), ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') @@ -546,7 +547,7 @@ class BasicOCSPResponse(Sequence): class ResponseBytes(Sequence): _fields = [ ('response_type', ResponseType), - ('response', OctetString), + ('response', ParsableOctetString), ] _oid_pair = ('response_type', 'response') diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 3256403..d4999d7 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -21,6 +21,7 @@ from .core import ( Integer, ObjectIdentifier, OctetString, + ParsableOctetString, Sequence, SequenceOf, SetOf, @@ -127,7 +128,7 @@ class CertId(ObjectIdentifier): class CertBag(Sequence): _fields = [ ('cert_id', CertId), - ('cert_value', OctetString, {'tag_type': 'explicit', 'tag': 0}), + ('cert_value', ParsableOctetString, {'tag_type': 'explicit', 'tag': 0}), ] _oid_pair = ('cert_id', 'cert_value') diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index e8c5700..0445925 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -38,6 +38,7 @@ from .core import ( ObjectIdentifier, OctetBitString, OctetString, + ParsableOctetString, PrintableString, Sequence, SequenceOf, @@ -1286,7 +1287,7 @@ class Extension(Sequence): _fields = [ ('extn_id', ExtensionId), ('critical', Boolean, {'default': False}), - ('extn_value', OctetString), + ('extn_value', ParsableOctetString), ] _oid_pair = ('extn_id', 'extn_value') diff --git a/tests/test_x509.py b/tests/test_x509.py index 33b88ff..a1e154b 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -1592,7 +1592,7 @@ class X509Tests(unittest.TestCase): public_key_params = subject_public_key_info['algorithm']['parameters'].chosen field_id = public_key_params['field_id'] curve = public_key_params['curve'] - subject_public_key = subject_public_key_info['public_key'].parsed + subject_public_key = subject_public_key_info['public_key'] extensions = tbs_certificate['extensions'] self.assertEqual( @@ -1688,7 +1688,7 @@ class X509Tests(unittest.TestCase): public_key_params['hash'].native ) self.assertEqual( - b'G\x9f\xcbs$\x1d\xc9\xdd\xd1-\xf1:\x9f\xb7\x04\xde \xd0X\x00\x93T\xf6\x89\xc7/\x87+\xf7\xf9=;4\xed\x9e{\x0e=WB\xdfx\x03\x0b\xcc1\xc6\x03\xd7\x9f`\x01', + b'\x04\x8b]Lq\xf7\xd6\xc6\xa3IcB\\G\x9f\xcbs$\x1d\xc9\xdd\xd1-\xf1:\x9f\xb7\x04\xde \xd0X\x00\x93T\xf6\x89\xc7/\x87+\xf7\xf9=;4\xed\x9e{\x0e=WB\xdfx\x03\x0b\xcc1\xc6\x03\xd7\x9f`\x01', subject_public_key.native ) self.assertEqual( -- cgit v1.2.3 From d3a152cf514c7fffb2f53e1e364722d63363ba8e Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:44:55 -0400 Subject: Improve debugging of Any values --- asn1crypto/core.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index aff7bac..f4f5c6f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -429,6 +429,29 @@ class Any(Asn1Value): # The parsed value object _parsed = None + def __init__(self, value=None, **kwargs): + """ + Sets the value of the object before passing to Asn1Value.__init__() + + :param value: + An Asn1Value object that will be set as the parsed value + """ + + Asn1Value.__init__(self, **kwargs) + + try: + if value is not None: + if not isinstance(value, Asn1Value): + raise ValueError('value must be an instance of Ans1Value, not %s' % value.__class__.__name__) + + self._parsed = (value, value.__class__, None) + self.contents = value.dump() + + except (ValueError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + raise e + @property def native(self): """ @@ -1906,8 +1929,15 @@ class Sequence(Asn1Value): new_value = self._make_value(field_name, field_spec, value_spec, field_params, value) - is_choice = isinstance(new_value, Choice) - if (is_choice and new_value.chosen.contents is None) or (not is_choice and new_value.contents is None): + 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 + + if invalid_value: raise ValueError('Value for field "%s" of %s is not set' % (field_name, self.__class__.__name__)) self.children[key] = new_value -- cgit v1.2.3 From 1c246a6382b1849589b5dde9cfcce7d4b47eb24e Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:46:12 -0400 Subject: Allow _spec_callback to return a (Asn1Value, None) tuple to override the defined field spec --- asn1crypto/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index f4f5c6f..fd4cf31 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2070,6 +2070,9 @@ class Sequence(Asn1Value): # the override, for situations such as OctetString and parse_as if isinstance(spec_override, tuple) and len(spec_override) == 2: field_spec, value_spec = spec_override #pylint: disable=W0633 + if value_spec is None: + value_spec = field_spec + spec_override = None else: value_spec = spec_override -- cgit v1.2.3 From fd066bd55746be3e241c9bbc5a35a3a4fd810d34 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:47:11 -0400 Subject: Ensure values assigned to Sequence fields and added to SequenceOf/SetOf have proper tagging --- asn1crypto/core.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index fd4cf31..9ddb031 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2143,10 +2143,18 @@ class Sequence(Asn1Value): wrapper._parsed = (new_value, new_value.__class__, None) #pylint: disable=W0212 new_value = wrapper + # If the new value has an incorrect tagging, it needs to be fixed. It + # could either be that a special tagging is required that is not + # present: if field_params and 'tag_type' in field_params and 'tag' in field_params: if field_params['tag_type'] != new_value.tag_type or field_params['tag'] != new_value.tag: new_value = new_value.retag(tag_type=field_params['tag_type'], tag=field_params['tag']) + # Or the issue could be that special tagging is present, but the field + # is a plain field + elif new_value.tag_type is not None: + new_value = new_value.untag() + return new_value def _parse_children(self, recurse=False): @@ -2443,11 +2451,11 @@ class SequenceOf(Asn1Value): """ if isinstance(value, self._child_spec): - return value + new_value = value elif issubclass(self._child_spec, Any): if isinstance(value, Asn1Value): - return value + new_value = value else: raise ValueError('Can not set a native python value to %s where the _child_spec is Any – value must be an instance of Asn1Value' % self.__class__.__name__) @@ -2459,11 +2467,19 @@ class SequenceOf(Asn1Value): wrapper.validate(value.class_, value.tag) wrapper._parsed = value #pylint: disable=W0212 value = wrapper - return value + new_value = value else: return self._child_spec(value=value) + if self._child_spec.tag_type is not None: + if new_value.tag_type != self._child_spec.tag_type: + new_value = new_value.retag(self._child_spec.tag_type, self._child_spec.tag) + elif new_value.tag_type is not None: + new_value = new_value.untag() + + return new_value + def __len__(self): """ :return: -- cgit v1.2.3 From c647b6122343799670981583f0d4ba3b49c71bbe Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:47:57 -0400 Subject: Add some length sanity checking to parse functionality --- asn1crypto/core.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 9ddb031..4717983 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3406,12 +3406,18 @@ def _parse(encoded_data, pointer=0): if len(encoded_data) == 0: return ((None, None, None, None, None, None), 0) + encoded_length = len(encoded_data) + def _slice(start, end): + if end > encoded_length: + raise ValueError('Insufficient data - %s bytes requested but only %s available' % (end, encoded_length)) + return encoded_data[start:end] + start = pointer class_, method, tag, num_bytes = _parse_id(encoded_data, pointer) pointer += num_bytes - length_octet = ord(encoded_data[pointer:pointer+1]) + length_octet = ord(_slice(pointer, pointer+1)) pointer += 1 length_type = length_octet >> 7 if length_type == 1: @@ -3419,22 +3425,22 @@ def _parse(encoded_data, pointer=0): remaining_length_octets = length_octet & 127 while remaining_length_octets > 0: length *= 256 - length += ord(encoded_data[pointer:pointer+1]) + length += ord(_slice(pointer, pointer+1)) pointer += 1 remaining_length_octets -= 1 else: length = length_octet & 127 - header = encoded_data[start:pointer] + header = _slice(start, pointer) # Indefinite length if length_type == 1 and length == 0: end_token = encoded_data.find(b'\x00\x00', pointer) - contents = encoded_data[pointer:end_token] + contents = _slice(pointer, end_token) pointer = end_token + 2 trailer = b'\x00\x00' else: - contents = encoded_data[pointer:pointer+length] + contents = _slice(pointer, pointer+length) pointer += length trailer = b'' -- cgit v1.2.3 From 33a412af4fbf67b4e4c3db32801b25fe61925a18 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:48:22 -0400 Subject: Clean up some linter warnings --- asn1crypto/x509.py | 2 +- tests/test_cms.py | 2 +- tests/test_ocsp.py | 2 +- tests/test_tsp.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 0445925..59c3f9b 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -870,7 +870,7 @@ class EDIPartyName(Sequence): class IPAddress(OctetString): - def parse(self, spec=None, spec_params=None): + def parse(self, spec=None, spec_params=None): #pylint: disable=W0613 """ This method is not applicable to IP addresses """ diff --git a/tests/test_cms.py b/tests/test_cms.py index 8bfb15a..a3caf8e 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -6,7 +6,7 @@ import os from datetime import datetime from collections import OrderedDict -from asn1crypto import cms, core, util +from asn1crypto import cms, util tests_root = os.path.dirname(__file__) diff --git a/tests/test_ocsp.py b/tests/test_ocsp.py index 808a6d4..3ef6404 100644 --- a/tests/test_ocsp.py +++ b/tests/test_ocsp.py @@ -6,7 +6,7 @@ import sys import os from datetime import datetime -from asn1crypto import ocsp, core, util +from asn1crypto import ocsp, util if sys.version_info < (3,): byte_cls = str diff --git a/tests/test_tsp.py b/tests/test_tsp.py index 586880f..b5f542f 100644 --- a/tests/test_tsp.py +++ b/tests/test_tsp.py @@ -6,7 +6,7 @@ import os from datetime import datetime from collections import OrderedDict -from asn1crypto import tsp, cms, core, util +from asn1crypto import tsp, cms, util tests_root = os.path.dirname(__file__) -- cgit v1.2.3 From 4e58977951abe2fc75a9c06198ea45cd699aa10e Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:50:22 -0400 Subject: Add Asn1Value.debug() for an improved debugging experience --- asn1crypto/core.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 4717983..a1dc00f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -52,6 +52,7 @@ import re from collections import OrderedDict from datetime import datetime from pprint import pprint +import binascii from . import _teletex_codec from .util import int_to_bytes, int_from_bytes, timezone @@ -112,6 +113,29 @@ METHOD_NUM_TO_NAME_MAP = { _SETUP_CLASSES = {} +def _basic_debug(prefix, self): + print('%s%s Object #%s' % (prefix, self.__class__.__name__, id(self))) + if self.header: + print('%s Header: 0x%s' % (prefix, binascii.hexlify(self.header or b'').decode('utf-8'))) + + has_header = self.method is not None and self.class_ is not None and self.tag is not None + if has_header: + method_name = METHOD_NUM_TO_NAME_MAP.get(self.method) + class_name = CLASS_NUM_TO_NAME_MAP.get(self.class_) + + if self.tag_type == 'explicit': + print('%s %s tag %s (explicitly tagged)' % (prefix, CLASS_NUM_TO_NAME_MAP.get(self.explicit_class), self.explicit_tag)) + if has_header: + print('%s %s %s %s' % (prefix, method_name, class_name, self.tag)) + elif self.tag_type == 'implicit': + if has_header: + print('%s %s %s tag %s (implicitly tagged)' % (prefix, method_name, class_name, self.tag)) + elif has_header: + print('%s %s %s tag %s' % (prefix, method_name, class_name, self.tag)) + + print('%s Data: 0x%s' % (prefix, binascii.hexlify(self.contents or b'').decode('utf-8'))) + + class Asn1Value(): """ The basis of all ASN.1 values @@ -262,7 +286,8 @@ class Asn1Value(): :return: A unicode string """ - return '<%s %s>' % (self.__class__.__name__, repr(self.contents)) + + return '<%s %s %s>' % (self.__class__.__name__, id(self), repr(self.contents or b'')) def retag(self, tag_type, tag): """ @@ -311,6 +336,20 @@ class Asn1Value(): if hasattr(other, '_parsed'): self._parsed = other._parsed + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + prefix = ' ' * nest_level + _basic_debug(prefix, self) + if hasattr(self, 'parsed'): + self.parsed.debug(nest_level + 2) + elif hasattr(self, 'chosen'): + self.chosen.debug(nest_level + 2) + else: + print('%s Native: %s' % (prefix, self.native)) + def dump(self, force=False): """ Encodes the value using DER @@ -2360,6 +2399,19 @@ class Sequence(Asn1Value): self._native = other._native self.children = other.children + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + prefix = ' ' * nest_level + _basic_debug(prefix, self) + for field_name in self: + child = self._lazy_child(self._field_map[field_name]) + if not isinstance(child, NoValue): + print('%s Field "%s"' % (prefix, field_name)) + child.debug(nest_level + 3) + def dump(self, force=False): """ Encodes the value using DER @@ -2687,6 +2739,16 @@ class SequenceOf(Asn1Value): self._native = other._native self.children = other.children + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + prefix = ' ' * nest_level + _basic_debug(prefix, self) + for child in self: + child.debug(nest_level + 1) + def dump(self, force=False): """ Encodes the value using DER -- cgit v1.2.3 From 6b982c3cb434687d08e991c3bf3ebb8ef2d55332 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 07:51:18 -0400 Subject: Removed pprint() since it didn't really add any value, especially with .debug() now available --- asn1crypto/core.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index a1dc00f..b32de69 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -51,7 +51,6 @@ import sys import re from collections import OrderedDict from datetime import datetime -from pprint import pprint import binascii from . import _teletex_codec @@ -382,13 +381,6 @@ class Asn1Value(): return self.header + self.contents + self.trailer - def pprint(self): - """ - Pretty prints the native representation of the value - """ - - pprint(self.native) - class ValueMap(): """ -- cgit v1.2.3 From 65d8bf0bcb95707e88ff3b0a8e6d116597bcf184 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 08:34:33 -0400 Subject: Asn1Value.header renamed to Asn1Value._header and Asn1Value.trailer renamed to Asn1Value._trailer --- asn1crypto/core.py | 114 ++++++++++++++++++++++++++--------------------------- asn1crypto/x509.py | 6 +-- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index b32de69..b5bdb03 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -114,8 +114,8 @@ _SETUP_CLASSES = {} def _basic_debug(prefix, self): print('%s%s Object #%s' % (prefix, self.__class__.__name__, id(self))) - if self.header: - print('%s Header: 0x%s' % (prefix, binascii.hexlify(self.header or b'').decode('utf-8'))) + if self._header: #pylint: disable=W0212 + print('%s Header: 0x%s' % (prefix, binascii.hexlify(self._header or b'').decode('utf-8'))) #pylint: disable=W0212 has_header = self.method is not None and self.class_ is not None and self.tag is not None if has_header: @@ -158,13 +158,13 @@ class Asn1Value(): explicit_tag = None # The BER/DER header bytes - header = None + _header = None # Raw encoded value bytes not including class, method, tag, length header contents = None # The BER/DER trailer bytes - trailer = b'' + _trailer = b'' # The native python representation of the value _native = None @@ -361,7 +361,7 @@ class Asn1Value(): A byte string of the DER-encoded value """ - if self.header is None: + if self._header is None: header = _dump_header(self.class_, self.method, self.tag, self.contents) trailer = b'' @@ -373,13 +373,13 @@ class Asn1Value(): container.contents = header + self.contents + trailer # Force the container to generate the header and footer container.dump() - header = container.header + header - trailer += container.trailer + header = container._header + header + trailer += container._trailer - self.header = header - self.trailer = trailer + self._header = header + self._trailer = trailer - return self.header + self.contents + self.trailer + return self._header + self.contents + self._trailer class ValueMap(): @@ -536,7 +536,7 @@ class Any(Asn1Value): passed_params = {} if not spec_params else spec_params.copy() passed_params['tag_type'] = self.tag_type passed_params['tag'] = self.tag - parsed_value, _ = _parse_build(self.header + self.contents + self.trailer, spec=spec, spec_params=passed_params) + parsed_value, _ = _parse_build(self._header + self.contents + self._trailer, spec=spec, spec_params=passed_params) self._parsed = (parsed_value, spec, spec_params) except (ValueError) as e: args = e.args[1:] @@ -773,12 +773,12 @@ class Choice(Asn1Value): """ self.contents = self.chosen.dump(force=force) - if self.header is None: + if self._header is None: if self.tag_type == 'explicit': - self.header = _dump_header(self.explicit_class, 1, self.explicit_tag, self.contents) + self._header = _dump_header(self.explicit_class, 1, self.explicit_tag, self.contents) else: - self.header = b'' - return self.header + self.contents + self._header = b'' + return self._header + self.contents class Primitive(Asn1Value): @@ -827,9 +827,9 @@ class Primitive(Asn1Value): self._native = value self.contents = value - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def dump(self, force=False): """ @@ -874,9 +874,9 @@ class AbstractString(Primitive): self._native = value self.contents = value.encode(self._encoding) - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def __unicode__(self): """ @@ -919,9 +919,9 @@ class Boolean(Primitive): self._native = bool(value) self.contents = b'\x00' if not value else b'\xff' - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' # Python 2 def __nonzero__(self): @@ -988,9 +988,9 @@ class Integer(Primitive, ValueMap): self._native = self._map[value] if self._map and value in self._map else value self.contents = int_to_bytes(value, signed=True) - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def __int__(self): """ @@ -1108,9 +1108,9 @@ class BitString(Primitive, ValueMap, object): value_bytes = (b'\x00' * (size_in_bytes - len(value_bytes))) + value_bytes self.contents = extra_bits_byte + value_bytes - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def __getitem__(self, key): """ @@ -1261,9 +1261,9 @@ class OctetBitString(Primitive): self._native = value # Set the unused bits to 0 self.contents = b'\x00' + value - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def __bytes__(self): """ @@ -1318,9 +1318,9 @@ class IntegerBitString(Primitive): self._native = value # Set the unused bits to 0 self.contents = b'\x00' + int_to_bytes(value, signed=True) - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' @property def native(self): @@ -1401,9 +1401,9 @@ class IntegerOctetString(Primitive): self._native = value # Set the unused bits to 0 self.contents = int_to_bytes(value, signed=True) - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' @property def native(self): @@ -1564,9 +1564,9 @@ class ParsableOctetBitString(ParsableOctetString): self._native = value # Set the unused bits to 0 self.contents = b'\x00' + value - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def __bytes__(self): """ @@ -1660,9 +1660,9 @@ class ObjectIdentifier(Primitive, ValueMap): part = part >> 7 self.contents += encoded_part - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def __unicode__(self): """ @@ -2049,9 +2049,9 @@ class Sequence(Asn1Value): if default_value.dump() == child_dump: continue self.contents += child_dump - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' #pylint: disable=W0212 def _setup(self): @@ -2645,9 +2645,9 @@ class SequenceOf(Asn1Value): for child in self: child_dump = child.dump(force=force) self.contents += child_dump - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' def _parse_children(self, recurse=False): """ @@ -3404,10 +3404,10 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value = spec(class_=class_) - value.header = header + value._header = header #pylint: disable=W0212 value.contents = contents if trailer is not None and trailer != b'': - value.trailer = trailer + value._trailer = trailer #pylint: disable=W0212 # Destroy any default value that our contents have overwritten value._native = None #pylint: disable=W0212 @@ -3417,14 +3417,14 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param original_value = value (class_, method, tag, header, contents, trailer), _ = _parse(value.contents) value = _build(class_, method, tag, header, contents, trailer, spec=spec) - value.header = original_value.header + header - value.trailer += original_value.trailer + value._header = original_value._header + header #pylint: disable=W0212 + value._trailer += original_value._trailer #pylint: disable=W0212 value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class value.explicit_tag = original_value.explicit_tag elif isinstance(value, Choice): - value.contents = value.header + value.contents - value.header = b'' + value.contents = value._header + value.contents #pylint: disable=W0212 + value._header = b'' #pylint: disable=W0212 try: # Force parsing the Choice now diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 59c3f9b..e800c8d 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -920,9 +920,9 @@ class IPAddress(OctetString): self._native = original_value self.contents = inet_pton(family, value) + cidr_bytes - self.header = None - if self.trailer != b'': - self.trailer = b'' + self._header = None + if self._trailer != b'': + self._trailer = b'' @property def native(self): -- cgit v1.2.3 From c297f3487d0f7fd05759bd4e9a2a0f1512b9d087 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 09:51:25 -0400 Subject: Added Asn1Value.copy() and tests for .copy(), .untag() and .retag() --- asn1crypto/core.py | 188 +++++++++++++++++++++++++++++++++++++++++++++++------ tests/test_core.py | 43 ++++++++++++ 2 files changed, 212 insertions(+), 19 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index b5bdb03..8e206a0 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -65,6 +65,7 @@ if sys.version_info <= (3,): chr_cls = chr range = xrange #pylint: disable=E0602,W0622 from datetime import timedelta + from cStringIO import StringIO as BytesIO #pylint: disable=F0401 # Python 3 else: @@ -72,6 +73,7 @@ else: byte_cls = bytes int_types = int py2 = False + from io import BytesIO def chr_cls(num): return bytes([num]) @@ -288,6 +290,22 @@ class Asn1Value(): return '<%s %s %s>' % (self.__class__.__name__, id(self), repr(self.contents or b'')) + def copy(self): + """ + Copies the object, preserving any special tagging from it + + :return: + An Asn1Value object + """ + + new_obj = self.__class__() + new_obj.tag_type = self.tag_type + new_obj.tag = self.tag + new_obj.explicit_class = self.explicit_class + new_obj.explicit_tag = self.explicit_tag + new_obj._copy(self) #pylint: disable=W0212 + return new_obj + def retag(self, tag_type, tag): """ Copies the object, applying a new tagging to it @@ -361,6 +379,8 @@ class Asn1Value(): A byte string of the DER-encoded value """ + contents = self.contents + if self._header is None: header = _dump_header(self.class_, self.method, self.tag, self.contents) trailer = b'' @@ -379,7 +399,7 @@ class Asn1Value(): self._header = header self._trailer = trailer - return self._header + self.contents + self._trailer + return self._header + contents + self._trailer class ValueMap(): @@ -1520,6 +1540,22 @@ class ParsableOctetString(Primitive): return self._parsed[0] + #pylint: disable=W0212 + def _copy(self, other): + """ + Copies the contents of another ParsableOctetString object to itself + + :param object: + Another instance of the same class + """ + + if self.__class__ != other.__class__: + raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + + self.contents = other.contents + self._native = other._native + self._parsed = other._parsed + def dump(self, force=False): """ Encodes the value using DER @@ -1815,6 +1851,13 @@ class Sequence(Asn1Value): # A list of child objects, in order of _fields children = None + # Sequence overrides .contents to be a property so that the mutated state + # of child objects can be checked to ensure everything is up-to-date + _contents = None + + # Variable to track if the object has been mutated + _mutated = False + # A list of tuples in one of the following forms. # # Option 1, a unicode string field name and a value class @@ -1882,6 +1925,45 @@ class Sequence(Asn1Value): e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args raise e + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the sequence + """ + + if self.children is None: + return self._contents + + if self._is_mutated(): + self._set_contents() + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the sequence + """ + + self._contents = value + + def _is_mutated(self): + """ + :return: + A boolean - if the sequence or any children (recursively) have been + mutated + """ + + mutated = self._mutated + if self.children is not None: + for child in self.children: + if isinstance(child, Sequence) or isinstance(child, SequenceOf): + mutated = mutated or child._is_mutated() #pylint: disable=W0212 + + return mutated + def _lazy_child(self, index): """ Builds a child object if the child has only been parsed into a tuple so far @@ -1975,7 +2057,7 @@ class Sequence(Asn1Value): if self._native is not None: self._native[self._fields[key][0]] = self.children[key].native - self._set_contents() + self._mutated = True def __delitem__(self, key): """ @@ -2007,7 +2089,7 @@ class Sequence(Asn1Value): self._native[info[0]] = None else: self.__setitem__(key, None) - self._set_contents() + self._mutated = True def __iter__(self): #pylint: disable=W0234 """ @@ -2031,7 +2113,7 @@ class Sequence(Asn1Value): if self.children is None: self._parse_children() - self.contents = b'' + contents = BytesIO() for index, info in enumerate(self._fields): child = self.children[index] if child is None: @@ -2048,7 +2130,9 @@ class Sequence(Asn1Value): default_value = info[1](**info[2]) if default_value.dump() == child_dump: continue - self.contents += child_dump + contents.write(child_dump) + self._contents = contents.getvalue() + self._header = None if self._trailer != b'': self._trailer = b'' @@ -2201,7 +2285,7 @@ class Sequence(Asn1Value): ValueError - when an error occurs parsing child objects """ - if self.contents is None: + if self._contents is None: if self._fields: self.children = [NoValue()] * len(self._fields) for index, info in enumerate(self._fields): @@ -2212,12 +2296,12 @@ class Sequence(Asn1Value): try: self.children = [] - contents_length = len(self.contents) + contents_length = len(self._contents) child_pointer = 0 field = 0 seen_field = 1 while child_pointer < contents_length: - parts, num_bytes = _parse(self.contents, pointer=child_pointer) + parts, num_bytes = _parse(self._contents, pointer=child_pointer) if field < len(self._fields): _, field_spec, value_spec, field_params, spec_override = self._determine_spec(field) @@ -2389,13 +2473,22 @@ class Sequence(Asn1Value): self.contents = other.contents self._native = other._native - self.children = other.children + if self.children is not None: + self.children = [] + for child in other.children: + if isinstance(child, tuple): + self.children.append(child) + else: + self.children.append(child.copy()) def debug(self, nest_level=1): """ Show the binary data and parsed data in a tree structure """ + if self.children is None: + self._parse_children() + prefix = ' ' * nest_level _basic_debug(prefix, self) for field_name in self: @@ -2436,6 +2529,13 @@ class SequenceOf(Asn1Value): # A list of child objects children = None + # SequenceOf overrides .contents to be a property so that the mutated state + # of child objects can be checked to ensure everything is up-to-date + _contents = None + + # Variable to track if the object has been mutated + _mutated = False + # An Asn1Value class to use when parsing children _child_spec = None @@ -2471,6 +2571,45 @@ class SequenceOf(Asn1Value): e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args raise e + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the sequence + """ + + if self.children is None: + return self._contents + + if self._is_mutated(): + self._set_contents() + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the sequence + """ + + self._contents = value + + def _is_mutated(self): + """ + :return: + A boolean - if the sequence or any children (recursively) have been + mutated + """ + + mutated = self._mutated + if self.children is not None: + for child in self.children: + if isinstance(child, Sequence) or isinstance(child, SequenceOf): + mutated = mutated or child._is_mutated() #pylint: disable=W0212 + + return mutated + def _lazy_child(self, index): """ Builds a child object if the child has only been parsed into a tuple so far @@ -2578,7 +2717,7 @@ class SequenceOf(Asn1Value): if self._native is not None: self._native[key] = self.children[key].native - self._set_contents() + self._mutated = True def __delitem__(self, key): """ @@ -2595,7 +2734,8 @@ class SequenceOf(Asn1Value): self.children.pop(key) if self._native is not None: self._native.pop(key) - self._set_contents() + + self._mutated = True def __iter__(self): #pylint: disable=W0234 """ @@ -2627,7 +2767,8 @@ class SequenceOf(Asn1Value): if self._native is not None: self._native.append(self.children[-1].native) - self._set_contents() + + self._mutated = True def _set_contents(self, force=False): """ @@ -2641,10 +2782,10 @@ class SequenceOf(Asn1Value): if self.children is None: self._parse_children() - self.contents = b'' + contents = BytesIO() for child in self: - child_dump = child.dump(force=force) - self.contents += child_dump + contents.write(child.dump(force=force)) + self._contents = contents.getvalue() self._header = None if self._trailer != b'': self._trailer = b'' @@ -2664,12 +2805,12 @@ class SequenceOf(Asn1Value): try: self.children = [] - if self.contents is None: + if self._contents is None: return - contents_length = len(self.contents) + contents_length = len(self._contents) child_pointer = 0 while child_pointer < contents_length: - parts, num_bytes = _parse(self.contents, pointer=child_pointer) + parts, num_bytes = _parse(self._contents, pointer=child_pointer) if self._child_spec: child = parts + (self._child_spec,) else: @@ -2729,13 +2870,22 @@ class SequenceOf(Asn1Value): self.contents = other.contents self._native = other._native - self.children = other.children + if self.children is not None: + self.children = [] + for child in other.children: + if isinstance(child, tuple): + self.children.append(child) + else: + self.children.append(child.copy()) def debug(self, nest_level=1): """ Show the binary data and parsed data in a tree structure """ + if self.children is None: + self._parse_children() + prefix = ' ' * nest_level _basic_debug(prefix, self) for child in self: diff --git a/tests/test_core.py b/tests/test_core.py index c381eae..b6195c3 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -46,6 +46,13 @@ class Seq(core.Sequence): } +class CopySeq(core.Sequence): + _fields = [ + ('name', core.UTF8String), + ('pair', Seq), + ] + + @DataDecorator class CoreTests(unittest.TestCase): @@ -203,3 +210,39 @@ class CoreTests(unittest.TestCase): seq = SequenceAny() with self.assertRaises(ValueError): seq.append(5) + + def test_copy(self): + a = core.Integer(200) + b = a.copy() + self.assertNotEqual(id(a), id(b)) + self.assertEqual(a.contents, b.contents) + self.assertEqual(a.dump(), b.dump()) + + def test_copy_mutable(self): + a = CopySeq({'name': 'foo', 'pair': {'id': '1.2.3', 'value': 5}}) + b = a.copy() + self.assertNotEqual(id(a), id(b)) + self.assertNotEqual(id(a['pair']), id(b['pair'])) + self.assertEqual(a.contents, b.contents) + self.assertEqual(a.dump(), b.dump()) + + self.assertEqual(a['pair']['value'].native, b['pair']['value'].native) + a['pair']['value'] = 6 + self.assertNotEqual(a['pair']['value'].native, b['pair']['value'].native) + + self.assertNotEqual(a.contents, b.contents) + self.assertNotEqual(a.dump(), b.dump()) + + def test_retag(self): + a = core.Integer(200) + b = a.retag('explicit', 0) + self.assertNotEqual(id(a), id(b)) + self.assertEqual(a.contents, b.contents) + self.assertNotEqual(a.dump(), b.dump()) + + def test_untag(self): + a = core.Integer(200, tag_type='explicit', tag=0) + b = a.untag() + self.assertNotEqual(id(a), id(b)) + self.assertEqual(a.contents, b.contents) + self.assertNotEqual(a.dump(), b.dump()) -- cgit v1.2.3 From 14b46c491295f8d502ea1edeba2964c36c17be09 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 10:18:40 -0400 Subject: Added table of contents to Universal Types docs page --- docs/universal_types.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/universal_types.md b/docs/universal_types.md index d048cb1..ef5690e 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -8,6 +8,26 @@ universal type classes. For a general overview of ASN.1 as used in cryptography, please see [A Layman's Guide to a Subset of ASN.1, BER, and DER](http://luca.ntop.org/Teaching/Appunti/asn1.html). +This page contains the following sections: + + - [Universal Types](#universal-types) + - [Basic Usage](#basic-usage) + - [Sequence](#sequence) + - [Set](#set) + - [SequenceOf](#sequenceof) + - [SetOf](#setof) + - [Integer](#integer) + - [Enumerated](#enumerated) + - [ObjectIdentifier](#objectidentifier) + - [BitString](#bitstring) + - [Strings](#strings) + - [UTCTime](#utctime) + - [GeneralizedTime](#generalizedtime) + - [Choice](#choice) + - [Any](#any) + - [Specification via OID](#specification-via-oid) + - [Explicit and Implicit Tagging](#explicit-and-implicit-tagging) + ## Universal Types For general purpose ASN.1 parsing, the `asn1crypto.core` module is used. It -- cgit v1.2.3 From 013dbeedc1a637d526ce099a6014ea30afe021d1 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 10:19:25 -0400 Subject: Update Universal Types docs to mention ParsableOctetString and ParsableOctetBitString --- docs/universal_types.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/universal_types.md b/docs/universal_types.md index ef5690e..e04d45e 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -86,6 +86,12 @@ include: | `IntegerBitString` | `int` | may be `long` on Python 2 | | `IntegerOctetString` | `int` | may be `long` on Python 2 | +For situations where the DER encoded bytes from one type is embedded in another, +the `ParsableOctetString` and `ParsableOctetBitString` classes exist. These +function the same as `OctetString` and `OctetBitString`, however they also +have an attribute `.parsed` and a method `.parse()` that allows for +parsing the content as ASN.1 structures. + ## Basic Usage All of the universal types implement two methods, the class method `.load()` for @@ -474,14 +480,17 @@ class MySequence(Sequence): Throughout the usage of ASN.1 in cryptography, a pattern is present where an `ObjectIdenfitier` is used to determine what specification should be used to interpret another field in a `Sequence`. Usually the other field is an instance -of `Any`, however ocassionally it is an `OctetString`. +of `Any`, however ocassionally it is an `OctetString` or `OctetBitString`. *asn1crypto* provides the `_oid_pair` and `_oid_specs` properties of the `Sequence` class to allow handling these situations. The `_oid_pair` is a tuple with two unicode string elements. The first is the name of the field that is an `ObjectIdentifier` and the second if the name of -the field that has a variable specification based on the first field. +the field that has a variable specification based on the first field. *In +situations where the value field should be an `OctetString` or `OctetBitString`, +`ParsableOctetString` and `ParsableOctetBitString` will need to be used instead +to allow for the sub-parsing of the contents.* The `_oid_specs` property is a `dict` object with `ObjectIdentifier` values as the keys (either dotted or mapped notation) and a type class as the value. When @@ -511,10 +520,6 @@ class MySequence(Sequence): } ``` -In some situations, the second field in `_oid_pair` is not an instance of `Any`, -but instead is an instance of `OctetString`. This is dictated by the ASN.1 -specification of the data structures being worked with. - ## Explicit and Implicit Tagging When working with `Sequence`, `Set` and `Choice` it is often necessary to -- cgit v1.2.3 From 532321232cc8146acee6e9a2acd0962870dda2d1 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 10:20:00 -0400 Subject: Added docs on .debug() and .copy() to Universal Types docs --- docs/universal_types.md | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/universal_types.md b/docs/universal_types.md index e04d45e..fa03ff8 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -94,8 +94,12 @@ parsing the content as ASN.1 structures. ## Basic Usage -All of the universal types implement two methods, the class method `.load()` for -parsing and the instance method `.dump()` for serialization. +All of the universal types implement four methods, a class method `.load()` and +the instance methods `.dump()`, `.copy()` and `.debug()`. + +`.load()` accepts a byte string of DER or BER encoded data and returns an +object of the class it was called on. `.dump()` returns the serialization of +an object into DER encoding. ```python from asn1crypto.core import Sequence @@ -104,7 +108,44 @@ parsed = Sequence.load(der_byte_string) serialized = parsed.dump() ``` -In addition to the two primary methods, every instance has a `.native` property +By default, *asn1crypto* tries to be efficient and caches serialized data for +better performance. If the input data is possibly BER encoded, but the output +must be DER encoded, the `force` parameter may be used with `.dump()`. + +```python +from asn1crypto.core import Sequence + +parsed = Sequence.load(der_byte_string) +der_serialized = parsed.dump(force=True) +``` + +The `.copy()` method creates a deep copy of an object, allowing child fields to +be modified without affecting the original. + +```python +from asn1crypto.core import Sequence + +seq1 = Sequence.load(der_byte_string) +seq2 = seq1.copy() +seq2[0] = seq1[0] + 1 +if seq1[0] != seq2[0]: + print('Copies have distinct contents') +``` + +The `.debug()` method is available to help in situations where interaction with +another ASN.1 serializer or parsing is not functioning as expected. Calling +this method will print a tree structure with information about the header bytes, +class, method, tag, special tagging, content bytes, native Python value, child +fields and any sub-parsed values. + +```python +from asn1crypto.core import Sequence + +parsed = Sequence.load(der_byte_string) +parsed.debug() +``` + +In addition to the available methods, every instance has a `.native` property that converts the data into a native Python data type. ```python -- cgit v1.2.3 From ab88492436c9803c6dbdb0a8a74462eb161bdfb7 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 10:20:25 -0400 Subject: Add docs on .untag() and .retag() to Universal Types documentation --- docs/universal_types.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/universal_types.md b/docs/universal_types.md index fa03ff8..b03c4a0 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -610,3 +610,24 @@ As is shown above, the keys `tag_type` and `tag` are used for tagging, and are passed to a type class constructor via the optional third element of a field or alternative tuple. The `tag_type` may be the unicode strings `'implicit'` or `'explicit'` and the `tag` may be any integer. + +If a tagging value needs its tagging changed, the `.untag()` method can be used +to create a copy of the object without explicit/implicit tagging. The `.retag()` +method can be used to change the tagging. This method accepts two parameters: +a unicode string `tag_type` and an integer `tag`. + +```python +person = Person(name='email', value='will@wbond.net') + +# Will display "implicit" +print(person.tag_type) + +# Will display nothing +print(person.untag().tag_type) + +# Will display 0 +print(person.tag) + +# Will display 1 +print(person.retag('implicit', 1).tag) +``` -- cgit v1.2.3 From 671243712ea7dd538f42c56adcade896f2927c69 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 11:47:09 -0400 Subject: Removed try/except from .native since it duplicated similar block in ._parse_children() --- asn1crypto/core.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8e206a0..7ef4ff4 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2440,23 +2440,18 @@ class Sequence(Asn1Value): return None if self._native is None: - try: - if self.children is None: - self._parse_children(recurse=True) - self._native = OrderedDict() - for index, child in enumerate(self.children): - if isinstance(child, tuple): - child = _build(*child) - self.children[index] = child - try: - name = self._fields[index][0] - except (IndexError): - name = str_cls(index) - self._native[name] = child.native - except (ValueError) as e: - args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args - raise e + if self.children is None: + self._parse_children(recurse=True) + self._native = OrderedDict() + for index, child in enumerate(self.children): + if isinstance(child, tuple): + child = _build(*child) + self.children[index] = child + try: + name = self._fields[index][0] + except (IndexError): + name = str_cls(index) + self._native[name] = child.native return self._native #pylint: disable=W0212 -- cgit v1.2.3 From 2f4790a7df1673819c655333119d32702d02ceec Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 3 Aug 2015 12:15:37 -0400 Subject: Fix a bug with defaulted fields in Sequence objects overwriting values from .contents --- asn1crypto/core.py | 71 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 7ef4ff4..d5b97ed 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -191,7 +191,7 @@ class Asn1Value(): return value #pylint: disable=W0613 - def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=None): + def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=None, contents=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 @@ -219,6 +219,9 @@ class Asn1Value(): :param default: The default value to use if the value is currently None + :param contents: + A byte string of the encoded contents of the value + :raises: ValueError - when tag_type, class_ or tag are invalid values """ @@ -260,7 +263,10 @@ class Asn1Value(): if tag is not None: self.tag = tag - if default is not None: + if contents is not None: + self.contents = contents + + elif default is not None: self.set(default) except (ValueError) as e: @@ -810,7 +816,7 @@ class Primitive(Asn1Value): method = 0 - def __init__(self, value=None, default=None, **kwargs): + def __init__(self, value=None, default=None, contents=None, **kwargs): """ Sets the value of the object before passing to Asn1Value.__init__() @@ -819,16 +825,23 @@ class Primitive(Asn1Value): :param default: The default value if no value is specified + + :param contents: + A byte string of the encoded contents of the value """ Asn1Value.__init__(self, **kwargs) try: - if value is not None: + if contents is not None: + self.contents = contents + + elif value is not None: self.set(value) elif default is not None: self.set(default) + except (ValueError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args @@ -1448,7 +1461,7 @@ class ParsableOctetString(Primitive): _parsed = None - def __init__(self, value=None, default=None, parsed=None, **kwargs): + def __init__(self, value=None, parsed=None, **kwargs): """ Allows providing a parsed object that will be serialized to get the byte string value @@ -1456,9 +1469,6 @@ class ParsableOctetString(Primitive): :param value: A native Python datatype to initialize the object value with - :param default: - The default value if no value is specified - :param parsed: If value is None and this is an Asn1Value object, this will be set as the parsed value, and the value will be obtained by calling @@ -1470,7 +1480,7 @@ class ParsableOctetString(Primitive): value = parsed.dump() set_parsed = True - Primitive.__init__(self, value=value, default=default, **kwargs) + Primitive.__init__(self, value=value, **kwargs) if set_parsed: self._parsed = (parsed, parsed.__class__, None) @@ -1905,7 +1915,14 @@ class Sequence(Asn1Value): Asn1Value.__init__(self, **kwargs) + check_existing = False if value is None and default is not None: + check_existing = True + if self.children is None: + if self.contents is None: + check_existing = False + else: + self._parse_children() value = default if value is not None: @@ -1917,9 +1934,18 @@ class Sequence(Asn1Value): keys = [info[0] for info in self._fields] else: keys = value.keys() + for key in keys: + # If we are setting defaults, but a real value has already + # been set for the field, then skip it + if check_existing: + index = self._field_map[key] + if index < len(self.children) and not isinstance(self.children[index], NoValue): + continue + if key in value: self.__setitem__(key, value[key]) + except (ValueError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args @@ -2534,7 +2560,7 @@ class SequenceOf(Asn1Value): # An Asn1Value class to use when parsing children _child_spec = None - def __init__(self, value=None, default=None, spec=None, **kwargs): + def __init__(self, value=None, default=None, contents=None, spec=None, **kwargs): """ Allows setting child objects and the _child_spec via the spec parameter before passing everything else along to Asn1Value.__init__() @@ -2545,6 +2571,9 @@ class SequenceOf(Asn1Value): :param default: The default value if no value is specified + :param contents: + A byte string of the encoded contents of the value + :param spec: A class derived from Asn1Value to use to parse children """ @@ -2555,12 +2584,15 @@ class SequenceOf(Asn1Value): Asn1Value.__init__(self, **kwargs) try: - if value is None and default is not None: - value = default + if contents is not None: + self.contents = contents + else: + if value is None and default is not None: + value = default - if value is not None: - for index, child in enumerate(value): - self.__setitem__(index, child) + if value is not None: + for index, child in enumerate(value): + self.__setitem__(index, child) except (ValueError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args @@ -3428,9 +3460,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # If an explicit specification was passed in, make sure it matches if spec is not None: if spec_params: - value = spec(**spec_params) + value = spec(contents=contents, **spec_params) else: - value = spec() + value = spec(contents=contents) if isinstance(value, Any): pass @@ -3500,7 +3532,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # since we will be parsing the contents and discarding the outer object # anyway a little further on elif spec_params and 'tag_type' in spec_params and spec_params['tag_type'] == 'explicit': - value = Asn1Value(**spec_params) + value = Asn1Value(contents=contents, **spec_params) # If no spec was specified, allow anything and just process what # is in the input data @@ -3547,10 +3579,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param spec = universal_specs[tag] - value = spec(class_=class_) + value = spec(contents=contents, class_=class_) value._header = header #pylint: disable=W0212 - value.contents = contents if trailer is not None and trailer != b'': value._trailer = trailer #pylint: disable=W0212 -- cgit v1.2.3 From 2870f94015a5c5b148b2533df8aad527b9685155 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 4 Aug 2015 15:06:01 -0400 Subject: Added dev-requirements.txt --- dev-requirements.txt | 2 ++ readme.md | 6 ++++++ 2 files changed, 8 insertions(+) create mode 100644 dev-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..fc7a185 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,2 @@ +coverage>=4.0b1 +pylint \ No newline at end of file diff --git a/readme.md b/readme.md index 4bb8341..5c8c4b7 100644 --- a/readme.md +++ b/readme.md @@ -65,6 +65,12 @@ links to the source for the various pre-defined type classes. ## Development +To install required development dependencies, execute: + +```bash +pip install -r dev-requirements.txt +``` + The following commands will run the test suite, linter and test coverage: ```bash -- cgit v1.2.3 From d007e0f810891529ad0299fa70f0074ae9d8c694 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 4 Aug 2015 21:50:39 -0400 Subject: Version 0.10.0 --- asn1crypto/__init__.py | 2 +- changelog.md | 34 ++++++++++++++++++++++++++++++++++ readme.md | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index c65130b..1f6a0d0 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,4 +2,4 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.9.0' +__version__ = '0.10.0' diff --git a/changelog.md b/changelog.md index 531ac8e..79045ea 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,39 @@ # changelog +## 0.10.0 + + - Added PEM encoding/decoding functionality + - `core.BitString` now uses item access instead of attributes for named bit + access + - `core.BitString.native` now uses a `set` of unicode strings when `_map` is + present + - Removed `core.Asn1Value.pprint()` method + - Added `core.ParsableOctetString` class + - Added `core.ParsableOctetBitString` class + - Added `core.Asn1Value.copy()` method + - Added `core.Asn1Value.debug()` method + - Added `core.SequenceOf.append()` method + - Added `core.Sequence.spec()` and `core.SequenceOf.spec()` methods + - Added correct IP address parsing to `x509.GeneralName` + - `x509.Name` and `x509.GeneralName` are now compared according to rules in + RFC5280 + - Added convenience attributes to: + - `algos.SignedDigestAlgorithm` + - `crl.CertificateList` + - `crl.RevokedCertificate` + - `keys.PublicKeyInfo` + - `ocsp.OCSPRequest` + - `ocsp.Request` + - `ocsp.OCSPResponse` + - `ocsp.SingleResponse` + - `x509.Certificate` + - `x509.Name` + - Added `asn1crypto.util` module with the following items: + - `int_to_bytes()` + - `int_from_bytes()` + - `timezone.utc` + - Added `setup.py clean` command + ## 0.9.0 - Initial release diff --git a/readme.md b/readme.md index 5c8c4b7..049abe8 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,7 @@ Python 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Version -0.9.0 - [changelog](changelog.md) +0.10.0 - [changelog](changelog.md) ## Installation -- cgit v1.2.3 From f239629e498d2fac2b7e8fb62b7a89e1b17d50ea Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 4 Aug 2015 22:05:51 -0400 Subject: Update install instructions version --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 049abe8..7808460 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,7 @@ Python 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Installation ```bash -pip install git+git://github.com/wbond/asn1crypto.git@0.9.0 +pip install git+git://github.com/wbond/asn1crypto.git@0.10.0 ``` ## Documentation -- cgit v1.2.3 From 5abcc3a5e698165be560d94504e747c21015d0c6 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 5 Aug 2015 08:17:54 -0400 Subject: Restrict IA5String to ASCII --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d5b97ed..52cab67 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3105,7 +3105,7 @@ class IA5String(AbstractString): """ tag = 22 - _encoding = 'latin1' + _encoding = 'ascii' class AbstractTime(AbstractString): -- cgit v1.2.3 From 6fdc54f1beb9f66c2671d3a3277b4e4f6b5d90d1 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 5 Aug 2015 09:38:38 -0400 Subject: Fix Python 2.7 support --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 52cab67..0f06f7d 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -137,7 +137,7 @@ def _basic_debug(prefix, self): print('%s Data: 0x%s' % (prefix, binascii.hexlify(self.contents or b'').decode('utf-8'))) -class Asn1Value(): +class Asn1Value(object): """ The basis of all ASN.1 values """ -- cgit v1.2.3 From efa1893631bea1c453bf1ae1ff013c610d9997e1 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 5 Aug 2015 09:41:33 -0400 Subject: Version 0.10.1 --- asn1crypto/__init__.py | 2 +- changelog.md | 4 ++++ readme.md | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 1f6a0d0..6625992 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,4 +2,4 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.10.0' +__version__ = '0.10.1' diff --git a/changelog.md b/changelog.md index 79045ea..8ea647e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # changelog +## 0.10.1 + + - Fixed bug in `core.Sequence` affecting Python 2.7 and pypy + ## 0.10.0 - Added PEM encoding/decoding functionality diff --git a/readme.md b/readme.md index 7808460..bd5dc6f 100644 --- a/readme.md +++ b/readme.md @@ -31,12 +31,12 @@ Python 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Version -0.10.0 - [changelog](changelog.md) +0.10.1 - [changelog](changelog.md) ## Installation ```bash -pip install git+git://github.com/wbond/asn1crypto.git@0.10.0 +pip install git+git://github.com/wbond/asn1crypto.git@0.10.1 ``` ## Documentation -- cgit v1.2.3 From d913db5370285790c2501992a35abd9b00288782 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 6 Aug 2015 12:44:15 -0400 Subject: Fix formatting of terms: ASN.1, X.509, RFC --- asn1crypto/core.py | 12 ++++++------ asn1crypto/crl.py | 4 ++-- asn1crypto/keys.py | 2 +- asn1crypto/x509.py | 23 ++++++++++++----------- changelog.md | 2 +- docs/readme.md | 2 +- readme.md | 20 ++++++++++---------- setup.py | 2 +- 8 files changed, 34 insertions(+), 33 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 0f06f7d..e77f7a4 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3418,22 +3418,22 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param Builds an Asn1Value object generically, or using a spec with optional params :param class_: - An integer representing the ASN1 class + An integer representing the ASN.1 class :param method: - An integer representing the ASN1 method + An integer representing the ASN.1 method :param tag: - An integer representing the ASN1 tag + An integer representing the ASN.1 tag :param header: - A byte string of the ASN1 header (class, method, tag, length) + A byte string of the ASN.1 header (class, method, tag, length) :param contents: - A byte string of the ASN1 value + A byte string of the ASN.1 value :param trailer: - A byte string of any ASN1 trailer (only used by indefinite length encodings) + A byte string of any ASN.1 trailer (only used by indefinite length encodings) :param spec: A class derived from Asn1Value that defines what class_ and tag the diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index c61bc57..a2be5bf 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -459,7 +459,7 @@ class CertificateList(Sequence): """ :return: A list of unicode strings that are URLs that should contain either - an individual DER-encoded X509 certificate, or a DER-encoded CMS + an individual DER-encoded X.509 certificate, or a DER-encoded CMS message containing multiple certificates """ @@ -491,7 +491,7 @@ class CertificateList(Sequence): if self.freshest_crl_value is not None: for distribution_point in self.freshest_crl_value: distribution_point_name = distribution_point['distribution_point'] - # RFC5280 indicates conforming CA should not use the relative form + # RFC 5280 indicates conforming CA should not use the relative form if distribution_point_name.name == 'name_relative_to_crl_issuer': continue # This library is currently only concerned with HTTP-based CRLs diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index c63561e..d49907c 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -123,7 +123,7 @@ class RSAPublicKey(Sequence): class DSAPrivateKey(Sequence): """ - The ASN1 structure that OpenSSL uses to store a DSA private key that is + The ASN.1 structure that OpenSSL uses to store a DSA private key that is not part of a PKCS#8 structure. Reversed engineered from english-language description on linked OpenSSL documentation page. diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index e800c8d..2be39f2 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1,7 +1,7 @@ # coding: utf-8 """ -ASN.1 type classes for X509 certificates. Exports the following items: +ASN.1 type classes for X.509 certificates. Exports the following items: - Attributes() - Certificate() @@ -246,7 +246,7 @@ class NameTypeAndValue(Sequence): def prepped_value(self): """ Returns the value after being processed by the internationalized string - preparation as specified by RFC5280 + preparation as specified by RFC 5280 :return: A unicode string @@ -312,22 +312,22 @@ class NameTypeAndValue(Sequence): # Prohibit step for char in string: if stringprep.in_table_a1(char): - raise ValueError('X509 Name objects may not contain unassigned code points') + raise ValueError('X.509 Name objects may not contain unassigned code points') if stringprep.in_table_c8(char): - raise ValueError('X509 Name objects may not contain change display or deprecated characters') + raise ValueError('X.509 Name objects may not contain change display or deprecated characters') if stringprep.in_table_c3(char): - raise ValueError('X509 Name objects may not contain private use characters') + raise ValueError('X.509 Name objects may not contain private use characters') if stringprep.in_table_c4(char): - raise ValueError('X509 Name objects may not contain non-character code points') + raise ValueError('X.509 Name objects may not contain non-character code points') if stringprep.in_table_c5(char): - raise ValueError('X509 Name objects may not contain surrogate code points') + raise ValueError('X.509 Name objects may not contain surrogate code points') if char == '\ufffd': - raise ValueError('X509 Name objects may not contain the replacement character') + raise ValueError('X.509 Name objects may not contain the replacement character') # Check bidirectional step - here we ensure that we are not mixing # left-to-right and right-to-left text in the string @@ -344,7 +344,7 @@ class NameTypeAndValue(Sequence): last_is_r_and_al = stringprep.in_table_d1(string[-1]) if has_l_cat or not first_is_r_and_al or not last_is_r_and_al: - raise ValueError('X509 Name object contains a malformed bidirectional sequence') + raise ValueError('X.509 Name object contains a malformed bidirectional sequence') # Insignificant space handling step string = ' ' + re.sub(' +', ' ', string).strip() + ' ' @@ -1839,7 +1839,7 @@ class Certificate(Sequence): for distribution_point in crl_distribution_points: distribution_point_name = distribution_point['distribution_point'] - # RFC5280 indicates conforming CA should not use the relative form + # RFC 5280 indicates conforming CA should not use the relative form if distribution_point_name.name == 'name_relative_to_crl_issuer': continue # This library is currently only concerned with HTTP-based CRLs @@ -1942,7 +1942,8 @@ class Certificate(Sequence): def self_issued(self): """ :return: - A boolean - if the certificate is self-issued, as defined by RFC5280 + A boolean - if the certificate is self-issued, as defined by RFC + 5280 """ if self._self_issued is None: diff --git a/changelog.md b/changelog.md index 8ea647e..700f9f8 100644 --- a/changelog.md +++ b/changelog.md @@ -20,7 +20,7 @@ - Added `core.Sequence.spec()` and `core.SequenceOf.spec()` methods - Added correct IP address parsing to `x509.GeneralName` - `x509.Name` and `x509.GeneralName` are now compared according to rules in - RFC5280 + RFC 5280 - Added convenience attributes to: - `algos.SignedDigestAlgorithm` - `crl.CertificateList` diff --git a/docs/readme.md b/docs/readme.md index 563d2b9..2ca99a0 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -13,7 +13,7 @@ links to the source for the various pre-defined type classes. - [Universal types](../asn1crypto/core.py), `asn1crypto.core` - [Digest, HMAC, signed digest and encryption algorithms](../asn1crypto/algos.py), `asn1crypto.algos` - [Private and public keys](../asn1crypto/keys.py), `asn1crypto.keys` - - [X509 certificates](../asn1crypto/x509.py), `asn1crypto.x509` + - [X.509 certificates](../asn1crypto/x509.py), `asn1crypto.x509` - [Certificate revocation lists (CRLs)](../asn1crypto/crl.py), `asn1crypto.crl` - [Online certificate status protocol (OCSP)](../asn1crypto/ocsp.py), `asn1crypto.ocsp` - [Certificate signing requests (CSRs)](../asn1crypto/csr.py), `asn1crypto.csr` diff --git a/readme.md b/readme.md index bd5dc6f..cf39370 100644 --- a/readme.md +++ b/readme.md @@ -6,18 +6,18 @@ a bunch of ASN.1 structures for use with various common cryptography standards: | Standard | Module | Source | | ---------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| X509 | [`asn1crypto.x509`](asn1crypto/x509.py) | [RFC5280](https://tools.ietf.org/html/rfc5280) | -| CRL | [`asn1crypto.crl`](asn1crypto/crl.py) | [RFC5280](https://tools.ietf.org/html/rfc5280) | -| CSR | [`asn1crypto.csr`](asn1crypto/csr.py) | [RFC2986](https://tools.ietf.org/html/rfc2986), [RFC2985](https://tools.ietf.org/html/rfc2985) | -| OCSP | [`asn1crypto.ocsp`](asn1crypto/ocsp.py) | [RFC6960](https://tools.ietf.org/html/rfc6960) | -| PKCS#12 | [`asn1crypto.pkcs12`](asn1crypto/pkcs12.py) | [RFC7292](https://tools.ietf.org/html/rfc7292) | -| PKCS#8 | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC5208](https://tools.ietf.org/html/rfc5208) | -| PKCS#1 v2.1 (RSA keys) | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC3447](https://tools.ietf.org/html/rfc3447) | -| DSA keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC3279](https://tools.ietf.org/html/rfc3279) | +| X.509 | [`asn1crypto.x509`](asn1crypto/x509.py) | [RFC 5280](https://tools.ietf.org/html/rfc5280) | +| CRL | [`asn1crypto.crl`](asn1crypto/crl.py) | [RFC 5280](https://tools.ietf.org/html/rfc5280) | +| CSR | [`asn1crypto.csr`](asn1crypto/csr.py) | [RFC 2986](https://tools.ietf.org/html/rfc2986), [RFC 2985](https://tools.ietf.org/html/rfc2985) | +| OCSP | [`asn1crypto.ocsp`](asn1crypto/ocsp.py) | [RFC 6960](https://tools.ietf.org/html/rfc6960) | +| PKCS#12 | [`asn1crypto.pkcs12`](asn1crypto/pkcs12.py) | [RFC 7292](https://tools.ietf.org/html/rfc7292) | +| PKCS#8 | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 5208](https://tools.ietf.org/html/rfc5208) | +| PKCS#1 v2.1 (RSA keys) | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3447](https://tools.ietf.org/html/rfc3447) | +| DSA keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3279](https://tools.ietf.org/html/rfc3279) | | Elliptic curve keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [SECG SEC1 V2](http://www.secg.org/sec1-v2.pdf) | | PKCS#5 v2.1 | [`asn1crypto.algos`](asn1crypto/algos.py) | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | -| CMS (and PKCS#7) | [`asn1crypto.cms`](asn1crypto/cms.py) | [RFC5652](https://tools.ietf.org/html/rfc5652), [RFC2315](https://tools.ietf.org/html/rfc2315) | -| TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC3161](https://tools.ietf.org/html/rfc3161) | +| CMS (and PKCS#7) | [`asn1crypto.cms`](asn1crypto/cms.py) | [RFC 5652](https://tools.ietf.org/html/rfc5652), [RFC 2315](https://tools.ietf.org/html/rfc2315) | +| TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC 3161](https://tools.ietf.org/html/rfc3161) | | PDF signatures | [`asn1crypto.pdf`](asn1crypto/pdf.py) | [PDF 1.7](http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf) | ## License diff --git a/setup.py b/setup.py index 0504f1e..838fab0 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ setup( name='asn1crypto', version=asn1crypto.__version__, - description='Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X509 and TSA', + description='Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSA', long_description='Docs for this project are maintained at https://github.com/wbond/asn1crypto#readme.', url='https://github.com/wbond/asn1crypto', -- cgit v1.2.3 From 5fb30308455b9935df5baec183e5883aab282e5d Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 6 Aug 2015 12:44:57 -0400 Subject: Fix generating the hashable version of an x509.RelativeDistinguishedName --- asn1crypto/x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 2be39f2..dc310bc 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -363,7 +363,7 @@ class RelativeDistinguishedName(SetOf): """ output = [] - values = self._get_values() + values = self._get_values(self) for key in sorted(values.keys()): output.append('%s: %s' % (key, values[key])) # Unit separator is used here since the normalization process for -- cgit v1.2.3 From 308c0f198d19b02ebff999d0e253a4392b54ae65 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 6 Aug 2015 12:56:18 -0400 Subject: Fix bugs with times in Python 2.7 --- asn1crypto/core.py | 4 ++++ asn1crypto/util.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index e77f7a4..b582f1c 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3174,6 +3174,8 @@ class UTCTime(AbstractTime): if isinstance(value, datetime): value = value.strftime('%y%m%d%H%M%SZ') + if py2: + value = value.decode('ascii') AbstractString.set(self, value) # Set it to None and let the class take care of converting the next @@ -3222,6 +3224,8 @@ class GeneralizedTime(AbstractTime): if isinstance(value, datetime): value = value.strftime('%Y%m%d%H%M%SZ') + if py2: + value = value.decode('ascii') AbstractString.set(self, value) # Set it to None and let the class take care of converting the next diff --git a/asn1crypto/util.py b/asn1crypto/util.py index a8d916f..90ae7b8 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -103,7 +103,7 @@ if sys.version_info <= (3,): return timedelta(0) def dst(self, _): - return None + return timedelta(0) class timezone(): -- cgit v1.2.3 From 69b9918a21e42865a1b822d3e452e7652ad518c5 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 6 Aug 2015 12:56:36 -0400 Subject: Added pki keyword to setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 838fab0..598525f 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ setup( 'Programming Language :: Python :: 3.4', ], - keywords='asn1 crypto', + keywords='asn1 crypto pki', packages=find_packages(exclude=['tests*', 'dev*']), -- cgit v1.2.3 From e474a6fab1f1a0d031934270ff5711393cad351c Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 6 Aug 2015 14:34:10 -0400 Subject: Ensure tagging is correct on values passed to a Choice --- asn1crypto/core.py | 64 ++++++++++++++++++++++++++++++++++++++++-------------- tests/test_core.py | 18 +++++++++++++++ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index b582f1c..0c18156 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -670,6 +670,8 @@ class Choice(Asn1Value): if not isinstance(value, spec): value = spec(value, **params) + else: + value = _fix_tagging(value, params) self._parsed = value except (ValueError) as e: @@ -2284,17 +2286,7 @@ class Sequence(Asn1Value): wrapper._parsed = (new_value, new_value.__class__, None) #pylint: disable=W0212 new_value = wrapper - # If the new value has an incorrect tagging, it needs to be fixed. It - # could either be that a special tagging is required that is not - # present: - if field_params and 'tag_type' in field_params and 'tag' in field_params: - if field_params['tag_type'] != new_value.tag_type or field_params['tag'] != new_value.tag: - new_value = new_value.retag(tag_type=field_params['tag_type'], tag=field_params['tag']) - - # Or the issue could be that special tagging is present, but the field - # is a plain field - elif new_value.tag_type is not None: - new_value = new_value.untag() + new_value = _fix_tagging(new_value, field_params) return new_value @@ -2682,13 +2674,15 @@ class SequenceOf(Asn1Value): else: return self._child_spec(value=value) + params = {} if self._child_spec.tag_type is not None: - if new_value.tag_type != self._child_spec.tag_type: - new_value = new_value.retag(self._child_spec.tag_type, self._child_spec.tag) - elif new_value.tag_type is not None: - new_value = new_value.untag() + params['tag_type'] = self._child_spec.tag_type + if params['tag_type'] == 'explicit': + params['tag'] = self._child_spec.explicit_tag + else: + params['tag'] = self._child_spec.tag - return new_value + return _fix_tagging(new_value, params) def __len__(self): """ @@ -3317,6 +3311,44 @@ class BMPString(AbstractString): _encoding = 'utf-16-be' +def _fix_tagging(value, params): + """ + Checks if a value is properly tagged based on the spec, and re/untags as + necessary + + :param value: + An Asn1Value object + + :param params: + A dict of spec params + + :return: + An Asn1Value that is properly tagged + """ + + if 'tag_type' in params: + required_tag_type = params['tag_type'] + retag = False + + if required_tag_type != value.tag_type: + retag = True + + elif required_tag_type == 'explicit' and value.explicit_tag != params['tag']: + retag = True + + elif required_tag_type == 'implicit' and value.tag != params['tag']: + retag = True + + if retag: + return value.retag(params['tag_type'], params['tag']) + return value + + if value.tag_type: + return value.untag() + + return value + + def _dump_header(class_, method, tag, contents): header = b'' diff --git a/tests/test_core.py b/tests/test_core.py index b6195c3..8fd37ed 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -53,6 +53,14 @@ class CopySeq(core.Sequence): ] +class NumChoice(core.Choice): + _alternatives = [ + ('one', core.Integer, {'tag_type': 'explicit', 'tag': 0}), + ('two', core.Integer, {'tag_type': 'implicit', 'tag': 1}), + ('three', core.Integer, {'tag_type': 'explicit', 'tag': 2}), + ] + + @DataDecorator class CoreTests(unittest.TestCase): @@ -246,3 +254,13 @@ class CoreTests(unittest.TestCase): self.assertNotEqual(id(a), id(b)) self.assertEqual(a.contents, b.contents) self.assertNotEqual(a.dump(), b.dump()) + + def test_fix_tagging_choice(self): + correct = core.Integer(200, tag_type='explicit', tag=2) + choice = NumChoice( + name='three', + value=core.Integer(200, tag_type='explicit', tag=1) + ) + self.assertEqual(correct.dump(), choice.dump()) + self.assertEqual(correct.tag_type, choice.chosen.tag_type) + self.assertEqual(correct.explicit_tag, choice.chosen.explicit_tag) -- cgit v1.2.3 From 0212f96c0a9d62340f1c72c4e644c3c9522d477c Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 6 Aug 2015 14:36:22 -0400 Subject: Moved _basic_debug() below class definitions, added docstring --- asn1crypto/core.py | 56 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 0c18156..bd8e106 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -114,28 +114,6 @@ METHOD_NUM_TO_NAME_MAP = { _SETUP_CLASSES = {} -def _basic_debug(prefix, self): - print('%s%s Object #%s' % (prefix, self.__class__.__name__, id(self))) - if self._header: #pylint: disable=W0212 - print('%s Header: 0x%s' % (prefix, binascii.hexlify(self._header or b'').decode('utf-8'))) #pylint: disable=W0212 - - has_header = self.method is not None and self.class_ is not None and self.tag is not None - if has_header: - method_name = METHOD_NUM_TO_NAME_MAP.get(self.method) - class_name = CLASS_NUM_TO_NAME_MAP.get(self.class_) - - if self.tag_type == 'explicit': - print('%s %s tag %s (explicitly tagged)' % (prefix, CLASS_NUM_TO_NAME_MAP.get(self.explicit_class), self.explicit_tag)) - if has_header: - print('%s %s %s %s' % (prefix, method_name, class_name, self.tag)) - elif self.tag_type == 'implicit': - if has_header: - print('%s %s %s tag %s (implicitly tagged)' % (prefix, method_name, class_name, self.tag)) - elif has_header: - print('%s %s %s tag %s' % (prefix, method_name, class_name, self.tag)) - - print('%s Data: 0x%s' % (prefix, binascii.hexlify(self.contents or b'').decode('utf-8'))) - class Asn1Value(object): """ @@ -3311,6 +3289,40 @@ class BMPString(AbstractString): _encoding = 'utf-16-be' +def _basic_debug(prefix, self): + """ + Prints out basic information about an Asn1Value object. Extracted for reuse + among different classes that customize the debug information. + + :param prefix: + A unicode string of spaces to prefix output line with + + :param self: + The object to print the debugging information about + """ + + print('%s%s Object #%s' % (prefix, self.__class__.__name__, id(self))) + if self._header: #pylint: disable=W0212 + print('%s Header: 0x%s' % (prefix, binascii.hexlify(self._header or b'').decode('utf-8'))) #pylint: disable=W0212 + + has_header = self.method is not None and self.class_ is not None and self.tag is not None + if has_header: + method_name = METHOD_NUM_TO_NAME_MAP.get(self.method) + class_name = CLASS_NUM_TO_NAME_MAP.get(self.class_) + + if self.tag_type == 'explicit': + print('%s %s tag %s (explicitly tagged)' % (prefix, CLASS_NUM_TO_NAME_MAP.get(self.explicit_class), self.explicit_tag)) + if has_header: + print('%s %s %s %s' % (prefix, method_name, class_name, self.tag)) + elif self.tag_type == 'implicit': + if has_header: + print('%s %s %s tag %s (implicitly tagged)' % (prefix, method_name, class_name, self.tag)) + elif has_header: + print('%s %s %s tag %s' % (prefix, method_name, class_name, self.tag)) + + print('%s Data: 0x%s' % (prefix, binascii.hexlify(self.contents or b'').decode('utf-8'))) + + def _fix_tagging(value, params): """ Checks if a value is properly tagged based on the spec, and re/untags as -- cgit v1.2.3 From 3e5dbfddc1992fab4e1693b776a952d88cbcff6a Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 6 Aug 2015 23:48:29 -0400 Subject: Fix serialization of an empty SequenceOf during constructor --- asn1crypto/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index bd8e106..6ec5751 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2563,6 +2563,11 @@ class SequenceOf(Asn1Value): if value is not None: for index, child in enumerate(value): self.__setitem__(index, child) + + # Make sure a blank list is serialized + if self.contents is None: + self._set_contents() + except (ValueError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args -- cgit v1.2.3 From d224d9e2a4d5ba4acd0126f2a0f5ae5c4e805877 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 7 Aug 2015 13:44:55 -0400 Subject: Allow comparing primitive types from core based on represented value --- asn1crypto/core.py | 41 +++++++++++++++++++++++++++++++++++++++++ tests/test_core.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 6ec5751..0b33a43 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -863,6 +863,46 @@ class Primitive(Asn1Value): return Asn1Value.dump(self) + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + :param other: + The other Primitive to compare to + + :return: + A boolean + """ + + if not isinstance(other, Primitive): + return False + + if self.contents != other.contents: + return False + + # We compare class tag numbers since object tag numbers could be + # different due to implicit or explicit tagging + if self.__class__.tag != other.__class__.tag: + return False + + if self.__class__ == other.__class__ and self.contents == other.contents: + return True + + # If the objects share a common base class that is not too low-level + # then we can compare the contents + self_bases = (set(self.__class__.__bases__) | {self.__class__}) - {Asn1Value, Primitive, ValueMap} + other_bases = (set(other.__class__.__bases__) | {other.__class__}) - {Asn1Value, Primitive, ValueMap} + if self_bases | other_bases: + return self.contents == other.contents + + # When tagging is going on, do the extra work of constructing new + # objects to see if the dumped representation are the same + if self.tag_type is not None or other.tag_type is not None: + return self.untag().dump() == other.untag().dump() + + return self.dump() == other.dump() + class AbstractString(Primitive): """ @@ -896,6 +936,7 @@ class AbstractString(Primitive): :return: A unicode string """ + return self.contents.decode(self._encoding) @property diff --git a/tests/test_core.py b/tests/test_core.py index 8fd37ed..b41e523 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -53,6 +53,13 @@ class CopySeq(core.Sequence): ] +class Enum(core.Enumerated): + _map = { + 0: 'a', + 1: 'b', + } + + class NumChoice(core.Choice): _alternatives = [ ('one', core.Integer, {'tag_type': 'explicit', 'tag': 0}), @@ -75,6 +82,32 @@ class CoreTests(unittest.TestCase): seq = SequenceAny() self.assertEqual(core.Any, seq.spec()) + #pylint: disable=C0326 + @staticmethod + def compare_primitive_info(): + return ( + (core.ObjectIdentifier('1.2.3'), core.ObjectIdentifier('1.2.3'), True), + (core.Integer(1), Enum(1), False), + (core.Integer(1), core.Integer(1, tag_type='implicit', tag=5), True), + (core.Integer(1), core.Integer(1, tag_type='explicit', tag=5), True), + (core.Integer(1), core.Integer(2), False), + (core.OctetString(b''), core.OctetString(b''), True), + (core.OctetString(b''), core.OctetString(b'1'), False), + (core.OctetString(b''), core.OctetBitString(b''), False), + (core.ParsableOctetString(b'12'), core.OctetString(b'12'), True), + (core.ParsableOctetBitString(b'12'), core.OctetBitString(b'12'), True), + (core.UTF8String('12'), core.UTF8String('12'), True), + (core.UTF8String('12'), core.UTF8String('1'), False), + (core.UTF8String('12'), core.IA5String('12'), False), + ) + + @data('compare_primitive_info') + def compare_primitive(self, one, two, equal): + if equal: + self.assertEqual(one, two) + else: + self.assertNotEqual(one, two) + #pylint: disable=C0326 @staticmethod def integer_info(): -- cgit v1.2.3 From 35701c95aab3323e9a0de337e1f44649c3144851 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 7 Aug 2015 13:45:21 -0400 Subject: Fixed handling of internationalized domain names, URLs and email addresses by adding x509.DNSName, x509.EmailAddress and x509.URI. Implemented comparison of new classes according to RFC 5280. --- asn1crypto/x509.py | 584 ++++++++++++++++++++++++++++++++++++++++++++--------- tests/test_x509.py | 97 +++++++++ 2 files changed, 585 insertions(+), 96 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index dc310bc..794544b 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -22,6 +22,8 @@ import stringprep import unicodedata import socket from collections import OrderedDict +from encodings import idna #pylint: disable=W0611 +import codecs from .core import ( Any, @@ -56,8 +58,16 @@ from .util import int_to_bytes, int_from_bytes if sys.version_info < (3,): str_cls = unicode #pylint: disable=E0602 + byte_cls = str + from urlparse import urlsplit, urlunsplit #pylint: disable=F0401 + from urllib import quote as urlquote, unquote as unquote_to_bytes #pylint: disable=E0611 + bytes_to_list = lambda byte_string: [ord(b) for b in byte_string] + else: str_cls = str + byte_cls = bytes + bytes_to_list = list + from urllib.parse import urlsplit, urlunsplit, quote as urlquote, unquote_to_bytes if sys.platform == 'win32': from ._win._ws2_32 import inet_ntop, inet_pton @@ -70,6 +80,359 @@ else: # and a few other supplementary sources, mostly due to extra supported # extension and name OIDs + +class DNSName(IA5String): + + _encoding = 'idna' + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.2 + + :param other: + Another DNSName object + + :return: + A boolean + """ + + if not isinstance(other, DNSName): + return False + + return self.contents.lower() == other.contents.lower() + + +class URI(IA5String): + + def set(self, value): + """ + Sets the value of the string + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + + self._normalized = True + self._native = value + self.contents = self._normalize(value) + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def _normalize(self, value): + """ + Normalizes and encodes a unicode IRI into an ASCII byte string + + :param value: + + """ + + parsed = urlsplit(value) + + scheme = _urlquote(parsed.scheme) + hostname = parsed.hostname + if hostname is not None: + hostname = hostname.encode('idna') + username = _urlquote(parsed.username) + password = _urlquote(parsed.password) + port = parsed.port + if port is not None: + port = str_cls(port).encode('ascii') + + netloc = b'' + if username is not None: + netloc += username + if password: + netloc += b':' + password + netloc += b'@' + if hostname is not None: + netloc += hostname + 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: + netloc += b':' + port + + path = _urlquote(parsed.path, safe='/') + query = _urlquote(parsed.query, safe='&=') + fragment = _urlquote(parsed.fragment) + + if query is None and fragment is None and path == b'/': + path = None + + # Python 2.7 compat + if path is None: + path = '' + + return urlunsplit((scheme, netloc, path, query, fragment)) + + def __unicode__(self): + """ + :return: + A unicode string + """ + + parsed = urlsplit(self.contents) + + scheme = parsed.scheme + if scheme is not None: + scheme = scheme.decode('ascii') + + username = _urlunquote(parsed.username, remap=[':', '@']) + password = _urlunquote(parsed.password, remap=[':', '@']) + hostname = parsed.hostname + if hostname: + hostname = hostname.decode('idna') + port = parsed.port + if port: + port = port.decode('ascii') + + netloc = '' + if username is not None: + netloc += username + if password: + netloc += ':' + password + netloc += '@' + if hostname is not None: + netloc += hostname + if port is not None: + netloc += ':' + port + + path = _urlunquote(parsed.path, remap=['/'], preserve=True) + query = _urlunquote(parsed.query, remap=['&', '='], preserve=True) + fragment = _urlunquote(parsed.fragment) + + return urlunsplit((scheme, netloc, path, query, fragment)) + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.4 + + :param other: + Another URI object + + :return: + A boolean + """ + + if not isinstance(other, URI): + return False + + return self._normalize(self.native) == self._normalize(other.native) + + +class EmailAddress(IA5String): + + _contents = None + + # If the value has gone through the .set() method, thus normalizing it + _normalized = False + + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the sequence + """ + + return self._contents + + @contents.setter + def contents(self, value): + """ + :param value: + A byte string of the DER-encoded contents of the sequence + """ + + self._normalized = False + self._contents = value + + def set(self, value): + """ + Sets the value of the string + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + + if value.find('@') != -1: + mailbox, hostname = value.rsplit('@', 1) + encoded_value = mailbox.encode('ascii') + b'@' + hostname.encode('idna') + else: + encoded_value = value.encode('ascii') + + self._native = value + self.contents = encoded_value + self._header = None + if self._trailer != b'': + self._trailer = b'' + + def __unicode__(self): + """ + :return: + A unicode string + """ + + if self.contents.find(b'@') == -1: + return self.contents.decode('ascii') + + mailbox, hostname = self.contents.rsplit(b'@', 1) + + return mailbox.decode('ascii') + '@' + hostname.decode('idna') + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + Equality as defined by https://tools.ietf.org/html/rfc5280#section-7.5 + + :param other: + Another EmailAddress object + + :return: + A boolean + """ + + if not isinstance(other, EmailAddress): + return False + + if not self._normalized: + self.set(self.native) + if not other._normalized: #pylint: disable=W0212 + other.set(other.native) + + if self._contents.find(b'@') == -1 or other._contents.find(b'@') == -1: #pylint: disable=W0212 + return self._contents == other._contents #pylint: disable=W0212 + + other_mailbox, other_hostname = other._contents.rsplit(b'@', 1) #pylint: disable=W0212 + mailbox, hostname = self._contents.rsplit(b'@', 1) + + if mailbox != other_mailbox: + return False + + if hostname.lower() != other_hostname.lower(): + return False + + return True + + +class IPAddress(OctetString): + def parse(self, spec=None, spec_params=None): #pylint: disable=W0613 + """ + This method is not applicable to IP addresses + """ + + raise ValueError('IP address values can not be parsed') + + def set(self, value): + """ + Sets the value of the object + + :param value: + A unicode string containing an IPv4 address, IPv4 address with CIDR, + an IPv6 address or IPv6 address with CIDR + """ + + if not isinstance(value, str_cls): + raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + + original_value = value + + has_cidr = value.find('/') != -1 + cidr = 0 + if has_cidr: + parts = value.split('/', 1) + value = parts[0] + cidr = int(parts[1]) + if cidr < 0: + raise ValueError('%s value contains a CIDR range less than 0' % self.__class__.__name__) + + if value.find(':') != -1: + family = socket.AF_INET6 + if cidr > 128: + raise ValueError('%s value contains a CIDR range bigger than 128, the maximum value for an IPv6 address' % self.__class__.__name__) + cidr_size = 128 + else: + family = socket.AF_INET + if cidr > 32: + raise ValueError('%s value contains a CIDR range bigger than 32, the maximum value for an IPv4 address' % self.__class__.__name__) + cidr_size = 32 + + cidr_bytes = b'' + if has_cidr: + cidr_mask = '1' * cidr + cidr_mask += '0' * (cidr_size - len(cidr_mask)) + cidr_bytes = int_to_bytes(int(cidr_mask, 2)) + cidr_bytes = (b'\x00' * ((cidr_size // 8) - len(cidr_bytes))) + cidr_bytes + + self._native = original_value + self.contents = inet_pton(family, value) + cidr_bytes + self._header = None + if self._trailer != b'': + self._trailer = b'' + + @property + def native(self): + """ + The a native Python datatype representation of this value + + :return: + A unicode string or None + """ + + if self.contents is None: + return None + + if self._native is None: + byte_string = self.__bytes__() + byte_len = len(byte_string) + cidr_int = None + if byte_len in {32, 16}: + value = inet_ntop(socket.AF_INET6, byte_string[0:16]) + if byte_len > 16: + cidr_int = int_from_bytes(byte_string[16:]) + elif byte_len in {8, 4}: + value = inet_ntop(socket.AF_INET, byte_string[0:4]) + if byte_len > 4: + cidr_int = int_from_bytes(byte_string[4:]) + if cidr_int is not None: + cidr_bits = '{0:b}'.format(cidr_int) + cidr = len(cidr_bits.rstrip('0')) + value = value + '/' + str_cls(cidr) + self._native = value + return self._native + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + """ + :param other: + Another IPAddress object + + :return: + A boolean + """ + + if not isinstance(other, IPAddress): + return False + + return self.contents == other.contents + + class Attribute(Sequence): _fields = [ ('type', ObjectIdentifier), @@ -232,12 +595,12 @@ class NameTypeAndValue(Sequence): 'dn_qualifier': DirectoryString, 'pseudonym': DirectoryString, # https://tools.ietf.org/html/rfc2985#page-26 - 'email_address': IA5String, + 'email_address': EmailAddress, # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf 'incorporation_locality': DirectoryString, 'incorporation_state_or_province': DirectoryString, 'incorporation_country': DirectoryString, - 'domain_component': IA5String, + 'domain_component': DNSName, } _prepped = None @@ -869,102 +1232,15 @@ class EDIPartyName(Sequence): ] -class IPAddress(OctetString): - def parse(self, spec=None, spec_params=None): #pylint: disable=W0613 - """ - This method is not applicable to IP addresses - """ - - raise ValueError('IP address values can not be parsed') - - def set(self, value): - """ - Sets the value of the object - - :param value: - A unicode string containing an IPv4 address, IPv4 address with CIDR, - an IPv6 address or IPv6 address with CIDR - """ - - if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) - - original_value = value - - has_cidr = value.find('/') != -1 - cidr = 0 - if has_cidr: - parts = value.split('/', 1) - value = parts[0] - cidr = int(parts[1]) - if cidr < 0: - raise ValueError('%s value contains a CIDR range less than 0' % self.__class__.__name__) - - if value.find(':') != -1: - family = socket.AF_INET6 - if cidr > 128: - raise ValueError('%s value contains a CIDR range bigger than 128, the maximum value for an IPv6 address' % self.__class__.__name__) - cidr_size = 128 - else: - family = socket.AF_INET - if cidr > 32: - raise ValueError('%s value contains a CIDR range bigger than 32, the maximum value for an IPv4 address' % self.__class__.__name__) - cidr_size = 32 - - cidr_bytes = b'' - if has_cidr: - cidr_mask = '1' * cidr - cidr_mask += '0' * (cidr_size - len(cidr_mask)) - cidr_bytes = int_to_bytes(int(cidr_mask, 2)) - cidr_bytes = (b'\x00' * ((cidr_size // 8) - len(cidr_bytes))) + cidr_bytes - - self._native = original_value - self.contents = inet_pton(family, value) + cidr_bytes - self._header = None - if self._trailer != b'': - self._trailer = b'' - - @property - def native(self): - """ - The a native Python datatype representation of this value - - :return: - A unicode string or None - """ - - if self.contents is None: - return None - - if self._native is None: - byte_string = self.__bytes__() - byte_len = len(byte_string) - cidr_int = None - if byte_len in {32, 16}: - value = inet_ntop(socket.AF_INET6, byte_string[0:16]) - if byte_len > 16: - cidr_int = int_from_bytes(byte_string[16:]) - elif byte_len in {8, 4}: - value = inet_ntop(socket.AF_INET, byte_string[0:4]) - if byte_len > 4: - cidr_int = int_from_bytes(byte_string[4:]) - if cidr_int is not None: - cidr_bits = '{0:b}'.format(cidr_int) - cidr = len(cidr_bits.rstrip('0')) - value = value + '/' + str_cls(cidr) - self._native = value - return self._native - - class GeneralName(Choice): _alternatives = [ ('other_name', AnotherName, {'tag_type': 'implicit', 'tag': 0}), - ('rfc822_name', IA5String, {'tag_type': 'implicit', 'tag': 1}), - ('dns_name', IA5String, {'tag_type': 'implicit', 'tag': 2}), + ('rfc822_name', EmailAddress, {'tag_type': 'implicit', 'tag': 1}), + ('dns_name', DNSName, {'tag_type': 'implicit', 'tag': 2}), ('x400_address', ORAddress, {'tag_type': 'implicit', 'tag': 3}), ('directory_name', Name, {'tag_type': 'explicit', 'tag': 4}), ('edi_party_name', EDIPartyName, {'tag_type': 'implicit', 'tag': 5}), - ('uniform_resource_identifier', IA5String, {'tag_type': 'implicit', 'tag': 6}), + ('uniform_resource_identifier', URI, {'tag_type': 'implicit', 'tag': 6}), ('ip_address', IPAddress, {'tag_type': 'implicit', 'tag': 7}), ('registered_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 8}), ] @@ -992,10 +1268,7 @@ class GeneralName(Choice): if self.name != other.name: return False - if self.name == 'directory_name': - return self.chosen == other.chosen - - return self.native == other.native + return self.chosen == other.chosen class GeneralNames(SequenceOf): @@ -1974,3 +2247,122 @@ class Certificate(Sequence): else: self._self_signed = 'maybe' return self._self_signed + + +def _iri_utf8_errors_handler(exc): + """ + Error handler for decoding UTF-8 parts of a URI into an IRI. Leaves byte + sequences encoded in %XX format, but as part of a unicode string. + + :param exc: + The UnicodeDecodeError exception + + :return: + A 2-element tuple of (replacement unicode string, integer index to + resume at) + """ + + bytes_as_ints = bytes_to_list(exc.object[exc.start:exc.end]) + replacements = ['%%%02x' % num for num in bytes_as_ints] + return (''.join(replacements), exc.end) + + +codecs.register_error('iriutf8', _iri_utf8_errors_handler) + + +def _urlquote(string, safe=''): + """ + Quotes a unicode string for use in a URL + + :param string: + A unicode string + + :param safe: + A unicode string of character to not encode + + :return: + None (if string is None) or an ASCII byte string of the quoted string + """ + + if string is None or string == '': + return None + + # Anything already hex quoted is pulled out of the URL and unquoted if + # possible + escapes = [] + if re.search('%[0-9a-fA-F]{2}', string): + # Try to unquote any percent values, restoring them if they are not + # valid UTF-8. Also, requote any safe chars since encoded versions of + # those are functionally different than the unquoted ones. + def _try_unescape(match): + byte_string = unquote_to_bytes(match.group(0)) + unicode_string = byte_string.decode('utf-8', errors='iriutf8') + for safe_char in list(safe): + unicode_string = unicode_string.replace(safe_char, '%%%02x' % ord(safe_char)) + return unicode_string + string = re.sub('(?:%[0-9a-fA-F]{2})+', _try_unescape, string) + + # Once we have the minimal set of hex quoted values, removed them from + # the string so that they are not double quoted + def _extract_escape(match): + escapes.append(match.group(0).encode('ascii')) + return '\x00' + string = re.sub('%[0-9a-fA-F]{2}', _extract_escape, string) + + output = urlquote(string.encode('utf-8'), safe=safe) + if not isinstance(output, byte_cls): + output = output.encode('ascii') + + # Restore the existing quoted values that we extracted + if len(escapes) > 0: + def _return_escape(_): + return escapes.pop(0) + output = re.sub(b'%00', _return_escape, output) + + return output + + +def _urlunquote(byte_string, remap=None, preserve=None): + """ + Unquotes a URI portion from a byte string into unicode using UTF-8 + + :param byte_string: + A byte string of the data to unquote + + :param remap: + A list of characters (as unicode) that should be re-mapped to a + %XX encoding. This is used when characters are not valid in part of a + URL. + + :param preserve: + A bool - indicates that the chars to be remapped if they occur in + non-hex form, should be preserved. E.g. / for URL path. + + :return: + A unicode string + """ + + if byte_string is None: + return byte_string + + byte_string = unquote_to_bytes(byte_string) + + if preserve: + replacements = ['\x1A', '\x1C', '\x1D', '\x1E', '\x1F'] + preserve_unmap = {} + for char in remap: + replacement = replacements.pop(0) + preserve_unmap[replacement] = char + byte_string = byte_string.replace(char.encode('ascii'), replacement.encode('ascii')) + + if remap: + for char in remap: + byte_string = byte_string.replace(char.encode('ascii'), ('%%%02x' % ord(char)).encode('ascii')) + + output = byte_string.decode('utf-8', errors='iriutf8') + + if preserve: + for replacement, original in preserve_unmap.items(): + output = output.replace(replacement, original) + + return output diff --git a/tests/test_x509.py b/tests/test_x509.py index a1e154b..c04c36f 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -49,6 +49,103 @@ class X509Tests(unittest.TestCase): self.assertEqual(der_bytes, x509.IPAddress(unicode_string).dump()) self.assertEqual(unicode_string, x509.IPAddress.load(der_bytes).native) + #pylint: disable=C0326 + @staticmethod + def compare_dnsname_info(): + return ( + ('google.com', 'google.com', True), + ('google.com', 'Google.com', True), + ('Bücher.ch', b'\x16\x10xn--bcher-kva.ch', True), + ('google.com', b'\x16\x0AGoogle.com', True), + ('google.com', b'\x16\x09Google.co', False), + ) + + @data('compare_dnsname_info') + def compare_dnsname(self, domain_one, domain_two, equal): + one = x509.DNSName(domain_one) + if isinstance(domain_two, byte_cls): + two = x509.DNSName.load(domain_two) + else: + two = x509.DNSName(domain_two) + if equal: + self.assertEqual(one, two) + else: + self.assertNotEqual(one, two) + + #pylint: disable=C0326 + @staticmethod + def compare_uri_info(): + return ( + ('http://google.com', 'http://google.com', True), + ('http://google.com/', 'http://Google.com', True), + ('http://google.com:80', 'http://google.com', True), + ('https://google.com', 'https://google.com:443/', True), + ('http://google.com/%41%42%43', 'http://google.com/ABC', True), + ('http://google.com/%41%42%43', 'http://google.com/abc', False), + ('http://google.com/%41%42%43/', 'http://google.com/ABC%2F', False), + ) + + @data('compare_uri_info') + def compare_uri(self, uri_one, uri_two, equal): + one = x509.URI(uri_one) + if isinstance(uri_two, byte_cls): + two = x509.URI.load(uri_two) + else: + two = x509.URI(uri_two) + if equal: + self.assertEqual(one, two) + else: + self.assertNotEqual(one, two) + + #pylint: disable=C0326 + @staticmethod + def compare_email_address_info(): + return ( + ('john@google.com', 'john@google.com', True), + ('john@google.com', 'john@Google.com', True), + ('john@google.com', 'John@google.com', False), + ('john@Bücher.ch', b'\x16\x15john@xn--bcher-kva.ch', True), + ('John@Bücher.ch', b'\x16\x15john@xn--bcher-kva.ch', False), + ('john@google.com', b'\x16\x0Fjohn@Google.com', True), + ('john@google.com', b'\x16\x0FJohn@google.com', False), + ('john@google.com', b'\x16\x0Ejohn@Google.co', False), + ) + + @data('compare_email_address_info') + def compare_email_address(self, email_one, email_two, equal): + one = x509.EmailAddress(email_one) + if isinstance(email_two, byte_cls): + two = x509.EmailAddress.load(email_two) + else: + two = x509.EmailAddress(email_two) + if equal: + self.assertEqual(one, two) + else: + self.assertNotEqual(one, two) + + #pylint: disable=C0326 + @staticmethod + def compare_ip_address_info(): + return ( + ('127.0.0.1', '127.0.0.1', True), + ('127.0.0.1', '127.0.0.2', False), + ('127.0.0.1', '127.0.0.1/32', False), + ('127.0.0.1/32', b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xFF', True), + ('127.0.0.1', b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xFF', False), + ) + + @data('compare_ip_address_info') + def compare_ip_address(self, email_one, email_two, equal): + one = x509.IPAddress(email_one) + if isinstance(email_two, byte_cls): + two = x509.IPAddress.load(email_two) + else: + two = x509.IPAddress(email_two) + if equal: + self.assertEqual(one, two) + else: + self.assertNotEqual(one, two) + #pylint: disable=C0326 @staticmethod def compare_name_info(): -- cgit v1.2.3 From 7b2b9e231cb3d87866230e6359119134a9633bc9 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 8 Aug 2015 08:14:32 -0400 Subject: Improved exception messages to include object module in addition to name --- asn1crypto/_errors.py | 24 ++++++++++ asn1crypto/core.py | 123 +++++++++++++++++++++++++------------------------- asn1crypto/keys.py | 5 +- asn1crypto/pem.py | 10 ++-- asn1crypto/x509.py | 13 +++--- 5 files changed, 102 insertions(+), 73 deletions(-) create mode 100644 asn1crypto/_errors.py diff --git a/asn1crypto/_errors.py b/asn1crypto/_errors.py new file mode 100644 index 0000000..6b38315 --- /dev/null +++ b/asn1crypto/_errors.py @@ -0,0 +1,24 @@ +# coding: utf-8 + +""" +Helpers for creating exceptions. Exports the following items: + + - object_name() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + + +def object_name(value): + """ + :param value: + A value to get the object name of + + :return: + A unicode string of the object name + """ + + cls = value.__class__ + if cls.__module__ == 'builtins': + return cls.__name__ + return '%s.%s' % (cls.__module__, cls.__name__) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 0b33a43..c2d7786 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -55,6 +55,7 @@ import binascii from . import _teletex_codec from .util import int_to_bytes, int_from_bytes, timezone +from ._errors import object_name # Python 2 if sys.version_info <= (3,): @@ -224,7 +225,7 @@ class Asn1Value(object): if tag is not None: if not isinstance(tag, int_types): - raise ValueError('tag must be an integer, not %s' % tag.__class__.__name__) + raise ValueError('tag must be an integer, not %s' % object_name(tag)) if tag_type == 'implicit': self.class_ = class_ @@ -249,7 +250,7 @@ class Asn1Value(object): except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args raise e def __str__(self): @@ -272,7 +273,7 @@ class Asn1Value(object): A unicode string """ - return '<%s %s %s>' % (self.__class__.__name__, id(self), repr(self.contents or b'')) + return '<%s %s %s>' % (object_name(self), id(self), repr(self.contents or b'')) def copy(self): """ @@ -330,7 +331,7 @@ class Asn1Value(object): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) self.contents = other.contents self._native = other._native @@ -477,14 +478,14 @@ class Any(Asn1Value): try: if value is not None: if not isinstance(value, Asn1Value): - raise ValueError('value must be an instance of Ans1Value, not %s' % value.__class__.__name__) + raise ValueError('value must be an instance of Ans1Value, not %s' % object_name(value)) self._parsed = (value, value.__class__, None) self.contents = value.dump() except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args raise e @property @@ -544,7 +545,7 @@ class Any(Asn1Value): self._parsed = (parsed_value, spec, spec_params) except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args raise e return self._parsed[0] @@ -639,7 +640,7 @@ class Choice(Asn1Value): if name is not None: if name not in self._name_map: - raise ValueError('The name specified, "%s", is not a valid alternative for %s' % (name, self.__class__.__name__)) + raise ValueError('The name specified, "%s", is not a valid alternative for %s' % (name, object_name(self))) self._choice = self._name_map[name] info = self._alternatives[self._choice] @@ -654,7 +655,7 @@ class Choice(Asn1Value): except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args raise e @property @@ -684,7 +685,7 @@ class Choice(Asn1Value): self._parsed, _ = _parse_build(self.contents, spec=info[1], spec_params=params) except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args raise e @property @@ -730,7 +731,7 @@ class Choice(Asn1Value): # This means the Choice was implicitly tagged if self.class_ is not None and self.tag is not None: if len(self._alternatives) > 1: - raise ValueError('%s was implicitly tagged, but more than one alternative exists' % self.__class__.__name__) + raise ValueError('%s was implicitly tagged, but more than one alternative exists' % object_name(self)) if id_ == (self.class_, self.tag): self._choice = 0 return @@ -738,7 +739,7 @@ class Choice(Asn1Value): asn1 = self._format_class_tag(class_, tag) asn1s = [self._format_class_tag(id_[0], id_[1]) for id_ in self._id_map] - raise ValueError('Value %s did not match the class and tag of any of the alternatives in %s: %s' % (asn1, self.__class__.__name__, '. '.join(asn1s))) + raise ValueError('Value %s did not match the class and tag of any of the alternatives in %s: %s' % (asn1, object_name(self), '. '.join(asn1s))) def _format_class_tag(self, class_, tag): """ @@ -758,7 +759,7 @@ class Choice(Asn1Value): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) self.contents = other.contents self._native = other._native @@ -824,7 +825,7 @@ class Primitive(Asn1Value): except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args raise e def set(self, value): @@ -836,7 +837,7 @@ class Primitive(Asn1Value): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a byte string, not %s' % (object_name(self), object_name(value))) self._native = value self.contents = value @@ -923,7 +924,7 @@ class AbstractString(Primitive): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) self._native = value self.contents = value.encode(self._encoding) @@ -1029,15 +1030,15 @@ class Integer(Primitive, ValueMap): if isinstance(value, str_cls): if self._map is None: - raise ValueError('%s value is a unicode string, but no _map provided' % self.__class__.__name__) + raise ValueError('%s value is a unicode string, but no _map provided' % object_name(self)) if value not in self._reverse_map: - raise ValueError('%s value, %s, is not present in the _map' % (self.__class__.__name__, value)) + raise ValueError('%s value, %s, is not present in the _map' % (object_name(self), value)) value = self._reverse_map[value] elif not isinstance(value, int_types): - raise ValueError('%s value must be an integer or unicode string when a name_map is provided, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be an integer or unicode string when a name_map is provided, not %s' % (object_name(self), object_name(value))) self._native = self._map[value] if self._map and value in self._map else value @@ -1106,7 +1107,7 @@ class BitString(Primitive, ValueMap, object): if isinstance(value, set): if self._map is None: - raise ValueError('%s _map has not been defined' % self.__class__.__name__) + raise ValueError('%s _map has not been defined' % object_name(self)) bits = [0] * self._size self._native = value @@ -1131,12 +1132,12 @@ class BitString(Primitive, ValueMap, object): value = ''.join(map(str_cls, value)) else: - raise ValueError('%s value must be a tuple of ones and zeros or a set of unicode strings, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a tuple of ones and zeros or a set of unicode strings, not %s' % (object_name(self), object_name(value))) if self._map is not None: size = self._size if len(value) > size: - raise ValueError('%s value must be at most %s bits long, specified was %s long' % (self.__class__.__name__, size, len(value))) + raise ValueError('%s value must be at most %s bits long, specified was %s long' % (object_name(self), size, len(value))) value += '0' * (size - len(value)) else: size = len(value) @@ -1184,10 +1185,10 @@ class BitString(Primitive, ValueMap, object): is_int = isinstance(key, int_types) if not is_int: if not isinstance(self._map, dict): - raise ValueError('%s _map has not been defined' % self.__class__.__name__) + raise ValueError('%s _map has not been defined' % object_name(self)) if key not in self._reverse_map: - raise ValueError('%s _map does not contain an entry for "%s"' % (self.__class__.__name__, key)) + raise ValueError('%s _map does not contain an entry for "%s"' % (object_name(self), key)) if self._native is None: _ = self.native @@ -1219,10 +1220,10 @@ class BitString(Primitive, ValueMap, object): is_int = isinstance(key, int_types) if not is_int: if self._map is None: - raise ValueError('%s _map has not been defined' % self.__class__.__name__) + raise ValueError('%s _map has not been defined' % object_name(self)) if key not in self._reverse_map: - raise ValueError('%s _map does not contain an entry for "%s"' % (self.__class__.__name__, key)) + raise ValueError('%s _map does not contain an entry for "%s"' % (object_name(self), key)) if self._native is None: _ = self.native @@ -1310,7 +1311,7 @@ class OctetBitString(Primitive): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a byte string, not %s' % (object_name(self), object_name(value))) self._native = value # Set the unused bits to 0 @@ -1367,7 +1368,7 @@ class IntegerBitString(Primitive): """ if not isinstance(value, int_types): - raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be an integer, not %s' % (object_name(self), object_name(value))) self._native = value # Set the unused bits to 0 @@ -1450,7 +1451,7 @@ class IntegerOctetString(Primitive): """ if not isinstance(value, int_types): - raise ValueError('%s value must be an integer, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be an integer, not %s' % (object_name(self), object_name(value))) self._native = value # Set the unused bits to 0 @@ -1581,7 +1582,7 @@ class ParsableOctetString(Primitive): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) self.contents = other.contents self._native = other._native @@ -1626,7 +1627,7 @@ class ParsableOctetBitString(ParsableOctetString): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a byte string, not %s' % (object_name(self), object_name(value))) self._native = value # Set the unused bits to 0 @@ -1700,7 +1701,7 @@ class ObjectIdentifier(Primitive, ValueMap): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) self._native = value @@ -1820,16 +1821,16 @@ class Enumerated(Integer): """ if not isinstance(value, int_types) and not isinstance(value, str_cls): - raise ValueError('%s value must be an integer or a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be an integer or a unicode string, not %s' % (object_name(self), object_name(value))) if isinstance(value, str_cls): if value not in self._reverse_map: - raise ValueError('%s value "%s" is not a valid value' % (self.__class__.__name__, value)) + raise ValueError('%s value "%s" is not a valid value' % (object_name(self), value)) value = self._reverse_map[value] elif value not in self._map: - raise ValueError('%s value %s is not a valid value' % (self.__class__.__name__, value)) + raise ValueError('%s value %s is not a valid value' % (object_name(self), value)) Integer.set(self, value) @@ -1969,7 +1970,7 @@ class Sequence(Asn1Value): except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args raise e @property @@ -2053,11 +2054,11 @@ class Sequence(Asn1Value): if not isinstance(key, int_types): if key not in self._field_map: - raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) + raise KeyError('No field named "%s" defined for %s' % (key, object_name(self))) key = self._field_map[key] if key >= len(self.children): - raise KeyError('No field numbered %s is present in this %s' % (key, self.__class__.__name__)) + raise KeyError('No field numbered %s is present in this %s' % (key, object_name(self))) return self._lazy_child(key) @@ -2082,7 +2083,7 @@ class Sequence(Asn1Value): if not isinstance(key, int_types): if key not in self._field_map: - raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) + raise KeyError('No field named "%s" defined for %s' % (key, object_name(self))) key = self._field_map[key] field_name, field_spec, value_spec, field_params, _ = self._determine_spec(key) @@ -2098,7 +2099,7 @@ class Sequence(Asn1Value): invalid_value = new_value.contents is None if invalid_value: - raise ValueError('Value for field "%s" of %s is not set' % (field_name, self.__class__.__name__)) + raise ValueError('Value for field "%s" of %s is not set' % (field_name, object_name(self))) self.children[key] = new_value @@ -2123,12 +2124,12 @@ class Sequence(Asn1Value): if not isinstance(key, int_types): if key not in self._field_map: - raise KeyError('No field named "%s" defined for %s' % (key, self.__class__.__name__)) + raise KeyError('No field named "%s" defined for %s' % (key, object_name(self))) key = self._field_map[key] info = self._fields[key] if len(info) < 3 or ('default' not in info[2] and 'optional' not in info[2]): - raise ValueError('Can not delete the value for the field "%s" of %s since it is not optional or defaulted' % (info[0], self.__class__.__name__)) + raise ValueError('Can not delete the value for the field "%s" of %s since it is not optional or defaulted' % (info[0], object_name(self))) if 'optional' in info[2]: self.children[key] = NoValue() @@ -2433,7 +2434,7 @@ class Sequence(Asn1Value): except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args raise e def spec(self, field_name): @@ -2453,10 +2454,10 @@ class Sequence(Asn1Value): """ if not isinstance(field_name, str_cls): - raise ValueError('field_name must be a unicode string, not %s' % field_name.__class__.__name__) + raise ValueError('field_name must be a unicode string, not %s' % object_name(field_name)) if self._fields is None: - raise ValueError('Unable to retrieve spec for field %s in the class %s because _fields has not been set' % (repr(field_name), self.__class__.__name__)) + raise ValueError('Unable to retrieve spec for field %s in the class %s because _fields has not been set' % (repr(field_name), object_name(self))) index = self._field_map[field_name] info = self._determine_spec(index) @@ -2501,7 +2502,7 @@ class Sequence(Asn1Value): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) self.contents = other.contents self._native = other._native @@ -2611,7 +2612,7 @@ class SequenceOf(Asn1Value): except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args raise e @property @@ -2683,11 +2684,11 @@ class SequenceOf(Asn1Value): if isinstance(value, Asn1Value): new_value = value else: - raise ValueError('Can not set a native python value to %s where the _child_spec is Any – value must be an instance of Asn1Value' % self.__class__.__name__) + raise ValueError('Can not set a native python value to %s where the _child_spec is Any – value must be an instance of Asn1Value' % object_name(self)) elif issubclass(self._child_spec, Choice): if not isinstance(value, Asn1Value): - raise ValueError('Can not set a native python value to %s where the _child_spec is the choice type %s – value must be an instance of Asn1Value' % (self.__class__.__name__, self._child_spec.__name__)) + raise ValueError('Can not set a native python value to %s where the _child_spec is the choice type %s – value must be an instance of Asn1Value' % (object_name(self), self._child_spec.__name__)) if not isinstance(value, self._child_spec): wrapper = self._child_spec() wrapper.validate(value.class_, value.tag) @@ -2868,7 +2869,7 @@ class SequenceOf(Asn1Value): child_pointer += num_bytes except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args raise e def spec(self): @@ -2911,7 +2912,7 @@ class SequenceOf(Asn1Value): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (other.__class__.__name__, self.__class__.__name__)) + raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) self.contents = other.contents self._native = other._native @@ -3053,7 +3054,7 @@ class Set(Sequence): child_map[index] = field_info[1](**field_info[2]) if missing: - raise ValueError('Missing required field "%s" from %s' % (field_info[0], self.__class__.__name__)) + raise ValueError('Missing required field "%s" from %s' % (field_info[0], object_name(self))) self.children = [] for index in range(0, total_fields): @@ -3061,7 +3062,7 @@ class Set(Sequence): except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % self.__class__.__name__,) + args + e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args raise e @@ -3347,7 +3348,7 @@ def _basic_debug(prefix, self): The object to print the debugging information about """ - print('%s%s Object #%s' % (prefix, self.__class__.__name__, id(self))) + print('%s%s Object #%s' % (prefix, object_name(self), id(self))) if self._header: #pylint: disable=W0212 print('%s Header: 0x%s' % (prefix, binascii.hexlify(self._header or b'').decode('utf-8'))) #pylint: disable=W0212 @@ -3566,7 +3567,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param raise ValueError( 'Error parsing %s - explicitly-tagged class should have been %s, but %s was found' % ( - value.__class__.__name__, + object_name(value), CLASS_NUM_TO_NAME_MAP.get(value.explicit_class), CLASS_NUM_TO_NAME_MAP.get(class_, class_) ) @@ -3575,7 +3576,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param raise ValueError( 'Error parsing %s - explicitly-tagged method should have been %s, but %s was found' % ( - value.__class__.__name__, + object_name(value), METHOD_NUM_TO_NAME_MAP.get(1), METHOD_NUM_TO_NAME_MAP.get(method, method) ) @@ -3584,7 +3585,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param raise ValueError( 'Error parsing %s - explicitly-tagged tag should have been %s, but %s was found' % ( - value.__class__.__name__, + object_name(value), value.explicit_tag, tag ) @@ -3598,7 +3599,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param raise ValueError( 'Error parsing %s - class should have been %s, but %s was found' % ( - value.__class__.__name__, + object_name(value), CLASS_NUM_TO_NAME_MAP.get(value.class_), CLASS_NUM_TO_NAME_MAP.get(class_, class_) ) @@ -3607,7 +3608,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param raise ValueError( 'Error parsing %s - method should have been %s, but %s was found' % ( - value.__class__.__name__, + object_name(value), METHOD_NUM_TO_NAME_MAP.get(value.method), METHOD_NUM_TO_NAME_MAP.get(method, method) ) @@ -3616,7 +3617,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param raise ValueError( 'Error parsing %s - tag should have been %s, but %s was found' % ( - value.__class__.__name__, + object_name(value), value.tag, tag ) @@ -3705,7 +3706,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value.parse(nested_spec) except (ValueError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % value.__class__.__name__,) + args + e.args = (e.args[0] + '\n while parsing %s' % object_name(value),) + args raise e return value diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index d49907c..35daa2d 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -46,6 +46,7 @@ from ._elliptic_curve import ( PrimePoint, ) from .util import int_from_bytes, int_to_bytes +from ._errors import object_name try: # Python 2 @@ -533,7 +534,7 @@ class PrivateKeyInfo(Sequence): """ if not isinstance(private_key, byte_cls) and not isinstance(private_key, Asn1Value): - raise ValueError('private_key must be a byte string or Asn1Value, not %s' % private_key.__class__.__name__) + raise ValueError('private_key must be a byte string or Asn1Value, not %s' % object_name(private_key)) if algorithm == 'rsa': if not isinstance(private_key, RSAPrivateKey): @@ -921,7 +922,7 @@ class PublicKeyInfo(Sequence): """ if not isinstance(public_key, byte_cls) and not isinstance(public_key, Asn1Value): - raise ValueError('public_key must be a byte string or Asn1Value, not %s' % public_key.__class__.__name__) + raise ValueError('public_key must be a byte string or Asn1Value, not %s' % object_name(public_key)) if algorithm != 'rsa': raise ValueError('algorithm must "rsa", not %s' % repr(algorithm)) diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py index e4cd64d..b960981 100644 --- a/asn1crypto/pem.py +++ b/asn1crypto/pem.py @@ -15,6 +15,8 @@ import sys import base64 import re +from ._errors import object_name + if sys.version_info < (3,): str_cls = unicode #pylint: disable=E0602 byte_cls = str @@ -39,7 +41,7 @@ def detect(byte_string): """ if not isinstance(byte_string, byte_cls): - raise ValueError('byte_string must be a byte string, not %s' % byte_string.__class__.__name__) + raise ValueError('byte_string must be a byte string, not %s' % object_name(byte_string)) return byte_string.find(b'-----BEGIN') != -1 or byte_string.find(b'---- BEGIN') != -1 @@ -65,10 +67,10 @@ def armor(type_name, der_bytes, headers=None): """ if not isinstance(der_bytes, byte_cls): - raise ValueError('der_bytes must be a byte string, not %s' % der_bytes.__class__.__name__) + raise ValueError('der_bytes must be a byte string, not %s' % object_name(der_bytes)) if not isinstance(type_name, str_cls): - raise ValueError('type_name must be a unicode string, not %s' % type_name.__class__.__name__) + raise ValueError('type_name must be a unicode string, not %s' % object_name(type_name)) type_name = type_name.upper().encode('ascii') @@ -116,7 +118,7 @@ def _unarmor(pem_bytes): """ if not isinstance(pem_bytes, byte_cls): - raise ValueError('pem_bytes must be a byte string, not %s' % pem_bytes.__class__.__name__) + raise ValueError('pem_bytes must be a byte string, not %s' % object_name(pem_bytes)) # Valid states include: "trash", "headers", "body" state = 'trash' diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 794544b..560a16f 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -55,6 +55,7 @@ from .core import ( from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo from .util import int_to_bytes, int_from_bytes +from ._errors import object_name if sys.version_info < (3,): str_cls = unicode #pylint: disable=E0602 @@ -116,7 +117,7 @@ class URI(IA5String): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) self._normalized = True self._native = value @@ -265,7 +266,7 @@ class EmailAddress(IA5String): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) if value.find('@') != -1: mailbox, hostname = value.rsplit('@', 1) @@ -347,7 +348,7 @@ class IPAddress(OctetString): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (self.__class__.__name__, value.__class__.__name__)) + raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) original_value = value @@ -358,17 +359,17 @@ class IPAddress(OctetString): value = parts[0] cidr = int(parts[1]) if cidr < 0: - raise ValueError('%s value contains a CIDR range less than 0' % self.__class__.__name__) + raise ValueError('%s value contains a CIDR range less than 0' % object_name(self)) if value.find(':') != -1: family = socket.AF_INET6 if cidr > 128: - raise ValueError('%s value contains a CIDR range bigger than 128, the maximum value for an IPv6 address' % self.__class__.__name__) + raise ValueError('%s value contains a CIDR range bigger than 128, the maximum value for an IPv6 address' % object_name(self)) cidr_size = 128 else: family = socket.AF_INET if cidr > 32: - raise ValueError('%s value contains a CIDR range bigger than 32, the maximum value for an IPv4 address' % self.__class__.__name__) + raise ValueError('%s value contains a CIDR range bigger than 32, the maximum value for an IPv4 address' % object_name(self)) cidr_size = 32 cidr_bytes = b'' -- cgit v1.2.3 From d35103ed83c74b5ba866a9e7c6b53e4cd00cbc79 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 10 Aug 2015 08:13:49 -0400 Subject: Added links to other crypto libraries --- readme.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/readme.md b/readme.md index cf39370..2824566 100644 --- a/readme.md +++ b/readme.md @@ -20,6 +20,13 @@ a bunch of ASN.1 structures for use with various common cryptography standards: | TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC 3161](https://tools.ietf.org/html/rfc3161) | | PDF signatures | [`asn1crypto.pdf`](asn1crypto/pdf.py) | [PDF 1.7](http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf) | +*asn1crypto* is part of the modularcrypto family of Python packages: + + - [asn1crypto](https://github.com/wbond/asn1crypto) + - [oscrypto](https://github.com/wbond/oscrypto) + - [certbuilder](https://github.com/wbond/certbuilder) + - [crlbuilder](https://github.com/wbond/crlbuilder) + ## License *asn1crypto* is licensed under the terms of the MIT license. See the -- cgit v1.2.3 From b8dafcebbfd947f502444786c0a5bfc60a713258 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 09:28:01 -0400 Subject: Fix dict literal that was typoed to a set literal --- asn1crypto/cms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index f243504..a0c503a 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -175,7 +175,7 @@ class AttCertValidityPeriod(Sequence): class AttributeCertificateInfoV1(Sequence): _fields = [ - ('version', AttCertVersion, {'default', 'v1'}), + ('version', AttCertVersion, {'default': 'v1'}), ('subject', AttCertSubject), ('issuer', GeneralNames), ('signature', SignedDigestAlgorithm), -- cgit v1.2.3 From 44b89191c503d812419d5e7d3f8202adccc318e4 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 09:34:01 -0400 Subject: Added OrderedDict shim for Python 2.6 --- asn1crypto/_ordereddict.py | 135 ++++++++++++++++++++++++++++++++++ asn1crypto/core.py | 2 +- asn1crypto/util.py | 3 + asn1crypto/x509.py | 2 +- tests/test_cms.py | 57 +++++++-------- tests/test_csr.py | 19 +++-- tests/test_keys.py | 15 ++-- tests/test_pem.py | 5 +- tests/test_tsp.py | 11 ++- tests/test_x509.py | 177 ++++++++++++++++++++++----------------------- 10 files changed, 279 insertions(+), 147 deletions(-) create mode 100644 asn1crypto/_ordereddict.py diff --git a/asn1crypto/_ordereddict.py b/asn1crypto/_ordereddict.py new file mode 100644 index 0000000..69af2c3 --- /dev/null +++ b/asn1crypto/_ordereddict.py @@ -0,0 +1,135 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# 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. + +import sys + +if not sys.version_info < (2, 7): + from collections import OrderedDict + + +else: + from UserDict import DictMixin #pylint: disable=F0401 + + + class OrderedDict(dict, DictMixin): + + def __init__(self, *args, **kwds): #pylint: disable=W0231 + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) + + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) + + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next_ = self.__map.pop(key) + prev[2] = next_ + next_[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.__end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() #pylint: disable=E0111 + else: + key = iter(self).next() + value = self.pop(key) + return key, value + + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def keys(self): + return list(self) + + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d + + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): + return False + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) + + def __ne__(self, other): + return not self == other diff --git a/asn1crypto/core.py b/asn1crypto/core.py index c2d7786..dbf6378 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -49,11 +49,11 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math import sys import re -from collections import OrderedDict from datetime import datetime import binascii from . import _teletex_codec +from ._ordereddict import OrderedDict from .util import int_to_bytes, int_from_bytes, timezone from ._errors import object_name diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 90ae7b8..f3d2869 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -4,6 +4,7 @@ Miscellaneous data helpers, including functions for converting integers to and from bytes and UTC timezone. Exports the following items: + - OrderedDict() - int_from_bytes() - int_to_bytes() - timezone.utc @@ -15,6 +16,8 @@ import sys import math +from ._ordereddict import OrderedDict + # Python 2 if sys.version_info <= (3,): diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 560a16f..c360187 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -21,10 +21,10 @@ import hashlib import stringprep import unicodedata import socket -from collections import OrderedDict from encodings import idna #pylint: disable=W0611 import codecs +from ._ordereddict import OrderedDict from .core import ( Any, BitString, diff --git a/tests/test_cms.py b/tests/test_cms.py index a3caf8e..cda4b05 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import unittest import os from datetime import datetime -from collections import OrderedDict from asn1crypto import cms, util @@ -160,10 +159,10 @@ class CMSTests(unittest.TestCase): recipient['version'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ( 'issuer', - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -230,7 +229,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]) @@ -268,10 +267,10 @@ class CMSTests(unittest.TestCase): signer['version'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ( 'issuer', - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -289,7 +288,7 @@ class CMSTests(unittest.TestCase): signer['sid'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]), @@ -328,7 +327,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), @@ -356,7 +355,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]) @@ -394,10 +393,10 @@ class CMSTests(unittest.TestCase): signer['version'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ( 'issuer', - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -415,7 +414,7 @@ class CMSTests(unittest.TestCase): signer['sid'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]), @@ -454,7 +453,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), @@ -482,7 +481,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]) @@ -494,18 +493,18 @@ class CMSTests(unittest.TestCase): encap_content_info['content_type'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('version', 'v0'), ( 'digest_algorithm', - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha1'), ('parameters', None), ]) ), ( 'encap_content_info', - OrderedDict([ + util.OrderedDict([ ('content_type', 'data'), ('content', b'This is the message to encapsulate in PKCS#7/CMS\n'), ]) @@ -540,10 +539,10 @@ class CMSTests(unittest.TestCase): signer['version'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ( 'issuer', - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -561,7 +560,7 @@ class CMSTests(unittest.TestCase): signer['sid'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]), @@ -576,7 +575,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), @@ -605,7 +604,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]) @@ -617,18 +616,18 @@ class CMSTests(unittest.TestCase): encap_content_info['content_type'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('version', 'v0'), ( 'digest_algorithm', - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha1'), ('parameters', None), ]) ), ( 'encap_content_info', - OrderedDict([ + util.OrderedDict([ ('content_type', 'data'), ('content', b'This is the message to encapsulate in PKCS#7/CMS\n'), ]) @@ -663,10 +662,10 @@ class CMSTests(unittest.TestCase): signer['version'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ( 'issuer', - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -684,7 +683,7 @@ class CMSTests(unittest.TestCase): signer['sid'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'sha256'), ('parameters', None), ]), @@ -699,7 +698,7 @@ class CMSTests(unittest.TestCase): ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'rsassa_pkcs1v15'), ('parameters', None), ]), diff --git a/tests/test_csr.py b/tests/test_csr.py index 4ffff69..772203e 100644 --- a/tests/test_csr.py +++ b/tests/test_csr.py @@ -4,9 +4,8 @@ from __future__ import unicode_literals import unittest import sys import os -from collections import OrderedDict -from asn1crypto import csr +from asn1crypto import csr, util if sys.version_info < (3,): byte_cls = str @@ -34,7 +33,7 @@ class CSRTests(unittest.TestCase): ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -46,7 +45,7 @@ class CSRTests(unittest.TestCase): cri['subject'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'rsa'), ('parameters', None), ]), @@ -78,7 +77,7 @@ class CSRTests(unittest.TestCase): ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -90,7 +89,7 @@ class CSRTests(unittest.TestCase): cri['subject'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('algorithm', 'rsa'), ('parameters', None), ]), @@ -106,24 +105,24 @@ class CSRTests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('type', 'extension_request'), ( 'values', [ [ - OrderedDict([ + util.OrderedDict([ ('extn_id', 'basic_constraints'), ('critical', False), ( 'extn_value', - OrderedDict([ + util.OrderedDict([ ('ca', False), ('path_len_constraint', None), ]) ), ]), - OrderedDict([ + util.OrderedDict([ ('extn_id', 'key_usage'), ('critical', False), ( diff --git a/tests/test_keys.py b/tests/test_keys.py index fc59ff8..6eb5b07 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -3,9 +3,8 @@ from __future__ import unicode_literals import unittest import os -from collections import OrderedDict -from asn1crypto import keys, core +from asn1crypto import keys, core, util from .unittest_data import DataDecorator, data @@ -148,18 +147,18 @@ class KeysTests(unittest.TestCase): key['private_key'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('version', 'ecdpVer1'), ( 'field_id', - OrderedDict([ + util.OrderedDict([ ('field_type', 'prime_field'), ('parameters', 115792089210356248762697446949407573530086143415290314195533631308867097853951) ]) ), ( 'curve', - OrderedDict([ + util.OrderedDict([ ('a', b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC'), ('b', b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B'), ('seed', b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90'), @@ -426,18 +425,18 @@ class KeysTests(unittest.TestCase): curve = ( 'specified', - OrderedDict([ + util.OrderedDict([ ('version', 'ecdpVer1'), ( 'field_id', - OrderedDict([ + util.OrderedDict([ ('field_type', 'prime_field'), ('parameters', 115792089210356248762697446949407573530086143415290314195533631308867097853951) ]) ), ( 'curve', - OrderedDict([ + util.OrderedDict([ ('a', b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC'), ('b', b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B'), ('seed', b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90'), diff --git a/tests/test_pem.py b/tests/test_pem.py index 744a746..05e5cbc 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -4,9 +4,8 @@ from __future__ import unicode_literals import unittest import sys import os -from collections import OrderedDict -from asn1crypto import pem +from asn1crypto import pem, util from .unittest_data import DataDecorator, data @@ -52,7 +51,7 @@ class PEMTests(unittest.TestCase): ('keys/test-third.crt', 'keys/test-third-der.crt', 'CERTIFICATE', {}), ('keys/test-pkcs8.key', 'keys/test-pkcs8-der.key', 'PRIVATE KEY', {}), ('test-third.csr', 'test-third-der.csr', 'CERTIFICATE REQUEST', {}), - ('keys/test-aes128.key', 'keys/test-aes128-der.key', 'RSA PRIVATE KEY', OrderedDict([('Proc-Type', '4,ENCRYPTED'), ('DEK-Info', 'AES-128-CBC,01F6EE04516C912788B11BD7377626C2')])), + ('keys/test-aes128.key', 'keys/test-aes128-der.key', 'RSA PRIVATE KEY', util.OrderedDict([('Proc-Type', '4,ENCRYPTED'), ('DEK-Info', 'AES-128-CBC,01F6EE04516C912788B11BD7377626C2')])), ) @data('unarmor_armor_files') diff --git a/tests/test_tsp.py b/tests/test_tsp.py index b5f542f..0a8f90d 100644 --- a/tests/test_tsp.py +++ b/tests/test_tsp.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import unittest import os from datetime import datetime -from collections import OrderedDict from asn1crypto import tsp, cms, util @@ -139,7 +138,7 @@ class TSPTests(unittest.TestCase): tst_info['nonce'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('organization_name', 'GeoTrust Inc'), ('common_name', 'GeoTrust Timestamping Signer 1'), @@ -167,10 +166,10 @@ class TSPTests(unittest.TestCase): signer_info['version'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ( 'issuer', - OrderedDict([ + util.OrderedDict([ ('country_name', 'ZA'), ('state_or_province_name', 'Western Cape'), ('locality_name', 'Durbanville'), @@ -223,11 +222,11 @@ class TSPTests(unittest.TestCase): signed_attrs[3]['type'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ( 'certs', [ - OrderedDict([ + util.OrderedDict([ ('cert_hash', b'\x22\x3C\xDA\x27\x07\x96\x73\x81\x6B\x60\x8A\x1B\x8C\xB0\xAB\x02\x30\x10\x7F\xCC'), ('issuer_serial', None), ]) diff --git a/tests/test_x509.py b/tests/test_x509.py index c04c36f..817e649 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import unittest import sys import os -from collections import OrderedDict from datetime import datetime from asn1crypto import x509, core, pem, util @@ -340,7 +339,7 @@ class X509Tests(unittest.TestCase): ('keys/test-third-der.crt', None), ('geotrust_certs/GeoTrust_Universal_CA.crt', None), ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [OrderedDict([('common_name', 'SymantecPKI-1-538')])]), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [util.OrderedDict([('common_name', 'SymantecPKI-1-538')])]), ('geotrust_certs/codex.crt', ['dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net', 'codexns.io']), ('lets_encrypt/isrgrootx1.pem', None), ('lets_encrypt/letsencryptauthorityx1.pem', None), @@ -401,19 +400,19 @@ class X509Tests(unittest.TestCase): ('lets_encrypt/letsencryptauthorityx2.pem', None), ( 'globalsign_example_keys/IssuingCA-der.cer', - OrderedDict([ + util.OrderedDict([ ( 'permitted_subtrees', [ - OrderedDict([ + util.OrderedDict([ ('base', 'onlythis.com'), ('minimum', 0), ('maximum', None) ]), - OrderedDict([ + util.OrderedDict([ ( 'base', - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'MA'), ('locality_name', 'Boston'), @@ -428,12 +427,12 @@ class X509Tests(unittest.TestCase): ( 'excluded_subtrees', [ - OrderedDict([ + util.OrderedDict([ ('base', '0.0.0.0/0'), ('minimum', 0), ('maximum', None) ]), - OrderedDict([ + util.OrderedDict([ ('base', '::/0'), ('minimum', 0), ('maximum', None) @@ -466,7 +465,7 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']), ('reasons', None), ('crl_issuer', None) @@ -476,7 +475,7 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/codex.crt', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://gm.symcb.com/gm.crl']), ('reasons', None), ('crl_issuer', None) @@ -487,7 +486,7 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx1.pem', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), ('reasons', None), ('crl_issuer', None) @@ -497,7 +496,7 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx2.pem', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), ('reasons', None), ('crl_issuer', None) @@ -507,7 +506,7 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/IssuingCA-der.cer', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), ('reasons', None), ('crl_issuer', None) @@ -516,7 +515,7 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/rootCA.cer', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), ('reasons', None), ('crl_issuer', None) @@ -545,12 +544,12 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', 'any_policy'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'https://www.geotrust.com/resources/cps') ]) @@ -562,20 +561,20 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/codex.crt', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '1.3.6.1.4.1.14370.1.6'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'https://www.geotrust.com/resources/repository/legal') ]), - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'user_notice'), ( 'qualifier', - OrderedDict([ + util.OrderedDict([ ('notice_ref', None), ('explicit_text', 'https://www.geotrust.com/resources/repository/legal') ]) @@ -590,16 +589,16 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx1.pem', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '2.23.140.1.2.1'), ('policy_qualifiers', None) ]), - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '1.3.6.1.4.1.44947.1.1.1'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'http://cps.root-x1.letsencrypt.org') ]) @@ -611,16 +610,16 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx2.pem', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '2.23.140.1.2.1'), ('policy_qualifiers', None) ]), - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '1.3.6.1.4.1.44947.1.1.1'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'http://cps.root-x1.letsencrypt.org') ]) @@ -632,12 +631,12 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/IssuingCA-der.cer', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'https://www.globalsign.com/repository/') ]) @@ -650,12 +649,12 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/SSL1.cer', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'https://www.globalsign.com/repository/') ]) @@ -667,12 +666,12 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/SSL2.cer', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'https://www.globalsign.com/repository/') ]) @@ -684,12 +683,12 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/SSL3.cer', [ - OrderedDict([ + util.OrderedDict([ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'), ( 'policy_qualifiers', [ - OrderedDict([ + util.OrderedDict([ ('policy_qualifier_id', 'certification_practice_statement'), ('qualifier', 'https://www.globalsign.com/repository/') ]) @@ -739,12 +738,12 @@ class X509Tests(unittest.TestCase): return ( ( 'keys/test-der.crt', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), ( 'authority_cert_issuer', [ - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -760,7 +759,7 @@ class X509Tests(unittest.TestCase): ), ( 'keys/test-inter-der.crt', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -768,7 +767,7 @@ class X509Tests(unittest.TestCase): ), ( 'keys/test-third-der.crt', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -776,7 +775,7 @@ class X509Tests(unittest.TestCase): ), ( 'geotrust_certs/GeoTrust_Universal_CA.crt', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -788,7 +787,7 @@ class X509Tests(unittest.TestCase): ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -796,7 +795,7 @@ class X509Tests(unittest.TestCase): ), ( 'geotrust_certs/codex.crt', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -808,7 +807,7 @@ class X509Tests(unittest.TestCase): ), ( 'lets_encrypt/letsencryptauthorityx1.pem', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -816,7 +815,7 @@ class X509Tests(unittest.TestCase): ), ( 'lets_encrypt/letsencryptauthorityx2.pem', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -824,7 +823,7 @@ class X509Tests(unittest.TestCase): ), ( 'globalsign_example_keys/IssuingCA-der.cer', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -836,7 +835,7 @@ class X509Tests(unittest.TestCase): ), ( 'globalsign_example_keys/SSL1.cer', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -844,7 +843,7 @@ class X509Tests(unittest.TestCase): ), ( 'globalsign_example_keys/SSL2.cer', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -852,7 +851,7 @@ class X509Tests(unittest.TestCase): ), ( 'globalsign_example_keys/SSL3.cer', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), ('authority_cert_issuer', None), ('authority_cert_serial_number', None) @@ -932,7 +931,7 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ - OrderedDict([ + util.OrderedDict([ ('access_method', 'ocsp'), ('access_location', 'http://g2.symcb.com') ]) @@ -941,11 +940,11 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/codex.crt', [ - OrderedDict([ + util.OrderedDict([ ('access_method', 'ocsp'), ('access_location', 'http://gm.symcd.com') ]), - OrderedDict([ + util.OrderedDict([ ('access_method', 'ca_issuers'), ('access_location', 'http://gm.symcb.com/gm.crt') ]), @@ -955,11 +954,11 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx1.pem', [ - OrderedDict([ + util.OrderedDict([ ('access_method', 'ocsp'), ('access_location', 'http://ocsp.root-x1.letsencrypt.org/') ]), - OrderedDict([ + util.OrderedDict([ ('access_method', 'ca_issuers'), ('access_location', 'http://cert.root-x1.letsencrypt.org/') ]) @@ -968,11 +967,11 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx2.pem', [ - OrderedDict([ + util.OrderedDict([ ('access_method', 'ocsp'), ('access_location', 'http://ocsp.root-x1.letsencrypt.org/') ]), - OrderedDict([ + util.OrderedDict([ ('access_method', 'ca_issuers'), ('access_location', 'http://cert.root-x1.letsencrypt.org/') ]) @@ -983,11 +982,11 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/SSL1.cer', [ - OrderedDict([ + util.OrderedDict([ ('access_method', 'ocsp'), ('access_location', 'http://ocsp.exampleovca.com/') ]), - OrderedDict([ + util.OrderedDict([ ('access_method', 'ca_issuers'), ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt') ]) @@ -996,11 +995,11 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/SSL2.cer', [ - OrderedDict([ + util.OrderedDict([ ('access_method', 'ocsp'), ('access_location', 'http://ocsp.exampleovca.com/') ]), - OrderedDict([ + util.OrderedDict([ ('access_method', 'ca_issuers'), ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt') ]) @@ -1009,11 +1008,11 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/SSL3.cer', [ - OrderedDict([ + util.OrderedDict([ ('access_method', 'ocsp'), ('access_location', 'http://ocsp.exampleovca.com/') ]), - OrderedDict([ + util.OrderedDict([ ('access_method', 'ca_issuers'), ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt') ]) @@ -1222,7 +1221,7 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']), ('reasons', None), ('crl_issuer', None) @@ -1232,7 +1231,7 @@ class X509Tests(unittest.TestCase): ( 'geotrust_certs/codex.crt', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://gm.symcb.com/gm.crl']), ('reasons', None), ('crl_issuer', None) @@ -1243,7 +1242,7 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx1.pem', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), ('reasons', None), ('crl_issuer', None) @@ -1253,7 +1252,7 @@ class X509Tests(unittest.TestCase): ( 'lets_encrypt/letsencryptauthorityx2.pem', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']), ('reasons', None), ('crl_issuer', None) @@ -1263,7 +1262,7 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/IssuingCA-der.cer', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), ('reasons', None), ('crl_issuer', None) @@ -1273,7 +1272,7 @@ class X509Tests(unittest.TestCase): ( 'globalsign_example_keys/rootCA.cer', [ - OrderedDict([ + util.OrderedDict([ ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']), ('reasons', None), ('crl_issuer', None) @@ -1425,7 +1424,7 @@ class X509Tests(unittest.TestCase): signature['parameters'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -1445,7 +1444,7 @@ class X509Tests(unittest.TestCase): validity['not_after'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -1497,22 +1496,22 @@ class X509Tests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('extn_id', 'key_identifier'), ('critical', False), ('extn_value', b'\xBE\x42\x85\x3D\xCC\xFF\xE3\xF9\x28\x02\x8F\x7E\x58\x56\xB4\xFD\x03\x5C\xEA\x4B'), ]), - OrderedDict([ + util.OrderedDict([ ('extn_id', 'authority_key_identifier'), ('critical', False), ( 'extn_value', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\xBE\x42\x85\x3D\xCC\xFF\xE3\xF9\x28\x02\x8F\x7E\x58\x56\xB4\xFD\x03\x5C\xEA\x4B'), ( 'authority_cert_issuer', [ - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -1527,12 +1526,12 @@ class X509Tests(unittest.TestCase): ]) ), ]), - OrderedDict([ + util.OrderedDict([ ('extn_id', 'basic_constraints'), ('critical', False), ( 'extn_value', - OrderedDict([ + util.OrderedDict([ ('ca', True), ('path_len_constraint', None) ]) @@ -1572,7 +1571,7 @@ class X509Tests(unittest.TestCase): signature['parameters'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -1592,7 +1591,7 @@ class X509Tests(unittest.TestCase): validity['not_after'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -1608,7 +1607,7 @@ class X509Tests(unittest.TestCase): subject_public_key_algorithm['algorithm'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('p', 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857), ('q', 71587850165936478337655415373676526523562874562337607790945426056266440596923), ('g', 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202), @@ -1644,29 +1643,29 @@ class X509Tests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('extn_id', 'key_identifier'), ('critical', False), ('extn_value', b'\x81\xA3\x37\x86\xF9\x99\x28\xF2\x74\x70\x60\x87\xF2\xD3\x7E\x8D\x19\x61\xA8\xBE'), ]), - OrderedDict([ + util.OrderedDict([ ('extn_id', 'authority_key_identifier'), ('critical', False), ( 'extn_value', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\x81\xA3\x37\x86\xF9\x99\x28\xF2\x74\x70\x60\x87\xF2\xD3\x7E\x8D\x19\x61\xA8\xBE'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None), ]) ), ]), - OrderedDict([ + util.OrderedDict([ ('extn_id', 'basic_constraints'), ('critical', False), ( 'extn_value', - OrderedDict([ + util.OrderedDict([ ('ca', True), ('path_len_constraint', None) ]) @@ -1709,7 +1708,7 @@ class X509Tests(unittest.TestCase): signature['parameters'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -1729,7 +1728,7 @@ class X509Tests(unittest.TestCase): validity['not_after'].native ) self.assertEqual( - OrderedDict([ + util.OrderedDict([ ('country_name', 'US'), ('state_or_province_name', 'Massachusetts'), ('locality_name', 'Newbury'), @@ -1813,29 +1812,29 @@ class X509Tests(unittest.TestCase): ) self.assertEqual( [ - OrderedDict([ + util.OrderedDict([ ('extn_id', 'key_identifier'), ('critical', False), ('extn_value', b'\x54\xAA\x54\x70\x6C\x34\x1A\x6D\xEB\x5D\x97\xD7\x1E\xFC\xD5\x24\x3C\x8A\x0E\xD7'), ]), - OrderedDict([ + util.OrderedDict([ ('extn_id', 'authority_key_identifier'), ('critical', False), ( 'extn_value', - OrderedDict([ + util.OrderedDict([ ('key_identifier', b'\x54\xAA\x54\x70\x6C\x34\x1A\x6D\xEB\x5D\x97\xD7\x1E\xFC\xD5\x24\x3C\x8A\x0E\xD7'), ('authority_cert_issuer', None), ('authority_cert_serial_number', None), ]) ), ]), - OrderedDict([ + util.OrderedDict([ ('extn_id', 'basic_constraints'), ('critical', False), ( 'extn_value', - OrderedDict([ + util.OrderedDict([ ('ca', True), ('path_len_constraint', None) ]) -- cgit v1.2.3 From 407e9e3f38b2900f281bfaeabf23538060927574 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 09:35:28 -0400 Subject: Removed set literals for Python 2.6 support --- asn1crypto/core.py | 4 ++-- asn1crypto/keys.py | 2 +- asn1crypto/x509.py | 6 +++--- tests/test_core.py | 12 ++++++------ tests/test_csr.py | 2 +- tests/test_x509.py | 48 ++++++++++++++++++++++++------------------------ 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index dbf6378..b55c42e 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -892,8 +892,8 @@ class Primitive(Asn1Value): # If the objects share a common base class that is not too low-level # then we can compare the contents - self_bases = (set(self.__class__.__bases__) | {self.__class__}) - {Asn1Value, Primitive, ValueMap} - other_bases = (set(other.__class__.__bases__) | {other.__class__}) - {Asn1Value, Primitive, ValueMap} + self_bases = (set(self.__class__.__bases__) | set([self.__class__])) - set([Asn1Value, Primitive, ValueMap]) + other_bases = (set(other.__class__.__bases__) | set([other.__class__])) - set([Asn1Value, Primitive, ValueMap]) if self_bases | other_bases: return self.contents == other.contents diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 35daa2d..e6e39b2 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -197,7 +197,7 @@ class _ECPoint(): y = int_from_bytes(remaining[field_len:]) return (x, y) - if first_byte not in {b'\x02', b'\x03'}: + if first_byte not in set([b'\x02', b'\x03']): raise ValueError('Invalid EC public key - first byte is incorrect') raise ValueError('Compressed representations of EC public keys are not supported due to patent US6252960') diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index c360187..454d71a 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -401,11 +401,11 @@ class IPAddress(OctetString): byte_string = self.__bytes__() byte_len = len(byte_string) cidr_int = None - if byte_len in {32, 16}: + if byte_len in set([32, 16]): value = inet_ntop(socket.AF_INET6, byte_string[0:16]) if byte_len > 16: cidr_int = int_from_bytes(byte_string[16:]) - elif byte_len in {8, 4}: + elif byte_len in set([8, 4]): value = inet_ntop(socket.AF_INET, byte_string[0:4]) if byte_len > 4: cidr_int = int_from_bytes(byte_string[4:]) @@ -871,7 +871,7 @@ class Name(Choice): if attribute_name not in name_dict: continue - if attribute_name in {'email_address', 'domain_component'}: + if attribute_name in set(['email_address', 'domain_component']): value = IA5String(name_dict[attribute_name]) else: value = DirectoryString(name='utf8_string', value=UTF8String(name_dict[attribute_name])) diff --git a/tests/test_core.py b/tests/test_core.py index b41e523..f49b2c2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -173,7 +173,7 @@ class CoreTests(unittest.TestCase): ( (0, 1, 1), b'\x03\x02\x00\x60', - {'one', 'two'} + set(['one', 'two']) ), ( (0,), @@ -181,9 +181,9 @@ class CoreTests(unittest.TestCase): set() ), ( - {'one', 'two'}, + set(['one', 'two']), b'\x03\x02\x00\x60', - {'one', 'two'} + set(['one', 'two']) ) ) @@ -201,10 +201,10 @@ class CoreTests(unittest.TestCase): self.assertEqual(True, 'one' in named.native) def test_mapped_bit_string_unset_bit(self): - named = NamedBits({'one', 'two'}) + named = NamedBits(set(['one', 'two'])) named['one'] = False self.assertEqual(True, named['two']) - self.assertEqual({'two'}, named.native) + self.assertEqual(set(['two']), named.native) def test_mapped_bit_string_sparse(self): named = NamedBits((0, 0, 0, 0, 0, 1)) @@ -216,7 +216,7 @@ class CoreTests(unittest.TestCase): named = NamedBits() named[1] = True self.assertEqual(True, named['one']) - self.assertEqual({'one'}, named.native) + self.assertEqual(set(['one']), named.native) def test_get_sequence_value(self): seq = SequenceOfInts([1, 2]) diff --git a/tests/test_csr.py b/tests/test_csr.py index 772203e..bf45220 100644 --- a/tests/test_csr.py +++ b/tests/test_csr.py @@ -127,7 +127,7 @@ class CSRTests(unittest.TestCase): ('critical', False), ( 'extn_value', - {'digital_signature', 'non_repudiation', 'key_encipherment'}, + set(['digital_signature', 'non_repudiation', 'key_encipherment']), ), ]) ] diff --git a/tests/test_x509.py b/tests/test_x509.py index 817e649..00c782c 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -221,18 +221,18 @@ class X509Tests(unittest.TestCase): ('keys/test-der.crt', set()), ('keys/test-inter-der.crt', set()), ('keys/test-third-der.crt', set()), - ('geotrust_certs/GeoTrust_Universal_CA.crt', {'basic_constraints', 'key_usage'}), - ('geotrust_certs/GeoTrust_Primary_CA.crt', {'basic_constraints', 'key_usage'}), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', {'basic_constraints', 'key_usage'}), - ('geotrust_certs/codex.crt', {'key_usage'}), - ('lets_encrypt/isrgrootx1.pem', {'key_usage', 'basic_constraints'}), - ('lets_encrypt/letsencryptauthorityx1.pem', {'key_usage', 'basic_constraints'}), - ('lets_encrypt/letsencryptauthorityx2.pem', {'key_usage', 'basic_constraints'}), - ('globalsign_example_keys/IssuingCA-der.cer', {'basic_constraints', 'key_usage'}), - ('globalsign_example_keys/rootCA.cer', {'basic_constraints', 'key_usage'}), - ('globalsign_example_keys/SSL1.cer', {'key_usage', 'extended_key_usage', 'basic_constraints'}), - ('globalsign_example_keys/SSL2.cer', {'key_usage', 'extended_key_usage', 'basic_constraints'}), - ('globalsign_example_keys/SSL3.cer', {'key_usage', 'extended_key_usage', 'basic_constraints'}), + ('geotrust_certs/GeoTrust_Universal_CA.crt', set(['basic_constraints', 'key_usage'])), + ('geotrust_certs/GeoTrust_Primary_CA.crt', set(['basic_constraints', 'key_usage'])), + ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', set(['basic_constraints', 'key_usage'])), + ('geotrust_certs/codex.crt', set(['key_usage'])), + ('lets_encrypt/isrgrootx1.pem', set(['key_usage', 'basic_constraints'])), + ('lets_encrypt/letsencryptauthorityx1.pem', set(['key_usage', 'basic_constraints'])), + ('lets_encrypt/letsencryptauthorityx2.pem', set(['key_usage', 'basic_constraints'])), + ('globalsign_example_keys/IssuingCA-der.cer', set(['basic_constraints', 'key_usage'])), + ('globalsign_example_keys/rootCA.cer', set(['basic_constraints', 'key_usage'])), + ('globalsign_example_keys/SSL1.cer', set(['key_usage', 'extended_key_usage', 'basic_constraints'])), + ('globalsign_example_keys/SSL2.cer', set(['key_usage', 'extended_key_usage', 'basic_constraints'])), + ('globalsign_example_keys/SSL3.cer', set(['key_usage', 'extended_key_usage', 'basic_constraints'])), ) @data('critical_extensions_info') @@ -276,51 +276,51 @@ class X509Tests(unittest.TestCase): ('keys/test-third-der.crt', None), ( 'geotrust_certs/GeoTrust_Universal_CA.crt', - {'digital_signature', 'key_cert_sign', 'crl_sign'} + set(['digital_signature', 'key_cert_sign', 'crl_sign']) ), ( 'geotrust_certs/GeoTrust_Primary_CA.crt', - {'key_cert_sign', 'crl_sign'} + set(['key_cert_sign', 'crl_sign']) ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', - {'key_cert_sign', 'crl_sign'} + set(['key_cert_sign', 'crl_sign']) ), ( 'geotrust_certs/codex.crt', - {'digital_signature', 'key_encipherment'} + set(['digital_signature', 'key_encipherment']) ), ( 'lets_encrypt/isrgrootx1.pem', - {'key_cert_sign', 'crl_sign'} + set(['key_cert_sign', 'crl_sign']) ), ( 'lets_encrypt/letsencryptauthorityx1.pem', - {'digital_signature', 'key_cert_sign', 'crl_sign'} + set(['digital_signature', 'key_cert_sign', 'crl_sign']) ), ( 'lets_encrypt/letsencryptauthorityx2.pem', - {'digital_signature', 'key_cert_sign', 'crl_sign'} + set(['digital_signature', 'key_cert_sign', 'crl_sign']) ), ( 'globalsign_example_keys/IssuingCA-der.cer', - {'key_cert_sign', 'crl_sign'} + set(['key_cert_sign', 'crl_sign']) ), ( 'globalsign_example_keys/rootCA.cer', - {'key_cert_sign', 'crl_sign'} + set(['key_cert_sign', 'crl_sign']) ), ( 'globalsign_example_keys/SSL1.cer', - {'digital_signature', 'key_encipherment'} + set(['digital_signature', 'key_encipherment']) ), ( 'globalsign_example_keys/SSL2.cer', - {'digital_signature', 'key_encipherment'} + set(['digital_signature', 'key_encipherment']) ), ( 'globalsign_example_keys/SSL3.cer', - {'digital_signature', 'key_encipherment'} + set(['digital_signature', 'key_encipherment']) ), ) -- cgit v1.2.3 From 284814a757c710b9eadfeb32999d2314cf3b6f1e Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 09:36:27 -0400 Subject: Consistent __future__ imports for tests --- tests/test_cms.py | 2 +- tests/test_core.py | 2 +- tests/test_crl.py | 2 +- tests/test_csr.py | 2 +- tests/test_keys.py | 2 +- tests/test_ocsp.py | 2 +- tests/test_pem.py | 2 +- tests/test_tsp.py | 2 +- tests/test_x509.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_cms.py b/tests/test_cms.py index cda4b05..7c5945f 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import os diff --git a/tests/test_core.py b/tests/test_core.py index f49b2c2..ab80536 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import os diff --git a/tests/test_crl.py b/tests/test_crl.py index 5367aa3..29dba58 100644 --- a/tests/test_crl.py +++ b/tests/test_crl.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import sys diff --git a/tests/test_csr.py b/tests/test_csr.py index bf45220..9cc8a8b 100644 --- a/tests/test_csr.py +++ b/tests/test_csr.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import sys diff --git a/tests/test_keys.py b/tests/test_keys.py index 6eb5b07..ebc54c7 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import os diff --git a/tests/test_ocsp.py b/tests/test_ocsp.py index 3ef6404..a4e4b6b 100644 --- a/tests/test_ocsp.py +++ b/tests/test_ocsp.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import sys diff --git a/tests/test_pem.py b/tests/test_pem.py index 05e5cbc..c38862d 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import sys diff --git a/tests/test_tsp.py b/tests/test_tsp.py index 0a8f90d..0571c4c 100644 --- a/tests/test_tsp.py +++ b/tests/test_tsp.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import os diff --git a/tests/test_x509.py b/tests/test_x509.py index 00c782c..1843196 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -1,5 +1,5 @@ # coding: utf-8 -from __future__ import unicode_literals +from __future__ import unicode_literals, division, absolute_import, print_function import unittest import sys -- cgit v1.2.3 From 153cee90759e6f772ecaa631a5d6e3ab54f731d5 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 09:37:48 -0400 Subject: Removed dict comprehension for Python 2.6 support --- asn1crypto/x509.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 454d71a..31e518e 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -796,7 +796,9 @@ class RelativeDistinguishedName(SetOf): values that have been prepped for comparison """ - return {ntv['type'].native: ntv.prepped_value for ntv in rdn} + output = {} + [output.update([(ntv['type'].native, ntv.prepped_value)]) for ntv in rdn] #pylint: disable=W0106 + return output class RDNSequence(SequenceOf): -- cgit v1.2.3 From 6e4af277f750f726db84ff03cc7e36e7df11dd53 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 09:38:20 -0400 Subject: URL encoding fixes for Python 2.6 compatibility --- asn1crypto/x509.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 31e518e..291d7fa 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2299,7 +2299,7 @@ def _urlquote(string, safe=''): # those are functionally different than the unquoted ones. def _try_unescape(match): byte_string = unquote_to_bytes(match.group(0)) - unicode_string = byte_string.decode('utf-8', errors='iriutf8') + unicode_string = byte_string.decode('utf-8', 'iriutf8') for safe_char in list(safe): unicode_string = unicode_string.replace(safe_char, '%%%02x' % ord(safe_char)) return unicode_string @@ -2312,7 +2312,7 @@ def _urlquote(string, safe=''): return '\x00' string = re.sub('%[0-9a-fA-F]{2}', _extract_escape, string) - output = urlquote(string.encode('utf-8'), safe=safe) + output = urlquote(string.encode('utf-8'), safe=safe.encode('utf-8')) if not isinstance(output, byte_cls): output = output.encode('ascii') @@ -2362,7 +2362,7 @@ def _urlunquote(byte_string, remap=None, preserve=None): for char in remap: byte_string = byte_string.replace(char.encode('ascii'), ('%%%02x' % ord(char)).encode('ascii')) - output = byte_string.decode('utf-8', errors='iriutf8') + output = byte_string.decode('utf-8', 'iriutf8') if preserve: for replacement, original in preserve_unmap.items(): -- cgit v1.2.3 From 9d65e68959a50e6f7972f36f521f357e067d77a9 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 09:38:56 -0400 Subject: Added unittest patches for Python 2.6 --- tests/_unittest_compat.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_cms.py | 2 ++ tests/test_core.py | 2 ++ tests/test_crl.py | 4 +++ tests/test_csr.py | 3 ++ tests/test_keys.py | 2 ++ tests/test_ocsp.py | 3 ++ tests/test_pem.py | 3 ++ tests/test_tsp.py | 2 ++ tests/test_x509.py | 3 ++ 10 files changed, 96 insertions(+) create mode 100644 tests/_unittest_compat.py diff --git a/tests/_unittest_compat.py b/tests/_unittest_compat.py new file mode 100644 index 0000000..3064061 --- /dev/null +++ b/tests/_unittest_compat.py @@ -0,0 +1,72 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import unittest + + +_non_local = {'patched': False} + + +def patch(): + if not sys.version_info < (2, 7): + return + + if _non_local['patched']: + return + + unittest.TestCase.assertIsInstance = _assert_is_instance + unittest.TestCase.assertRaises = _assert_raises + _non_local['patched'] = True + + +def _assert_is_instance(self, obj, cls, msg=None): + """Same as self.assertTrue(isinstance(obj, cls)), with a nicer + default message.""" + if not isinstance(obj, cls): + if not msg: + msg = '%s is not an instance of %r' % (obj, cls) + self.fail(msg) + + +def _assert_raises(self, excClass, callableObj=None, *args, **kwargs): + context = _AssertRaisesContext(excClass, self) + if callableObj is None: + return context + with context: + callableObj(*args, **kwargs) + + +class _AssertRaisesContext(object): + """A context manager used to implement TestCase.assertRaises* methods.""" + + def __init__(self, expected, test_case, expected_regexp=None): + self.expected = expected + self.failureException = test_case.failureException + self.expected_regexp = expected_regexp + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, tb): + if exc_type is None: + try: + exc_name = self.expected.__name__ + except AttributeError: + exc_name = str(self.expected) + raise self.failureException( + "{0} not raised".format(exc_name)) + if not issubclass(exc_type, self.expected): + # let unexpected exceptions pass through + return False + self.exception = exc_value # store for later retrieval + if self.expected_regexp is None: + return True + + expected_regexp = self.expected_regexp + if not expected_regexp.search(str(exc_value)): + raise self.failureException( + '"%s" does not match "%s"' % + (expected_regexp.pattern, str(exc_value)) + ) + return True diff --git a/tests/test_cms.py b/tests/test_cms.py index 7c5945f..c8a6c41 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -6,7 +6,9 @@ import os from datetime import datetime from asn1crypto import cms, util +from ._unittest_compat import patch +patch() tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') diff --git a/tests/test_core.py b/tests/test_core.py index ab80536..a978ed2 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,7 +7,9 @@ import os from asn1crypto import core from .unittest_data import DataDecorator, data +from ._unittest_compat import patch +patch() tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') diff --git a/tests/test_crl.py b/tests/test_crl.py index 29dba58..6ee483d 100644 --- a/tests/test_crl.py +++ b/tests/test_crl.py @@ -7,6 +7,10 @@ import os from asn1crypto import crl +from ._unittest_compat import patch + +patch() + if sys.version_info < (3,): byte_cls = str num_cls = long #pylint: disable=E0602 diff --git a/tests/test_csr.py b/tests/test_csr.py index 9cc8a8b..7e9dbd5 100644 --- a/tests/test_csr.py +++ b/tests/test_csr.py @@ -6,6 +6,9 @@ import sys import os from asn1crypto import csr, util +from ._unittest_compat import patch + +patch() if sys.version_info < (3,): byte_cls = str diff --git a/tests/test_keys.py b/tests/test_keys.py index ebc54c7..8a03182 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -7,7 +7,9 @@ import os from asn1crypto import keys, core, util from .unittest_data import DataDecorator, data +from ._unittest_compat import patch +patch() tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') diff --git a/tests/test_ocsp.py b/tests/test_ocsp.py index a4e4b6b..9357b27 100644 --- a/tests/test_ocsp.py +++ b/tests/test_ocsp.py @@ -7,6 +7,9 @@ import os from datetime import datetime from asn1crypto import ocsp, util +from ._unittest_compat import patch + +patch() if sys.version_info < (3,): byte_cls = str diff --git a/tests/test_pem.py b/tests/test_pem.py index c38862d..30c94ec 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -8,6 +8,9 @@ import os from asn1crypto import pem, util from .unittest_data import DataDecorator, data +from ._unittest_compat import patch + +patch() if sys.version_info < (3,): byte_cls = str diff --git a/tests/test_tsp.py b/tests/test_tsp.py index 0571c4c..a416872 100644 --- a/tests/test_tsp.py +++ b/tests/test_tsp.py @@ -6,7 +6,9 @@ import os from datetime import datetime from asn1crypto import tsp, cms, util +from ._unittest_compat import patch +patch() tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') diff --git a/tests/test_x509.py b/tests/test_x509.py index 1843196..83463eb 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -9,6 +9,9 @@ from datetime import datetime from asn1crypto import x509, core, pem, util from .unittest_data import DataDecorator, data +from ._unittest_compat import patch + +patch() if sys.version_info < (3,): byte_cls = str -- cgit v1.2.3 From 3f4a61ca9c818aa502d99cb8ce36b5b5e75896ea Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 24 Aug 2015 10:45:00 -0400 Subject: Note Python 2.6 compatibility in the readme and setup.py --- readme.md | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 2824566..05dc4a4 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: ## Dependencies -Python 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* +Python 2.6, 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Version diff --git a/setup.py b/setup.py index 598525f..493d97b 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ setup( 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', -- cgit v1.2.3 From df6b89791ba51f588c92bfab37a3e8ba5c65e875 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 31 Aug 2015 08:53:12 -0400 Subject: Changed x509.Certificate.self_issued and .self_signed to return true even if the certificate is not a CA cert --- asn1crypto/x509.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 291d7fa..178a567 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2223,9 +2223,7 @@ class Certificate(Sequence): """ if self._self_issued is None: - self._self_issued = False - if self.ca: - self._self_issued = self.subject == self.issuer + self._self_issued = self.subject == self.issuer return self._self_issued @property -- cgit v1.2.3 From 872ffddc6e400eb3c58410ce0491936cd86db1d3 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 31 Aug 2015 10:42:04 -0400 Subject: Fix a bug generating the human friendly form of an X.509 Name when multiple elements of the same type are present --- asn1crypto/x509.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 178a567..aec3c6b 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -951,8 +951,10 @@ class Name(Choice): for key in data: value = data[key] if isinstance(value, list): - value = ', '.join(value) - to_join.append('%s: %s' % (key, value.native)) + value = ', '.join([sub_value.native for sub_value in value]) + else: + value = value.native + to_join.append('%s: %s' % (key, value)) has_comma = False for element in to_join: -- cgit v1.2.3 From 9a968c8a36a01ba123d56d9655a35f0e3d9e929a Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 31 Aug 2015 10:42:52 -0400 Subject: Export inet_ntop() and inet_pton() from util for IP address comparisons --- asn1crypto/util.py | 10 +++++++++- asn1crypto/x509.py | 7 +------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/asn1crypto/util.py b/asn1crypto/util.py index f3d2869..2265fcc 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 + - inet_ntop() + - inet_pton() """ from __future__ import unicode_literals, division, absolute_import, print_function @@ -16,7 +18,13 @@ import sys import math -from ._ordereddict import OrderedDict +from ._ordereddict import OrderedDict #pylint: disable=W0611 + + +if sys.platform == 'win32': + from ._win._ws2_32 import inet_ntop, inet_pton #pylint: disable=W0611 +else: + from socket import inet_ntop, inet_pton # Python 2 diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index aec3c6b..56cf120 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -54,7 +54,7 @@ from .core import ( ) from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo -from .util import int_to_bytes, int_from_bytes +from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton from ._errors import object_name if sys.version_info < (3,): @@ -70,11 +70,6 @@ else: bytes_to_list = list from urllib.parse import urlsplit, urlunsplit, quote as urlquote, unquote_to_bytes -if sys.platform == 'win32': - from ._win._ws2_32 import inet_ntop, inet_pton -else: - from socket import inet_ntop, inet_pton - # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 -- cgit v1.2.3 From 06cf099bc8b45bf9bb643e3e3743dd2888c7a1f9 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 31 Aug 2015 11:43:45 -0400 Subject: Update x509.Certificate.valid_domains to match RFC 6125 in terms of valid domain lists --- asn1crypto/x509.py | 23 ++++++++++++++--------- tests/test_x509.py | 4 ++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 56cf120..7f88698 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2155,15 +2155,6 @@ class Certificate(Sequence): if self._valid_domains is None: self._valid_domains = [] - # If the common name in the subject looks like a domain, add it - pattern = re.compile('^(\\*\\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$') - for rdn in self.subject.chosen: - for name_type_value in rdn: - if name_type_value['type'].native == 'common_name': - value = name_type_value['value'].native - if pattern.match(value): - self._valid_domains.append(value) - # For the subject alt name extension, we can look at the name of # the choice selected since it distinguishes between domain names, # email addresses, IPs, etc @@ -2172,6 +2163,20 @@ class Certificate(Sequence): if general_name.name == 'dns_name' and general_name.native not in self._valid_domains: self._valid_domains.append(general_name.native) + # If there was no subject alt name extension, and the common name + # in the subject looks like a domain, that is considered the valid + # list. This is done because according to + # https://tools.ietf.org/html/rfc6125#section-6.4.4, the common + # name should not be used if the subject alt name is present. + else: + pattern = re.compile('^(\\*\\.)?(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}$') + for rdn in self.subject.chosen: + for name_type_value in rdn: + if name_type_value['type'].native == 'common_name': + value = name_type_value['value'].native + if pattern.match(value): + self._valid_domains.append(value) + return self._valid_domains @property diff --git a/tests/test_x509.py b/tests/test_x509.py index 83463eb..17edb19 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -1303,14 +1303,14 @@ class X509Tests(unittest.TestCase): ('geotrust_certs/GeoTrust_Universal_CA.crt', []), ('geotrust_certs/GeoTrust_Primary_CA.crt', []), ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', []), - ('geotrust_certs/codex.crt', ['codexns.io', 'dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net']), + ('geotrust_certs/codex.crt', ['dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net', 'codexns.io']), ('lets_encrypt/isrgrootx1.pem', []), ('lets_encrypt/letsencryptauthorityx1.pem', []), ('lets_encrypt/letsencryptauthorityx2.pem', []), ('globalsign_example_keys/IssuingCA-der.cer', []), ('globalsign_example_keys/rootCA.cer', []), ('globalsign_example_keys/SSL1.cer', ['anything.example.com']), - ('globalsign_example_keys/SSL2.cer', ['*.google.com', 'anything.example.com']), + ('globalsign_example_keys/SSL2.cer', ['anything.example.com']), ('globalsign_example_keys/SSL3.cer', ['*.google.com']), ) -- cgit v1.2.3 From d4fc7ea74013eb96a92ec90de06d4eaf305be45f Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 31 Aug 2015 11:44:11 -0400 Subject: Added x509.Certificate.is_valid_domain_ip() --- asn1crypto/x509.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_x509.py | 17 +++++++ 2 files changed, 147 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 7f88698..bfd4920 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2251,6 +2251,136 @@ class Certificate(Sequence): self._self_signed = 'maybe' return self._self_signed + def is_valid_domain_ip(self, domain_ip): + """ + Check if a domain name or IP address is valid according to the + certificate + + :param domain_ip: + A unicode string of a domain name or IP address + + :return: + A boolean - if the domain or IP is valid for the certificate + """ + + if not isinstance(domain_ip, str_cls): + raise TypeError('domain_ip must be a unicode string, not %s' % object_name(domain_ip)) + + encoded_domain_ip = domain_ip.encode('idna').decode('ascii').lower() + + is_ipv6 = encoded_domain_ip.find(':') != -1 + is_ipv4 = not is_ipv6 and re.match('^\\d+\\.\\d+\\.\\d+\\.\\d+$', encoded_domain_ip) + is_domain = not is_ipv6 and not is_ipv4 + + # Handle domain name checks + if is_domain: + if not self.valid_domains: + return False + + domain_labels = encoded_domain_ip.split('.') + + for valid_domain in self.valid_domains: + encoded_valid_domain = valid_domain.encode('idna').decode('ascii').lower() + valid_domain_labels = encoded_valid_domain.split('.') + + # The domain must be equal in label length to match + if len(valid_domain_labels) != len(domain_labels): + continue + + if valid_domain_labels == domain_labels: + return True + + if self._is_wildcard_domain(encoded_valid_domain) and self._is_wildcard_match(domain_labels, valid_domain_labels): + return True + + return False + + # Handle IP address checks + if not self.valid_ips: + return False + + family = socket.AF_INET if is_ipv4 else socket.AF_INET6 + normalized_ip = inet_pton(family, encoded_domain_ip) + + for valid_ip in self.valid_ips: + valid_family = socket.AF_INET if valid_ip.find('.') != -1 else socket.AF_INET6 + normalized_valid_ip = inet_pton(valid_family, valid_ip) + + if normalized_valid_ip == normalized_ip: + return True + + return False + + def _is_wildcard_domain(self, domain): + """ + Checks if a domain is a valid wildcard according to + https://tools.ietf.org/html/rfc6125#section-6.4.3 + + :param domain: + A unicode string of the domain name, where any U-labels from an IDN + have been converted to A-labels + + :return: + A boolean - if the domain is a valid wildcard domain + """ + + # The * character must be present for a wildcard match, and if there is + # most than one, it is an invalid wildcard specification + if domain.count('*') != 1: + return False + + labels = domain.lower().split('.') + + if not labels: + return False + + # Wildcards may only appear in the left-most label + if labels[0].find('*') == -1: + return False + + # Wildcards may not be embedded in an A-label from an IDN + if labels[0][0:4] == 'xn--': + return False + + return True + + def _is_wildcard_match(self, domain_labels, valid_domain_labels): + """ + Determines if the labels in a domain are a match for labels from a + wildcard valid domain name + + :param domain_labels: + A list of unicode strings, with A-label form for IDNs, of the labels + in the domain name to check + + :param valid_domain_labels: + A list of unicode strings, with A-label form for IDNs, of the labels + in a wildcard domain pattern + + :return: + A boolean - if the domain matches the valid domain + """ + + first_domain_label = domain_labels[0] + other_domain_labels = domain_labels[1:] + + wildcard_label = valid_domain_labels[0] + other_valid_domain_labels = valid_domain_labels[1:] + + # The wildcard is only allowed in the first label, so if + # The subsequent labels are not equal, there is no match + if other_domain_labels != other_valid_domain_labels: + return False + + if wildcard_label == '*': + return True + + wildcard_regex = re.compile('^' + wildcard_label.replace('*', '.*') + '$') + if wildcard_regex.match(first_domain_label): + return True + + return False + def _iri_utf8_errors_handler(exc): """ diff --git a/tests/test_x509.py b/tests/test_x509.py index 17edb19..54abff3 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -33,6 +33,23 @@ class X509Tests(unittest.TestCase): _, _, cert_bytes = pem.unarmor(cert_bytes) return x509.Certificate.load(cert_bytes) + #pylint: disable=C0326 + @staticmethod + def is_valid_domain_ip_info(): + return ( + ('geotrust_certs/codex.crt', 'codexns.io', True), + ('geotrust_certs/codex.crt', 'dev.codexns.io', True), + ('geotrust_certs/codex.crt', 'rc.codexns.io', True), + ('geotrust_certs/codex.crt', 'foo.codexns.io', False), + ('geotrust_certs/codex.crt', '1.2.3.4', False), + ('geotrust_certs/codex.crt', '1::1', False), + ) + + @data('is_valid_domain_ip_info') + def is_valid_domain_ip(self, cert, domain_ip, result): + cert = self._load_cert(cert) + self.assertEqual(result, cert.is_valid_domain_ip(domain_ip)) + #pylint: disable=C0326 @staticmethod def ip_address_info(): -- cgit v1.2.3 From 5314f4bfe09bf34ce81d8801e6084fb0d2d0732f Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 1 Sep 2015 13:25:47 -0400 Subject: Add .sha256 and .sha1 attributes to x509.Certificate --- asn1crypto/x509.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index bfd4920..99ec55e 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1651,6 +1651,8 @@ class Certificate(Sequence): _valid_ips = None _self_issued = None _self_signed = None + _sha1 = None + _sha256 = None def _set_extensions(self): """ @@ -2251,6 +2253,29 @@ class Certificate(Sequence): self._self_signed = 'maybe' return self._self_signed + @property + def sha1(self): + """ + :return: + The SHA1 hash of the DER-encoded bytes of this complete certificate + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(self.dump()).digest() + return self._sha1 + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this complete + certificate + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(self.dump()).digest() + return self._sha256 + def is_valid_domain_ip(self, domain_ip): """ Check if a domain name or IP address is valid according to the -- cgit v1.2.3 From 8ea67eb3bccfa3be7eeea2391adf0cfa641e99e5 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 2 Sep 2015 17:11:33 -0400 Subject: Version 0.11.0 --- asn1crypto/__init__.py | 3 ++- changelog.md | 15 +++++++++++++++ readme.md | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 6625992..aad19c2 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,4 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.10.1' +__version__ = '0.11.0' +__version_info__ = (0, 11, 0) diff --git a/changelog.md b/changelog.md index 700f9f8..a3ef84b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,20 @@ # changelog +## 0.11.0 + + - Added Python 2.6 support + - Added ability to compare primitive type objects + - Implemented proper support for internationalized domains, URLs and email + addresses in `x509.Certificate` + - Comparing `x509.Name` and `x509.GeneralName` objects adheres to RFC 5280 + - `x509.Certificate.self_signed` and `x509.Certificate.self_issued` no longer + require that certificate is for a CA + - Fixed `x509.Certificate.valid_domains` to adhere to RFC 6125 + - Added `x509.Certificate.is_valid_domain_ip()` + - Added `x509.Certificate.sha1` and `x509.Certificate.sha256` + - Exposed `util.inet_ntop()` and `util.inet_pton()` for IP address encoding + - Improved exception messages for improper types to include type's module name + ## 0.10.1 - Fixed bug in `core.Sequence` affecting Python 2.7 and pypy diff --git a/readme.md b/readme.md index 05dc4a4..fb9b9ec 100644 --- a/readme.md +++ b/readme.md @@ -38,12 +38,12 @@ Python 2.6, 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Version -0.10.1 - [changelog](changelog.md) +0.11.0 - [changelog](changelog.md) ## Installation ```bash -pip install git+git://github.com/wbond/asn1crypto.git@0.10.1 +pip install git+git://github.com/wbond/asn1crypto.git@0.11.0 ``` ## Documentation -- cgit v1.2.3 From 5a0feb4141214e52c8c7282751de3d43f27cb32c Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 2 Sep 2015 17:12:25 -0400 Subject: Mention Python 3.2 in readme --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index fb9b9ec..18bd40f 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: ## Dependencies -Python 2.6, 2.7, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* +Python 2.6, 2.7, 3.2, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* ## Version -- cgit v1.2.3 From a26664fcbd7766e8d2659a7f14e4d1b1ca2f0ab0 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 7 Oct 2015 11:57:35 -0400 Subject: Replace pylint with flake8, improve exception class specificity --- .pep8 | 2 + .pylintrc | 17 - asn1crypto/_elliptic_curve.py | 23 +- asn1crypto/_errors.py | 41 +- asn1crypto/_ffi.py | 2 +- asn1crypto/_int.py | 3 +- asn1crypto/_ordereddict.py | 10 +- asn1crypto/_types.py | 36 + asn1crypto/_win/_ws2_32.py | 12 +- asn1crypto/algos.py | 146 ++- asn1crypto/cms.py | 8 +- asn1crypto/core.py | 693 ++++++++++---- asn1crypto/crl.py | 1 - asn1crypto/csr.py | 1 - asn1crypto/keys.py | 120 ++- asn1crypto/ocsp.py | 1 - asn1crypto/pdf.py | 19 +- asn1crypto/pem.py | 62 +- asn1crypto/pkcs12.py | 3 +- asn1crypto/tsp.py | 18 +- asn1crypto/util.py | 18 +- asn1crypto/x509.py | 175 +++- dev-requirements.txt | 2 +- dev/lint.py | 19 +- tests/_unittest_compat.py | 4 +- tests/test_cms.py | 84 +- tests/test_core.py | 57 +- tests/test_crl.py | 2 +- tests/test_csr.py | 7 +- tests/test_keys.py | 198 ++-- tests/test_ocsp.py | 1 - tests/test_pem.py | 83 +- tests/test_tsp.py | 6 +- tests/test_x509.py | 2104 +++++++++++++++++++++++++++++++---------- tests/unittest_data.py | 10 +- 35 files changed, 2942 insertions(+), 1046 deletions(-) create mode 100644 .pep8 delete mode 100644 .pylintrc create mode 100644 asn1crypto/_types.py diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/.pep8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index c59352d..0000000 --- a/.pylintrc +++ /dev/null @@ -1,17 +0,0 @@ -[MASTER] - -ignore=.git - -[MESSAGES CONTROL] - -enable=W1619,R0401,W1602,W0211,E1604,W0622,W1605,C0204,W0123,E0236,E0701,W0142,W0234,E1306,W0222,E0202,W0612,C0202,W0199,W1628,W0611,W1614,C0328,F0401,C0303,W0122,W0104,W1623,W0640,W0108,E0602,E0105,E0211,W1601,W0120,E0235,E0108,W1300,W1608,F0202,E0222,W1302,W0403,E1601,E0239,W1621,W0101,E1302,C0203,E0104,W0410,W1616,W0109,E1305,E0238,E0603,C0327,C0326,W0621,E0109,E0101,W0404,W0102,W1612,W1607,W1304,W1613,E1606,E0221,W0150,W0613,E0611,E0100,E1304,C0103,E1608,E0213,F0220,W0223,W1622,C0121,W1617,W0231,W0105,W0106,W1501,W0711,E1605,W0604,W0602,W1627,W0212,W1503,E1301,W0301,W0311,W1606,W1305,W0233,W1306,W1618,W0402,W1626,E0106,E0102,W0632,W0401,W1615,W0633,E0601,W0702,W0704,W0614,W1604,E1602,W1624,W0406,E0203,W0623,C0102,E1300,E0111,E0711,C0321,W1603,E0237,W1401,W1632,E0604,W1303,E1310,W0232,W1631,C0330,E0107,W0601,W1307,W1611,E1607,W0603,W0107,W0221,W0312,W0710,W1629,W1402,C0112,W0703,W1630,E0103,W1610,W0631,E0710,W1502,W0110,E0712,C0304,E0703,E1303,E1603,W1301,W1609,E0110,W1620,W1625,W1633,E0702,E0001 - -disable=E1200,R0901,I0020,I0013,R0914,E1120,I0001,W0512,C1001,F0001,R0923,C0301,E1127,R0922,R0915,I0011,E1102,E1002,F0010,E1123,E1111,C0103,C0302,E1121,R0921,F0002,W0511,W1201,R0903,R0902,I0010,C0401,R0904,I0021,I0022,E1126,R0913,E1205,E1001,E1004,E0012,R0801,C0402,R0911,E1125,E1101,E0011,W0332,W1001,W1111,I0012,E1003,R0912,E1206,E1124,E1201,F0003,W1202,R0201,C0111,C0325,W0141,W0201 - -[FORMAT] - -expected-line-ending-format=LF - -[IMPORTS] - -deprecated-modules=optparse diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index bd18c67..7e41cc5 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -48,19 +48,8 @@ THE SOFTWARE. from __future__ import unicode_literals, division, absolute_import, print_function -import sys - from ._int import inverse_mod -if sys.version_info < (3,): - byte_cls = str - int_types = (int, long) #pylint: disable=E0602 - -else: - byte_cls = bytes - int_types = int - - class PrimeCurve(): """ @@ -297,7 +286,7 @@ SECP256R1_BASE_POINT = PrimePoint( # NIST Curve P-384: SECP384R1_CURVE = PrimeCurve( - 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, + 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, # noqa -3, 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef ) @@ -311,13 +300,13 @@ SECP384R1_BASE_POINT = PrimePoint( # NIST Curve P-521: SECP521R1_CURVE = PrimeCurve( - 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, + 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, # noqa -3, - 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 + 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 # noqa ) SECP521R1_BASE_POINT = PrimePoint( SECP521R1_CURVE, - 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, - 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, - 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 + 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, # noqa + 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, # noqa + 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 # noqa ) diff --git a/asn1crypto/_errors.py b/asn1crypto/_errors.py index 6b38315..a96fcaa 100644 --- a/asn1crypto/_errors.py +++ b/asn1crypto/_errors.py @@ -1,24 +1,45 @@ # coding: utf-8 """ -Helpers for creating exceptions. Exports the following items: +Helper for formatting exception messages. Exports the following items: - - object_name() + - single_line() """ from __future__ import unicode_literals, division, absolute_import, print_function +import re +import textwrap -def object_name(value): + +def unwrap(string, *params): """ - :param value: - A value to get the object name of + Takes a multi-line string and does the following: + + - dedents + - converts newlines with text before and after into a single line + - strips leading and trailing whitespace + + :param string: + The string to format + + :param *params: + Params to interpolate into the string :return: - A unicode string of the object name + The formatted string """ - cls = value.__class__ - if cls.__module__ == 'builtins': - return cls.__name__ - return '%s.%s' % (cls.__module__, cls.__name__) + output = textwrap.dedent(string) + + # Unwrap lines, taking into account bulleted lists, ordered lists and + # underlines consisting of = signs + if output.find('\n') != -1: + output = re.sub('(?<=\\S)\n(?=[^ \n\t\\d\\*\\-=])', ' ', output) + + if params: + output = output % params + + output = output.strip() + + return output diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py index 375aeb3..d484195 100644 --- a/asn1crypto/_ffi.py +++ b/asn1crypto/_ffi.py @@ -53,7 +53,7 @@ except (ImportError): def unicode_buffer(initializer): return create_unicode_buffer(initializer) - def bytes_from_buffer(buffer, maxlen=None): #pylint: disable=W0613 + def bytes_from_buffer(buffer, maxlen=None): return buffer.raw def null(): diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index 1011c5f..62adc21 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -39,7 +39,6 @@ from ._ffi import LibraryNotFoundError, FFIEngineError, buffer_from_bytes, bytes from .util import int_to_bytes, int_from_bytes - # First try to use ctypes or cffi with OpenSSL for better performance try: try: @@ -117,7 +116,7 @@ except (LibraryNotFoundError, FFIEngineError): 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 + 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. diff --git a/asn1crypto/_ordereddict.py b/asn1crypto/_ordereddict.py index 69af2c3..d4c107b 100644 --- a/asn1crypto/_ordereddict.py +++ b/asn1crypto/_ordereddict.py @@ -25,14 +25,12 @@ import sys if not sys.version_info < (2, 7): from collections import OrderedDict - else: - from UserDict import DictMixin #pylint: disable=F0401 - + from UserDict import DictMixin class OrderedDict(dict, DictMixin): - def __init__(self, *args, **kwds): #pylint: disable=W0231 + def __init__(self, *args, **kwds): if len(args) > 1: raise TypeError('expected at most 1 arguments, got %d' % len(args)) try: @@ -78,7 +76,7 @@ else: if not self: raise KeyError('dictionary is empty') if last: - key = reversed(self).next() #pylint: disable=E0111 + key = reversed(self).next() else: key = iter(self).next() value = self.pop(key) @@ -125,7 +123,7 @@ else: if isinstance(other, OrderedDict): if len(self) != len(other): return False - for p, q in zip(self.items(), other.items()): + for p, q in zip(self.items(), other.items()): if p != q: return False return True diff --git a/asn1crypto/_types.py b/asn1crypto/_types.py new file mode 100644 index 0000000..30816c7 --- /dev/null +++ b/asn1crypto/_types.py @@ -0,0 +1,36 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys +import inspect + + +if sys.version_info < (3,): + str_cls = unicode # noqa + byte_cls = str + int_types = (int, long) # noqa + +else: + str_cls = str + byte_cls = bytes + int_types = int + + +def type_name(value): + """ + Returns a user-readable name for the type of an object + + :param value: + A value to get the type name of + + :return: + A unicode string of the object's type name + """ + + if inspect.isclass(value): + cls = value + else: + cls = value.__class__ + if cls.__module__ in set(['builtins', '__builtin__']): + return cls.__name__ + return '%s.%s' % (cls.__module__, cls.__name__) diff --git a/asn1crypto/_win/_ws2_32.py b/asn1crypto/_win/_ws2_32.py index 51de74a..bba48dc 100644 --- a/asn1crypto/_win/_ws2_32.py +++ b/asn1crypto/_win/_ws2_32.py @@ -3,10 +3,18 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import socket -from .._ffi import FFIEngineError, buffer_from_bytes, bytes_from_buffer, cast_void_p, is_null, unicode_buffer, string_from_buffer +from .._ffi import ( + buffer_from_bytes, + bytes_from_buffer, + cast_void_p, + FFIEngineError, + is_null, + string_from_buffer, + unicode_buffer, +) try: - from ._ws2_32_cffi import ws2_32 #pylint: disable=W0611 + from ._ws2_32_cffi import ws2_32 except (FFIEngineError): from ._ws2_32_ctypes import ws2_32 diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index a003d7e..b90bf30 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -18,6 +18,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 .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence @@ -225,7 +226,12 @@ class SignedDigestAlgorithm(Sequence): if algorithm in algo_map: return algo_map[algorithm] - raise ValueError('Signature algorithm not known for %s' % algorithm) + raise ValueError(unwrap( + ''' + Signature algorithm not known for %s + ''', + algorithm + )) @property def hash_algo(self): @@ -260,7 +266,12 @@ class SignedDigestAlgorithm(Sequence): if algorithm == 'rsassa_pss': return self['parameters']['hash_algorithm']['algorithm'].native - raise ValueError('Hash algorithm not known for %s' % algorithm) + raise ValueError(unwrap( + ''' + Hash algorithm not known for %s + ''', + algorithm + )) class Pbkdf2Salt(Choice): @@ -450,7 +461,8 @@ class EncryptionAlgorithm(Sequence): Returns the name of the key derivation function to use. :return: - A unicode from of one of the following: "pbkdf1", "pbkdf2", "pkcs12_kdf" + A unicode from of one of the following: "pbkdf1", "pbkdf2", + "pkcs12_kdf" """ encryption_algo = self['algorithm'].native @@ -468,9 +480,21 @@ class EncryptionAlgorithm(Sequence): if encryption_algo == 'pkcs12': return 'pkcs12_kdf' - raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation function' % encryption_algo) + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation function + ''', + encryption_algo + )) @property def kdf_hmac(self): @@ -478,7 +502,8 @@ class EncryptionAlgorithm(Sequence): Returns the HMAC algorithm to use with the KDF. :return: - A unicode string of one of the following: "md2", "md5", "sha1", "sha224", "sha256", "sha384", "sha512" + A unicode string of one of the following: "md2", "md5", "sha1", + "sha224", "sha256", "sha384", "sha512" """ encryption_algo = self['algorithm'].native @@ -491,9 +516,21 @@ class EncryptionAlgorithm(Sequence): _, hmac_algo, _ = encryption_algo.split('_', 2) return hmac_algo - raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation hmac algorithm' % encryption_algo) + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation hmac algorithm + ''', + encryption_algo + )) @property def kdf_salt(self): @@ -510,7 +547,13 @@ class EncryptionAlgorithm(Sequence): salt = self['parameters']['key_derivation_func']['parameters']['salt'] if salt.name == 'other_source': - raise ValueError('Can not determine key derivation salt - the reversed-for-future-use other source salt choice was specified in the PBKDF2 params structure') + raise ValueError(unwrap( + ''' + Can not determine key derivation salt - the + reserved-for-future-use other source salt choice was + specified in the PBKDF2 params structure + ''' + )) return salt.native @@ -518,9 +561,21 @@ class EncryptionAlgorithm(Sequence): if encryption_algo.find('_') != -1: return self['parameters']['salt'].native - raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation salt' % encryption_algo) + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation salt + ''', + encryption_algo + )) @property def kdf_iterations(self): @@ -540,9 +595,21 @@ class EncryptionAlgorithm(Sequence): if encryption_algo.find('_') != -1: return self['parameters']['iterations'].native - raise ValueError('Encryption algorithm "%s" does not have a registered key derivation function' % encryption_algo) - - raise ValueError('Unrecognized encryption algorithm "%s", can not determine key derivation iterations' % encryption_algo) + raise ValueError(unwrap( + ''' + Encryption algorithm "%s" does not have a registered key + derivation function + ''', + encryption_algo + )) + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s", can not determine key + derivation iterations + ''', + encryption_algo + )) @property def key_length(self): @@ -577,11 +644,12 @@ class EncryptionAlgorithm(Sequence): rc2_params = self['parameters'].parsed['encryption_scheme']['parameters'].parsed rc2_parameter_version = rc2_params['rc2_parameter_version'].native - # See page 24 of http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf + # See page 24 of + # http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf encoded_key_bits_map = { 160: 5, # 40-bit 120: 8, # 64-bit - 58: 16, # 128-bit + 58: 16, # 128-bit } if rc2_parameter_version in encoded_key_bits_map: @@ -593,7 +661,12 @@ class EncryptionAlgorithm(Sequence): if rc2_parameter_version is None: return 4 # 32-bit default - raise ValueError('Invalid RC2 parameter version found in EncryptionAlgorithm parameters') + raise ValueError(unwrap( + ''' + Invalid RC2 parameter version found in EncryptionAlgorithm + parameters + ''' + )) if encryption_algo == 'pbes2': key_length = self['parameters']['key_derivation_func']['parameters']['key_length'].native @@ -623,7 +696,12 @@ class EncryptionAlgorithm(Sequence): 'pkcs12_sha1_rc2_40': 5, }[encryption_algo] - raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) @property def encryption_cipher(self): @@ -633,7 +711,8 @@ class EncryptionAlgorithm(Sequence): between different variations of TripleDES, AES, and the RC* ciphers. :return: - A unicode string from one of the following: "rc2", "rc5", "des", "tripledes", "aes" + A unicode string from one of the following: "rc2", "rc5", "des", + "tripledes", "aes" """ encryption_algo = self['algorithm'].native @@ -669,7 +748,12 @@ class EncryptionAlgorithm(Sequence): 'pkcs12_sha1_rc2_40': 'rc2', }[encryption_algo] - raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) @property def encryption_block_size(self): @@ -715,7 +799,12 @@ class EncryptionAlgorithm(Sequence): 'pkcs12_sha1_rc2_40': 8, }[encryption_algo] - raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) @property def encryption_iv(self): @@ -748,7 +837,12 @@ class EncryptionAlgorithm(Sequence): if encryption_algo.find('.') == -1: return None - raise ValueError('Unrecognized encryption algorithm "%s"' % encryption_algo) + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) class Pbes2Params(Sequence): @@ -783,4 +877,4 @@ class Pkcs5MacAlgorithm(Sequence): } -EncryptionAlgorithm._oid_specs['pbes2'] = Pbes2Params #pylint: disable=W0212 +EncryptionAlgorithm._oid_specs['pbes2'] = Pbes2Params diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index a0c503a..2fbb646 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -52,7 +52,6 @@ from .ocsp import OCSPResponse from .x509 import Attributes, Certificate, Extensions, GeneralNames, Name - # These structures are taken from # ftp://ftp.rsasecurity.com/pub/pkcs/ascii/pkcs-6.asc @@ -63,6 +62,7 @@ class ExtendedCertificateInfo(Sequence): ('attributes', Attributes), ] + class ExtendedCertificate(Sequence): _fields = [ ('extended_certificate_info', ExtendedCertificateInfo), @@ -661,7 +661,7 @@ class CompressedData(Sequence): return self._decompressed -ContentInfo._oid_specs = { #pylint: disable=W0212 +ContentInfo._oid_specs = { 'data': OctetString, 'signed_data': SignedData, 'enveloped_data': EnvelopedData, @@ -674,7 +674,7 @@ ContentInfo._oid_specs = { #pylint: disable=W0212 } -EncapsulatedContentInfo._oid_specs = { #pylint: disable=W0212 +EncapsulatedContentInfo._oid_specs = { 'signed_data': SignedData, 'enveloped_data': EnvelopedData, 'signed_and_enveloped_data': SignedAndEnvelopedData, @@ -686,7 +686,7 @@ EncapsulatedContentInfo._oid_specs = { #pylint: disable=W0212 } -CMSAttribute._oid_specs = { #pylint: disable=W0212 +CMSAttribute._oid_specs = { 'content_type': SetOfContentType, 'message_digest': SetOfOctetString, 'signing_time': SetOfTime, diff --git a/asn1crypto/core.py b/asn1crypto/core.py index b55c42e..754306d 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -54,25 +54,20 @@ import binascii from . import _teletex_codec from ._ordereddict import OrderedDict +from ._errors import unwrap +from ._types import type_name, str_cls, byte_cls, int_types from .util import int_to_bytes, int_from_bytes, timezone -from ._errors import object_name # Python 2 if sys.version_info <= (3,): - str_cls = unicode #pylint: disable=E0602 - byte_cls = str - int_types = (int, long) #pylint: disable=E0602 py2 = True chr_cls = chr - range = xrange #pylint: disable=E0602,W0622 + range = xrange # noqa from datetime import timedelta - from cStringIO import StringIO as BytesIO #pylint: disable=F0401 + from cStringIO import StringIO as BytesIO # Python 3 else: - str_cls = str - byte_cls = bytes - int_types = int py2 = False from io import BytesIO @@ -80,7 +75,6 @@ else: return bytes([num]) - _teletex_codec.register() @@ -115,7 +109,6 @@ METHOD_NUM_TO_NAME_MAP = { _SETUP_CLASSES = {} - class Asn1Value(object): """ The basis of all ASN.1 values @@ -169,7 +162,6 @@ class Asn1Value(object): value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs) return value - #pylint: disable=W0613 def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=None, contents=None): """ The optional parameter is not used, but rather included so we don't @@ -214,18 +206,34 @@ class Asn1Value(object): if tag_type is not None: if tag_type not in ('implicit', 'explicit'): - raise ValueError('tag_type must be one of "implicit", "explicit", not %s' % repr(tag_type)) + raise ValueError(unwrap( + ''' + tag_type must be one of "implicit", "explicit", not %s + ''', + repr(tag_type) + )) self.tag_type = tag_type if class_ is None: class_ = 'context' if class_ not in CLASS_NAME_TO_NUM_MAP: - raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) + raise ValueError(unwrap( + ''' + class_ must be one of "universal", "application", + "context", "private", not %s + ''', + repr(class_) + )) class_ = CLASS_NAME_TO_NUM_MAP[class_] if tag is not None: if not isinstance(tag, int_types): - raise ValueError('tag must be an integer, not %s' % object_name(tag)) + raise TypeError(unwrap( + ''' + tag must be an integer, not %s + ''', + type_name(tag) + )) if tag_type == 'implicit': self.class_ = class_ @@ -236,7 +244,13 @@ class Asn1Value(object): else: if class_ is not None: if class_ not in CLASS_NUM_TO_NAME_MAP: - raise ValueError('class_ must be one of "universal", "application", "context", "private", not %s' % repr(class_)) + raise ValueError(unwrap( + ''' + class_ must be one of "universal", "application", + "context", "private", not %s + ''', + repr(class_) + )) self.class_ = CLASS_NAME_TO_NUM_MAP[class_] if tag is not None: @@ -248,9 +262,9 @@ class Asn1Value(object): elif default is not None: self.set(default) - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args raise e def __str__(self): @@ -273,7 +287,7 @@ class Asn1Value(object): A unicode string """ - return '<%s %s %s>' % (object_name(self), id(self), repr(self.contents or b'')) + return '<%s %s %s>' % (type_name(self), id(self), repr(self.contents or b'')) def copy(self): """ @@ -288,7 +302,7 @@ class Asn1Value(object): new_obj.tag = self.tag new_obj.explicit_class = self.explicit_class new_obj.explicit_tag = self.explicit_tag - new_obj._copy(self) #pylint: disable=W0212 + new_obj._copy(self) return new_obj def retag(self, tag_type, tag): @@ -306,7 +320,7 @@ class Asn1Value(object): """ new_obj = self.__class__(tag_type=tag_type, tag=tag) - new_obj._copy(self) #pylint: disable=W0212 + new_obj._copy(self) return new_obj def untag(self): @@ -318,10 +332,9 @@ class Asn1Value(object): """ new_obj = self.__class__() - new_obj._copy(self) #pylint: disable=W0212 + new_obj._copy(self) return new_obj - #pylint: disable=W0212 def _copy(self, other): """ Copies the contents of another Asn1Value object to itself @@ -331,7 +344,13 @@ class Asn1Value(object): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) self.contents = other.contents self._native = other._native @@ -401,7 +420,6 @@ class ValueMap(): # from _map the first time it is needed _reverse_map = None - #pylint: disable=W0212 def _setup(self): """ Generates _reverse_map from _map @@ -453,7 +471,6 @@ class NoValue(Asn1Value): return b'' - class Any(Asn1Value): """ A value class that can contain any value, and allows for easy parsing of @@ -478,14 +495,19 @@ class Any(Asn1Value): try: if value is not None: if not isinstance(value, Asn1Value): - raise ValueError('value must be an instance of Ans1Value, not %s' % object_name(value)) + raise TypeError(unwrap( + ''' + value must be an instance of Ans1Value, not %s + ''', + type_name(value) + )) self._parsed = (value, value.__class__, None) self.contents = value.dump() - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args raise e @property @@ -541,11 +563,15 @@ class Any(Asn1Value): passed_params = {} if not spec_params else spec_params.copy() passed_params['tag_type'] = self.tag_type passed_params['tag'] = self.tag - parsed_value, _ = _parse_build(self._header + self.contents + self._trailer, spec=spec, spec_params=passed_params) + parsed_value, _ = _parse_build( + self._header + self.contents + self._trailer, + spec=spec, + spec_params=passed_params + ) self._parsed = (parsed_value, spec, spec_params) - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e return self._parsed[0] @@ -598,7 +624,6 @@ class Choice(Asn1Value): # A dict that maps alternative names to an index in _alternatives _name_map = None - #pylint: disable=W0212 def _setup(self): """ Generates _id_map from _alternatives to allow validating contents @@ -636,11 +661,24 @@ class Choice(Asn1Value): try: if tag_type == 'implicit': - raise ValueError('The Choice type can not be implicitly tagged even if in an implicit module - due to its nature any tagging must be explicit') + raise ValueError(unwrap( + ''' + The Choice type can not be implicitly tagged even if in an + implicit module - due to its nature any tagging must be + explicit + ''' + )) if name is not None: if name not in self._name_map: - raise ValueError('The name specified, "%s", is not a valid alternative for %s' % (name, object_name(self))) + raise ValueError(unwrap( + ''' + The name specified, "%s", is not a valid alternative + for %s + ''', + name, + type_name(self) + )) self._choice = self._name_map[name] info = self._alternatives[self._choice] @@ -653,9 +691,9 @@ class Choice(Asn1Value): value = _fix_tagging(value, params) self._parsed = value - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args raise e @property @@ -683,9 +721,9 @@ class Choice(Asn1Value): info = self._alternatives[self._choice] params = info[2] if len(info) > 2 else {} self._parsed, _ = _parse_build(self.contents, spec=info[1], spec_params=params) - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e @property @@ -731,15 +769,29 @@ class Choice(Asn1Value): # This means the Choice was implicitly tagged if self.class_ is not None and self.tag is not None: if len(self._alternatives) > 1: - raise ValueError('%s was implicitly tagged, but more than one alternative exists' % object_name(self)) + raise ValueError(unwrap( + ''' + %s was implicitly tagged, but more than one alternative + exists + ''', + type_name(self) + )) if id_ == (self.class_, self.tag): self._choice = 0 return asn1 = self._format_class_tag(class_, tag) - asn1s = [self._format_class_tag(id_[0], id_[1]) for id_ in self._id_map] - - raise ValueError('Value %s did not match the class and tag of any of the alternatives in %s: %s' % (asn1, object_name(self), '. '.join(asn1s))) + asn1s = [self._format_class_tag(pair[0], pair[1]) for pair in self._id_map] + + raise ValueError(unwrap( + ''' + Value %s did not match the class and tag of any of the alternatives + in %s: %s + ''', + asn1, + type_name(self), + '. '.join(asn1s) + )) def _format_class_tag(self, class_, tag): """ @@ -749,7 +801,6 @@ class Choice(Asn1Value): return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag) - #pylint: disable=W0212 def _copy(self, other): """ Copies the contents of another Asn1Value object to itself @@ -759,7 +810,13 @@ class Choice(Asn1Value): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) self.contents = other.contents self._native = other._native @@ -823,9 +880,9 @@ class Primitive(Asn1Value): elif default is not None: self.set(default) - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args raise e def set(self, value): @@ -837,7 +894,13 @@ class Primitive(Asn1Value): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) self._native = value self.contents = value @@ -924,7 +987,13 @@ class AbstractString(Primitive): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) self._native = value self.contents = value.encode(self._encoding) @@ -1030,15 +1099,33 @@ class Integer(Primitive, ValueMap): if isinstance(value, str_cls): if self._map is None: - raise ValueError('%s value is a unicode string, but no _map provided' % object_name(self)) + raise ValueError(unwrap( + ''' + %s value is a unicode string, but no _map provided + ''', + type_name(self) + )) if value not in self._reverse_map: - raise ValueError('%s value, %s, is not present in the _map' % (object_name(self), value)) + raise ValueError(unwrap( + ''' + %s value, %s, is not present in the _map + ''', + type_name(self), + value + )) value = self._reverse_map[value] elif not isinstance(value, int_types): - raise ValueError('%s value must be an integer or unicode string when a name_map is provided, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be an integer or unicode string when a name_map + is provided, not %s + ''', + type_name(self), + type_name(value) + )) self._native = self._map[value] if self._map and value in self._map else value @@ -1082,7 +1169,6 @@ class BitString(Primitive, ValueMap, object): _size = None - #pylint: disable=W0212 def _setup(self): """ Generates _reverse_map from _map @@ -1107,7 +1193,12 @@ class BitString(Primitive, ValueMap, object): if isinstance(value, set): if self._map is None: - raise ValueError('%s _map has not been defined' % object_name(self)) + raise ValueError(unwrap( + ''' + %s _map has not been defined + ''', + type_name(self) + )) bits = [0] * self._size self._native = value @@ -1132,12 +1223,26 @@ class BitString(Primitive, ValueMap, object): value = ''.join(map(str_cls, value)) else: - raise ValueError('%s value must be a tuple of ones and zeros or a set of unicode strings, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a tuple of ones and zeros or a set of unicode + strings, not %s + ''', + type_name(self), + type_name(value) + )) if self._map is not None: size = self._size if len(value) > size: - raise ValueError('%s value must be at most %s bits long, specified was %s long' % (object_name(self), size, len(value))) + raise ValueError(unwrap( + ''' + %s value must be at most %s bits long, specified was %s long + ''', + type_name(self), + size, + len(value) + )) value += '0' * (size - len(value)) else: size = len(value) @@ -1185,13 +1290,24 @@ class BitString(Primitive, ValueMap, object): is_int = isinstance(key, int_types) if not is_int: if not isinstance(self._map, dict): - raise ValueError('%s _map has not been defined' % object_name(self)) + raise ValueError(unwrap( + ''' + %s _map has not been defined + ''', + type_name(self) + )) if key not in self._reverse_map: - raise ValueError('%s _map does not contain an entry for "%s"' % (object_name(self), key)) + raise ValueError(unwrap( + ''' + %s _map does not contain an entry for "%s" + ''', + type_name(self), + key + )) if self._native is None: - _ = self.native + self.native if self._map is None: if len(self._native) >= key + 1: @@ -1220,13 +1336,24 @@ class BitString(Primitive, ValueMap, object): is_int = isinstance(key, int_types) if not is_int: if self._map is None: - raise ValueError('%s _map has not been defined' % object_name(self)) + raise ValueError(unwrap( + ''' + %s _map has not been defined + ''', + type_name(self) + )) if key not in self._reverse_map: - raise ValueError('%s _map does not contain an entry for "%s"' % (object_name(self), key)) + raise ValueError(unwrap( + ''' + %s _map does not contain an entry for "%s" + ''', + type_name(self), + key + )) if self._native is None: - _ = self.native + self.native if self._map is None: new_native = list(self._native) @@ -1278,7 +1405,7 @@ class BitString(Primitive, ValueMap, object): # 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] + bit_string = bit_string[0:0 - extra_bits] bits = tuple(map(int, tuple(bit_string))) if self._map: @@ -1311,7 +1438,13 @@ class OctetBitString(Primitive): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) self._native = value # Set the unused bits to 0 @@ -1368,7 +1501,13 @@ class IntegerBitString(Primitive): """ if not isinstance(value, int_types): - raise ValueError('%s value must be an integer, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be an integer, not %s + ''', + type_name(self), + type_name(value) + )) self._native = value # Set the unused bits to 0 @@ -1393,7 +1532,7 @@ class IntegerBitString(Primitive): extra_bits = int_from_bytes(self.contents[0:1]) if extra_bits > 0: bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) - bit_string = bit_string[0:0-extra_bits] + bit_string = bit_string[0:0 - extra_bits] self._native = int(bit_string, 2) else: self._native = int_from_bytes(self.contents[1:]) @@ -1451,7 +1590,13 @@ class IntegerOctetString(Primitive): """ if not isinstance(value, int_types): - raise ValueError('%s value must be an integer, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be an integer, not %s + ''', + type_name(self), + type_name(value) + )) self._native = value # Set the unused bits to 0 @@ -1572,7 +1717,6 @@ class ParsableOctetString(Primitive): return self._parsed[0] - #pylint: disable=W0212 def _copy(self, other): """ Copies the contents of another ParsableOctetString object to itself @@ -1582,7 +1726,13 @@ class ParsableOctetString(Primitive): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) self.contents = other.contents self._native = other._native @@ -1627,7 +1777,13 @@ class ParsableOctetBitString(ParsableOctetString): """ if not isinstance(value, byte_cls): - raise ValueError('%s value must be a byte string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) self._native = value # Set the unused bits to 0 @@ -1701,7 +1857,13 @@ class ObjectIdentifier(Primitive, ValueMap): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) self._native = value @@ -1821,16 +1983,34 @@ class Enumerated(Integer): """ if not isinstance(value, int_types) and not isinstance(value, str_cls): - raise ValueError('%s value must be an integer or a unicode string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be an integer or a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) if isinstance(value, str_cls): if value not in self._reverse_map: - raise ValueError('%s value "%s" is not a valid value' % (object_name(self), value)) + raise ValueError(unwrap( + ''' + %s value "%s" is not a valid value + ''', + type_name(self), + value + )) value = self._reverse_map[value] elif value not in self._map: - raise ValueError('%s value %s is not a valid value' % (object_name(self), value)) + raise ValueError(unwrap( + ''' + %s value %s is not a valid value + ''', + type_name(self), + value + )) Integer.set(self, value) @@ -1968,9 +2148,9 @@ class Sequence(Asn1Value): if key in value: self.__setitem__(key, value[key]) - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args raise e @property @@ -2008,7 +2188,7 @@ class Sequence(Asn1Value): if self.children is not None: for child in self.children: if isinstance(child, Sequence) or isinstance(child, SequenceOf): - mutated = mutated or child._is_mutated() #pylint: disable=W0212 + mutated = mutated or child._is_mutated() return mutated @@ -2054,11 +2234,23 @@ class Sequence(Asn1Value): if not isinstance(key, int_types): if key not in self._field_map: - raise KeyError('No field named "%s" defined for %s' % (key, object_name(self))) + raise KeyError(unwrap( + ''' + No field named "%s" defined for %s + ''', + key, + type_name(self) + )) key = self._field_map[key] if key >= len(self.children): - raise KeyError('No field numbered %s is present in this %s' % (key, object_name(self))) + raise KeyError(unwrap( + ''' + No field numbered %s is present in this %s + ''', + key, + type_name(self) + )) return self._lazy_child(key) @@ -2083,7 +2275,13 @@ class Sequence(Asn1Value): if not isinstance(key, int_types): if key not in self._field_map: - raise KeyError('No field named "%s" defined for %s' % (key, object_name(self))) + raise KeyError(unwrap( + ''' + No field named "%s" defined for %s + ''', + key, + type_name(self) + )) key = self._field_map[key] field_name, field_spec, value_spec, field_params, _ = self._determine_spec(key) @@ -2099,7 +2297,13 @@ class Sequence(Asn1Value): invalid_value = new_value.contents is None if invalid_value: - raise ValueError('Value for field "%s" of %s is not set' % (field_name, object_name(self))) + raise ValueError(unwrap( + ''' + Value for field "%s" of %s is not set + ''', + field_name, + type_name(self) + )) self.children[key] = new_value @@ -2124,12 +2328,25 @@ class Sequence(Asn1Value): if not isinstance(key, int_types): if key not in self._field_map: - raise KeyError('No field named "%s" defined for %s' % (key, object_name(self))) + raise KeyError(unwrap( + ''' + No field named "%s" defined for %s + ''', + key, + type_name(self) + )) key = self._field_map[key] info = self._fields[key] if len(info) < 3 or ('default' not in info[2] and 'optional' not in info[2]): - raise ValueError('Can not delete the value for the field "%s" of %s since it is not optional or defaulted' % (info[0], object_name(self))) + raise ValueError(unwrap( + ''' + Can not delete the value for the field "%s" of %s since it is + not optional or defaulted + ''', + info[0], + type_name(self) + )) if 'optional' in info[2]: self.children[key] = NoValue() @@ -2139,7 +2356,7 @@ class Sequence(Asn1Value): self.__setitem__(key, None) self._mutated = True - def __iter__(self): #pylint: disable=W0234 + def __iter__(self): """ :return: An iterator of field key names @@ -2185,7 +2402,6 @@ class Sequence(Asn1Value): if self._trailer != b'': self._trailer = b'' - #pylint: disable=W0212 def _setup(self): """ Generates _field_map, _field_ids and _oid_nums for use in parsing @@ -2232,7 +2448,7 @@ class Sequence(Asn1Value): # Allow a spec callback to specify both the base spec and # the override, for situations such as OctetString and parse_as if isinstance(spec_override, tuple) and len(spec_override) == 2: - field_spec, value_spec = spec_override #pylint: disable=W0633 + field_spec, value_spec = spec_override if value_spec is None: value_spec = field_spec spec_override = None @@ -2275,11 +2491,18 @@ class Sequence(Asn1Value): if issubclass(value_spec, Choice): if not isinstance(value, Asn1Value): - raise ValueError('Can not set a native python value to %s, which has the choice type of %s – value must be an instance of Asn1Value' % (field_name, value_spec.__name__)) + raise ValueError(unwrap( + ''' + Can not set a native python value to %s, which has the + choice type of %s – value must be an instance of Asn1Value + ''', + field_name, + type_name(value_spec) + )) if not isinstance(value, value_spec): wrapper = value_spec() wrapper.validate(value.class_, value.tag) - wrapper._parsed = value #pylint: disable=W0212 + wrapper._parsed = value new_value = wrapper else: new_value = value @@ -2303,7 +2526,7 @@ class Sequence(Asn1Value): # appropriate encoded value. if specs_different and not is_any: wrapper = field_spec(value=new_value.dump(), **field_params) - wrapper._parsed = (new_value, new_value.__class__, None) #pylint: disable=W0212 + wrapper._parsed = (new_value, new_value.__class__, None) new_value = wrapper new_value = _fix_tagging(new_value, field_params) @@ -2360,7 +2583,7 @@ class Sequence(Asn1Value): tester = field_spec(**field_params) tester.validate(*id_) choice_match = True - except (ValueError): #pylint: disable=W0704 + except (ValueError): pass if not choice_match: @@ -2393,17 +2616,18 @@ class Sequence(Asn1Value): prev_field -= 1 plural = 's' if len(missed_fields) > 1 else '' missed_field_names = ', '.join(missed_fields) - raise ValueError( - 'Data for field %s (%s class, %s method, tag %s) does not match the field definition%s of %s' % - ( - seen_field, - CLASS_NUM_TO_NAME_MAP.get(parts[0]), - METHOD_NUM_TO_NAME_MAP.get(parts[1]), - parts[2], - plural, - missed_field_names - ) - ) + raise ValueError(unwrap( + ''' + Data for field %s (%s class, %s method, tag %s) does + not match the field definition%s of %s + ''', + seen_field, + CLASS_NUM_TO_NAME_MAP.get(parts[0]), + METHOD_NUM_TO_NAME_MAP.get(parts[1]), + parts[2], + plural, + missed_field_names + )) else: child = parts @@ -2411,7 +2635,7 @@ class Sequence(Asn1Value): if recurse: child = _build(*child) if isinstance(child, (Sequence, SequenceOf)): - child._parse_children(recurse=True) #pylint: disable=W0212 + child._parse_children(recurse=True) self.children.append(child) child_pointer += num_bytes @@ -2429,12 +2653,17 @@ class Sequence(Asn1Value): elif 'optional' in field_params: self.children.append(NoValue()) else: - raise ValueError('Field "%s" is missing from structure' % field_info[0]) + raise ValueError(unwrap( + ''' + Field "%s" is missing from structure + ''', + field_info[0] + )) index += 1 - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e def spec(self, field_name): @@ -2454,10 +2683,22 @@ class Sequence(Asn1Value): """ if not isinstance(field_name, str_cls): - raise ValueError('field_name must be a unicode string, not %s' % object_name(field_name)) + raise TypeError(unwrap( + ''' + field_name must be a unicode string, not %s + ''', + type_name(field_name) + )) if self._fields is None: - raise ValueError('Unable to retrieve spec for field %s in the class %s because _fields has not been set' % (repr(field_name), object_name(self))) + raise ValueError(unwrap( + ''' + Unable to retrieve spec for field %s in the class %s because + _fields has not been set + ''', + repr(field_name), + type_name(self) + )) index = self._field_map[field_name] info = self._determine_spec(index) @@ -2492,7 +2733,6 @@ class Sequence(Asn1Value): self._native[name] = child.native return self._native - #pylint: disable=W0212 def _copy(self, other): """ Copies the contents of another Asn1Value object to itself @@ -2502,7 +2742,13 @@ class Sequence(Asn1Value): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) self.contents = other.contents self._native = other._native @@ -2610,9 +2856,9 @@ class SequenceOf(Asn1Value): if self.contents is None: self._set_contents() - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args raise e @property @@ -2650,7 +2896,7 @@ class SequenceOf(Asn1Value): if self.children is not None: for child in self.children: if isinstance(child, Sequence) or isinstance(child, SequenceOf): - mutated = mutated or child._is_mutated() #pylint: disable=W0212 + mutated = mutated or child._is_mutated() return mutated @@ -2684,15 +2930,29 @@ class SequenceOf(Asn1Value): if isinstance(value, Asn1Value): new_value = value else: - raise ValueError('Can not set a native python value to %s where the _child_spec is Any – value must be an instance of Asn1Value' % object_name(self)) + raise ValueError(unwrap( + ''' + Can not set a native python value to %s where the + _child_spec is Any – value must be an instance of Asn1Value + ''', + type_name(self) + )) elif issubclass(self._child_spec, Choice): if not isinstance(value, Asn1Value): - raise ValueError('Can not set a native python value to %s where the _child_spec is the choice type %s – value must be an instance of Asn1Value' % (object_name(self), self._child_spec.__name__)) + raise ValueError(unwrap( + ''' + Can not set a native python value to %s where the + _child_spec is the choice type %s – value must be an + instance of Asn1Value + ''', + type_name(self), + self._child_spec.__name__ + )) if not isinstance(value, self._child_spec): wrapper = self._child_spec() wrapper.validate(value.class_, value.tag) - wrapper._parsed = value #pylint: disable=W0212 + wrapper._parsed = value value = wrapper new_value = value @@ -2783,7 +3043,7 @@ class SequenceOf(Asn1Value): self._mutated = True - def __iter__(self): #pylint: disable=W0234 + def __iter__(self): """ :return: An iter() of child objects @@ -2864,12 +3124,12 @@ class SequenceOf(Asn1Value): if recurse: child = _build(*child) if isinstance(child, (Sequence, SequenceOf)): - child._parse_children(recurse=True) #pylint: disable=W0212 + child._parse_children(recurse=True) self.children.append(child) child_pointer += num_bytes - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e def spec(self): @@ -2902,7 +3162,6 @@ class SequenceOf(Asn1Value): self._native = [child.native for child in self] return self._native - #pylint: disable=W0212 def _copy(self, other): """ Copies the contents of another Asn1Value object to itself @@ -2912,7 +3171,13 @@ class SequenceOf(Asn1Value): """ if self.__class__ != other.__class__: - raise ValueError('Can not copy values from %s object to %s object' % (object_name(other), object_name(self))) + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) self.contents = other.contents self._native = other._native @@ -2969,7 +3234,6 @@ class Set(Sequence): # as values that are the index of the field in _fields _field_ids = None - #pylint: disable=W0212 def _setup(self): """ Generates _field_map, _field_ids and _oid_nums for use in parsing @@ -3030,7 +3294,7 @@ class Set(Sequence): if recurse: child = _build(*child) if isinstance(child, (Sequence, SequenceOf)): - child._parse_children(recurse=True) #pylint: disable=W0212 + child._parse_children(recurse=True) child_map[field] = child child_pointer += num_bytes @@ -3054,15 +3318,21 @@ class Set(Sequence): child_map[index] = field_info[1](**field_info[2]) if missing: - raise ValueError('Missing required field "%s" from %s' % (field_info[0], object_name(self))) + raise ValueError(unwrap( + ''' + Missing required field "%s" from %s + ''', + field_info[0], + type_name(self) + )) self.children = [] for index in range(0, total_fields): self.children.append(child_map[index]) - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % object_name(self),) + args + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e @@ -3222,6 +3492,7 @@ class UTCTime(AbstractTime): return string + class GeneralizedTime(AbstractTime): """ Represents a generalized time from ASN.1 as a Python datetime.datetime @@ -3348,9 +3619,9 @@ def _basic_debug(prefix, self): The object to print the debugging information about """ - print('%s%s Object #%s' % (prefix, object_name(self), id(self))) - if self._header: #pylint: disable=W0212 - print('%s Header: 0x%s' % (prefix, binascii.hexlify(self._header or b'').decode('utf-8'))) #pylint: disable=W0212 + print('%s%s Object #%s' % (prefix, type_name(self), id(self))) + if self._header: + print('%s Header: 0x%s' % (prefix, binascii.hexlify(self._header or b'').decode('utf-8'))) has_header = self.method is not None and self.class_ is not None and self.tag is not None if has_header: @@ -3358,12 +3629,21 @@ def _basic_debug(prefix, self): class_name = CLASS_NUM_TO_NAME_MAP.get(self.class_) if self.tag_type == 'explicit': - print('%s %s tag %s (explicitly tagged)' % (prefix, CLASS_NUM_TO_NAME_MAP.get(self.explicit_class), self.explicit_tag)) + print( + '%s %s tag %s (explicitly tagged)' % + ( + prefix, + CLASS_NUM_TO_NAME_MAP.get(self.explicit_class), + self.explicit_tag + ) + ) if has_header: print('%s %s %s %s' % (prefix, method_name, class_name, self.tag)) + elif self.tag_type == 'implicit': if has_header: print('%s %s %s tag %s (implicitly tagged)' % (prefix, method_name, class_name, self.tag)) + elif has_header: print('%s %s %s tag %s' % (prefix, method_name, class_name, self.tag)) @@ -3485,7 +3765,7 @@ def _parse_id(encoded_data, pointer): original_pointer = pointer - first_octet = ord(encoded_data[pointer:pointer+1]) + first_octet = ord(encoded_data[pointer:pointer + 1]) pointer += 1 class_ = first_octet >> 6 @@ -3496,7 +3776,7 @@ def _parse_id(encoded_data, pointer): if tag == 31: tag = 0 while True: - num = ord(encoded_data[pointer:pointer+1]) + num = ord(encoded_data[pointer:pointer + 1]) pointer += 1 tag *= 128 tag += num & 127 @@ -3564,64 +3844,68 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param elif value.tag_type == 'explicit': if class_ != value.explicit_class: - raise ValueError( - 'Error parsing %s - explicitly-tagged class should have been %s, but %s was found' % - ( - object_name(value), - CLASS_NUM_TO_NAME_MAP.get(value.explicit_class), - CLASS_NUM_TO_NAME_MAP.get(class_, class_) - ) - ) + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged class should have been + %s, but %s was found + ''', + type_name(value), + CLASS_NUM_TO_NAME_MAP.get(value.explicit_class), + CLASS_NUM_TO_NAME_MAP.get(class_, class_) + )) if method != 1: - raise ValueError( - 'Error parsing %s - explicitly-tagged method should have been %s, but %s was found' % - ( - object_name(value), - METHOD_NUM_TO_NAME_MAP.get(1), - METHOD_NUM_TO_NAME_MAP.get(method, method) - ) - ) + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged method should have + been %s, but %s was found + ''', + type_name(value), + METHOD_NUM_TO_NAME_MAP.get(1), + METHOD_NUM_TO_NAME_MAP.get(method, method) + )) if tag != value.explicit_tag: - raise ValueError( - 'Error parsing %s - explicitly-tagged tag should have been %s, but %s was found' % - ( - object_name(value), - value.explicit_tag, - tag - ) - ) + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged tag should have been + %s, but %s was found + ''', + type_name(value), + value.explicit_tag, + tag + )) elif isinstance(value, Choice): value.validate(class_, tag) else: if class_ != value.class_: - raise ValueError( - 'Error parsing %s - class should have been %s, but %s was found' % - ( - object_name(value), - CLASS_NUM_TO_NAME_MAP.get(value.class_), - CLASS_NUM_TO_NAME_MAP.get(class_, class_) - ) - ) + raise ValueError(unwrap( + ''' + Error parsing %s - class should have been %s, but %s was + found + ''', + type_name(value), + CLASS_NUM_TO_NAME_MAP.get(value.class_), + CLASS_NUM_TO_NAME_MAP.get(class_, class_) + )) if method != value.method: - raise ValueError( - 'Error parsing %s - method should have been %s, but %s was found' % - ( - object_name(value), - METHOD_NUM_TO_NAME_MAP.get(value.method), - METHOD_NUM_TO_NAME_MAP.get(method, method) - ) - ) + raise ValueError(unwrap( + ''' + Error parsing %s - method should have been %s, but %s was found + ''', + type_name(value), + METHOD_NUM_TO_NAME_MAP.get(value.method), + METHOD_NUM_TO_NAME_MAP.get(method, method) + )) if tag != value.tag: - raise ValueError( - 'Error parsing %s - tag should have been %s, but %s was found' % - ( - object_name(value), - value.tag, - 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 @@ -3663,39 +3947,39 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param 30: BMPString } if tag not in universal_specs: - raise ValueError( - 'Unknown element - %s class, %s method, tag %s' % - ( - CLASS_NUM_TO_NAME_MAP.get(class_), - METHOD_NUM_TO_NAME_MAP.get(method), - tag, - ) - ) + raise ValueError(unwrap( + ''' + Unknown element - %s class, %s method, tag %s + ''', + CLASS_NUM_TO_NAME_MAP.get(class_), + METHOD_NUM_TO_NAME_MAP.get(method), + tag + )) spec = universal_specs[tag] value = spec(contents=contents, class_=class_) - value._header = header #pylint: disable=W0212 + value._header = header if trailer is not None and trailer != b'': - value._trailer = trailer #pylint: disable=W0212 + value._trailer = trailer # Destroy any default value that our contents have overwritten - value._native = None #pylint: disable=W0212 + value._native = None # For explicitly tagged values, parse the inner value and pull it out if value.tag_type == 'explicit': original_value = value (class_, method, tag, header, contents, trailer), _ = _parse(value.contents) value = _build(class_, method, tag, header, contents, trailer, spec=spec) - value._header = original_value._header + header #pylint: disable=W0212 - value._trailer += original_value._trailer #pylint: disable=W0212 + value._header = original_value._header + header + value._trailer += original_value._trailer value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class value.explicit_tag = original_value.explicit_tag elif isinstance(value, Choice): - value.contents = value._header + value.contents #pylint: disable=W0212 - value._header = b'' #pylint: disable=W0212 + value.contents = value._header + value.contents + value._header = b'' try: # Force parsing the Choice now @@ -3704,9 +3988,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param if nested_spec: value.parse(nested_spec) - except (ValueError) as e: + except (ValueError, TypeError) as e: args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % object_name(value),) + args + e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args raise e return value @@ -3732,9 +4016,16 @@ def _parse(encoded_data, pointer=0): return ((None, None, None, None, None, None), 0) encoded_length = len(encoded_data) + def _slice(start, end): if end > encoded_length: - raise ValueError('Insufficient data - %s bytes requested but only %s available' % (end, encoded_length)) + raise ValueError(unwrap( + ''' + Insufficient data - %s bytes requested but only %s available + ''', + end, + encoded_length + )) return encoded_data[start:end] start = pointer @@ -3742,7 +4033,7 @@ def _parse(encoded_data, pointer=0): class_, method, tag, num_bytes = _parse_id(encoded_data, pointer) pointer += num_bytes - length_octet = ord(_slice(pointer, pointer+1)) + length_octet = ord(_slice(pointer, pointer + 1)) pointer += 1 length_type = length_octet >> 7 if length_type == 1: @@ -3750,7 +4041,7 @@ def _parse(encoded_data, pointer=0): remaining_length_octets = length_octet & 127 while remaining_length_octets > 0: length *= 256 - length += ord(_slice(pointer, pointer+1)) + length += ord(_slice(pointer, pointer + 1)) pointer += 1 remaining_length_octets -= 1 else: @@ -3765,7 +4056,7 @@ def _parse(encoded_data, pointer=0): pointer = end_token + 2 trailer = b'\x00\x00' else: - contents = _slice(pointer, pointer+length) + contents = _slice(pointer, pointer + length) pointer += length trailer = b'' diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index a2be5bf..c017aa2 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -35,7 +35,6 @@ from .x509 import ( ) - # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 diff --git a/asn1crypto/csr.py b/asn1crypto/csr.py index 05085cf..511675c 100644 --- a/asn1crypto/csr.py +++ b/asn1crypto/csr.py @@ -17,7 +17,6 @@ from .keys import PublicKeyInfo from .x509 import DirectoryString, Extensions, Name - # The structures in this file are taken from https://tools.ietf.org/html/rfc2986 # and https://tools.ietf.org/html/rfc2985 diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index e6e39b2..a44a4fc 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -45,19 +45,9 @@ from ._elliptic_curve import ( PrimeCurve, PrimePoint, ) +from ._errors import unwrap from .util import int_from_bytes, int_to_bytes -from ._errors import object_name - -try: - # Python 2 - str_cls = unicode #pylint: disable=E0602 - byte_cls = str - -except NameError: - # Python 3 - str_cls = str - byte_cls = bytes - +from ._types import type_name, str_cls, byte_cls class OtherPrimeInfo(Sequence): @@ -198,9 +188,18 @@ class _ECPoint(): return (x, y) if first_byte not in set([b'\x02', b'\x03']): - raise ValueError('Invalid EC public key - first byte is incorrect') + raise ValueError(unwrap( + ''' + Invalid EC public key - first byte is incorrect + ''' + )) - raise ValueError('Compressed representations of EC public keys are not supported due to patent US6252960') + raise ValueError(unwrap( + ''' + Compressed representations of EC public keys are not supported due + to patent US6252960 + ''' + )) class ECPoint(OctetString, _ECPoint): @@ -534,7 +533,12 @@ class PrivateKeyInfo(Sequence): """ if not isinstance(private_key, byte_cls) and not isinstance(private_key, Asn1Value): - raise ValueError('private_key must be a byte string or Asn1Value, not %s' % object_name(private_key)) + raise TypeError(unwrap( + ''' + private_key must be a byte string or Asn1Value, not %s + ''', + type_name(private_key) + )) if algorithm == 'rsa': if not isinstance(private_key, RSAPrivateKey): @@ -555,14 +559,19 @@ class PrivateKeyInfo(Sequence): params = private_key['parameters'] del private_key['parameters'] else: - raise ValueError('algorithm must be one of "rsa", "dsa", "ec", not %s' % repr(algorithm)) + raise ValueError(unwrap( + ''' + algorithm must be one of "rsa", "dsa", "ec", not %s + ''', + repr(algorithm) + )) private_key_algo = PrivateKeyAlgorithm() private_key_algo['algorithm'] = PrivateKeyAlgorithmId(algorithm) private_key_algo['parameters'] = params container = cls() - container._algorithm = algorithm #pylint: disable=W0212 + container._algorithm = algorithm container['version'] = Integer(0) container['private_key_algorithm'] = private_key_algo container['private_key'] = private_key @@ -570,7 +579,7 @@ class PrivateKeyInfo(Sequence): # Here we save the DSA public key if possible since it is not contained # within the PKCS#8 structure for a DSA key if algorithm == 'dsa': - container._public_key = public_key #pylint: disable=W0212 + container._public_key = public_key return container @@ -602,11 +611,21 @@ class PrivateKeyInfo(Sequence): curve_type, details = self.curve if curve_type == 'implicit_ca': - raise ValueError('Unable to compute public key for EC key using Implicit CA parameters') + 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('Unable to compute public key for EC key over a 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'], @@ -618,7 +637,13 @@ class PrivateKeyInfo(Sequence): elif curve_type == 'named': if details not in ('secp192r1', 'secp224r1', 'secp256r1', 'secp384r1', 'secp521r1'): - raise ValueError('Unable to compute public key for EC named curve %s, parameters not currently included' % details) + raise ValueError(unwrap( + ''' + Unable to compute public key for EC named curve %s, + parameters not currently included + ''', + details + )) base_point = { 'secp192r1': SECP192R1_BASE_POINT, @@ -677,7 +702,12 @@ class PrivateKeyInfo(Sequence): """ if self.algorithm != 'ec': - raise ValueError('Only EC keys have a curve, this key is %s' % self.algorithm.upper()) + raise ValueError(unwrap( + ''' + Only EC keys have a curve, this key is %s + ''', + self.algorithm.upper() + )) params = self['private_key_algorithm']['parameters'] chosen = params.chosen @@ -703,7 +733,13 @@ class PrivateKeyInfo(Sequence): """ if self.algorithm != 'dsa': - raise ValueError('Only DSA keys are generated using a hash algorithm, this key is %s' % self.algorithm.upper()) + raise ValueError(unwrap( + ''' + Only DSA keys are generated using a hash algorithm, this key is + %s + ''', + self.algorithm.upper() + )) byte_len = math.log(self['private_key_algorithm']['parameters']['q'].native, 2) / 8 @@ -922,10 +958,20 @@ class PublicKeyInfo(Sequence): """ if not isinstance(public_key, byte_cls) and not isinstance(public_key, Asn1Value): - raise ValueError('public_key must be a byte string or Asn1Value, not %s' % object_name(public_key)) + raise TypeError(unwrap( + ''' + public_key must be a byte string or Asn1Value, not %s + ''', + type_name(public_key) + )) if algorithm != 'rsa': - raise ValueError('algorithm must "rsa", not %s' % repr(algorithm)) + raise ValueError(unwrap( + ''' + algorithm must "rsa", not %s + ''', + repr(algorithm) + )) algo = PublicKeyAlgorithm() algo['algorithm'] = PublicKeyAlgorithmId(algorithm) @@ -953,7 +999,14 @@ class PublicKeyInfo(Sequence): key_type = self.algorithm.upper() a_an = 'an' if key_type == 'EC' else 'a' - raise ValueError('Only RSA public keys may be unwrapped - this key is %s %s public key' % (a_an, key_type)) + raise ValueError(unwrap( + ''' + Only RSA public keys may be unwrapped - this key is %s %s public + key + ''', + a_an, + key_type + )) @property def curve(self): @@ -972,7 +1025,12 @@ class PublicKeyInfo(Sequence): """ if self.algorithm != 'ec': - raise ValueError('Only EC keys have a curve, this key is %s' % self.algorithm.upper()) + raise ValueError(unwrap( + ''' + Only EC keys have a curve, this key is %s + ''', + self.algorithm.upper() + )) params = self['algorithm']['parameters'] chosen = params.chosen @@ -998,7 +1056,13 @@ class PublicKeyInfo(Sequence): """ if self.algorithm != 'dsa': - raise ValueError('Only DSA keys are generated using a hash algorithm, this key is %s' % self.algorithm.upper()) + raise ValueError(unwrap( + ''' + Only DSA keys are generated using a hash algorithm, this key is + %s + ''', + self.algorithm.upper() + )) byte_len = math.log(self['algorithm']['parameters']['q'].native, 2) / 8 diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 8ce5735..cd2c5c9 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -33,7 +33,6 @@ from .keys import PublicKeyAlgorithm from .x509 import Certificate, GeneralName, GeneralNames, Name - # The structures in this file are taken from https://tools.ietf.org/html/rfc6960 diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index 5a23323..c83f0ae 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -22,7 +22,6 @@ from .ocsp import OCSPResponse from .x509 import ExtensionId, Extension, GeneralName, KeyPurposeId - class AdobeArchiveRevInfo(Sequence): _fields = [ ('version', Integer) @@ -56,12 +55,12 @@ class SetOfRevocationInfoArchival(SetOf): _child_spec = RevocationInfoArchival -ExtensionId._map['1.2.840.113583.1.1.9.2'] = 'adobe_archive_rev_info' #pylint: disable=W0212 -ExtensionId._map['1.2.840.113583.1.1.9.1'] = 'adobe_timestamp' #pylint: disable=W0212 -ExtensionId._map['1.2.840.113583.1.1.10'] = 'adobe_ppklite_credential' #pylint: disable=W0212 -Extension._oid_specs['adobe_archive_rev_info'] = AdobeArchiveRevInfo #pylint: disable=W0212 -Extension._oid_specs['adobe_timestamp'] = AdobeTimestamp #pylint: disable=W0212 -Extension._oid_specs['adobe_ppklite_credential'] = Null #pylint: disable=W0212 -KeyPurposeId._map['1.2.840.113583.1.1.5'] = 'pdf_signing' #pylint: disable=W0212 -CMSAttributeType._map['1.2.840.113583.1.1.8'] = 'adobe_revocation_info_archival' #pylint: disable=W0212 -CMSAttribute._oid_specs['adobe_revocation_info_archival'] = SetOfRevocationInfoArchival #pylint: disable=W0212 +ExtensionId._map['1.2.840.113583.1.1.9.2'] = 'adobe_archive_rev_info' +ExtensionId._map['1.2.840.113583.1.1.9.1'] = 'adobe_timestamp' +ExtensionId._map['1.2.840.113583.1.1.10'] = 'adobe_ppklite_credential' +Extension._oid_specs['adobe_archive_rev_info'] = AdobeArchiveRevInfo +Extension._oid_specs['adobe_timestamp'] = AdobeTimestamp +Extension._oid_specs['adobe_ppklite_credential'] = Null +KeyPurposeId._map['1.2.840.113583.1.1.5'] = 'pdf_signing' +CMSAttributeType._map['1.2.840.113583.1.1.8'] = 'adobe_revocation_info_archival' +CMSAttribute._oid_specs['adobe_revocation_info_archival'] = SetOfRevocationInfoArchival diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py index b960981..2ee0088 100644 --- a/asn1crypto/pem.py +++ b/asn1crypto/pem.py @@ -15,19 +15,15 @@ import sys import base64 import re -from ._errors import object_name +from ._errors import unwrap +from ._types import type_name, str_cls, byte_cls if sys.version_info < (3,): - str_cls = unicode #pylint: disable=E0602 - byte_cls = str - from cStringIO import StringIO as BytesIO #pylint: disable=F0401 + from cStringIO import StringIO as BytesIO else: - str_cls = str - byte_cls = bytes from io import BytesIO - def detect(byte_string): """ Detect if a byte string seems to contain a PEM-encoded block @@ -41,7 +37,12 @@ def detect(byte_string): """ if not isinstance(byte_string, byte_cls): - raise ValueError('byte_string must be a byte string, not %s' % object_name(byte_string)) + raise TypeError(unwrap( + ''' + byte_string must be a byte string, not %s + ''', + type_name(byte_string) + )) return byte_string.find(b'-----BEGIN') != -1 or byte_string.find(b'---- BEGIN') != -1 @@ -67,10 +68,19 @@ def armor(type_name, der_bytes, headers=None): """ if not isinstance(der_bytes, byte_cls): - raise ValueError('der_bytes must be a byte string, not %s' % object_name(der_bytes)) + raise TypeError(unwrap( + ''' + der_bytes must be a byte string, not %s + ''' % type_name(der_bytes) + )) if not isinstance(type_name, str_cls): - raise ValueError('type_name must be a unicode string, not %s' % object_name(type_name)) + raise TypeError(unwrap( + ''' + type_name must be a unicode string, not %s + ''', + type_name(type_name) + )) type_name = type_name.upper().encode('ascii') @@ -89,7 +99,7 @@ def armor(type_name, der_bytes, headers=None): b64_len = len(b64_bytes) i = 0 while i < b64_len: - output.write(b64_bytes[i:i+64]) + output.write(b64_bytes[i:i + 64]) output.write(b'\n') i += 64 output.write(b'-----END ') @@ -110,21 +120,26 @@ def _unarmor(pem_bytes): ValueError - when the pem_bytes do not appear to be PEM-encoded bytes :return: - A generator of 3-element tuples in the format: (type_name, headers, - der_bytes). The type_name is a unicode string of what is between + A generator of 3-element tuples in the format: (object_type, headers, + der_bytes). The object_type is a unicode string of what is between "-----BEGIN " and "-----". Examples include: "CERTIFICATE", "PUBLIC KEY", "PRIVATE KEY". The headers is a dict containing any lines in the form "Name: Value" that are right after the begin line. """ if not isinstance(pem_bytes, byte_cls): - raise ValueError('pem_bytes must be a byte string, not %s' % object_name(pem_bytes)) + raise TypeError(unwrap( + ''' + pem_bytes must be a byte string, not %s + ''', + type_name(pem_bytes) + )) # Valid states include: "trash", "headers", "body" state = 'trash' headers = {} base64_data = b'' - type_name = None + object_type = None found_start = False found_end = False @@ -139,7 +154,7 @@ def _unarmor(pem_bytes): type_name_match = re.match(b'^(?:---- |-----)BEGIN ([A-Z0-9 ]+)(?: ----|-----)', line) if not type_name_match: continue - type_name = type_name_match.group(1).decode('ascii') + object_type = type_name_match.group(1).decode('ascii') found_start = True state = 'headers' @@ -158,19 +173,24 @@ def _unarmor(pem_bytes): if line[0:5] in (b'-----', b'---- '): der_bytes = base64.b64decode(base64_data) - yield (type_name, headers, der_bytes) + yield (object_type, headers, der_bytes) state = 'trash' headers = {} base64_data = b'' - type_name = None + object_type = None found_end = True continue base64_data += line if not found_start or not found_end: - raise ValueError('pem_bytes does not appear to contain PEM-encoded data - no BEGIN/END combination found') + raise ValueError(unwrap( + ''' + pem_bytes does not appear to contain PEM-encoded data - no + BEGIN/END combination found + ''' + )) def unarmor(pem_bytes, multiple=False): @@ -187,8 +207,8 @@ def unarmor(pem_bytes, multiple=False): ValueError - when the pem_bytes do not appear to be PEM-encoded bytes :return: - A 3-element tuple (type_name, headers, der_bytes). The type_name is a - unicode string of what is between "-----BEGIN " and "-----". Examples + A 3-element tuple (object_name, headers, der_bytes). The object_name is + a unicode string of what is between "-----BEGIN " and "-----". Examples include: "CERTIFICATE", "PUBLIC KEY", "PRIVATE KEY". The headers is a dict containing any lines in the form "Name: Value" that are right after the begin line. diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index d4999d7..32ca18d 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -31,7 +31,6 @@ from .keys import PrivateKeyInfo, EncryptedPrivateKeyInfo from .x509 import Certificate - # The structures in this file are taken from https://tools.ietf.org/html/rfc7292 class MacData(Sequence): @@ -173,4 +172,4 @@ class SafeBag(Sequence): } -SafeContents._child_spec = SafeBag #pylint: disable=W0212 +SafeContents._child_spec = SafeBag diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py index a3e851c..2f8910c 100644 --- a/asn1crypto/tsp.py +++ b/asn1crypto/tsp.py @@ -294,12 +294,12 @@ class SetOfSigningCertificatesV2(SetOf): _child_spec = SigningCertificateV2 -EncapsulatedContentInfo._oid_specs['tst_info'] = TSTInfo #pylint: disable=W0212 -EncapsulatedContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 -ContentInfo._oid_specs['timestamped_data'] = TimeStampedData #pylint: disable=W0212 -ContentType._map['1.2.840.113549.1.9.16.1.4'] = 'tst_info' #pylint: disable=W0212 -ContentType._map['1.2.840.113549.1.9.16.1.31'] = 'timestamped_data' #pylint: disable=W0212 -CMSAttributeType._map['1.2.840.113549.1.9.16.2.12'] = 'signing_certificate' #pylint: disable=W0212 -CMSAttribute._oid_specs['signing_certificate'] = SetOfSigningCertificates #pylint: disable=W0212 -CMSAttributeType._map['1.2.840.113549.1.9.16.2.47'] = 'signing_certificate_v2' #pylint: disable=W0212 -CMSAttribute._oid_specs['signing_certificate_v2'] = SetOfSigningCertificatesV2 #pylint: disable=W0212 +EncapsulatedContentInfo._oid_specs['tst_info'] = TSTInfo +EncapsulatedContentInfo._oid_specs['timestamped_data'] = TimeStampedData +ContentInfo._oid_specs['timestamped_data'] = TimeStampedData +ContentType._map['1.2.840.113549.1.9.16.1.4'] = 'tst_info' +ContentType._map['1.2.840.113549.1.9.16.1.31'] = 'timestamped_data' +CMSAttributeType._map['1.2.840.113549.1.9.16.2.12'] = 'signing_certificate' +CMSAttribute._oid_specs['signing_certificate'] = SetOfSigningCertificates +CMSAttributeType._map['1.2.840.113549.1.9.16.2.47'] = 'signing_certificate_v2' +CMSAttribute._oid_specs['signing_certificate_v2'] = SetOfSigningCertificatesV2 diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 2265fcc..e83a3bb 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -18,13 +18,13 @@ import sys import math -from ._ordereddict import OrderedDict #pylint: disable=W0611 +from ._ordereddict import OrderedDict # noqa if sys.platform == 'win32': - from ._win._ws2_32 import inet_ntop, inet_pton #pylint: disable=W0611 + from ._win._ws2_32 import inet_ntop, inet_pton else: - from socket import inet_ntop, inet_pton + from socket import inet_ntop, inet_pton # noqa # Python 2 @@ -32,7 +32,6 @@ if sys.version_info <= (3,): from datetime import timedelta, tzinfo - def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string @@ -92,7 +91,7 @@ if sys.version_info <= (3,): An integer """ - num = long(value.encode("hex"), 16) #pylint: disable=E0602 + num = long(value.encode("hex"), 16) # noqa if not signed: return num @@ -104,8 +103,7 @@ if sys.version_info <= (3,): return num - - class utc(tzinfo): + class utc(tzinfo): # noqa def tzname(self, _): return 'UTC+00:00' @@ -116,8 +114,7 @@ if sys.version_info <= (3,): def dst(self, _): return timedelta(0) - - class timezone(): + class timezone(): # noqa utc = utc() @@ -125,8 +122,7 @@ if sys.version_info <= (3,): # Python 3 else: - from datetime import timezone #pylint: disable=W0611 - + from datetime import timezone # noqa def int_to_bytes(value, signed=False, width=None): """ diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 99ec55e..0598106 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -21,7 +21,7 @@ import hashlib import stringprep import unicodedata import socket -from encodings import idna #pylint: disable=W0611 +from encodings import idna # noqa import codecs from ._ordereddict import OrderedDict @@ -55,23 +55,19 @@ from .core import ( from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton -from ._errors import object_name +from ._errors import unwrap +from ._types import type_name, str_cls, byte_cls if sys.version_info < (3,): - str_cls = unicode #pylint: disable=E0602 - byte_cls = str - from urlparse import urlsplit, urlunsplit #pylint: disable=F0401 - from urllib import quote as urlquote, unquote as unquote_to_bytes #pylint: disable=E0611 + from urlparse import urlsplit, urlunsplit + from urllib import quote as urlquote, unquote as unquote_to_bytes bytes_to_list = lambda byte_string: [ord(b) for b in byte_string] else: - str_cls = str - byte_cls = bytes bytes_to_list = list from urllib.parse import urlsplit, urlunsplit, quote as urlquote, unquote_to_bytes - # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 # and a few other supplementary sources, mostly due to extra supported # extension and name OIDs @@ -112,7 +108,13 @@ class URI(IA5String): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) self._normalized = True self._native = value @@ -261,7 +263,13 @@ class EmailAddress(IA5String): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) if value.find('@') != -1: mailbox, hostname = value.rsplit('@', 1) @@ -307,13 +315,13 @@ class EmailAddress(IA5String): if not self._normalized: self.set(self.native) - if not other._normalized: #pylint: disable=W0212 + if not other._normalized: other.set(other.native) - if self._contents.find(b'@') == -1 or other._contents.find(b'@') == -1: #pylint: disable=W0212 - return self._contents == other._contents #pylint: disable=W0212 + if self._contents.find(b'@') == -1 or other._contents.find(b'@') == -1: + return self._contents == other._contents - other_mailbox, other_hostname = other._contents.rsplit(b'@', 1) #pylint: disable=W0212 + other_mailbox, other_hostname = other._contents.rsplit(b'@', 1) mailbox, hostname = self._contents.rsplit(b'@', 1) if mailbox != other_mailbox: @@ -326,12 +334,16 @@ class EmailAddress(IA5String): class IPAddress(OctetString): - def parse(self, spec=None, spec_params=None): #pylint: disable=W0613 + def parse(self, spec=None, spec_params=None): """ This method is not applicable to IP addresses """ - raise ValueError('IP address values can not be parsed') + raise ValueError(unwrap( + ''' + IP address values can not be parsed + ''' + )) def set(self, value): """ @@ -343,7 +355,13 @@ class IPAddress(OctetString): """ if not isinstance(value, str_cls): - raise ValueError('%s value must be a unicode string, not %s' % (object_name(self), object_name(value))) + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) original_value = value @@ -354,17 +372,34 @@ class IPAddress(OctetString): value = parts[0] cidr = int(parts[1]) if cidr < 0: - raise ValueError('%s value contains a CIDR range less than 0' % object_name(self)) + raise ValueError(unwrap( + ''' + %s value contains a CIDR range less than 0 + ''', + type_name(self) + )) if value.find(':') != -1: family = socket.AF_INET6 if cidr > 128: - raise ValueError('%s value contains a CIDR range bigger than 128, the maximum value for an IPv6 address' % object_name(self)) + raise ValueError(unwrap( + ''' + %s value contains a CIDR range bigger than 128, the maximum + value for an IPv6 address + ''', + type_name(self) + )) cidr_size = 128 else: family = socket.AF_INET if cidr > 32: - raise ValueError('%s value contains a CIDR range bigger than 32, the maximum value for an IPv4 address' % object_name(self)) + raise ValueError(unwrap( + ''' + %s value contains a CIDR range bigger than 32, the maximum + value for an IPv4 address + ''', + type_name(self) + )) cidr_size = 32 cidr_bytes = b'' @@ -659,7 +694,12 @@ class NameTypeAndValue(Sequence): string = re.sub('\ud834[\udd73-\udd7a]|\udb40[\udc20-\udc7f]|\U000e0001', '', string) else: string = re.sub('[\U0001D173-\U0001D17A\U000E0020-\U000E007F\U000e0001]', '', string) - string = re.sub('[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb]+', '', string) + string = re.sub( + '[\u0000-\u0008\u000e-\u001f\u007f-\u0084\u0086-\u009f\u06dd\u070f\u180e\u200c-\u200f' + '\u202a-\u202e\u2060-\u2063\u206a-\u206f\ufeff\ufff9-\ufffb]+', + '', + string + ) string = string.replace('\u200b', '') string = re.sub('[\u00a0\u1680\u2000-\u200a\u2028-\u2029\u202f\u205f\u3000]', ' ', string) @@ -671,22 +711,47 @@ class NameTypeAndValue(Sequence): # Prohibit step for char in string: if stringprep.in_table_a1(char): - raise ValueError('X.509 Name objects may not contain unassigned code points') + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain unassigned code points + ''' + )) if stringprep.in_table_c8(char): - raise ValueError('X.509 Name objects may not contain change display or deprecated characters') + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain change display or + zzzzdeprecated characters + ''' + )) if stringprep.in_table_c3(char): - raise ValueError('X.509 Name objects may not contain private use characters') + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain private use characters + ''' + )) if stringprep.in_table_c4(char): - raise ValueError('X.509 Name objects may not contain non-character code points') + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain non-character code points + ''' + )) if stringprep.in_table_c5(char): - raise ValueError('X.509 Name objects may not contain surrogate code points') + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain surrogate code points + ''' + )) if char == '\ufffd': - raise ValueError('X.509 Name objects may not contain the replacement character') + raise ValueError(unwrap( + ''' + X.509 Name objects may not contain the replacement character + ''' + )) # Check bidirectional step - here we ensure that we are not mixing # left-to-right and right-to-left text in the string @@ -703,7 +768,12 @@ class NameTypeAndValue(Sequence): last_is_r_and_al = stringprep.in_table_d1(string[-1]) if has_l_cat or not first_is_r_and_al or not last_is_r_and_al: - raise ValueError('X.509 Name object contains a malformed bidirectional sequence') + raise ValueError(unwrap( + ''' + X.509 Name object contains a malformed bidirectional + sequence + ''' + )) # Insignificant space handling step string = ' ' + re.sub(' +', ' ', string).strip() + ' ' @@ -759,8 +829,8 @@ class RelativeDistinguishedName(SetOf): self_values = self._get_values(self) other_values = self._get_values(other) - for type_name in self_types: - if self_values[type_name] != other_values[type_name]: + for type_name_ in self_types: + if self_values[type_name_] != other_values[type_name_]: return False return True @@ -792,7 +862,7 @@ class RelativeDistinguishedName(SetOf): """ output = {} - [output.update([(ntv['type'].native, ntv.prepped_value)]) for ntv in rdn] #pylint: disable=W0106 + [output.update([(ntv['type'].native, ntv.prepped_value)]) for ntv in rdn] return output @@ -1260,10 +1330,21 @@ class GeneralName(Choice): """ if self.name in ('other_name', 'x400_address', 'edi_party_name'): - raise ValueError('comparison is not supported for GeneralName objects of choice %s' % self.name) + raise ValueError(unwrap( + ''' + Comparison is not supported for GeneralName objects of + choice %s + ''', + self.name + )) if other.name in ('other_name', 'x400_address', 'edi_party_name'): - raise ValueError('comparison is not supported for GeneralName objects of choice %s' % other.name) + raise ValueError(unwrap( + ''' + Comparison is not supported for GeneralName objects of choice + %s''', + other.name + )) if self.name != other.name: return False @@ -1364,7 +1445,12 @@ class DistributionPoint(Sequence): self._url = None name = self['distribution_point'] if name.name != 'full_name': - raise ValueError('CRL distribution points that are relative to the issuer are not supported') + raise ValueError(unwrap( + ''' + CRL distribution points that are relative to the issuer are + not supported + ''' + )) for general_name in name.chosen: if general_name.name == 'uniform_resource_identifier': @@ -2059,12 +2145,13 @@ class Certificate(Sequence): """ if self._authority_issuer_serial is False: - if self.authority_key_identifier_value and self.authority_key_identifier_value['authority_cert_issuer'].native: - authority_issuer = self.authority_key_identifier_value['authority_cert_issuer'][0].chosen + akiv = self.authority_key_identifier_value + if akiv and akiv['authority_cert_issuer'].native: + issuer = self.authority_key_identifier_value['authority_cert_issuer'][0].chosen # We untag the element since it is tagged via being a choice from GeneralName - authority_issuer = authority_issuer.untag() + issuer = issuer.untag() authority_serial = self.authority_key_identifier_value['authority_cert_serial_number'].native - self._authority_issuer_serial = authority_issuer.sha256 + b':' + str_cls(authority_serial).encode('ascii') + self._authority_issuer_serial = issuer.sha256 + b':' + str_cls(authority_serial).encode('ascii') else: self._authority_issuer_serial = None return self._authority_issuer_serial @@ -2289,7 +2376,12 @@ class Certificate(Sequence): """ if not isinstance(domain_ip, str_cls): - raise TypeError('domain_ip must be a unicode string, not %s' % object_name(domain_ip)) + raise TypeError(unwrap( + ''' + domain_ip must be a unicode string, not %s + ''', + type_name(domain_ip) + )) encoded_domain_ip = domain_ip.encode('idna').decode('ascii').lower() @@ -2315,7 +2407,8 @@ class Certificate(Sequence): if valid_domain_labels == domain_labels: return True - if self._is_wildcard_domain(encoded_valid_domain) and self._is_wildcard_match(domain_labels, valid_domain_labels): + is_wildcard = self._is_wildcard_domain(encoded_valid_domain) + if is_wildcard and self._is_wildcard_match(domain_labels, valid_domain_labels): return True return False diff --git a/dev-requirements.txt b/dev-requirements.txt index fc7a185..a178187 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,2 @@ coverage>=4.0b1 -pylint \ No newline at end of file +flake8 \ No newline at end of file diff --git a/dev/lint.py b/dev/lint.py index 7f5f15b..ec35573 100644 --- a/dev/lint.py +++ b/dev/lint.py @@ -3,23 +3,22 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import os -from pylint.lint import Run +from flake8.engine import get_style_guide cur_dir = os.path.dirname(__file__) -rc_path = os.path.join(cur_dir, '..', '.pylintrc') +config_file = os.path.join(cur_dir, '..', '.pep8') def run(): - print('Running pylint...') + print('Running flake8...') - files = [] - for root, _, filenames in os.walk('../asn1crypto/'): + flake8_style = get_style_guide(config_file=config_file) + + paths = [] + for root, _, filenames in os.walk('asn1crypto'): for filename in filenames: if not filename.endswith('.py'): continue - files.append(os.path.join(root, filename)) - - args = ['--rcfile=%s' % rc_path] + files - - Run(args) + paths.append(os.path.join(root, filename)) + flake8_style.check_files(paths) diff --git a/tests/_unittest_compat.py b/tests/_unittest_compat.py index 3064061..7b867b4 100644 --- a/tests/_unittest_compat.py +++ b/tests/_unittest_compat.py @@ -29,7 +29,7 @@ def _assert_is_instance(self, obj, cls, msg=None): self.fail(msg) -def _assert_raises(self, excClass, callableObj=None, *args, **kwargs): +def _assert_raises(self, excClass, callableObj=None, *args, **kwargs): # noqa context = _AssertRaisesContext(excClass, self) if callableObj is None: return context @@ -59,7 +59,7 @@ class _AssertRaisesContext(object): if not issubclass(exc_type, self.expected): # let unexpected exceptions pass through return False - self.exception = exc_value # store for later retrieval + self.exception = exc_value # store for later retrieval if self.expected_regexp is None: return True diff --git a/tests/test_cms.py b/tests/test_cms.py index c8a6c41..6adb15d 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -14,7 +14,6 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') - class CMSTests(unittest.TestCase): def test_parse_content_info_data(self): @@ -57,7 +56,9 @@ class CMSTests(unittest.TestCase): compressed_data['encap_content_info']['content_type'].native ) self.assertEqual( - b'\x78\x9C\x0B\xC9\xC8\x2C\x56\x00\xA2\x92\x8C\x54\x85\xDC\xD4\xE2\xE2\xC4\xF4\x54\x85\x92\x7C\x85\xD4\xBC\xE4\xC4\x82\xE2\xD2\x9C\xC4\x92\x54\x85\xCC\x3C\x85\x00\x6F\xE7\x60\x65\x73\x7D\x67\xDF\x60\x2E\x00\xB5\xCF\x10\x71', + b'\x78\x9C\x0B\xC9\xC8\x2C\x56\x00\xA2\x92\x8C\x54\x85\xDC\xD4\xE2\xE2\xC4\xF4\x54\x85\x92\x7C\x85\xD4\xBC' + b'\xE4\xC4\x82\xE2\xD2\x9C\xC4\x92\x54\x85\xCC\x3C\x85\x00\x6F\xE7\x60\x65\x73\x7D\x67\xDF\x60\x2E\x00\xB5' + b'\xCF\x10\x71', compressed_data['encap_content_info']['content'].native ) self.assertEqual( @@ -128,7 +129,9 @@ class CMSTests(unittest.TestCase): encrypted_content_info['content_encryption_algorithm']['parameters'].native ) self.assertEqual( - b'\x80\xEE\x34\x8B\xFC\x04\x69\x4F\xBE\x15\x1C\x0C\x39\x2E\xF3\xEA\x8E\xEE\x17\x0D\x39\xC7\x4B\x6C\x4B\x13\xEF\x17\x82\x0D\xED\xBA\x6D\x2F\x3B\xAB\x4E\xEB\xF0\xDB\xD9\x6E\x1C\xC2\x3C\x1C\x4C\xFA\xF3\x98\x9B\x89\xBD\x48\x77\x07\xE2\x6B\x71\xCF\xB7\xFF\xCE\xA5', + b'\x80\xEE\x34\x8B\xFC\x04\x69\x4F\xBE\x15\x1C\x0C\x39\x2E\xF3\xEA\x8E\xEE\x17\x0D\x39\xC7\x4B\x6C\x4B' + b'\x13\xEF\x17\x82\x0D\xED\xBA\x6D\x2F\x3B\xAB\x4E\xEB\xF0\xDB\xD9\x6E\x1C\xC2\x3C\x1C\x4C\xFA\xF3\x98' + b'\x9B\x89\xBD\x48\x77\x07\xE2\x6B\x71\xCF\xB7\xFF\xCE\xA5', encrypted_content_info['encrypted_content'].native ) @@ -190,7 +193,17 @@ class CMSTests(unittest.TestCase): recipient['key_encryption_algorithm']['parameters'].native ) self.assertEqual( - b'\x97\x0A\xFD\x3B\x5C\x27\x45\x69\xCC\xDD\x45\x9E\xA7\x3C\x07\x27\x35\x16\x20\x21\xE4\x6E\x1D\xF8\x5B\xE8\x7F\xD8\x40\x41\xE9\xF2\x92\xCD\xC8\xC5\x03\x95\xEC\x6C\x0B\x97\x71\x87\x86\x3C\xEB\x68\x84\x06\x4E\xE6\xD0\xC4\x7D\x32\xFE\xA6\x06\xC9\xD5\xE1\x8B\xDA\xBF\x96\x5C\x20\x15\x49\x64\x7A\xA2\x4C\xFF\x8B\x0D\xEA\x76\x35\x9B\x7C\x43\xF7\x21\x95\x26\xE7\x70\x30\x98\x5F\x0D\x5E\x4A\xCB\xAD\x47\xDF\x46\xDA\x1F\x0E\xE2\xFE\x3A\x40\xD9\xF2\xDC\x0C\x97\xD9\x91\xED\x34\x8D\xF3\x73\xB0\x90\xF9\xDD\x31\x4D\x37\x93\x81\xD3\x92\xCB\x72\x4A\xD6\x9D\x01\x82\x85\xD5\x1F\xE2\xAA\x32\x12\x82\x4E\x17\xF6\xAA\x58\xDE\xBD\x1B\x80\xAF\x61\xF1\x8A\xD1\x7F\x9D\x41\x6A\xC0\xE4\xC7\x7E\x17\xDC\x94\x33\xE9\x74\x7E\xE9\xF8\x5C\x30\x87\x9B\xD6\xF0\xE3\x4A\xB7\xE3\xCC\x51\x8A\xD4\x37\xF1\xF9\x33\xB5\xD6\x1F\x36\xC1\x6F\x91\xA8\x5F\xE2\x6B\x08\xC7\x9D\xE8\xFD\xDC\xE8\x78\xE0\xC0\xC7\xCF\xC5\xEE\x60\xEC\x54\xFF\x1A\x9C\xF7\x4E\x2C\xD0\x88\xDC\xC2\x1F\xDC\x8A\x37\x9B\x71\x20\xFF\xFD\x6C\xE5\xBA\x8C\xDF\x0E\x3F\x20\xC6\xCB\x08\xA7\x07\xDB\x83', + b'\x97\x0A\xFD\x3B\x5C\x27\x45\x69\xCC\xDD\x45\x9E\xA7\x3C\x07\x27\x35\x16\x20\x21\xE4\x6E\x1D\xF8' + b'\x5B\xE8\x7F\xD8\x40\x41\xE9\xF2\x92\xCD\xC8\xC5\x03\x95\xEC\x6C\x0B\x97\x71\x87\x86\x3C\xEB\x68' + b'\x84\x06\x4E\xE6\xD0\xC4\x7D\x32\xFE\xA6\x06\xC9\xD5\xE1\x8B\xDA\xBF\x96\x5C\x20\x15\x49\x64\x7A' + b'\xA2\x4C\xFF\x8B\x0D\xEA\x76\x35\x9B\x7C\x43\xF7\x21\x95\x26\xE7\x70\x30\x98\x5F\x0D\x5E\x4A\xCB' + b'\xAD\x47\xDF\x46\xDA\x1F\x0E\xE2\xFE\x3A\x40\xD9\xF2\xDC\x0C\x97\xD9\x91\xED\x34\x8D\xF3\x73\xB0' + b'\x90\xF9\xDD\x31\x4D\x37\x93\x81\xD3\x92\xCB\x72\x4A\xD6\x9D\x01\x82\x85\xD5\x1F\xE2\xAA\x32\x12' + b'\x82\x4E\x17\xF6\xAA\x58\xDE\xBD\x1B\x80\xAF\x61\xF1\x8A\xD1\x7F\x9D\x41\x6A\xC0\xE4\xC7\x7E\x17' + b'\xDC\x94\x33\xE9\x74\x7E\xE9\xF8\x5C\x30\x87\x9B\xD6\xF0\xE3\x4A\xB7\xE3\xCC\x51\x8A\xD4\x37\xF1' + b'\xF9\x33\xB5\xD6\x1F\x36\xC1\x6F\x91\xA8\x5F\xE2\x6B\x08\xC7\x9D\xE8\xFD\xDC\xE8\x78\xE0\xC0\xC7' + b'\xCF\xC5\xEE\x60\xEC\x54\xFF\x1A\x9C\xF7\x4E\x2C\xD0\x88\xDC\xC2\x1F\xDC\x8A\x37\x9B\x71\x20\xFF' + b'\xFD\x6C\xE5\xBA\x8C\xDF\x0E\x3F\x20\xC6\xCB\x08\xA7\x07\xDB\x83', recipient['encrypted_key'].native ) self.assertEqual( @@ -206,7 +219,9 @@ class CMSTests(unittest.TestCase): encrypted_content_info['content_encryption_algorithm']['parameters'].native ) self.assertEqual( - b'\xDC\x88\x55\x08\xE5\x67\x70\x49\x99\x54\xFD\xF8\x40\x7C\x38\xD5\x78\x1D\x6A\x95\x6D\x1E\xC4\x12\x39\xFE\xC0\x76\xDC\xF5\x79\x1A\x69\xA1\xB9\x40\x1E\xCF\xC8\x79\x3E\xF3\x38\xB4\x90\x00\x27\xD1\xB5\x64\xAB\x99\x51\x13\xF1\x0A', + b'\xDC\x88\x55\x08\xE5\x67\x70\x49\x99\x54\xFD\xF8\x40\x7C\x38\xD5\x78\x1D\x6A\x95\x6D\x1E\xC4\x12' + b'\x39\xFE\xC0\x76\xDC\xF5\x79\x1A\x69\xA1\xB9\x40\x1E\xCF\xC8\x79\x3E\xF3\x38\xB4\x90\x00\x27\xD1' + b'\xB5\x64\xAB\x99\x51\x13\xF1\x0A', encrypted_content_info['encrypted_content'].native ) self.assertEqual( @@ -324,7 +339,8 @@ class CMSTests(unittest.TestCase): signed_attrs[2]['type'].native ) self.assertEqual( - b'\xA1\x30\xE2\x87\x90\x5A\x58\x15\x7A\x44\x54\x7A\xB9\xBC\xAE\xD3\x00\xF3\xEC\x3E\x97\xFF\x03\x20\x79\x34\x9D\x62\xAA\x20\xA5\x1D', + b'\xA1\x30\xE2\x87\x90\x5A\x58\x15\x7A\x44\x54\x7A\xB9\xBC\xAE\xD3\x00\xF3\xEC\x3E\x97\xFF' + b'\x03\x20\x79\x34\x9D\x62\xAA\x20\xA5\x1D', signed_attrs[2]['values'][0].native ) @@ -336,7 +352,18 @@ class CMSTests(unittest.TestCase): signer['signature_algorithm'].native ) self.assertEqual( - b'\xAC\x2F\xE3\x25\x39\x8F\xD3\xDF\x80\x4F\x0D\xBA\xB1\xEE\x99\x09\xA9\x21\xBB\xDF\x3C\x1E\x70\xDA\xDF\xC4\x0F\x1D\x10\x29\xBC\x94\xBE\xF8\xA8\xC2\x2D\x2A\x1F\x14\xBC\x4A\x5B\x66\x7F\x6F\xE4\xDF\x82\x4D\xD9\x3F\xEB\x89\xAA\x05\x1A\xE5\x58\xCE\xC4\x33\x53\x6E\xE4\x66\xF9\x21\xCF\x80\x35\x46\x88\xB5\x6A\xEA\x5C\x54\x49\x40\x31\xD6\xDC\x20\xD8\xA0\x63\x8C\xC1\xC3\xA1\x72\x5D\x0D\xCE\x43\xB1\x5C\xD8\x32\x3F\xA9\xE7\xBB\xD9\x56\xAE\xE7\xFB\x7C\x37\x32\x8B\x93\xC2\xC4\x47\xDD\x00\xFB\x1C\xEF\xC3\x68\x32\xDC\x06\x26\x17\x45\xF5\xB3\xDC\xD8\x5C\x2B\xC1\x8B\x97\x93\xB8\xF1\x85\xE2\x92\x3B\xC4\x6A\x6A\x89\xC5\x14\x51\x4A\x06\x11\x54\xB0\x29\x07\x75\xD8\xDF\x6B\xFB\x21\xE4\xA4\x09\x17\xAF\xAC\xA0\xF5\xC0\xFE\x7B\x03\x04\x40\x41\x57\xC4\xFD\x58\x1D\x10\x5E\xAC\x23\xAB\xAA\x80\x95\x96\x02\x71\x84\x9C\x0A\xBD\x54\xC4\xA2\x47\xAA\xE7\xC3\x09\x13\x6E\x26\x7D\x72\xAA\xA9\x0B\xF3\xCC\xC4\x48\xB4\x97\x14\x00\x47\x2A\x6B\xD3\x93\x3F\xD8\xFD\xAA\xB9\xFB\xFB\xD5\x09\x8D\x82\x8B\xDE\x0F\xED\x39\x6D\x7B\xDC\x76\x8B\xA6\x4E\x9B\x7A\xBA', + b'\xAC\x2F\xE3\x25\x39\x8F\xD3\xDF\x80\x4F\x0D\xBA\xB1\xEE\x99\x09\xA9\x21\xBB\xDF\x3C\x1E' + b'\x70\xDA\xDF\xC4\x0F\x1D\x10\x29\xBC\x94\xBE\xF8\xA8\xC2\x2D\x2A\x1F\x14\xBC\x4A\x5B\x66' + b'\x7F\x6F\xE4\xDF\x82\x4D\xD9\x3F\xEB\x89\xAA\x05\x1A\xE5\x58\xCE\xC4\x33\x53\x6E\xE4\x66' + b'\xF9\x21\xCF\x80\x35\x46\x88\xB5\x6A\xEA\x5C\x54\x49\x40\x31\xD6\xDC\x20\xD8\xA0\x63\x8C' + b'\xC1\xC3\xA1\x72\x5D\x0D\xCE\x43\xB1\x5C\xD8\x32\x3F\xA9\xE7\xBB\xD9\x56\xAE\xE7\xFB\x7C' + b'\x37\x32\x8B\x93\xC2\xC4\x47\xDD\x00\xFB\x1C\xEF\xC3\x68\x32\xDC\x06\x26\x17\x45\xF5\xB3' + b'\xDC\xD8\x5C\x2B\xC1\x8B\x97\x93\xB8\xF1\x85\xE2\x92\x3B\xC4\x6A\x6A\x89\xC5\x14\x51\x4A' + b'\x06\x11\x54\xB0\x29\x07\x75\xD8\xDF\x6B\xFB\x21\xE4\xA4\x09\x17\xAF\xAC\xA0\xF5\xC0\xFE' + b'\x7B\x03\x04\x40\x41\x57\xC4\xFD\x58\x1D\x10\x5E\xAC\x23\xAB\xAA\x80\x95\x96\x02\x71\x84' + b'\x9C\x0A\xBD\x54\xC4\xA2\x47\xAA\xE7\xC3\x09\x13\x6E\x26\x7D\x72\xAA\xA9\x0B\xF3\xCC\xC4' + b'\x48\xB4\x97\x14\x00\x47\x2A\x6B\xD3\x93\x3F\xD8\xFD\xAA\xB9\xFB\xFB\xD5\x09\x8D\x82\x8B' + b'\xDE\x0F\xED\x39\x6D\x7B\xDC\x76\x8B\xA6\x4E\x9B\x7A\xBA', signer['signature'].native ) @@ -450,7 +477,8 @@ class CMSTests(unittest.TestCase): signed_attrs[2]['type'].native ) self.assertEqual( - b'\x52\x88\x25\x47\x15\x5B\x2D\x50\x44\x68\x05\x24\xC8\x71\x5A\xCC\x62\x28\x36\x17\xB7\x68\xEE\xA1\x12\x90\x96\x4F\x94\xAE\xDB\x79', + b'\x52\x88\x25\x47\x15\x5B\x2D\x50\x44\x68\x05\x24\xC8\x71\x5A\xCC\x62\x28\x36\x17\xB7\x68' + b'\xEE\xA1\x12\x90\x96\x4F\x94\xAE\xDB\x79', signed_attrs[2]['values'][0].native ) @@ -462,7 +490,18 @@ class CMSTests(unittest.TestCase): signer['signature_algorithm'].native ) self.assertEqual( - b'\x43\x66\xEE\xF4\x6A\x02\x6F\xFE\x0D\xAE\xE6\xF3\x7A\x8F\x2C\x8E\x26\xB6\x25\x68\xEF\x5B\x4B\x4F\x9C\xE4\xE6\x71\x42\x22\xEC\x97\xFC\x53\xD9\xD6\x36\x1F\xA1\x32\x35\xFF\xA9\x95\x45\x50\x36\x36\x0C\x9A\x10\x6F\x06\xB6\x9D\x25\x10\x08\xF5\xF4\xE1\x68\x62\x60\xE5\xBF\xBD\xE2\x9F\xBD\x8A\x10\x29\x3B\xAF\xE7\xD6\x55\x7C\xEE\x3B\xFB\x93\x42\xE0\xB4\x4F\x89\xD0\x7B\x18\x51\x85\x90\x47\xF0\x5E\xE1\x15\x2C\xC1\x9A\xF1\x49\xE8\x11\x29\x17\x2E\x77\xD3\x35\x10\xAA\xCD\x32\x07\x32\x74\xCF\x2D\x89\xBD\xEF\xC7\xC9\xE7\xEC\x90\x44\xCE\x0B\xC5\x97\x00\x26\x67\x8A\x89\x5B\xFA\x46\xB2\x92\xD5\xCB\xA3\x52\x16\xDC\xF0\xF0\x79\xCB\x90\x93\x8E\x26\xB3\xEB\x8F\xBD\x54\x06\xD6\xB0\xA0\x04\x47\x7C\x63\xFC\x88\x5A\xE3\x81\xDF\x1E\x4D\x39\xFD\xF5\xA0\xE2\xD3\xAB\x13\xC1\xCF\x50\xB2\x0B\xC9\x36\xD6\xCB\xEA\x55\x39\x97\x8E\x34\x47\xE3\x6B\x44\x4A\x0E\x03\xAF\x41\xB2\x47\x2E\x26\xA3\x6B\x5F\xA1\x5C\x86\xA1\x96\x37\x02\xD3\x7C\x5F\xC1\xAF\x81\xE4\x1A\xD9\x87\x44\xB5\xB3\x5C\x45\x6C\xFF\x97\x4C\x3A\xB4\x2F\x5C\x2F\x86\x15\x51\x71\xA6\x27\x68', + b'\x43\x66\xEE\xF4\x6A\x02\x6F\xFE\x0D\xAE\xE6\xF3\x7A\x8F\x2C\x8E\x26\xB6\x25\x68\xEF\x5B' + b'\x4B\x4F\x9C\xE4\xE6\x71\x42\x22\xEC\x97\xFC\x53\xD9\xD6\x36\x1F\xA1\x32\x35\xFF\xA9\x95' + b'\x45\x50\x36\x36\x0C\x9A\x10\x6F\x06\xB6\x9D\x25\x10\x08\xF5\xF4\xE1\x68\x62\x60\xE5\xBF' + b'\xBD\xE2\x9F\xBD\x8A\x10\x29\x3B\xAF\xE7\xD6\x55\x7C\xEE\x3B\xFB\x93\x42\xE0\xB4\x4F\x89' + b'\xD0\x7B\x18\x51\x85\x90\x47\xF0\x5E\xE1\x15\x2C\xC1\x9A\xF1\x49\xE8\x11\x29\x17\x2E\x77' + b'\xD3\x35\x10\xAA\xCD\x32\x07\x32\x74\xCF\x2D\x89\xBD\xEF\xC7\xC9\xE7\xEC\x90\x44\xCE\x0B' + b'\xC5\x97\x00\x26\x67\x8A\x89\x5B\xFA\x46\xB2\x92\xD5\xCB\xA3\x52\x16\xDC\xF0\xF0\x79\xCB' + b'\x90\x93\x8E\x26\xB3\xEB\x8F\xBD\x54\x06\xD6\xB0\xA0\x04\x47\x7C\x63\xFC\x88\x5A\xE3\x81' + b'\xDF\x1E\x4D\x39\xFD\xF5\xA0\xE2\xD3\xAB\x13\xC1\xCF\x50\xB2\x0B\xC9\x36\xD6\xCB\xEA\x55' + b'\x39\x97\x8E\x34\x47\xE3\x6B\x44\x4A\x0E\x03\xAF\x41\xB2\x47\x2E\x26\xA3\x6B\x5F\xA1\x5C' + b'\x86\xA1\x96\x37\x02\xD3\x7C\x5F\xC1\xAF\x81\xE4\x1A\xD9\x87\x44\xB5\xB3\x5C\x45\x6C\xFF' + b'\x97\x4C\x3A\xB4\x2F\x5C\x2F\x86\x15\x51\x71\xA6\x27\x68', signer['signature'].native ) @@ -584,11 +623,21 @@ class CMSTests(unittest.TestCase): signer['signature_algorithm'].native ) self.assertEqual( - b'\x70\xBC\x18\x82\x41\xD6\xD8\xE7\x5C\xDC\x42\x27\xA5\xA8\xAA\x8B\x16\x15\x61\x3A\xE5\x47\x53\xFD\x8F\x45\xA3\x82\xE2\x72\x44\x07\xD1\xCB\xBF\xB4\x85\x4A\x2A\x16\x19\xDE\xDC\x53\x15\xCF\x98\xEE\x5C\x0E\xDF\xDE\xC8\x79\xCE\x2B\x38\x61\x36\xB0\xA1\xCB\x94\xD6\x4F\xCD\x83\xEF\x0C\xC9\x23\xA0\x7B\x8B\x65\x40\x5C\x3D\xA8\x3E\xCC\x0D\x1F\x17\x23\xF3\x74\x9F\x7E\x88\xF8\xF3\xBE\x4E\x19\x95\x0F\xEB\x95\x55\x69\xB4\xAA\xC3\x2A\x36\x03\x93\x1C\xDC\xE5\x65\x3F\x4E\x5E\x03\xC8\x56\xD8\x57\x8F\xE8\x2D\x85\x32\xDA\xFD\x79\xD4\xDD\x88\xCA\xA3\x14\x41\xE4\x3B\x03\x88\x0E\x2B\x76\xDC\x44\x3D\x4D\xFF\xB2\xC8\xC3\x83\xB1\x33\x37\x53\x51\x33\x4B\xCA\x1A\xAD\x7E\x6A\xBC\x61\x8B\x84\xDB\x7F\xCF\x61\xB2\x1D\x21\x83\xCF\xB8\x3F\xC6\x98\xED\xD8\x66\x06\xCF\x03\x30\x96\x9D\xB4\x7A\x16\xDF\x6E\xA7\x30\xEB\x77\xF7\x40\x13\xFB\xF2\xAC\x41\x79\x9D\xDC\xC0\xED\x4B\x8B\x19\xEE\x05\x3D\x61\x20\x39\x7E\x80\x1D\x3A\x23\x69\x48\x43\x60\x8B\x3E\x63\xAD\x01\x7A\xDE\x6F\x01\xBA\x51\xF3\x4B\x14\xBF\x6B\x77\x1A\x32\xC2\x0C\x93\xCC\x35\xBC\x66\xC6\x69', + b'\x70\xBC\x18\x82\x41\xD6\xD8\xE7\x5C\xDC\x42\x27\xA5\xA8\xAA\x8B\x16\x15\x61\x3A\xE5\x47' + b'\x53\xFD\x8F\x45\xA3\x82\xE2\x72\x44\x07\xD1\xCB\xBF\xB4\x85\x4A\x2A\x16\x19\xDE\xDC\x53' + b'\x15\xCF\x98\xEE\x5C\x0E\xDF\xDE\xC8\x79\xCE\x2B\x38\x61\x36\xB0\xA1\xCB\x94\xD6\x4F\xCD' + b'\x83\xEF\x0C\xC9\x23\xA0\x7B\x8B\x65\x40\x5C\x3D\xA8\x3E\xCC\x0D\x1F\x17\x23\xF3\x74\x9F' + b'\x7E\x88\xF8\xF3\xBE\x4E\x19\x95\x0F\xEB\x95\x55\x69\xB4\xAA\xC3\x2A\x36\x03\x93\x1C\xDC' + b'\xE5\x65\x3F\x4E\x5E\x03\xC8\x56\xD8\x57\x8F\xE8\x2D\x85\x32\xDA\xFD\x79\xD4\xDD\x88\xCA' + b'\xA3\x14\x41\xE4\x3B\x03\x88\x0E\x2B\x76\xDC\x44\x3D\x4D\xFF\xB2\xC8\xC3\x83\xB1\x33\x37' + b'\x53\x51\x33\x4B\xCA\x1A\xAD\x7E\x6A\xBC\x61\x8B\x84\xDB\x7F\xCF\x61\xB2\x1D\x21\x83\xCF' + b'\xB8\x3F\xC6\x98\xED\xD8\x66\x06\xCF\x03\x30\x96\x9D\xB4\x7A\x16\xDF\x6E\xA7\x30\xEB\x77' + b'\xF7\x40\x13\xFB\xF2\xAC\x41\x79\x9D\xDC\xC0\xED\x4B\x8B\x19\xEE\x05\x3D\x61\x20\x39\x7E' + b'\x80\x1D\x3A\x23\x69\x48\x43\x60\x8B\x3E\x63\xAD\x01\x7A\xDE\x6F\x01\xBA\x51\xF3\x4B\x14' + b'\xBF\x6B\x77\x1A\x32\xC2\x0C\x93\xCC\x35\xBC\x66\xC6\x69', signer['signature'].native ) - def test_parse_content_info_pkcs7_signed_digested_data(self): with open(os.path.join(fixtures_dir, 'pkcs7-signed-digested.der'), 'rb') as f: info = cms.ContentInfo.load(f.read()) @@ -707,6 +756,17 @@ class CMSTests(unittest.TestCase): signer['signature_algorithm'].native ) self.assertEqual( - b'\x70\xBC\x18\x82\x41\xD6\xD8\xE7\x5C\xDC\x42\x27\xA5\xA8\xAA\x8B\x16\x15\x61\x3A\xE5\x47\x53\xFD\x8F\x45\xA3\x82\xE2\x72\x44\x07\xD1\xCB\xBF\xB4\x85\x4A\x2A\x16\x19\xDE\xDC\x53\x15\xCF\x98\xEE\x5C\x0E\xDF\xDE\xC8\x79\xCE\x2B\x38\x61\x36\xB0\xA1\xCB\x94\xD6\x4F\xCD\x83\xEF\x0C\xC9\x23\xA0\x7B\x8B\x65\x40\x5C\x3D\xA8\x3E\xCC\x0D\x1F\x17\x23\xF3\x74\x9F\x7E\x88\xF8\xF3\xBE\x4E\x19\x95\x0F\xEB\x95\x55\x69\xB4\xAA\xC3\x2A\x36\x03\x93\x1C\xDC\xE5\x65\x3F\x4E\x5E\x03\xC8\x56\xD8\x57\x8F\xE8\x2D\x85\x32\xDA\xFD\x79\xD4\xDD\x88\xCA\xA3\x14\x41\xE4\x3B\x03\x88\x0E\x2B\x76\xDC\x44\x3D\x4D\xFF\xB2\xC8\xC3\x83\xB1\x33\x37\x53\x51\x33\x4B\xCA\x1A\xAD\x7E\x6A\xBC\x61\x8B\x84\xDB\x7F\xCF\x61\xB2\x1D\x21\x83\xCF\xB8\x3F\xC6\x98\xED\xD8\x66\x06\xCF\x03\x30\x96\x9D\xB4\x7A\x16\xDF\x6E\xA7\x30\xEB\x77\xF7\x40\x13\xFB\xF2\xAC\x41\x79\x9D\xDC\xC0\xED\x4B\x8B\x19\xEE\x05\x3D\x61\x20\x39\x7E\x80\x1D\x3A\x23\x69\x48\x43\x60\x8B\x3E\x63\xAD\x01\x7A\xDE\x6F\x01\xBA\x51\xF3\x4B\x14\xBF\x6B\x77\x1A\x32\xC2\x0C\x93\xCC\x35\xBC\x66\xC6\x69', + b'\x70\xBC\x18\x82\x41\xD6\xD8\xE7\x5C\xDC\x42\x27\xA5\xA8\xAA\x8B\x16\x15\x61\x3A\xE5\x47' + b'\x53\xFD\x8F\x45\xA3\x82\xE2\x72\x44\x07\xD1\xCB\xBF\xB4\x85\x4A\x2A\x16\x19\xDE\xDC\x53' + b'\x15\xCF\x98\xEE\x5C\x0E\xDF\xDE\xC8\x79\xCE\x2B\x38\x61\x36\xB0\xA1\xCB\x94\xD6\x4F\xCD' + b'\x83\xEF\x0C\xC9\x23\xA0\x7B\x8B\x65\x40\x5C\x3D\xA8\x3E\xCC\x0D\x1F\x17\x23\xF3\x74\x9F' + b'\x7E\x88\xF8\xF3\xBE\x4E\x19\x95\x0F\xEB\x95\x55\x69\xB4\xAA\xC3\x2A\x36\x03\x93\x1C\xDC' + b'\xE5\x65\x3F\x4E\x5E\x03\xC8\x56\xD8\x57\x8F\xE8\x2D\x85\x32\xDA\xFD\x79\xD4\xDD\x88\xCA' + b'\xA3\x14\x41\xE4\x3B\x03\x88\x0E\x2B\x76\xDC\x44\x3D\x4D\xFF\xB2\xC8\xC3\x83\xB1\x33\x37' + b'\x53\x51\x33\x4B\xCA\x1A\xAD\x7E\x6A\xBC\x61\x8B\x84\xDB\x7F\xCF\x61\xB2\x1D\x21\x83\xCF' + b'\xB8\x3F\xC6\x98\xED\xD8\x66\x06\xCF\x03\x30\x96\x9D\xB4\x7A\x16\xDF\x6E\xA7\x30\xEB\x77' + b'\xF7\x40\x13\xFB\xF2\xAC\x41\x79\x9D\xDC\xC0\xED\x4B\x8B\x19\xEE\x05\x3D\x61\x20\x39\x7E' + b'\x80\x1D\x3A\x23\x69\x48\x43\x60\x8B\x3E\x63\xAD\x01\x7A\xDE\x6F\x01\xBA\x51\xF3\x4B\x14' + b'\xBF\x6B\x77\x1A\x32\xC2\x0C\x93\xCC\x35\xBC\x66\xC6\x69', signer['signature'].native ) diff --git a/tests/test_core.py b/tests/test_core.py index a978ed2..2cc1de6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,7 +6,7 @@ import os from asn1crypto import core -from .unittest_data import DataDecorator, data +from .unittest_data import data_decorator, data from ._unittest_compat import patch patch() @@ -70,7 +70,7 @@ class NumChoice(core.Choice): ] -@DataDecorator +@data_decorator class CoreTests(unittest.TestCase): def test_sequence_spec(self): @@ -84,23 +84,22 @@ class CoreTests(unittest.TestCase): seq = SequenceAny() self.assertEqual(core.Any, seq.spec()) - #pylint: disable=C0326 @staticmethod def compare_primitive_info(): return ( - (core.ObjectIdentifier('1.2.3'), core.ObjectIdentifier('1.2.3'), True), - (core.Integer(1), Enum(1), False), - (core.Integer(1), core.Integer(1, tag_type='implicit', tag=5), True), - (core.Integer(1), core.Integer(1, tag_type='explicit', tag=5), True), - (core.Integer(1), core.Integer(2), False), - (core.OctetString(b''), core.OctetString(b''), True), - (core.OctetString(b''), core.OctetString(b'1'), False), - (core.OctetString(b''), core.OctetBitString(b''), False), - (core.ParsableOctetString(b'12'), core.OctetString(b'12'), True), - (core.ParsableOctetBitString(b'12'), core.OctetBitString(b'12'), True), - (core.UTF8String('12'), core.UTF8String('12'), True), - (core.UTF8String('12'), core.UTF8String('1'), False), - (core.UTF8String('12'), core.IA5String('12'), False), + (core.ObjectIdentifier('1.2.3'), core.ObjectIdentifier('1.2.3'), True), + (core.Integer(1), Enum(1), False), + (core.Integer(1), core.Integer(1, tag_type='implicit', tag=5), True), + (core.Integer(1), core.Integer(1, tag_type='explicit', tag=5), True), + (core.Integer(1), core.Integer(2), False), + (core.OctetString(b''), core.OctetString(b''), True), + (core.OctetString(b''), core.OctetString(b'1'), False), + (core.OctetString(b''), core.OctetBitString(b''), False), + (core.ParsableOctetString(b'12'), core.OctetString(b'12'), True), + (core.ParsableOctetBitString(b'12'), core.OctetBitString(b'12'), True), + (core.UTF8String('12'), core.UTF8String('12'), True), + (core.UTF8String('12'), core.UTF8String('1'), False), + (core.UTF8String('12'), core.IA5String('12'), False), ) @data('compare_primitive_info') @@ -110,19 +109,18 @@ class CoreTests(unittest.TestCase): else: self.assertNotEqual(one, two) - #pylint: disable=C0326 @staticmethod def integer_info(): return ( - (0, b'\x02\x01\x00'), - (255, b'\x02\x02\x00\xFF'), - (128, b'\x02\x02\x00\x80'), - (127, b'\x02\x01\x7F'), - (-127, b'\x02\x01\x81'), - (-127, b'\x02\x01\x81'), - (32768, b'\x02\x03\x00\x80\x00'), - (-32768, b'\x02\x02\x80\x00'), - (-32769, b'\x02\x03\xFF\x7F\xFF'), + (0, b'\x02\x01\x00'), + (255, b'\x02\x02\x00\xFF'), + (128, b'\x02\x02\x00\x80'), + (127, b'\x02\x01\x7F'), + (-127, b'\x02\x01\x81'), + (-127, b'\x02\x01\x81'), + (32768, b'\x02\x03\x00\x80\x00'), + (-32768, b'\x02\x02\x80\x00'), + (-32769, b'\x02\x03\xFF\x7F\xFF'), ) @data('integer_info') @@ -131,11 +129,10 @@ class CoreTests(unittest.TestCase): self.assertEqual(der_bytes, i.dump()) self.assertEqual(native, core.Integer.load(der_bytes).native) - #pylint: disable=C0326 @staticmethod def type_info(): return ( - ('universal/object_identifier.der', core.ObjectIdentifier, '1.2.840.113549.1.1.1'), + ('universal/object_identifier.der', core.ObjectIdentifier, '1.2.840.113549.1.1.1'), ) @data('type_info') @@ -147,11 +144,10 @@ class CoreTests(unittest.TestCase): self.assertEqual(native, parsed.native) self.assertEqual(der, parsed.dump(force=True)) - #pylint: disable=C0326 @staticmethod def bit_string_info(): return ( - ((0, 1, 1), b'\x03\x02\x05\x60'), + ((0, 1, 1), b'\x03\x02\x05\x60'), ((0, 1, 1, 0, 0, 0, 0, 0), b'\x03\x02\x00\x60'), ) @@ -168,7 +164,6 @@ class CoreTests(unittest.TestCase): self.assertEqual(False, named[1]) self.assertEqual(True, named[0]) - #pylint: disable=C0326 @staticmethod def mapped_bit_string_info(): return ( diff --git a/tests/test_crl.py b/tests/test_crl.py index 6ee483d..f030635 100644 --- a/tests/test_crl.py +++ b/tests/test_crl.py @@ -13,7 +13,7 @@ patch() if sys.version_info < (3,): byte_cls = str - num_cls = long #pylint: disable=E0602 + num_cls = long # noqa else: byte_cls = bytes num_cls = int diff --git a/tests/test_csr.py b/tests/test_csr.py index 7e9dbd5..b950971 100644 --- a/tests/test_csr.py +++ b/tests/test_csr.py @@ -12,7 +12,7 @@ patch() if sys.version_info < (3,): byte_cls = str - num_cls = long #pylint: disable=E0602 + num_cls = long # noqa else: byte_cls = bytes num_cls = int @@ -55,7 +55,7 @@ class CSRTests(unittest.TestCase): cri['subject_pk_info']['algorithm'].native ) self.assertEqual( - 24141757533938720807477509823483015516687050697622322097001928034085434547050399731881871694642845241206788286795830006142635608141713689209738431462004600429798152826994774062467402648660593454536565119527837471261495586474194846971065722669734666949739228862107500673350843489920495869942508240779131331715037662761414997889327943217889802893638175792326783316531272170879284118280173511200768884738639370318760377047837471530387161553030663446359575963736475504659902898072137674205021477968813148345198711103071746476009234601299344030395455052526948041544669303473529511160643491569274897838845918784633403435929, + 24141757533938720807477509823483015516687050697622322097001928034085434547050399731881871694642845241206788286795830006142635608141713689209738431462004600429798152826994774062467402648660593454536565119527837471261495586474194846971065722669734666949739228862107500673350843489920495869942508240779131331715037662761414997889327943217889802893638175792326783316531272170879284118280173511200768884738639370318760377047837471530387161553030663446359575963736475504659902898072137674205021477968813148345198711103071746476009234601299344030395455052526948041544669303473529511160643491569274897838845918784633403435929, # noqa cri['subject_pk_info']['public_key'].parsed['modulus'].native ) self.assertEqual( @@ -67,7 +67,6 @@ class CSRTests(unittest.TestCase): cri['attributes'].native ) - def test_parse_csr2(self): with open(os.path.join(fixtures_dir, 'test-third-der.csr'), 'rb') as f: certification_request = csr.CertificationRequest.load(f.read()) @@ -99,7 +98,7 @@ class CSRTests(unittest.TestCase): cri['subject_pk_info']['algorithm'].native ) self.assertEqual( - 24242772097421005542208203320016703216069397492249392798445262959177221203301502279838173203064357049006693856302147277901773700963054800321566171864477088538775137040886151390015408166478059887940234405152693144166884492162723776487601158833605063151869850475289834250129252480954724818505034734280077580919995584375189497366089269712298471489896645221362055822887892887126082288043106492130176555423739906252380437817155678204772878611148787130925042126257401487070141904017757131876614711613405231164930930771261221451019736883391322299033324412671768599041417705072563016759224152503535867541947310239343903761461, + 24242772097421005542208203320016703216069397492249392798445262959177221203301502279838173203064357049006693856302147277901773700963054800321566171864477088538775137040886151390015408166478059887940234405152693144166884492162723776487601158833605063151869850475289834250129252480954724818505034734280077580919995584375189497366089269712298471489896645221362055822887892887126082288043106492130176555423739906252380437817155678204772878611148787130925042126257401487070141904017757131876614711613405231164930930771261221451019736883391322299033324412671768599041417705072563016759224152503535867541947310239343903761461, # noqa cri['subject_pk_info']['public_key'].parsed['modulus'].native ) self.assertEqual( diff --git a/tests/test_keys.py b/tests/test_keys.py index 8a03182..70b0ae2 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -6,7 +6,7 @@ import os from asn1crypto import keys, core, util -from .unittest_data import DataDecorator, data +from .unittest_data import data_decorator, data from ._unittest_compat import patch patch() @@ -15,7 +15,7 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') -@DataDecorator +@data_decorator class KeysTests(unittest.TestCase): def test_parse_rsa_private_key(self): @@ -27,7 +27,7 @@ class KeysTests(unittest.TestCase): key['version'].native ) self.assertEqual( - 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, # noqa key['modulus'].native ) self.assertEqual( @@ -35,27 +35,27 @@ class KeysTests(unittest.TestCase): key['public_exponent'].native ) self.assertEqual( - 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, + 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, # noqa key['private_exponent'].native ) self.assertEqual( - 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, + 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, # noqa key['prime1'].native ) self.assertEqual( - 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, + 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, # noqa key['prime2'].native ) self.assertEqual( - 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, + 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, # noqa key['exponent1'].native ) self.assertEqual( - 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, + 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, # noqa key['exponent2'].native ) self.assertEqual( - 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, + 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, # noqa key['coefficient'].native ) self.assertEqual( @@ -72,7 +72,7 @@ class KeysTests(unittest.TestCase): key[0].native ) self.assertEqual( - 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, # noqa key[1].native ) self.assertEqual( @@ -80,32 +80,32 @@ class KeysTests(unittest.TestCase): key[2].native ) self.assertEqual( - 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, + 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, # noqa key[3].native ) self.assertEqual( - 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, + 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, # noqa key[4].native ) self.assertEqual( - 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, + 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, # noqa key[5].native ) self.assertEqual( - 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, + 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, # noqa key[6].native ) self.assertEqual( - 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, + 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, # noqa key[7].native ) self.assertEqual( - 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, + 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, # noqa key[8].native ) with self.assertRaises(KeyError): - key[9].native #pylint: disable=W0104 + key[9].native def test_parse_dsa_private_key(self): with open(os.path.join(fixtures_dir, 'keys/test-dsa-der.key'), 'rb') as f: @@ -116,7 +116,7 @@ class KeysTests(unittest.TestCase): key['version'].native ) self.assertEqual( - 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857, + 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857, # noqa key['p'].native ) self.assertEqual( @@ -124,11 +124,11 @@ class KeysTests(unittest.TestCase): key['q'].native ) self.assertEqual( - 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202, + 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202, # noqa key['g'].native ) self.assertEqual( - 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, + 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, # noqa key['public_key'].native ) self.assertEqual( @@ -161,14 +161,25 @@ class KeysTests(unittest.TestCase): ( 'curve', util.OrderedDict([ - ('a', b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC'), - ('b', b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B'), + ( + 'a', + b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC' + ), + ( + 'b', + b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC' + b'\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B' + ), ('seed', b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90'), ]) ), ( 'base', - b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5' + b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77' + b'\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42' + b'\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B' + b'\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5' ), ( 'order', @@ -180,7 +191,10 @@ class KeysTests(unittest.TestCase): key['parameters'].native ) self.assertEqual( - b'\x04\x8B\x5D\x4C\x71\xF7\xD6\xC6\xA3\x49\x63\x42\x5C\x47\x9F\xCB\x73\x24\x1D\xC9\xDD\xD1\x2D\xF1\x3A\x9F\xB7\x04\xDE\x20\xD0\x58\x00\x93\x54\xF6\x89\xC7\x2F\x87\x2B\xF7\xF9\x3D\x3B\x34\xED\x9E\x7B\x0E\x3D\x57\x42\xDF\x78\x03\x0B\xCC\x31\xC6\x03\xD7\x9F\x60\x01', + b'\x04\x8B\x5D\x4C\x71\xF7\xD6\xC6\xA3\x49\x63\x42\x5C\x47\x9F\xCB\x73\x24\x1D\xC9\xDD' + b'\xD1\x2D\xF1\x3A\x9F\xB7\x04\xDE\x20\xD0\x58\x00\x93\x54\xF6\x89\xC7\x2F\x87\x2B\xF7' + b'\xF9\x3D\x3B\x34\xED\x9E\x7B\x0E\x3D\x57\x42\xDF\x78\x03\x0B\xCC\x31\xC6\x03\xD7\x9F' + b'\x60\x01', key['public_key'].native ) @@ -189,7 +203,7 @@ class KeysTests(unittest.TestCase): key = keys.RSAPublicKey.load(f.read()) self.assertEqual( - 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, # noqa key['modulus'].native ) self.assertEqual( @@ -212,7 +226,7 @@ class KeysTests(unittest.TestCase): key['algorithm']['parameters'].native ) self.assertEqual( - 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, # noqa public_key['modulus'].native ) self.assertEqual( @@ -244,7 +258,7 @@ class KeysTests(unittest.TestCase): key['version'].native ) self.assertEqual( - 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, # noqa key['modulus'].native ) self.assertEqual( @@ -252,27 +266,27 @@ class KeysTests(unittest.TestCase): key['public_exponent'].native ) self.assertEqual( - 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, + 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, # noqa key['private_exponent'].native ) self.assertEqual( - 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, + 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, # noqa key['prime1'].native ) self.assertEqual( - 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, + 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, # noqa key['prime2'].native ) self.assertEqual( - 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, + 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, # noqa key['exponent1'].native ) self.assertEqual( - 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, + 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, # noqa key['exponent2'].native ) self.assertEqual( - 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, + 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, # noqa key['coefficient'].native ) self.assertEqual( @@ -285,14 +299,13 @@ class KeysTests(unittest.TestCase): key_info['attributes'].native ) - #pylint: disable=C0326 @staticmethod def key_sha1_hashes(): return ( - ('keys/test-public-der.key', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), - ('keys/test-public-dsa-der.key', b'\x81\xa37\x86\xf9\x99(\xf2tp`\x87\xf2\xd3~\x8d\x19a\xa8\xbe'), + ('keys/test-public-der.key', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), + ('keys/test-public-dsa-der.key', b'\x81\xa37\x86\xf9\x99(\xf2tp`\x87\xf2\xd3~\x8d\x19a\xa8\xbe'), ('keys/test-public-ec-named-der.key', b'#\x8d\xee\xeeGH*\xe45T\xb8\xfdVh\x16_\xe2\xaa\xcd\x81'), - ('keys/test-public-ec-der.key', b'T\xaaTpl4\x1am\xeb]\x97\xd7\x1e\xfc\xd5$<\x8a\x0e\xd7'), + ('keys/test-public-ec-der.key', b'T\xaaTpl4\x1am\xeb]\x97\xd7\x1e\xfc\xd5$<\x8a\x0e\xd7'), ) @data('key_sha1_hashes') @@ -302,14 +315,25 @@ class KeysTests(unittest.TestCase): self.assertEqual(sha1, public_key.sha1) - #pylint: disable=C0326 @staticmethod def key_sha256_hashes(): return ( - ('keys/test-public-der.key', b'\xd9\x80\xdf\x94J\x8e\x1e\xf5z\xd2o\x8eS\xa8\x03qX\x9a[\x17g\x12\x89\xc5\xcc\xca\x04\x94\xf2R|F'), - ('keys/test-public-dsa-der.key', b'<\x10X\xbf=\xe4\xec3\xb9\xb2 \x11\xce9\xca\xd4\x95\xcf\xf9\xbc\x91q]O\x8f4\xbf\xdb\xdc\xe2\xd6\x82'), - ('keys/test-public-ec-named-der.key', b'\x87e \xb4\x13\x8cu\xdd\x11\x92\xa4\xd9;\x8e\xe5"p\xb2\xb7\xa7\xcb8\x88\x16;f\xb9\xf8I\x86J\x1c'), - ('keys/test-public-ec-der.key', b'\xf3\xa3k\xe0\xbf\xa9\xd9sl\xaa\x99\xe7\x9c-\xec\xb9\x0e\xe2d\xe9\xc3$\xb9\x893\x99A\xc19ec_'), + ( + 'keys/test-public-der.key', + b'\xd9\x80\xdf\x94J\x8e\x1e\xf5z\xd2o\x8eS\xa8\x03qX\x9a[\x17g\x12\x89\xc5\xcc\xca\x04\x94\xf2R|F' + ), + ( + 'keys/test-public-dsa-der.key', + b'<\x10X\xbf=\xe4\xec3\xb9\xb2 \x11\xce9\xca\xd4\x95\xcf\xf9\xbc\x91q]O\x8f4\xbf\xdb\xdc\xe2\xd6\x82' + ), + ( + 'keys/test-public-ec-named-der.key', + b'\x87e \xb4\x13\x8cu\xdd\x11\x92\xa4\xd9;\x8e\xe5"p\xb2\xb7\xa7\xcb8\x88\x16;f\xb9\xf8I\x86J\x1c' + ), + ( + 'keys/test-public-ec-der.key', + b'\xf3\xa3k\xe0\xbf\xa9\xd9sl\xaa\x99\xe7\x9c-\xec\xb9\x0e\xe2d\xe9\xc3$\xb9\x893\x99A\xc19ec_' + ), ) @data('key_sha256_hashes') @@ -319,14 +343,37 @@ class KeysTests(unittest.TestCase): self.assertEqual(sha256, public_key.sha256) - #pylint: disable=C0326 @staticmethod def key_pairs(): return ( - ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-public-dsa-der.key', 'dsa', 3072), - ('ec_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-public-ec-named-der.key', 'ec', 256), - ('ec', 'keys/test-pkcs8-ec-der.key', 'keys/test-public-ec-der.key', 'ec', 256), - ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-public-der.key', 'rsa', 2048), + ( + 'dsa', + 'keys/test-pkcs8-dsa-der.key', + 'keys/test-public-dsa-der.key', + 'dsa', + 3072 + ), + ( + 'ec_named', + 'keys/test-pkcs8-ec-named-der.key', + 'keys/test-public-ec-named-der.key', + 'ec', + 256 + ), + ( + 'ec', + 'keys/test-pkcs8-ec-der.key', + 'keys/test-public-ec-der.key', + 'ec', + 256 + ), + ( + 'rsa', + 'keys/test-pkcs8-der.key', + 'keys/test-public-der.key', + 'rsa', + 2048 + ), ) @data('key_pairs', True) @@ -345,7 +392,7 @@ class KeysTests(unittest.TestCase): with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: public_key = keys.PublicKeyInfo.load(f.read()) - self.assertEqual(public_key['public_key'].native, private_key._compute_public_key().native) #pylint: disable=W0212 + self.assertEqual(public_key['public_key'].native, private_key._compute_public_key().native) @data('key_pairs', True) def public_key_property(self, private_key_file, public_key_file, *_): @@ -376,14 +423,29 @@ class KeysTests(unittest.TestCase): self.assertEqual(bit_size, private_key.bit_size) self.assertEqual(bit_size, public_key.bit_size) - #pylint: disable=C0326 @staticmethod def key_variations(): return ( - ('dsa', 'keys/test-pkcs8-dsa-der.key', 'keys/test-dsa-der.key',), - ('ec_named', 'keys/test-pkcs8-ec-named-der.key', 'keys/test-ec-named-der.key',), - ('ec', 'keys/test-pkcs8-ec-der.key', 'keys/test-ec-der.key',), - ('rsa', 'keys/test-pkcs8-der.key', 'keys/test-der.key',), + ( + 'dsa', + 'keys/test-pkcs8-dsa-der.key', + 'keys/test-dsa-der.key', + ), + ( + 'ec_named', + 'keys/test-pkcs8-ec-named-der.key', + 'keys/test-ec-named-der.key', + ), + ( + 'ec', + 'keys/test-pkcs8-ec-der.key', + 'keys/test-ec-der.key', + ), + ( + 'rsa', + 'keys/test-pkcs8-der.key', + 'keys/test-der.key', + ), ) @data('key_variations', True) @@ -399,14 +461,14 @@ class KeysTests(unittest.TestCase): with open(os.path.join(fixtures_dir, 'keys/test-pkcs8-der.key'), 'rb') as f: private_key = keys.PrivateKeyInfo.load(f.read()) - with self.assertRaises(ValueError) as _: - _ = private_key.curve + with self.assertRaises(ValueError): + private_key.curve with open(os.path.join(fixtures_dir, 'keys/test-public-rsa-der.key'), 'rb') as f: public_key = keys.PublicKeyInfo.load(f.read()) - with self.assertRaises(ValueError) as _: - _ = public_key.curve + with self.assertRaises(ValueError): + public_key.curve def test_curve_info_name(self): with open(os.path.join(fixtures_dir, 'keys/test-pkcs8-ec-named-der.key'), 'rb') as f: @@ -439,14 +501,28 @@ class KeysTests(unittest.TestCase): ( 'curve', util.OrderedDict([ - ('a', b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC'), - ('b', b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B'), - ('seed', b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90'), + ( + 'a', + b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC' + ), + ( + 'b', + b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC' + b'\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B' + ), + ( + 'seed', + b'\xC4\x9D\x36\x08\x86\xE7\x04\x93\x6A\x66\x78\xE1\x13\x9D\x26\xB7\x81\x9F\x7E\x90' + ), ]) ), ( 'base', - b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5' + b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77\x03\x7D' + b'\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F' + b'\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40' + b'\x68\x37\xBF\x51\xF5' ), ( 'order', diff --git a/tests/test_ocsp.py b/tests/test_ocsp.py index 9357b27..c3492c1 100644 --- a/tests/test_ocsp.py +++ b/tests/test_ocsp.py @@ -21,7 +21,6 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') - class OCSPTests(unittest.TestCase): def test_parse_request(self): diff --git a/tests/test_pem.py b/tests/test_pem.py index 30c94ec..05dcd3d 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -7,14 +7,14 @@ import os from asn1crypto import pem, util -from .unittest_data import DataDecorator, data +from .unittest_data import data_decorator, data from ._unittest_compat import patch patch() if sys.version_info < (3,): byte_cls = str - num_cls = long #pylint: disable=E0602 + num_cls = long # noqa else: byte_cls = bytes num_cls = int @@ -24,19 +24,36 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') -@DataDecorator +@data_decorator class PEMTests(unittest.TestCase): - #pylint: disable=C0326 @staticmethod def detect_files(): return ( - ('keys/test-der.crt', False), - ('keys/test-inter-der.crt', False), - ('keys/test-third-der.crt', False), - ('keys/test.crt', True), - ('keys/test-inter.crt', True), - ('keys/test-third.crt', True), + ( + 'keys/test-der.crt', + False + ), + ( + 'keys/test-inter-der.crt', + False + ), + ( + 'keys/test-third-der.crt', + False + ), + ( + 'keys/test.crt', + True + ), + ( + 'keys/test-inter.crt', + True + ), + ( + 'keys/test-third.crt', + True + ), ) @data('detect_files') @@ -45,16 +62,48 @@ class PEMTests(unittest.TestCase): byte_string = f.read() self.assertEqual(is_pem, pem.detect(byte_string)) - #pylint: disable=C0326 @staticmethod def unarmor_armor_files(): return ( - ('keys/test.crt', 'keys/test-der.crt', 'CERTIFICATE', {}), - ('keys/test-inter.crt', 'keys/test-inter-der.crt', 'CERTIFICATE', {}), - ('keys/test-third.crt', 'keys/test-third-der.crt', 'CERTIFICATE', {}), - ('keys/test-pkcs8.key', 'keys/test-pkcs8-der.key', 'PRIVATE KEY', {}), - ('test-third.csr', 'test-third-der.csr', 'CERTIFICATE REQUEST', {}), - ('keys/test-aes128.key', 'keys/test-aes128-der.key', 'RSA PRIVATE KEY', util.OrderedDict([('Proc-Type', '4,ENCRYPTED'), ('DEK-Info', 'AES-128-CBC,01F6EE04516C912788B11BD7377626C2')])), + ( + 'keys/test.crt', + 'keys/test-der.crt', + 'CERTIFICATE', + {} + ), + ( + 'keys/test-inter.crt', + 'keys/test-inter-der.crt', + 'CERTIFICATE', + {} + ), + ( + 'keys/test-third.crt', + 'keys/test-third-der.crt', + 'CERTIFICATE', + {} + ), + ( + 'keys/test-pkcs8.key', + 'keys/test-pkcs8-der.key', + 'PRIVATE KEY', + {} + ), + ( + 'test-third.csr', + 'test-third-der.csr', + 'CERTIFICATE REQUEST', + {} + ), + ( + 'keys/test-aes128.key', + 'keys/test-aes128-der.key', + 'RSA PRIVATE KEY', + util.OrderedDict([ + ('Proc-Type', '4,ENCRYPTED'), + ('DEK-Info', 'AES-128-CBC,01F6EE04516C912788B11BD7377626C2') + ]) + ), ) @data('unarmor_armor_files') diff --git a/tests/test_tsp.py b/tests/test_tsp.py index a416872..ee6a2e1 100644 --- a/tests/test_tsp.py +++ b/tests/test_tsp.py @@ -14,7 +14,6 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') - class TSPTests(unittest.TestCase): def test_parse_request(self): @@ -229,7 +228,10 @@ class TSPTests(unittest.TestCase): 'certs', [ util.OrderedDict([ - ('cert_hash', b'\x22\x3C\xDA\x27\x07\x96\x73\x81\x6B\x60\x8A\x1B\x8C\xB0\xAB\x02\x30\x10\x7F\xCC'), + ( + 'cert_hash', + b'\x22\x3C\xDA\x27\x07\x96\x73\x81\x6B\x60\x8A\x1B\x8C\xB0\xAB\x02\x30\x10\x7F\xCC' + ), ('issuer_serial', None), ]) ] diff --git a/tests/test_x509.py b/tests/test_x509.py index 54abff3..4a5586b 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -8,7 +8,7 @@ from datetime import datetime from asn1crypto import x509, core, pem, util -from .unittest_data import DataDecorator, data +from .unittest_data import data_decorator, data from ._unittest_compat import patch patch() @@ -23,7 +23,7 @@ tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') -@DataDecorator +@data_decorator class X509Tests(unittest.TestCase): def _load_cert(self, relative_path): @@ -33,16 +33,39 @@ class X509Tests(unittest.TestCase): _, _, cert_bytes = pem.unarmor(cert_bytes) return x509.Certificate.load(cert_bytes) - #pylint: disable=C0326 @staticmethod def is_valid_domain_ip_info(): return ( - ('geotrust_certs/codex.crt', 'codexns.io', True), - ('geotrust_certs/codex.crt', 'dev.codexns.io', True), - ('geotrust_certs/codex.crt', 'rc.codexns.io', True), - ('geotrust_certs/codex.crt', 'foo.codexns.io', False), - ('geotrust_certs/codex.crt', '1.2.3.4', False), - ('geotrust_certs/codex.crt', '1::1', False), + ( + 'geotrust_certs/codex.crt', + 'codexns.io', + True + ), + ( + 'geotrust_certs/codex.crt', + 'dev.codexns.io', + True + ), + ( + 'geotrust_certs/codex.crt', + 'rc.codexns.io', + True + ), + ( + 'geotrust_certs/codex.crt', + 'foo.codexns.io', + False + ), + ( + 'geotrust_certs/codex.crt', + '1.2.3.4', + False + ), + ( + 'geotrust_certs/codex.crt', + '1::1', + False + ), ) @data('is_valid_domain_ip_info') @@ -50,17 +73,39 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(cert) self.assertEqual(result, cert.is_valid_domain_ip(domain_ip)) - #pylint: disable=C0326 @staticmethod def ip_address_info(): return ( - ('127.0.0.1', b'\x04\x04\x7F\x00\x00\x01'), - ('255.255.255.255', b'\x04\x04\xFF\xFF\xFF\xFF'), - ('127.0.0.1/28', b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xF0'), - ('255.255.255.255/0', b'\x04\x08\xFF\xFF\xFF\xFF\x00\x00\x00\x00'), - ('af::ed', b'\x04\x10\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xED'), - ('af::ed/128', b'\x04\x20\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xED\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'), - ('af::ed/0', b'\x04\x20\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), + ( + '127.0.0.1', + b'\x04\x04\x7F\x00\x00\x01' + ), + ( + '255.255.255.255', + b'\x04\x04\xFF\xFF\xFF\xFF' + ), + ( + '127.0.0.1/28', + b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xF0' + ), + ( + '255.255.255.255/0', + b'\x04\x08\xFF\xFF\xFF\xFF\x00\x00\x00\x00' + ), + ( + 'af::ed', + b'\x04\x10\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xED' + ), + ( + 'af::ed/128', + b'\x04\x20\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xED\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + ), + ( + 'af::ed/0', + b'\x04\x20\x00\xAF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\xED\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + ), ) @data('ip_address_info') @@ -68,15 +113,34 @@ class X509Tests(unittest.TestCase): self.assertEqual(der_bytes, x509.IPAddress(unicode_string).dump()) self.assertEqual(unicode_string, x509.IPAddress.load(der_bytes).native) - #pylint: disable=C0326 @staticmethod def compare_dnsname_info(): return ( - ('google.com', 'google.com', True), - ('google.com', 'Google.com', True), - ('Bücher.ch', b'\x16\x10xn--bcher-kva.ch', True), - ('google.com', b'\x16\x0AGoogle.com', True), - ('google.com', b'\x16\x09Google.co', False), + ( + 'google.com', + 'google.com', + True + ), + ( + 'google.com', + 'Google.com', + True + ), + ( + 'Bücher.ch', + b'\x16\x10xn--bcher-kva.ch', + True + ), + ( + 'google.com', + b'\x16\x0AGoogle.com', + True + ), + ( + 'google.com', + b'\x16\x09Google.co', + False + ), ) @data('compare_dnsname_info') @@ -91,17 +155,44 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(one, two) - #pylint: disable=C0326 @staticmethod def compare_uri_info(): return ( - ('http://google.com', 'http://google.com', True), - ('http://google.com/', 'http://Google.com', True), - ('http://google.com:80', 'http://google.com', True), - ('https://google.com', 'https://google.com:443/', True), - ('http://google.com/%41%42%43', 'http://google.com/ABC', True), - ('http://google.com/%41%42%43', 'http://google.com/abc', False), - ('http://google.com/%41%42%43/', 'http://google.com/ABC%2F', False), + ( + 'http://google.com', + 'http://google.com', + True + ), + ( + 'http://google.com/', + 'http://Google.com', + True + ), + ( + 'http://google.com:80', + 'http://google.com', + True + ), + ( + 'https://google.com', + 'https://google.com:443/', + True + ), + ( + 'http://google.com/%41%42%43', + 'http://google.com/ABC', + True + ), + ( + 'http://google.com/%41%42%43', + 'http://google.com/abc', + False + ), + ( + 'http://google.com/%41%42%43/', + 'http://google.com/ABC%2F', + False + ), ) @data('compare_uri_info') @@ -116,18 +207,49 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(one, two) - #pylint: disable=C0326 @staticmethod def compare_email_address_info(): return ( - ('john@google.com', 'john@google.com', True), - ('john@google.com', 'john@Google.com', True), - ('john@google.com', 'John@google.com', False), - ('john@Bücher.ch', b'\x16\x15john@xn--bcher-kva.ch', True), - ('John@Bücher.ch', b'\x16\x15john@xn--bcher-kva.ch', False), - ('john@google.com', b'\x16\x0Fjohn@Google.com', True), - ('john@google.com', b'\x16\x0FJohn@google.com', False), - ('john@google.com', b'\x16\x0Ejohn@Google.co', False), + ( + 'john@google.com', + 'john@google.com', + True + ), + ( + 'john@google.com', + 'john@Google.com', + True + ), + ( + 'john@google.com', + 'John@google.com', + False + ), + ( + 'john@Bücher.ch', + b'\x16\x15john@xn--bcher-kva.ch', + True + ), + ( + 'John@Bücher.ch', + b'\x16\x15john@xn--bcher-kva.ch', + False + ), + ( + 'john@google.com', + b'\x16\x0Fjohn@Google.com', + True + ), + ( + 'john@google.com', + b'\x16\x0FJohn@google.com', + False + ), + ( + 'john@google.com', + b'\x16\x0Ejohn@Google.co', + False + ), ) @data('compare_email_address_info') @@ -142,15 +264,34 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(one, two) - #pylint: disable=C0326 @staticmethod def compare_ip_address_info(): return ( - ('127.0.0.1', '127.0.0.1', True), - ('127.0.0.1', '127.0.0.2', False), - ('127.0.0.1', '127.0.0.1/32', False), - ('127.0.0.1/32', b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xFF', True), - ('127.0.0.1', b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xFF', False), + ( + '127.0.0.1', + '127.0.0.1', + True + ), + ( + '127.0.0.1', + '127.0.0.2', + False + ), + ( + '127.0.0.1', + '127.0.0.1/32', + False + ), + ( + '127.0.0.1/32', + b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xFF', + True + ), + ( + '127.0.0.1', + b'\x04\x08\x7F\x00\x00\x01\xFF\xFF\xFF\xFF', + False + ), ) @data('compare_ip_address_info') @@ -165,7 +306,6 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(one, two) - #pylint: disable=C0326 @staticmethod def compare_name_info(): return ( @@ -217,15 +357,34 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(general_name_1, general_name_2) - #pylint: disable=C0326 @staticmethod def signature_algo_info(): return ( - ('keys/test-der.crt', 'rsassa_pkcs1v15', 'sha256'), - ('keys/test-inter-der.crt', 'rsassa_pkcs1v15', 'sha256'), - ('keys/test-dsa-der.crt', 'dsa', 'sha256'), - ('keys/test-third-der.crt', 'rsassa_pkcs1v15', 'sha256'), - ('keys/test-ec-der.crt', 'ecdsa', 'sha256'), + ( + 'keys/test-der.crt', + 'rsassa_pkcs1v15', + 'sha256' + ), + ( + 'keys/test-inter-der.crt', + 'rsassa_pkcs1v15', + 'sha256' + ), + ( + 'keys/test-dsa-der.crt', + 'dsa', + 'sha256' + ), + ( + 'keys/test-third-der.crt', + 'rsassa_pkcs1v15', + 'sha256' + ), + ( + 'keys/test-ec-der.crt', + 'ecdsa', + 'sha256' + ), ) @data('signature_algo_info') @@ -234,25 +393,69 @@ class X509Tests(unittest.TestCase): self.assertEqual(signature_algo, cert['signature_algorithm'].signature_algo) self.assertEqual(hash_algo, cert['signature_algorithm'].hash_algo) - #pylint: disable=C0326 @staticmethod def critical_extensions_info(): return ( - ('keys/test-der.crt', set()), - ('keys/test-inter-der.crt', set()), - ('keys/test-third-der.crt', set()), - ('geotrust_certs/GeoTrust_Universal_CA.crt', set(['basic_constraints', 'key_usage'])), - ('geotrust_certs/GeoTrust_Primary_CA.crt', set(['basic_constraints', 'key_usage'])), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', set(['basic_constraints', 'key_usage'])), - ('geotrust_certs/codex.crt', set(['key_usage'])), - ('lets_encrypt/isrgrootx1.pem', set(['key_usage', 'basic_constraints'])), - ('lets_encrypt/letsencryptauthorityx1.pem', set(['key_usage', 'basic_constraints'])), - ('lets_encrypt/letsencryptauthorityx2.pem', set(['key_usage', 'basic_constraints'])), - ('globalsign_example_keys/IssuingCA-der.cer', set(['basic_constraints', 'key_usage'])), - ('globalsign_example_keys/rootCA.cer', set(['basic_constraints', 'key_usage'])), - ('globalsign_example_keys/SSL1.cer', set(['key_usage', 'extended_key_usage', 'basic_constraints'])), - ('globalsign_example_keys/SSL2.cer', set(['key_usage', 'extended_key_usage', 'basic_constraints'])), - ('globalsign_example_keys/SSL3.cer', set(['key_usage', 'extended_key_usage', 'basic_constraints'])), + ( + 'keys/test-der.crt', + set() + ), + ( + 'keys/test-inter-der.crt', + set() + ), + ( + 'keys/test-third-der.crt', + set() + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + set(['basic_constraints', 'key_usage']) + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + set(['basic_constraints', 'key_usage']) + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + set(['basic_constraints', 'key_usage']) + ), + ( + 'geotrust_certs/codex.crt', + set(['key_usage']) + ), + ( + 'lets_encrypt/isrgrootx1.pem', + set(['key_usage', 'basic_constraints']) + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + set(['key_usage', 'basic_constraints']) + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + set(['key_usage', 'basic_constraints']) + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + set(['basic_constraints', 'key_usage']) + ), + ( + 'globalsign_example_keys/rootCA.cer', + set(['basic_constraints', 'key_usage']) + ), + ( + 'globalsign_example_keys/SSL1.cer', + set(['key_usage', 'extended_key_usage', 'basic_constraints']) + ), + ( + 'globalsign_example_keys/SSL2.cer', + set(['key_usage', 'extended_key_usage', 'basic_constraints']) + ), + ( + 'globalsign_example_keys/SSL3.cer', + set(['key_usage', 'extended_key_usage', 'basic_constraints']) + ), ) @data('critical_extensions_info') @@ -260,25 +463,69 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(critical_extensions, cert.critical_extensions) - #pylint: disable=C0326 @staticmethod def key_identifier_value_info(): return ( - ('keys/test-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), - ('keys/test-inter-der.crt', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), - ('keys/test-third-der.crt', b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r'), - ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), - ('geotrust_certs/GeoTrust_Primary_CA.crt', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), - ('geotrust_certs/codex.crt', None), - ('lets_encrypt/isrgrootx1.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), - ('lets_encrypt/letsencryptauthorityx1.pem', b'\xa8Jjc\x04}\xdd\xba\xe6\xd19\xb7\xa6Ee\xef\xf3\xa8\xec\xa1'), - ('lets_encrypt/letsencryptauthorityx2.pem', b'\xc5\xb1\xabNL\xb1\xcdd0\x93~\xc1\x84\x99\x05\xab\xe6\x03\xe2%'), - ('globalsign_example_keys/IssuingCA-der.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), - ('globalsign_example_keys/rootCA.cer', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), - ('globalsign_example_keys/SSL1.cer', b'\x94a\x04\x92\x04L\xe6\xffh\xa8\x96\xafy\xd2\xf32\x84\xae[\xcf'), - ('globalsign_example_keys/SSL2.cer', b'\xd2\xb7\x15\x7fd0\x07(p\x83\xca(\xfa\x88\x96\xde\x9e\xfc\x8a='), - ('globalsign_example_keys/SSL3.cer', b'G\xde\xa4\xe7\xea`\xe7\xee6\xc8\xf1\xd5\xb0F\x07\x07\x9eBh\xce'), + ( + 'keys/test-der.crt', + b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK' + ), + ( + 'keys/test-inter-der.crt', + b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02' + ), + ( + 'keys/test-third-der.crt', + b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r' + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6' + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92' + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3' + ), + ( + 'geotrust_certs/codex.crt', + None + ), + ( + 'lets_encrypt/isrgrootx1.pem', + b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn' + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + b'\xa8Jjc\x04}\xdd\xba\xe6\xd19\xb7\xa6Ee\xef\xf3\xa8\xec\xa1' + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + b'\xc5\xb1\xabNL\xb1\xcdd0\x93~\xc1\x84\x99\x05\xab\xe6\x03\xe2%' + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91" + ), + ( + 'globalsign_example_keys/rootCA.cer', + b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e' + ), + ( + 'globalsign_example_keys/SSL1.cer', + b'\x94a\x04\x92\x04L\xe6\xffh\xa8\x96\xafy\xd2\xf32\x84\xae[\xcf' + ), + ( + 'globalsign_example_keys/SSL2.cer', + b'\xd2\xb7\x15\x7fd0\x07(p\x83\xca(\xfa\x88\x96\xde\x9e\xfc\x8a=' + ), + ( + 'globalsign_example_keys/SSL3.cer', + b'G\xde\xa4\xe7\xea`\xe7\xee6\xc8\xf1\xd5\xb0F\x07\x07\x9eBh\xce' + ), ) @data('key_identifier_value_info') @@ -287,13 +534,21 @@ class X509Tests(unittest.TestCase): value = cert.key_identifier_value self.assertEqual(key_identifier_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def key_usage_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), ( 'geotrust_certs/GeoTrust_Universal_CA.crt', set(['digital_signature', 'key_cert_sign', 'crl_sign']) @@ -350,121 +605,254 @@ class X509Tests(unittest.TestCase): value = cert.key_usage_value self.assertEqual(key_usage_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def subject_alt_name_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [util.OrderedDict([('common_name', 'SymantecPKI-1-538')])]), - ('geotrust_certs/codex.crt', ['dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net', 'codexns.io']), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', None), - ('lets_encrypt/letsencryptauthorityx2.pem', None), - ('globalsign_example_keys/IssuingCA-der.cer', None), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', ['anything.example.com']), - ('globalsign_example_keys/SSL2.cer', ['anything.example.com']), - ('globalsign_example_keys/SSL3.cer', None), - ) - - @data('subject_alt_name_value_info') - def subject_alt_name_value(self, relative_path, subject_alt_name_value): - cert = self._load_cert(relative_path) - value = cert.subject_alt_name_value - self.assertEqual(subject_alt_name_value, value.native if value else None) - - #pylint: disable=C0326 - @staticmethod - def basic_constraints_value_info(): - return ( - ('keys/test-der.crt', {'ca': True, 'path_len_constraint': None}), - ('keys/test-inter-der.crt', {'ca': True, 'path_len_constraint': None}), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', {'ca': True, 'path_len_constraint': None}), - ('geotrust_certs/GeoTrust_Primary_CA.crt', {'ca': True, 'path_len_constraint': None}), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', {'ca': True, 'path_len_constraint': 0}), - ('geotrust_certs/codex.crt', {'ca': False, 'path_len_constraint': None}), - ('lets_encrypt/isrgrootx1.pem', {'ca': True, 'path_len_constraint': None}), - ('lets_encrypt/letsencryptauthorityx1.pem', {'ca': True, 'path_len_constraint': 0}), - ('lets_encrypt/letsencryptauthorityx2.pem', {'ca': True, 'path_len_constraint': 0}), - ('globalsign_example_keys/IssuingCA-der.cer', {'ca': True, 'path_len_constraint': None}), - ('globalsign_example_keys/rootCA.cer', {'ca': True, 'path_len_constraint': None}), - ('globalsign_example_keys/SSL1.cer', {'ca': False, 'path_len_constraint': None}), - ('globalsign_example_keys/SSL2.cer', {'ca': False, 'path_len_constraint': None}), - ('globalsign_example_keys/SSL3.cer', {'ca': False, 'path_len_constraint': None}), - ) - - @data('basic_constraints_value_info') - def basic_constraints_value(self, relative_path, basic_constraints_value): - cert = self._load_cert(relative_path) - value = cert.basic_constraints_value - self.assertEqual(basic_constraints_value, value.native if value else None) - - #pylint: disable=C0326 - @staticmethod - def name_constraints_value_info(): - return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), - ('geotrust_certs/codex.crt', None), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', None), - ('lets_encrypt/letsencryptauthorityx2.pem', None), ( - 'globalsign_example_keys/IssuingCA-der.cer', - util.OrderedDict([ - ( - 'permitted_subtrees', - [ - util.OrderedDict([ - ('base', 'onlythis.com'), - ('minimum', 0), - ('maximum', None) - ]), - util.OrderedDict([ - ( - 'base', - util.OrderedDict([ - ('country_name', 'US'), - ('state_or_province_name', 'MA'), - ('locality_name', 'Boston'), - ('organization_name', 'Example LLC') - ]) - ), - ('minimum', 0), - ('maximum', None) - ]) - ] - ), - ( - 'excluded_subtrees', - [ - util.OrderedDict([ - ('base', '0.0.0.0/0'), - ('minimum', 0), - ('maximum', None) - ]), - util.OrderedDict([ - ('base', '::/0'), - ('minimum', 0), + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + [ + util.OrderedDict([ + ('common_name', 'SymantecPKI-1-538') + ]) + ] + ), + ( + 'geotrust_certs/codex.crt', + ['dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net', 'codexns.io'] + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + None + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + None + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + ['anything.example.com'] + ), + ( + 'globalsign_example_keys/SSL2.cer', + ['anything.example.com'] + ), + ( + 'globalsign_example_keys/SSL3.cer', + None + ), + ) + + @data('subject_alt_name_value_info') + def subject_alt_name_value(self, relative_path, subject_alt_name_value): + cert = self._load_cert(relative_path) + value = cert.subject_alt_name_value + self.assertEqual(subject_alt_name_value, value.native if value else None) + + @staticmethod + def basic_constraints_value_info(): + return ( + ( + 'keys/test-der.crt', + {'ca': True, 'path_len_constraint': None} + ), + ( + 'keys/test-inter-der.crt', + {'ca': True, 'path_len_constraint': None} + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + {'ca': True, 'path_len_constraint': None} + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + {'ca': True, 'path_len_constraint': None} + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + {'ca': True, 'path_len_constraint': 0} + ), + ( + 'geotrust_certs/codex.crt', + {'ca': False, 'path_len_constraint': None} + ), + ( + 'lets_encrypt/isrgrootx1.pem', + {'ca': True, 'path_len_constraint': None} + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + {'ca': True, 'path_len_constraint': 0} + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + {'ca': True, 'path_len_constraint': 0} + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + {'ca': True, 'path_len_constraint': None} + ), + ( + 'globalsign_example_keys/rootCA.cer', + {'ca': True, 'path_len_constraint': None} + ), + ( + 'globalsign_example_keys/SSL1.cer', + {'ca': False, 'path_len_constraint': None} + ), + ( + 'globalsign_example_keys/SSL2.cer', + {'ca': False, 'path_len_constraint': None} + ), + ( + 'globalsign_example_keys/SSL3.cer', + {'ca': False, 'path_len_constraint': None} + ), + ) + + @data('basic_constraints_value_info') + def basic_constraints_value(self, relative_path, basic_constraints_value): + cert = self._load_cert(relative_path) + value = cert.basic_constraints_value + self.assertEqual(basic_constraints_value, value.native if value else None) + + @staticmethod + def name_constraints_value_info(): + return ( + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + None + ), + ( + 'geotrust_certs/codex.crt', + None + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + None + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + util.OrderedDict([ + ( + 'permitted_subtrees', + [ + util.OrderedDict([ + ('base', 'onlythis.com'), + ('minimum', 0), + ('maximum', None) + ]), + util.OrderedDict([ + ( + 'base', + util.OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'MA'), + ('locality_name', 'Boston'), + ('organization_name', 'Example LLC') + ]) + ), + ('minimum', 0), + ('maximum', None) + ]) + ] + ), + ( + 'excluded_subtrees', + [ + util.OrderedDict([ + ('base', '0.0.0.0/0'), + ('minimum', 0), + ('maximum', None) + ]), + util.OrderedDict([ + ('base', '::/0'), + ('minimum', 0), ('maximum', None) ]) ] ), ]) ), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', None), - ('globalsign_example_keys/SSL2.cer', None), - ('globalsign_example_keys/SSL3.cer', None), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + None + ), + ( + 'globalsign_example_keys/SSL2.cer', + None + ), + ( + 'globalsign_example_keys/SSL3.cer', + None + ), ) @data('name_constraints_value_info') @@ -473,15 +861,29 @@ class X509Tests(unittest.TestCase): value = cert.name_constraints_value self.assertEqual(name_constraints_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def crl_distribution_points_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ @@ -502,7 +904,10 @@ class X509Tests(unittest.TestCase): ]) ] ), - ('lets_encrypt/isrgrootx1.pem', None), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), ( 'lets_encrypt/letsencryptauthorityx1.pem', [ @@ -541,9 +946,18 @@ class X509Tests(unittest.TestCase): ('crl_issuer', None) ]) ]), - ('globalsign_example_keys/SSL1.cer', None), - ('globalsign_example_keys/SSL2.cer', None), - ('globalsign_example_keys/SSL3.cer', None), + ( + 'globalsign_example_keys/SSL1.cer', + None + ), + ( + 'globalsign_example_keys/SSL2.cer', + None + ), + ( + 'globalsign_example_keys/SSL3.cer', + None + ), ) @data('crl_distribution_points_value_info') @@ -552,15 +966,29 @@ class X509Tests(unittest.TestCase): value = cert.crl_distribution_points_value self.assertEqual(crl_distribution_points_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def certificate_policies_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ @@ -605,7 +1033,10 @@ class X509Tests(unittest.TestCase): ]) ] ), - ('lets_encrypt/isrgrootx1.pem', None), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), ( 'lets_encrypt/letsencryptauthorityx1.pem', [ @@ -665,7 +1096,10 @@ class X509Tests(unittest.TestCase): ]) ] ), - ('globalsign_example_keys/rootCA.cer', None), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), ( 'globalsign_example_keys/SSL1.cer', [ @@ -725,25 +1159,69 @@ class X509Tests(unittest.TestCase): value = cert.certificate_policies_value self.assertEqual(certificate_policies_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def policy_mappings_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), - ('geotrust_certs/codex.crt', None), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', None), - ('lets_encrypt/letsencryptauthorityx2.pem', None), - ('globalsign_example_keys/IssuingCA-der.cer', None), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', None), - ('globalsign_example_keys/SSL2.cer', None), - ('globalsign_example_keys/SSL3.cer', None), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + None + ), + ( + 'geotrust_certs/codex.crt', + None + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + None + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + None + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + None + ), + ( + 'globalsign_example_keys/SSL2.cer', + None + ), + ( + 'globalsign_example_keys/SSL3.cer', + None + ), ) @data('policy_mappings_value_info') @@ -752,7 +1230,6 @@ class X509Tests(unittest.TestCase): value = cert.policy_mappings_value self.assertEqual(policy_mappings_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def authority_key_identifier_value_info(): return ( @@ -885,25 +1362,69 @@ class X509Tests(unittest.TestCase): value = cert.authority_key_identifier_value self.assertEqual(authority_key_identifier_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def policy_constraints_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), - ('geotrust_certs/codex.crt', None), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', None), - ('lets_encrypt/letsencryptauthorityx2.pem', None), - ('globalsign_example_keys/IssuingCA-der.cer', None), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', None), - ('globalsign_example_keys/SSL2.cer', None), - ('globalsign_example_keys/SSL3.cer', None), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + None + ), + ( + 'geotrust_certs/codex.crt', + None + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + None + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + None + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + None + ), + ( + 'globalsign_example_keys/SSL2.cer', + None + ), + ( + 'globalsign_example_keys/SSL3.cer', + None + ), ) @data('policy_constraints_value_info') @@ -912,25 +1433,68 @@ class X509Tests(unittest.TestCase): value = cert.policy_constraints_value self.assertEqual(policy_constraints_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def extended_key_usage_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), - ('geotrust_certs/codex.crt', ['server_auth', 'client_auth']), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', None), - ('lets_encrypt/letsencryptauthorityx2.pem', None), - ('globalsign_example_keys/IssuingCA-der.cer', None), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', ['server_auth', 'client_auth']), - ('globalsign_example_keys/SSL2.cer', ['server_auth', 'client_auth']), - ('globalsign_example_keys/SSL3.cer', ['server_auth', 'client_auth']), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + None + ), + ( + 'geotrust_certs/codex.crt', + ['server_auth', 'client_auth']), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + None + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + None + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + ['server_auth', 'client_auth'] + ), + ( + 'globalsign_example_keys/SSL2.cer', + ['server_auth', 'client_auth'] + ), + ( + 'globalsign_example_keys/SSL3.cer', + ['server_auth', 'client_auth'] + ), ) @data('extended_key_usage_value_info') @@ -939,15 +1503,29 @@ class X509Tests(unittest.TestCase): value = cert.extended_key_usage_value self.assertEqual(extended_key_usage_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def authority_information_access_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ @@ -970,7 +1548,10 @@ class X509Tests(unittest.TestCase): ]), ] ), - ('lets_encrypt/isrgrootx1.pem', None), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), ( 'lets_encrypt/letsencryptauthorityx1.pem', [ @@ -997,8 +1578,14 @@ class X509Tests(unittest.TestCase): ]) ] ), - ('globalsign_example_keys/IssuingCA-der.cer', None), - ('globalsign_example_keys/rootCA.cer', None), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + None + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), ( 'globalsign_example_keys/SSL1.cer', [ @@ -1046,78 +1633,210 @@ class X509Tests(unittest.TestCase): value = cert.authority_information_access_value self.assertEqual(authority_information_access_value, value.native if value else None) - #pylint: disable=C0326 @staticmethod def ocsp_no_check_value_info(): return ( - ('keys/test-der.crt', None), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), - ('geotrust_certs/codex.crt', None), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', None), - ('lets_encrypt/letsencryptauthorityx2.pem', None), - ('globalsign_example_keys/IssuingCA-der.cer', None), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', None), - ('globalsign_example_keys/SSL2.cer', None), - ('globalsign_example_keys/SSL3.cer', None), - ) - - @data('ocsp_no_check_value_info') - def ocsp_no_check_value(self, relative_path, ocsp_no_check_value): - cert = self._load_cert(relative_path) - value = cert.ocsp_no_check_value - self.assertEqual(ocsp_no_check_value, value.native if value else None) - - #pylint: disable=C0326 - @staticmethod - def serial_number_info(): - return ( - ('keys/test-der.crt', 13683582341504654466), - ('keys/test-inter-der.crt', 1590137), - ('keys/test-third-der.crt', 2474902313), - ('geotrust_certs/GeoTrust_Universal_CA.crt', 1), - ('geotrust_certs/GeoTrust_Primary_CA.crt', 32798226551256963324313806436981982369), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', 146934555852773531829332059263122711876), - ('geotrust_certs/codex.crt', 130338219198307073574879940486642352162), - ('lets_encrypt/isrgrootx1.pem', 172886928669790476064670243504169061120), - ('lets_encrypt/letsencryptauthorityx1.pem', 307817870430047279283060309415759825539), - ('lets_encrypt/letsencryptauthorityx2.pem', 199666138109676817050168330923544141416), - ('globalsign_example_keys/IssuingCA-der.cer', 43543335419752), - ('globalsign_example_keys/rootCA.cer', 342514332211132), - ('globalsign_example_keys/SSL1.cer', 425155524522), - ('globalsign_example_keys/SSL2.cer', 425155524522), - ('globalsign_example_keys/SSL3.cer', 425155524522), - ) - - @data('serial_number_info') - def serial_number(self, relative_path, serial_number): - cert = self._load_cert(relative_path) - self.assertEqual(serial_number, cert.serial_number) - - #pylint: disable=C0326 - @staticmethod - def key_identifier_info(): - return ( - ('keys/test-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), - ('keys/test-inter-der.crt', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), - ('keys/test-third-der.crt', b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r'), - ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), - ('geotrust_certs/GeoTrust_Primary_CA.crt', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), - ('geotrust_certs/codex.crt', None), - ('lets_encrypt/isrgrootx1.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), - ('lets_encrypt/letsencryptauthorityx1.pem', b'\xa8Jjc\x04}\xdd\xba\xe6\xd19\xb7\xa6Ee\xef\xf3\xa8\xec\xa1'), - ('lets_encrypt/letsencryptauthorityx2.pem', b'\xc5\xb1\xabNL\xb1\xcdd0\x93~\xc1\x84\x99\x05\xab\xe6\x03\xe2%'), - ('globalsign_example_keys/IssuingCA-der.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), - ('globalsign_example_keys/rootCA.cer', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), - ('globalsign_example_keys/SSL1.cer', b'\x94a\x04\x92\x04L\xe6\xffh\xa8\x96\xafy\xd2\xf32\x84\xae[\xcf'), - ('globalsign_example_keys/SSL2.cer', b'\xd2\xb7\x15\x7fd0\x07(p\x83\xca(\xfa\x88\x96\xde\x9e\xfc\x8a='), - ('globalsign_example_keys/SSL3.cer', b'G\xde\xa4\xe7\xea`\xe7\xee6\xc8\xf1\xd5\xb0F\x07\x07\x9eBh\xce'), + ( + 'keys/test-der.crt', + None + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + None + ), + ( + 'geotrust_certs/codex.crt', + None + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + None + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + None + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + None + ), + ( + 'globalsign_example_keys/SSL2.cer', + None + ), + ( + 'globalsign_example_keys/SSL3.cer', + None + ), + ) + + @data('ocsp_no_check_value_info') + def ocsp_no_check_value(self, relative_path, ocsp_no_check_value): + cert = self._load_cert(relative_path) + value = cert.ocsp_no_check_value + self.assertEqual(ocsp_no_check_value, value.native if value else None) + + @staticmethod + def serial_number_info(): + return ( + ( + 'keys/test-der.crt', + 13683582341504654466 + ), + ( + 'keys/test-inter-der.crt', + 1590137 + ), + ( + 'keys/test-third-der.crt', + 2474902313 + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + 1 + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + 32798226551256963324313806436981982369 + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + 146934555852773531829332059263122711876 + ), + ( + 'geotrust_certs/codex.crt', + 130338219198307073574879940486642352162 + ), + ( + 'lets_encrypt/isrgrootx1.pem', + 172886928669790476064670243504169061120 + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + 307817870430047279283060309415759825539 + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + 199666138109676817050168330923544141416 + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + 43543335419752 + ), + ( + 'globalsign_example_keys/rootCA.cer', + 342514332211132 + ), + ( + 'globalsign_example_keys/SSL1.cer', + 425155524522 + ), + ( + 'globalsign_example_keys/SSL2.cer', + 425155524522 + ), + ( + 'globalsign_example_keys/SSL3.cer', + 425155524522 + ), + ) + + @data('serial_number_info') + def serial_number(self, relative_path, serial_number): + cert = self._load_cert(relative_path) + self.assertEqual(serial_number, cert.serial_number) + + @staticmethod + def key_identifier_info(): + return ( + ( + 'keys/test-der.crt', + b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK' + ), + ( + 'keys/test-inter-der.crt', + b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02' + ), + ( + 'keys/test-third-der.crt', + b'D8\xe0\xe0&\x85\xbf\x98\x86\xdc\x1b\xe1\x1d\xf520\xbe\xab\xac\r' + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6' + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92' + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3' + ), + ( + 'geotrust_certs/codex.crt', + None + ), + ( + 'lets_encrypt/isrgrootx1.pem', + b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn' + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + b'\xa8Jjc\x04}\xdd\xba\xe6\xd19\xb7\xa6Ee\xef\xf3\xa8\xec\xa1' + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + b'\xc5\xb1\xabNL\xb1\xcdd0\x93~\xc1\x84\x99\x05\xab\xe6\x03\xe2%' + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91" + ), + ( + 'globalsign_example_keys/rootCA.cer', + b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e' + ), + ( + 'globalsign_example_keys/SSL1.cer', + b'\x94a\x04\x92\x04L\xe6\xffh\xa8\x96\xafy\xd2\xf32\x84\xae[\xcf' + ), + ( + 'globalsign_example_keys/SSL2.cer', + b'\xd2\xb7\x15\x7fd0\x07(p\x83\xca(\xfa\x88\x96\xde\x9e\xfc\x8a=' + ), + ( + 'globalsign_example_keys/SSL3.cer', + b'G\xde\xa4\xe7\xea`\xe7\xee6\xc8\xf1\xd5\xb0F\x07\x07\x9eBh\xce' + ), ) @data('key_identifier_info') @@ -1125,25 +1844,83 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(key_identifier, cert.key_identifier) - #pylint: disable=C0326 @staticmethod def issuer_serial_info(): return ( - ('keys/test-der.crt', b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:13683582341504654466'), - ('keys/test-inter-der.crt', b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:1590137'), - ('keys/test-third-der.crt', b'\xed{\x9b\xbf\x9b\xdbd\xa4\xea\xf2#+H\x96\xcd\x80\x99\xf6\xecCM\x94\x07\x02\xe2\x18\xf3\x83\x8c8%\x01:2474902313'), - ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xa1\x848\xf2\xe5w\xee\xec\xce\xfefJC+\xdf\x97\x7f\xd2Y\xe3\xdc\xa0D7~\x07\xd9\x9dzL@g:1'), - ('geotrust_certs/GeoTrust_Primary_CA.crt', b'\xdcg\x0c\x80\x03\xb3D\xa0v\xe2\xee\xec\x8b\xd6\x82\x01\xf0\x13\x0cwT\xb4\x8f\x80\x0eT\x9d\xbf\xbf\xa4\x11\x80:32798226551256963324313806436981982369'), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b'\xdcg\x0c\x80\x03\xb3D\xa0v\xe2\xee\xec\x8b\xd6\x82\x01\xf0\x13\x0cwT\xb4\x8f\x80\x0eT\x9d\xbf\xbf\xa4\x11\x80:146934555852773531829332059263122711876'), - ('geotrust_certs/codex.crt', b'x\x12\xe0\x15\x00d;\xc3\xb9/\xf6\x13\n\xd8\xe2\xddY\xf7\xaf*=C\x01<\x86\xf5\x9f_\xab;e\xd1:130338219198307073574879940486642352162'), - ('lets_encrypt/isrgrootx1.pem', b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-0\xf8:172886928669790476064670243504169061120'), - ('lets_encrypt/letsencryptauthorityx1.pem', b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-0\xf8:307817870430047279283060309415759825539'), - ('lets_encrypt/letsencryptauthorityx2.pem', b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-0\xf8:199666138109676817050168330923544141416'), - ('globalsign_example_keys/IssuingCA-der.cer', b'\xd2\xe7\xca\x10\xc1\x91\x92Y^A\x11\xd3Rz\xd5\x93\x19wk\x11\xef\xaa\x9c\xad\x10\x8ak\x8a\x08-\x0c\xff:43543335419752'), - ('globalsign_example_keys/rootCA.cer', b'\xd2\xe7\xca\x10\xc1\x91\x92Y^A\x11\xd3Rz\xd5\x93\x19wk\x11\xef\xaa\x9c\xad\x10\x8ak\x8a\x08-\x0c\xff:342514332211132'), - ('globalsign_example_keys/SSL1.cer', b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f\xf0\x1d\xbc\x9f\xe4:425155524522'), - ('globalsign_example_keys/SSL2.cer', b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f\xf0\x1d\xbc\x9f\xe4:425155524522'), - ('globalsign_example_keys/SSL3.cer', b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f\xf0\x1d\xbc\x9f\xe4:425155524522'), + ( + 'keys/test-der.crt', + b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c' + b'\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:13683582341504654466' + ), + ( + 'keys/test-inter-der.crt', + b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c' + b'\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:1590137' + ), + ( + 'keys/test-third-der.crt', + b'\xed{\x9b\xbf\x9b\xdbd\xa4\xea\xf2#+H\x96\xcd\x80\x99\xf6\xecCM\x94' + b'\x07\x02\xe2\x18\xf3\x83\x8c8%\x01:2474902313' + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + b'\xa1\x848\xf2\xe5w\xee\xec\xce\xfefJC+\xdf\x97\x7f\xd2Y\xe3\xdc\xa0D7~\x07\xd9\x9dzL@g:1' + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + b'\xdcg\x0c\x80\x03\xb3D\xa0v\xe2\xee\xec\x8b\xd6\x82\x01\xf0\x13\x0cwT' + b'\xb4\x8f\x80\x0eT\x9d\xbf\xbf\xa4\x11\x80:32798226551256963324313806436981982369' + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + b'\xdcg\x0c\x80\x03\xb3D\xa0v\xe2\xee\xec\x8b\xd6\x82\x01\xf0\x13\x0cwT' + b'\xb4\x8f\x80\x0eT\x9d\xbf\xbf\xa4\x11\x80:146934555852773531829332059263122711876' + ), + ( + 'geotrust_certs/codex.crt', + b'x\x12\xe0\x15\x00d;\xc3\xb9/\xf6\x13\n\xd8\xe2\xddY\xf7\xaf*=C\x01<\x86\xf5\x9f' + b'_\xab;e\xd1:130338219198307073574879940486642352162' + ), + ( + 'lets_encrypt/isrgrootx1.pem', + b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e' + b'-0\xf8:172886928669790476064670243504169061120' + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-' + b'0\xf8:307817870430047279283060309415759825539' + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + b'\xf6\xdb/\xbd\x9d\xd8]\x92Y\xdd\xb3\xc6\xde}{/\xec?>\x0c\xef\x17a\xbc\xbf3 W\x1e-' + b'0\xf8:199666138109676817050168330923544141416' + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + b'\xd2\xe7\xca\x10\xc1\x91\x92Y^A\x11\xd3Rz\xd5\x93\x19wk\x11\xef\xaa\x9c\xad\x10' + b'\x8ak\x8a\x08-\x0c\xff:43543335419752' + ), + ( + 'globalsign_example_keys/rootCA.cer', + b'\xd2\xe7\xca\x10\xc1\x91\x92Y^A\x11\xd3Rz\xd5\x93\x19wk\x11\xef\xaa\x9c\xad\x10' + b'\x8ak\x8a\x08-\x0c\xff:342514332211132' + ), + ( + 'globalsign_example_keys/SSL1.cer', + b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f' + b'\xf0\x1d\xbc\x9f\xe4:425155524522' + ), + ( + 'globalsign_example_keys/SSL2.cer', + b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f' + b'\xf0\x1d\xbc\x9f\xe4:425155524522' + ), + ( + 'globalsign_example_keys/SSL3.cer', + b'_\xc0S\xb1\xeb}\xe3\x8e\xe4{\xdb\xd7\xe2\xd9}=3\x97|\x0c\x1e\xecz\xcc\x92u\x1f' + b'\xf0\x1d\xbc\x9f\xe4:425155524522' + ), ) @data('issuer_serial_info') @@ -1151,25 +1928,69 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(issuer_serial, cert.issuer_serial) - #pylint: disable=C0326 @staticmethod def authority_key_identifier_info(): return ( - ('keys/test-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), - ('keys/test-inter-der.crt', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'), - ('keys/test-third-der.crt', b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02'), - ('geotrust_certs/GeoTrust_Universal_CA.crt', b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6'), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92'), - ('geotrust_certs/codex.crt', b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3'), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), - ('lets_encrypt/letsencryptauthorityx2.pem', b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn'), - ('globalsign_example_keys/IssuingCA-der.cer', b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e'), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), - ('globalsign_example_keys/SSL2.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), - ('globalsign_example_keys/SSL3.cer', b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91"), + ( + 'keys/test-der.crt', + b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK' + ), + ( + 'keys/test-inter-der.crt', + b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK' + ), + ( + 'keys/test-third-der.crt', + b'\xd2\n\xfd.%\xd1\xb7!\xd7P~\xbb\xa4}\xbf4\xefR^\x02' + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + b'\xda\xbb.\xaa\xb0\x0c\xb8\x88&Qt\\m\x03\xd3\xc0\xd8\x8fz\xd6' + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + b',\xd5PA\x97\x15\x8b\xf0\x8f6a[J\xfbk\xd9\x99\xc93\x92' + ), + ( + 'geotrust_certs/codex.crt', + b'\xde\xcf\\P\xb7\xae\x02\x1f\x15\x17\xaa\x16\xe8\r\xb5(\x9djZ\xf3' + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn' + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + b'y\xb4Y\xe6{\xb6\xe5\xe4\x01s\x80\x08\x88\xc8\x1aX\xf6\xe9\x9bn' + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + b'd|\\\xe1\xe0`8NH\x9f\x05\xbcUc~?\xaeM\xf7\x1e' + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91" + ), + ( + 'globalsign_example_keys/SSL2.cer', + b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91" + ), + ( + 'globalsign_example_keys/SSL3.cer', + b"'\xf8/\xe9]\xd7\r\xf4\xa8\xea\x87\x99=\xfd\x8e\xb3\x9e@\xd0\x91" + ), ) @data('authority_key_identifier_info') @@ -1177,25 +1998,70 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(authority_key_identifier, cert.authority_key_identifier) - #pylint: disable=C0326 @staticmethod def authority_issuer_serial_info(): return ( - ('keys/test-der.crt', b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:13683582341504654466'), - ('keys/test-inter-der.crt', None), - ('keys/test-third-der.crt', None), - ('geotrust_certs/GeoTrust_Universal_CA.crt', None), - ('geotrust_certs/GeoTrust_Primary_CA.crt', None), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', None), - ('geotrust_certs/codex.crt', None), - ('lets_encrypt/isrgrootx1.pem', None), - ('lets_encrypt/letsencryptauthorityx1.pem', None), - ('lets_encrypt/letsencryptauthorityx2.pem', None), - ('globalsign_example_keys/IssuingCA-der.cer', None), - ('globalsign_example_keys/rootCA.cer', None), - ('globalsign_example_keys/SSL1.cer', None), - ('globalsign_example_keys/SSL2.cer', None), - ('globalsign_example_keys/SSL3.cer', None), + ( + 'keys/test-der.crt', + b'\xdd\x8a\x19x\xae`\x19=\xa7\xf8\x00\xb9\xfbx\xf8\xedu\xb8!\xf8\x8c' + b'\xdb\x1f\x99\'7w\x93\xb4\xa4\'\xa0:13683582341504654466' + ), + ( + 'keys/test-inter-der.crt', + None + ), + ( + 'keys/test-third-der.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + None + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + None + ), + ( + 'geotrust_certs/codex.crt', + None + ), + ( + 'lets_encrypt/isrgrootx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + None + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + None + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + None + ), + ( + 'globalsign_example_keys/rootCA.cer', + None + ), + ( + 'globalsign_example_keys/SSL1.cer', + None + ), + ( + 'globalsign_example_keys/SSL2.cer', + None + ), + ( + 'globalsign_example_keys/SSL3.cer', + None + ), ) @data('authority_issuer_serial_info') @@ -1203,25 +2069,69 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(authority_issuer_serial, cert.authority_issuer_serial) - #pylint: disable=C0326 @staticmethod def ocsp_urls_info(): return ( - ('keys/test-der.crt', []), - ('keys/test-inter-der.crt', []), - ('keys/test-third-der.crt', []), - ('geotrust_certs/GeoTrust_Universal_CA.crt', []), - ('geotrust_certs/GeoTrust_Primary_CA.crt', []), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', ['http://g2.symcb.com']), - ('geotrust_certs/codex.crt', ['http://gm.symcd.com']), - ('lets_encrypt/isrgrootx1.pem', []), - ('lets_encrypt/letsencryptauthorityx1.pem', ['http://ocsp.root-x1.letsencrypt.org/']), - ('lets_encrypt/letsencryptauthorityx2.pem', ['http://ocsp.root-x1.letsencrypt.org/']), - ('globalsign_example_keys/IssuingCA-der.cer', []), - ('globalsign_example_keys/rootCA.cer', []), - ('globalsign_example_keys/SSL1.cer', ['http://ocsp.exampleovca.com/']), - ('globalsign_example_keys/SSL2.cer', ['http://ocsp.exampleovca.com/']), - ('globalsign_example_keys/SSL3.cer', ['http://ocsp.exampleovca.com/']), + ( + 'keys/test-der.crt', + [] + ), + ( + 'keys/test-inter-der.crt', + [] + ), + ( + 'keys/test-third-der.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + ['http://g2.symcb.com'] + ), + ( + 'geotrust_certs/codex.crt', + ['http://gm.symcd.com'] + ), + ( + 'lets_encrypt/isrgrootx1.pem', + [] + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + ['http://ocsp.root-x1.letsencrypt.org/'] + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + ['http://ocsp.root-x1.letsencrypt.org/'] + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + [] + ), + ( + 'globalsign_example_keys/rootCA.cer', + [] + ), + ( + 'globalsign_example_keys/SSL1.cer', + ['http://ocsp.exampleovca.com/'] + ), + ( + 'globalsign_example_keys/SSL2.cer', + ['http://ocsp.exampleovca.com/'] + ), + ( + 'globalsign_example_keys/SSL3.cer', + ['http://ocsp.exampleovca.com/'] + ), ) @data('ocsp_urls_info') @@ -1229,15 +2139,29 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(ocsp_url, cert.ocsp_urls) - #pylint: disable=C0326 @staticmethod def crl_distribution_points_info(): return ( - ('keys/test-der.crt', []), - ('keys/test-inter-der.crt', []), - ('keys/test-third-der.crt', []), - ('geotrust_certs/GeoTrust_Universal_CA.crt', []), - ('geotrust_certs/GeoTrust_Primary_CA.crt', []), + ( + 'keys/test-der.crt', + [] + ), + ( + 'keys/test-inter-der.crt', + [] + ), + ( + 'keys/test-third-der.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + [] + ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', [ @@ -1258,7 +2182,10 @@ class X509Tests(unittest.TestCase): ]) ] ), - ('lets_encrypt/isrgrootx1.pem', []), + ( + 'lets_encrypt/isrgrootx1.pem', + [] + ), ( 'lets_encrypt/letsencryptauthorityx1.pem', [ @@ -1299,9 +2226,18 @@ class X509Tests(unittest.TestCase): ]) ] ), - ('globalsign_example_keys/SSL1.cer', []), - ('globalsign_example_keys/SSL2.cer', []), - ('globalsign_example_keys/SSL3.cer', []), + ( + 'globalsign_example_keys/SSL1.cer', + [] + ), + ( + 'globalsign_example_keys/SSL2.cer', + [] + ), + ( + 'globalsign_example_keys/SSL3.cer', + [] + ), ) @data('crl_distribution_points_info') @@ -1310,25 +2246,69 @@ class X509Tests(unittest.TestCase): points = [point.native for point in cert.crl_distribution_points] self.assertEqual(crl_distribution_point, points) - #pylint: disable=C0326 @staticmethod def valid_domains_info(): return ( - ('keys/test-der.crt', []), - ('keys/test-inter-der.crt', []), - ('keys/test-third-der.crt', []), - ('geotrust_certs/GeoTrust_Universal_CA.crt', []), - ('geotrust_certs/GeoTrust_Primary_CA.crt', []), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', []), - ('geotrust_certs/codex.crt', ['dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net', 'codexns.io']), - ('lets_encrypt/isrgrootx1.pem', []), - ('lets_encrypt/letsencryptauthorityx1.pem', []), - ('lets_encrypt/letsencryptauthorityx2.pem', []), - ('globalsign_example_keys/IssuingCA-der.cer', []), - ('globalsign_example_keys/rootCA.cer', []), - ('globalsign_example_keys/SSL1.cer', ['anything.example.com']), - ('globalsign_example_keys/SSL2.cer', ['anything.example.com']), - ('globalsign_example_keys/SSL3.cer', ['*.google.com']), + ( + 'keys/test-der.crt', + [] + ), + ( + 'keys/test-inter-der.crt', + [] + ), + ( + 'keys/test-third-der.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + [] + ), + ( + 'geotrust_certs/codex.crt', + ['dev.codexns.io', 'rc.codexns.io', 'packagecontrol.io', 'wbond.net', 'codexns.io'] + ), + ( + 'lets_encrypt/isrgrootx1.pem', + [] + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + [] + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + [] + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + [] + ), + ( + 'globalsign_example_keys/rootCA.cer', + [] + ), + ( + 'globalsign_example_keys/SSL1.cer', + ['anything.example.com'] + ), + ( + 'globalsign_example_keys/SSL2.cer', + ['anything.example.com'] + ), + ( + 'globalsign_example_keys/SSL3.cer', + ['*.google.com'] + ), ) @data('valid_domains_info') @@ -1336,25 +2316,69 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(valid_domains, cert.valid_domains) - #pylint: disable=C0326 @staticmethod def valid_ips_info(): return ( - ('keys/test-der.crt', []), - ('keys/test-inter-der.crt', []), - ('keys/test-third-der.crt', []), - ('geotrust_certs/GeoTrust_Universal_CA.crt', []), - ('geotrust_certs/GeoTrust_Primary_CA.crt', []), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', []), - ('geotrust_certs/codex.crt', []), - ('lets_encrypt/isrgrootx1.pem', []), - ('lets_encrypt/letsencryptauthorityx1.pem', []), - ('lets_encrypt/letsencryptauthorityx2.pem', []), - ('globalsign_example_keys/IssuingCA-der.cer', []), - ('globalsign_example_keys/rootCA.cer', []), - ('globalsign_example_keys/SSL1.cer', []), - ('globalsign_example_keys/SSL2.cer', []), - ('globalsign_example_keys/SSL3.cer', []), + ( + 'keys/test-der.crt', + [] + ), + ( + 'keys/test-inter-der.crt', + [] + ), + ( + 'keys/test-third-der.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + [] + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + [] + ), + ( + 'geotrust_certs/codex.crt', + [] + ), + ( + 'lets_encrypt/isrgrootx1.pem', + [] + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + [] + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + [] + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + [] + ), + ( + 'globalsign_example_keys/rootCA.cer', + [] + ), + ( + 'globalsign_example_keys/SSL1.cer', + [] + ), + ( + 'globalsign_example_keys/SSL2.cer', + [] + ), + ( + 'globalsign_example_keys/SSL3.cer', + [] + ), ) @data('valid_ips_info') @@ -1362,25 +2386,69 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(crl_url, cert.valid_ips) - #pylint: disable=C0326 @staticmethod def self_issued_info(): return ( - ('keys/test-der.crt', True), - ('keys/test-inter-der.crt', False), - ('keys/test-third-der.crt', False), - ('geotrust_certs/GeoTrust_Universal_CA.crt', True), - ('geotrust_certs/GeoTrust_Primary_CA.crt', True), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', False), - ('geotrust_certs/codex.crt', False), - ('lets_encrypt/isrgrootx1.pem', True), - ('lets_encrypt/letsencryptauthorityx1.pem', False), - ('lets_encrypt/letsencryptauthorityx2.pem', False), - ('globalsign_example_keys/IssuingCA-der.cer', False), - ('globalsign_example_keys/rootCA.cer', True), - ('globalsign_example_keys/SSL1.cer', False), - ('globalsign_example_keys/SSL2.cer', False), - ('globalsign_example_keys/SSL3.cer', False), + ( + 'keys/test-der.crt', + True + ), + ( + 'keys/test-inter-der.crt', + False + ), + ( + 'keys/test-third-der.crt', + False + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + True + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + True + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + False + ), + ( + 'geotrust_certs/codex.crt', + False + ), + ( + 'lets_encrypt/isrgrootx1.pem', + True + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + False + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + False + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + False + ), + ( + 'globalsign_example_keys/rootCA.cer', + True + ), + ( + 'globalsign_example_keys/SSL1.cer', + False + ), + ( + 'globalsign_example_keys/SSL2.cer', + False + ), + ( + 'globalsign_example_keys/SSL3.cer', + False + ), ) @data('self_issued_info') @@ -1388,25 +2456,69 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(self_issued, cert.self_issued) - #pylint: disable=C0326 @staticmethod def self_signed_info(): return ( - ('keys/test-der.crt', 'yes'), - ('keys/test-inter-der.crt', 'no'), - ('keys/test-third-der.crt', 'no'), - ('geotrust_certs/GeoTrust_Universal_CA.crt', 'yes'), - ('geotrust_certs/GeoTrust_Primary_CA.crt', 'yes'), - ('geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', 'no'), - ('geotrust_certs/codex.crt', 'no'), - ('lets_encrypt/isrgrootx1.pem', 'yes'), - ('lets_encrypt/letsencryptauthorityx1.pem', 'no'), - ('lets_encrypt/letsencryptauthorityx2.pem', 'no'), - ('globalsign_example_keys/IssuingCA-der.cer', 'no'), - ('globalsign_example_keys/rootCA.cer', 'yes'), - ('globalsign_example_keys/SSL1.cer', 'no'), - ('globalsign_example_keys/SSL2.cer', 'no'), - ('globalsign_example_keys/SSL3.cer', 'no'), + ( + 'keys/test-der.crt', + 'yes' + ), + ( + 'keys/test-inter-der.crt', + 'no' + ), + ( + 'keys/test-third-der.crt', + 'no' + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + 'yes' + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + 'yes' + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + 'no' + ), + ( + 'geotrust_certs/codex.crt', + 'no' + ), + ( + 'lets_encrypt/isrgrootx1.pem', + 'yes' + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + 'no' + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + 'no' + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + 'no' + ), + ( + 'globalsign_example_keys/rootCA.cer', + 'yes' + ), + ( + 'globalsign_example_keys/SSL1.cer', + 'no' + ), + ( + 'globalsign_example_keys/SSL2.cer', + 'no' + ), + ( + 'globalsign_example_keys/SSL3.cer', + 'no' + ), ) @data('self_signed_info') @@ -1484,7 +2596,7 @@ class X509Tests(unittest.TestCase): subject_public_key_algorithm['parameters'].native ) self.assertEqual( - 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, + 23903990516906431865559598284199534387004799030432486061102966678620221767754702651554142956492614440585611990224871381291841413369032752409360196079700921141819811294444393525264295297988924243231844876926173670633422654261873814968313363171188082579071492839040415373948505938897419917635370450127498164824808630475648771544810334682447182123219422360569466851807131368135806769502898151721274383486320505905826683946456552230958810028663378886363555981449715929872558073101554364803925363048965464124465016494920967179276744892632783712377912841537032383450409486298694116013299423220523450956288827030007092359007, # noqa subject_public_key['modulus'].native ) self.assertEqual( @@ -1527,7 +2639,10 @@ class X509Tests(unittest.TestCase): ( 'extn_value', util.OrderedDict([ - ('key_identifier', b'\xBE\x42\x85\x3D\xCC\xFF\xE3\xF9\x28\x02\x8F\x7E\x58\x56\xB4\xFD\x03\x5C\xEA\x4B'), + ( + 'key_identifier', + b'\xBE\x42\x85\x3D\xCC\xFF\xE3\xF9\x28\x02\x8F\x7E\x58\x56\xB4\xFD\x03\x5C\xEA\x4B' + ), ( 'authority_cert_issuer', [ @@ -1628,14 +2743,14 @@ class X509Tests(unittest.TestCase): ) self.assertEqual( util.OrderedDict([ - ('p', 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857), + ('p', 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857), # noqa ('q', 71587850165936478337655415373676526523562874562337607790945426056266440596923), - ('g', 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202), + ('g', 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202), # noqa ]), subject_public_key_algorithm['parameters'].native ) self.assertEqual( - 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, + 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, # noqa subject_public_key.native ) self.assertEqual( @@ -1674,7 +2789,10 @@ class X509Tests(unittest.TestCase): ( 'extn_value', util.OrderedDict([ - ('key_identifier', b'\x81\xA3\x37\x86\xF9\x99\x28\xF2\x74\x70\x60\x87\xF2\xD3\x7E\x8D\x19\x61\xA8\xBE'), + ( + 'key_identifier', + b'\x81\xA3\x37\x86\xF9\x99\x28\xF2\x74\x70\x60\x87\xF2\xD3\x7E\x8D\x19\x61\xA8\xBE' + ), ('authority_cert_issuer', None), ('authority_cert_serial_number', None), ]) @@ -1776,11 +2894,13 @@ class X509Tests(unittest.TestCase): field_id['parameters'].native ) self.assertEqual( - b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC', + b'\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC', curve['a'].native ) self.assertEqual( - b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B', + b'\x5A\xC6\x35\xD8\xAA\x3A\x93\xE7\xB3\xEB\xBD\x55\x76\x98\x86\xBC' + b'\x65\x1D\x06\xB0\xCC\x53\xB0\xF6\x3B\xCE\x3C\x3E\x27\xD2\x60\x4B', curve['b'].native ) self.assertEqual( @@ -1788,7 +2908,10 @@ class X509Tests(unittest.TestCase): curve['seed'].native ) self.assertEqual( - b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40\xF2\x77\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5', + b'\x04\x6B\x17\xD1\xF2\xE1\x2C\x42\x47\xF8\xBC\xE6\xE5\x63\xA4\x40' + b'\xF2\x77\x03\x7D\x81\x2D\xEB\x33\xA0\xF4\xA1\x39\x45\xD8\x98\xC2' + b'\x96\x4F\xE3\x42\xE2\xFE\x1A\x7F\x9B\x8E\xE7\xEB\x4A\x7C\x0F\x9E' + b'\x16\x2B\xCE\x33\x57\x6B\x31\x5E\xCE\xCB\xB6\x40\x68\x37\xBF\x51\xF5', public_key_params['base'].native ) self.assertEqual( @@ -1804,7 +2927,9 @@ class X509Tests(unittest.TestCase): public_key_params['hash'].native ) self.assertEqual( - b'\x04\x8b]Lq\xf7\xd6\xc6\xa3IcB\\G\x9f\xcbs$\x1d\xc9\xdd\xd1-\xf1:\x9f\xb7\x04\xde \xd0X\x00\x93T\xf6\x89\xc7/\x87+\xf7\xf9=;4\xed\x9e{\x0e=WB\xdfx\x03\x0b\xcc1\xc6\x03\xd7\x9f`\x01', + b'\x04\x8b]Lq\xf7\xd6\xc6\xa3IcB\\G\x9f\xcbs$\x1d\xc9\xdd\xd1-\xf1:\x9f' + b'\xb7\x04\xde \xd0X\x00\x93T\xf6\x89\xc7/\x87+\xf7\xf9=;4\xed\x9e{\x0e' + b'=WB\xdfx\x03\x0b\xcc1\xc6\x03\xd7\x9f`\x01', subject_public_key.native ) self.assertEqual( @@ -1843,7 +2968,10 @@ class X509Tests(unittest.TestCase): ( 'extn_value', util.OrderedDict([ - ('key_identifier', b'\x54\xAA\x54\x70\x6C\x34\x1A\x6D\xEB\x5D\x97\xD7\x1E\xFC\xD5\x24\x3C\x8A\x0E\xD7'), + ( + 'key_identifier', + b'\x54\xAA\x54\x70\x6C\x34\x1A\x6D\xEB\x5D\x97\xD7\x1E\xFC\xD5\x24\x3C\x8A\x0E\xD7' + ), ('authority_cert_issuer', None), ('authority_cert_serial_number', None), ]) diff --git a/tests/unittest_data.py b/tests/unittest_data.py index 25dd58c..5ceac54 100644 --- a/tests/unittest_data.py +++ b/tests/unittest_data.py @@ -25,20 +25,20 @@ def data(provider_method, first_param_name_suffix=False): """ def test_func_decorator(test_func): - test_func._provider_method = provider_method #pylint: disable=W0212 - test_func._provider_name_suffix = first_param_name_suffix #pylint: disable=W0212 + test_func._provider_method = provider_method + test_func._provider_name_suffix = first_param_name_suffix return test_func return test_func_decorator -def DataDecorator(cls): +def data_decorator(cls): """ A class decorator that works with the @provider decorator to generate test method from a data provider """ def generate_test_func(name, original_function, num, params): - if original_function._provider_name_suffix: #pylint: disable=W0212 + if original_function._provider_name_suffix: data_name = params[0] params = params[1:] else: @@ -53,7 +53,7 @@ def DataDecorator(cls): func = getattr(cls, name) if hasattr(func, '_provider_method'): num = 1 - for params in getattr(cls, func._provider_method)(): #pylint: disable=W0212 + for params in getattr(cls, func._provider_method)(): generate_test_func(name, func, num, params) num += 1 -- cgit v1.2.3 From 3a487a43806fd0d7a0cab50d7f0433daa9121813 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 7 Oct 2015 12:08:07 -0400 Subject: Flake8 fixes for setup.py --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 493d97b..e9aa388 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,6 @@ from setuptools import setup, find_packages, Command import asn1crypto - class CleanCommand(Command): user_options = [] @@ -35,7 +34,10 @@ setup( name='asn1crypto', version=asn1crypto.__version__, - description='Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSA', + description=( + 'Fast ASN.1 parser and serializer with definitions for private keys, public keys, ' + 'certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSA' + ), long_description='Docs for this project are maintained at https://github.com/wbond/asn1crypto#readme.', url='https://github.com/wbond/asn1crypto', -- cgit v1.2.3 From 5cf77ba993aa0e326c1dd76dc37528d999454eaa Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 8 Oct 2015 09:47:34 -0400 Subject: Import order cleanup --- asn1crypto/_errors.py | 2 +- asn1crypto/_ffi.py | 1 + asn1crypto/_int.py | 8 +++++++- asn1crypto/_ordereddict.py | 2 ++ asn1crypto/_perf/_big_num_ctypes.py | 2 +- asn1crypto/_types.py | 2 +- asn1crypto/algos.py | 9 ++++++++- asn1crypto/core.py | 18 ++++++++++-------- asn1crypto/csr.py | 9 ++++++++- asn1crypto/keys.py | 22 +++++++++++----------- asn1crypto/pdf.py | 7 ++++++- asn1crypto/pem.py | 2 +- asn1crypto/pkcs12.py | 2 +- asn1crypto/tsp.py | 21 +++++++++++++-------- asn1crypto/util.py | 4 +--- asn1crypto/x509.py | 30 ++++++++++++++++++++---------- 16 files changed, 92 insertions(+), 49 deletions(-) diff --git a/asn1crypto/_errors.py b/asn1crypto/_errors.py index a96fcaa..cc785a5 100644 --- a/asn1crypto/_errors.py +++ b/asn1crypto/_errors.py @@ -3,7 +3,7 @@ """ Helper for formatting exception messages. Exports the following items: - - single_line() + - unwrap() """ from __future__ import unicode_literals, division, absolute_import, print_function diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py index d484195..2fc196f 100644 --- a/asn1crypto/_ffi.py +++ b/asn1crypto/_ffi.py @@ -45,6 +45,7 @@ try: return ffi.string(buffer) except (ImportError): + from ctypes import create_string_buffer, create_unicode_buffer, cast, c_void_p def buffer_from_bytes(initializer): diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index 62adc21..a50b667 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -35,7 +35,13 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math -from ._ffi import LibraryNotFoundError, FFIEngineError, buffer_from_bytes, bytes_from_buffer, null +from ._ffi import ( + buffer_from_bytes, + bytes_from_buffer, + FFIEngineError, + LibraryNotFoundError, + null, +) from .util import int_to_bytes, int_from_bytes diff --git a/asn1crypto/_ordereddict.py b/asn1crypto/_ordereddict.py index d4c107b..2f18ab5 100644 --- a/asn1crypto/_ordereddict.py +++ b/asn1crypto/_ordereddict.py @@ -23,9 +23,11 @@ import sys if not sys.version_info < (2, 7): + from collections import OrderedDict else: + from UserDict import DictMixin class OrderedDict(dict, DictMixin): diff --git a/asn1crypto/_perf/_big_num_ctypes.py b/asn1crypto/_perf/_big_num_ctypes.py index 6bd4cb7..dc4d853 100644 --- a/asn1crypto/_perf/_big_num_ctypes.py +++ b/asn1crypto/_perf/_big_num_ctypes.py @@ -21,8 +21,8 @@ interfacing with libcrypto. from __future__ import unicode_literals, division, absolute_import, print_function -from ctypes.util import find_library from ctypes import CDLL, c_int, c_char_p, c_void_p +from ctypes.util import find_library from .._ffi import LibraryNotFoundError, FFIEngineError diff --git a/asn1crypto/_types.py b/asn1crypto/_types.py index 30816c7..a82a9e9 100644 --- a/asn1crypto/_types.py +++ b/asn1crypto/_types.py @@ -1,8 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, division, absolute_import, print_function -import sys import inspect +import sys if sys.version_info < (3,): diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index b90bf30..5c62329 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -19,7 +19,14 @@ 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 .core import Any, Choice, Integer, ObjectIdentifier, OctetString, Sequence +from .core import ( + Any, + Choice, + Integer, + ObjectIdentifier, + OctetString, + Sequence, +) # Structures and OIDs in this file are pulled from diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 754306d..a39278e 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -46,31 +46,33 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function -import math -import sys -import re from datetime import datetime import binascii +import math +import re +import sys from . import _teletex_codec -from ._ordereddict import OrderedDict from ._errors import unwrap +from ._ordereddict import OrderedDict from ._types import type_name, str_cls, byte_cls, int_types from .util import int_to_bytes, int_from_bytes, timezone # Python 2 if sys.version_info <= (3,): + from cStringIO import StringIO as BytesIO + from datetime import timedelta + + range = xrange # noqa py2 = True chr_cls = chr - range = xrange # noqa - from datetime import timedelta - from cStringIO import StringIO as BytesIO # Python 3 else: - py2 = False from io import BytesIO + py2 = False + def chr_cls(num): return bytes([num]) diff --git a/asn1crypto/csr.py b/asn1crypto/csr.py index 511675c..ce6835a 100644 --- a/asn1crypto/csr.py +++ b/asn1crypto/csr.py @@ -12,7 +12,14 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function from .algos import SignedDigestAlgorithm -from .core import Any, Integer, ObjectIdentifier, OctetBitString, Sequence, SetOf +from .core import ( + Any, + Integer, + ObjectIdentifier, + OctetBitString, + Sequence, + SetOf, +) from .keys import PublicKeyInfo from .x509 import DirectoryString, Extensions, Name diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index a44a4fc..81ab0aa 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -19,6 +19,17 @@ 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 .algos import DigestAlgorithm, EncryptionAlgorithm from .core import ( Any, @@ -36,18 +47,7 @@ from .core import ( SequenceOf, SetOf, ) -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 .util import int_from_bytes, int_to_bytes -from ._types import type_name, str_cls, byte_cls class OtherPrimeInfo(Sequence): diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index c83f0ae..3c3a826 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -19,7 +19,12 @@ from .core import ( ) from .crl import CertificateList from .ocsp import OCSPResponse -from .x509 import ExtensionId, Extension, GeneralName, KeyPurposeId +from .x509 import ( + Extension, + ExtensionId, + GeneralName, + KeyPurposeId, +) class AdobeArchiveRevInfo(Sequence): diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py index 2ee0088..8cb6024 100644 --- a/asn1crypto/pem.py +++ b/asn1crypto/pem.py @@ -11,9 +11,9 @@ Encoding DER to PEM and decoding PEM to DER. Exports the following items: from __future__ import unicode_literals, division, absolute_import, print_function -import sys import base64 import re +import sys from ._errors import unwrap from ._types import type_name, str_cls, byte_cls diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 32ca18d..ac08246 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -15,6 +15,7 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function from .algos import DigestInfo +from .cms import ContentInfo, SignedData from .core import ( Any, BMPString, @@ -26,7 +27,6 @@ from .core import ( SequenceOf, SetOf, ) -from .cms import ContentInfo, SignedData from .keys import PrivateKeyInfo, EncryptedPrivateKeyInfo from .x509 import Certificate diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py index 2f8910c..8f2f463 100644 --- a/asn1crypto/tsp.py +++ b/asn1crypto/tsp.py @@ -18,6 +18,13 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function from .algos import DigestAlgorithm +from .cms import ( + CMSAttribute, + CMSAttributeType, + ContentInfo, + ContentType, + EncapsulatedContentInfo, +) from .core import ( Any, BitString, @@ -33,15 +40,13 @@ from .core import ( SetOf, UTF8String, ) -from .cms import ( - CMSAttribute, - CMSAttributeType, - ContentInfo, - ContentType, - EncapsulatedContentInfo, -) from .crl import CertificateList -from .x509 import Attributes, CertificatePolicies, GeneralName, GeneralNames +from .x509 import ( + Attributes, + CertificatePolicies, + GeneralName, + GeneralNames, +) # The structures in this file are based on https://tools.ietf.org/html/rfc3161, diff --git a/asn1crypto/util.py b/asn1crypto/util.py index e83a3bb..750064d 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -14,13 +14,11 @@ from bytes and UTC timezone. Exports the following items: from __future__ import unicode_literals, division, absolute_import, print_function -import sys import math - +import sys from ._ordereddict import OrderedDict # noqa - if sys.platform == 'win32': from ._win._ws2_32 import inet_ntop, inet_pton else: diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 0598106..0987169 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -15,16 +15,19 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function -import sys -import re +from encodings import idna # noqa +import codecs import hashlib +import re +import socket import stringprep +import sys import unicodedata -import socket -from encodings import idna # noqa -import codecs +from ._errors import unwrap from ._ordereddict import OrderedDict +from ._types import type_name, str_cls, byte_cls +from .algos import SignedDigestAlgorithm from .core import ( Any, BitString, @@ -52,20 +55,27 @@ from .core import ( UTF8String, VisibleString, ) -from .algos import SignedDigestAlgorithm from .keys import PublicKeyInfo from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton -from ._errors import unwrap -from ._types import type_name, str_cls, byte_cls if sys.version_info < (3,): from urlparse import urlsplit, urlunsplit - from urllib import quote as urlquote, unquote as unquote_to_bytes + from urllib import ( + quote as urlquote, + unquote as unquote_to_bytes, + ) + bytes_to_list = lambda byte_string: [ord(b) for b in byte_string] else: + from urllib.parse import ( + quote as urlquote, + unquote_to_bytes, + urlsplit, + urlunsplit, + ) + bytes_to_list = list - from urllib.parse import urlsplit, urlunsplit, quote as urlquote, unquote_to_bytes # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 -- cgit v1.2.3 From 6f06aeaee5a5e8f12745a29003b9f551322bfac8 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 8 Oct 2015 09:48:01 -0400 Subject: Lint fix --- asn1crypto/_win/_ws2_32.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/asn1crypto/_win/_ws2_32.py b/asn1crypto/_win/_ws2_32.py index bba48dc..ffef85e 100644 --- a/asn1crypto/_win/_ws2_32.py +++ b/asn1crypto/_win/_ws2_32.py @@ -19,9 +19,8 @@ except (FFIEngineError): from ._ws2_32_ctypes import ws2_32 -class ws2_32_const(): - AF_INET = 2 - AF_INET6 = 23 +AF_INET = 2 +AF_INET6 = 23 def inet_ntop(address_family, packed_ip): @@ -39,11 +38,11 @@ def inet_ntop(address_family, packed_ip): """ family = { - socket.AF_INET: ws2_32_const.AF_INET, - socket.AF_INET6: ws2_32_const.AF_INET6, + socket.AF_INET: AF_INET, + socket.AF_INET6: AF_INET6, }[address_family] - buffer_size = 46 if family == ws2_32_const.AF_INET6 else 16 + buffer_size = 46 if family == AF_INET6 else 16 buffer = unicode_buffer(buffer_size) packed_ip_buffer = buffer_from_bytes(packed_ip) result = ws2_32.InetNtopW(family, cast_void_p(packed_ip_buffer), buffer, buffer_size) @@ -68,11 +67,11 @@ def inet_pton(address_family, ip_string): """ family = { - socket.AF_INET: ws2_32_const.AF_INET, - socket.AF_INET6: ws2_32_const.AF_INET6, + socket.AF_INET: AF_INET, + socket.AF_INET6: AF_INET6, }[address_family] - buffer_size = 16 if family == ws2_32_const.AF_INET6 else 4 + buffer_size = 16 if family == AF_INET6 else 4 buffer = buffer_from_bytes(buffer_size) result = ws2_32.InetPtonW(family, ip_string, buffer) if result != 1: -- cgit v1.2.3 From 0d9d833a344b5ce9d87f82fd173f292b1192b030 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 8 Oct 2015 11:55:40 -0400 Subject: Handle DSA certificates without parameters in keys.PublicKeyInfo.hash_algo --- asn1crypto/keys.py | 9 +++- tests/fixtures/DSAParametersInheritedCACert.crt | Bin 0 -> 546 bytes tests/test_x509.py | 68 ++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/DSAParametersInheritedCACert.crt diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 81ab0aa..2f79fad 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -1052,7 +1052,8 @@ class PublicKeyInfo(Sequence): ValueError - when the key is not a DSA key :return: - A unicode string of "sha1" or "sha2" + A unicode string of "sha1" or "sha2" or None if no parameters are + present """ if self.algorithm != 'dsa': @@ -1064,7 +1065,11 @@ class PublicKeyInfo(Sequence): self.algorithm.upper() )) - byte_len = math.log(self['algorithm']['parameters']['q'].native, 2) / 8 + parameters = self['algorithm']['parameters'] + if parameters.native is None: + return None + + byte_len = math.log(parameters['q'].native, 2) / 8 return 'sha1' if byte_len <= 20 else 'sha2' diff --git a/tests/fixtures/DSAParametersInheritedCACert.crt b/tests/fixtures/DSAParametersInheritedCACert.crt new file mode 100644 index 0000000..5e2fa5b Binary files /dev/null and b/tests/fixtures/DSAParametersInheritedCACert.crt differ diff --git a/tests/test_x509.py b/tests/test_x509.py index 4a5586b..d3539b4 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -2813,6 +2813,74 @@ class X509Tests(unittest.TestCase): extensions.native ) + def test_parse_dsa_certificate_inheritance(self): + cert = self._load_cert('DSAParametersInheritedCACert.crt') + + tbs_certificate = cert['tbs_certificate'] + signature = tbs_certificate['signature'] + issuer = tbs_certificate['issuer'] + validity = tbs_certificate['validity'] + subject = tbs_certificate['subject'] + subject_public_key_info = tbs_certificate['subject_public_key_info'] + subject_public_key_algorithm = subject_public_key_info['algorithm'] + + self.assertEqual( + 'v3', + tbs_certificate['version'].native + ) + self.assertEqual( + 2, + tbs_certificate['serial_number'].native + ) + self.assertEqual( + 'sha1_dsa', + signature['algorithm'].native + ) + self.assertEqual( + None, + signature['parameters'].native + ) + self.assertEqual( + util.OrderedDict([ + ('country_name', 'US'), + ('organization_name', 'Test Certificates 2011'), + ('common_name', 'DSA CA'), + ]), + issuer.native + ) + self.assertEqual( + datetime(2010, 1, 1, 8, 30, tzinfo=util.timezone.utc), + validity['not_before'].native + ) + self.assertEqual( + datetime(2030, 12, 31, 8, 30, tzinfo=util.timezone.utc), + validity['not_after'].native + ) + self.assertEqual( + util.OrderedDict([ + ('country_name', 'US'), + ('organization_name', 'Test Certificates 2011'), + ('common_name', 'DSA Parameters Inherited CA'), + ]), + subject.native + ) + self.assertEqual( + 'dsa', + subject_public_key_algorithm['algorithm'].native + ) + self.assertEqual( + None, + subject_public_key_algorithm['parameters'].native + ) + self.assertEqual( + 'dsa', + subject_public_key_info.algorithm + ) + self.assertEqual( + None, + subject_public_key_info.hash_algo + ) + def test_parse_ec_certificate(self): cert = self._load_cert('keys/test-ec-der.crt') -- cgit v1.2.3 From 20ed8904377eaa39c46896183f8b3a5e5aa2f1ed Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 8 Oct 2015 12:43:04 -0400 Subject: Ensure core.UTCTime interprets <= 49 as 20xx and >= 50 as 19xx per RFC 5280 section 4.1.2.5.1 --- asn1crypto/core.py | 10 ++++++++-- tests/test_core.py | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index a39278e..856a7c2 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3486,11 +3486,17 @@ class UTCTime(AbstractTime): strlen = len(string) + year_num = int(string[0:2]) + if year_num < 50: + prefix = '20' + else: + prefix = '19' + if strlen == 10: - return datetime.strptime(string, '%y%m%d%H%M') + return datetime.strptime(prefix + string, '%Y%m%d%H%M') if strlen == 12: - return datetime.strptime(string, '%y%m%d%H%M%S') + return datetime.strptime(prefix + string, '%Y%m%d%H%M%S') return string diff --git a/tests/test_core.py b/tests/test_core.py index 2cc1de6..7a09cc0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,8 +3,9 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import unittest import os +from datetime import datetime -from asn1crypto import core +from asn1crypto import core, util from .unittest_data import data_decorator, data from ._unittest_compat import patch @@ -129,6 +130,20 @@ class CoreTests(unittest.TestCase): self.assertEqual(der_bytes, i.dump()) self.assertEqual(native, core.Integer.load(der_bytes).native) + @staticmethod + def utctime_info(): + return ( + (datetime(2030, 12, 31, 8, 30, 0, tzinfo=util.timezone.utc), b'\x17\x0D301231083000Z'), + (datetime(2049, 12, 31, 8, 30, 0, tzinfo=util.timezone.utc), b'\x17\x0D491231083000Z'), + (datetime(1950, 12, 31, 8, 30, 0, tzinfo=util.timezone.utc), b'\x17\x0D501231083000Z'), + ) + + @data('utctime_info') + def utctime(self, native, der_bytes): + u = core.UTCTime(native) + self.assertEqual(der_bytes, u.dump()) + self.assertEqual(native, core.UTCTime.load(der_bytes).native) + @staticmethod def type_info(): return ( -- cgit v1.2.3 From 9bcb1793c937bc2ee1e4669d96b2e20a19ce69be Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 00:48:37 -0400 Subject: Added crl.CertificateList.sha256 and .sha1 --- asn1crypto/crl.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index c017aa2..0baf5b4 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -11,6 +11,8 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function +import hashlib + from .algos import SignedDigestAlgorithm from .core import ( Boolean, @@ -300,6 +302,8 @@ class CertificateList(Sequence): _authority_information_access_value = None _issuer_cert_urls = None _delta_crl_distribution_points = None + _sha1 = None + _sha256 = None def _set_extensions(self): """ @@ -499,3 +503,34 @@ class CertificateList(Sequence): self._delta_crl_distribution_points.append(distribution_point) return self._delta_crl_distribution_points + + @property + def signature(self): + """ + :return: + A byte string of the signature + """ + + return self['signature'].native + + @property + def sha1(self): + """ + :return: + The SHA1 hash of the DER-encoded bytes of this certificate list + """ + + if self._sha1 is None: + self._sha1 = hashlib.sha1(self.dump()).digest() + return self._sha1 + + @property + def sha256(self): + """ + :return: + The SHA-256 hash of the DER-encoded bytes of this certificate list + """ + + if self._sha256 is None: + self._sha256 = hashlib.sha256(self.dump()).digest() + return self._sha256 -- cgit v1.2.3 From 21db1218c28c8c489a31648c27724f1fdbcd5725 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 00:49:25 -0400 Subject: Ensure proper RFC 5280 encoding with x509.Name.build() --- asn1crypto/x509.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 0987169..4c8a763 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -948,10 +948,20 @@ class Name(Choice): if attribute_name not in name_dict: continue - if attribute_name in set(['email_address', 'domain_component']): - value = IA5String(name_dict[attribute_name]) + if attribute_name == 'email_address': + value = EmailAddress(name_dict[attribute_name]) + elif attribute_name == 'domain_component': + value = DNSName(name_dict[attribute_name]) + elif attribute_name in set(['dn_qualifier', 'country_name', 'serial_number']): + value = DirectoryString( + name='printable_string', + value=PrintableString(name_dict[attribute_name]) + ) else: - value = DirectoryString(name='utf8_string', value=UTF8String(name_dict[attribute_name])) + value = DirectoryString( + name='utf8_string', + value=UTF8String(name_dict[attribute_name]) + ) attributes.append(NameTypeAndValue({ 'type': attribute_name, -- cgit v1.2.3 From 0c707ddf36e69b48aef1b2fef36194fd19c5cee9 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 01:01:19 -0400 Subject: Version 0.11.1 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 9 +++++++++ readme.md | 7 ++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index aad19c2..39a97cf 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.11.0' -__version_info__ = (0, 11, 0) +__version__ = '0.11.1' +__version_info__ = (0, 11, 1) diff --git a/changelog.md b/changelog.md index a3ef84b..ea83499 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # changelog +## 0.11.1 + + - Corrected `core.UTCTime` to interpret year <= 49 as 20xx and >= 50 as 19xx + - `keys.PublicKeyInfo.hash_algo` can now handle DSA keys without parameters + - Added `crl.CertificateList.sha256` and `crl.CertificateList.sha1` + - Fixed `x509.Name.build()` to properly encode `country_name`, `serial_number` + and `dn_qualifier` as `core.PrintableString` as specified in RFC 5280, + instead of `core.UTF8String` + ## 0.11.0 - Added Python 2.6 support diff --git a/readme.md b/readme.md index 18bd40f..373ff86 100644 --- a/readme.md +++ b/readme.md @@ -34,16 +34,17 @@ a bunch of ASN.1 structures for use with various common cryptography standards: ## Dependencies -Python 2.6, 2.7, 3.2, 3.3, 3.4, pypy or pypy3. *No third-party packages required.* +Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, pypy or pypy3. *No third-party packages +required.* ## Version -0.11.0 - [changelog](changelog.md) +0.11.1 - [changelog](changelog.md) ## Installation ```bash -pip install git+git://github.com/wbond/asn1crypto.git@0.11.0 +pip install asn1crypto ``` ## Documentation -- cgit v1.2.3 From bc3f0f6039ca3447d883d41b4ac955aac72139be Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 01:07:32 -0400 Subject: Added some pypi trove classifiers --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index e9aa388..110fd6b 100644 --- a/setup.py +++ b/setup.py @@ -56,8 +56,13 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: Implementation :: PyPy', + + 'Topic :: Security :: Cryptography', ], keywords='asn1 crypto pki', -- cgit v1.2.3 From 581e2753780f659e075e0ca3deccd3068dee15c0 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 09:38:41 -0400 Subject: Ensure Asn1Value.copy() performs a full deep copy --- asn1crypto/core.py | 94 +++++++++++++++++++++++++++++++++++++++++++----------- tests/test_core.py | 23 +++++++++++++ 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 856a7c2..d77bacb 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -48,6 +48,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi from datetime import datetime import binascii +import copy import math import re import sys @@ -291,9 +292,9 @@ class Asn1Value(object): return '<%s %s %s>' % (type_name(self), id(self), repr(self.contents or b'')) - def copy(self): + def _new_instance(self): """ - Copies the object, preserving any special tagging from it + Constructs a new copy of the current object, preserving any tagging :return: An Asn1Value object @@ -304,9 +305,46 @@ class Asn1Value(object): new_obj.tag = self.tag new_obj.explicit_class = self.explicit_class new_obj.explicit_tag = self.explicit_tag - new_obj._copy(self) return new_obj + def __copy__(self): + """ + Implements the copy.copy() interface + + :return: + A new shallow copy of the current Asn1Value object + """ + + new_obj = self._new_instance() + new_obj._copy(self, copy.copy) + return new_obj + + def __deepcopy__(self, memo): + """ + Implements the copy.deepcopy() interface + + :param memo: + A dict for memoization + + :return: + A new deep copy of the current Asn1Value object + """ + + new_obj = self._new_instance() + memo[id(self)] = new_obj + new_obj._copy(self, copy.deepcopy) + return new_obj + + def copy(self): + """ + Copies the object, preserving any special tagging from it + + :return: + An Asn1Value object + """ + + return copy.deepcopy(self) + def retag(self, tag_type, tag): """ Copies the object, applying a new tagging to it @@ -322,7 +360,7 @@ class Asn1Value(object): """ new_obj = self.__class__(tag_type=tag_type, tag=tag) - new_obj._copy(self) + new_obj._copy(self, copy.deepcopy) return new_obj def untag(self): @@ -334,15 +372,19 @@ class Asn1Value(object): """ new_obj = self.__class__() - new_obj._copy(self) + new_obj._copy(self, copy.deepcopy) return new_obj - def _copy(self, other): + def _copy(self, other, copy_func): """ Copies the contents of another Asn1Value 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 """ if self.__class__ != other.__class__: @@ -355,9 +397,9 @@ class Asn1Value(object): )) self.contents = other.contents - self._native = other._native + self._native = copy_func(other._native) if hasattr(other, '_parsed'): - self._parsed = other._parsed + self._parsed = copy_func(other._parsed) def debug(self, nest_level=1): """ @@ -803,12 +845,16 @@ class Choice(Asn1Value): return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag) - def _copy(self, other): + def _copy(self, other, copy_func): """ Copies the contents of another Asn1Value 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 """ if self.__class__ != other.__class__: @@ -821,10 +867,10 @@ class Choice(Asn1Value): )) self.contents = other.contents - self._native = other._native + self._native = copy_func(other._native) self._choice = other._choice self._name = other._name - self._parsed = other._parsed + self._parsed = copy_func(other._parsed) def dump(self, force=False): """ @@ -1719,12 +1765,16 @@ class ParsableOctetString(Primitive): return self._parsed[0] - def _copy(self, other): + def _copy(self, other, copy_func): """ Copies the contents of another ParsableOctetString 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 """ if self.__class__ != other.__class__: @@ -1737,8 +1787,8 @@ class ParsableOctetString(Primitive): )) self.contents = other.contents - self._native = other._native - self._parsed = other._parsed + self._native = copy_func(other._native) + self._parsed = copy_func(other._parsed) def dump(self, force=False): """ @@ -2735,12 +2785,16 @@ class Sequence(Asn1Value): self._native[name] = child.native return self._native - def _copy(self, other): + def _copy(self, other, copy_func): """ Copies the contents of another Asn1Value 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 """ if self.__class__ != other.__class__: @@ -2753,7 +2807,7 @@ class Sequence(Asn1Value): )) self.contents = other.contents - self._native = other._native + self._native = copy_func(other._native) if self.children is not None: self.children = [] for child in other.children: @@ -3164,12 +3218,16 @@ class SequenceOf(Asn1Value): self._native = [child.native for child in self] return self._native - def _copy(self, other): + def _copy(self, other, copy_func): """ Copies the contents of another Asn1Value 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 """ if self.__class__ != other.__class__: @@ -3182,7 +3240,7 @@ class SequenceOf(Asn1Value): )) self.contents = other.contents - self._native = other._native + self._native = copy_func(other._native) if self.children is not None: self.children = [] for child in other.children: diff --git a/tests/test_core.py b/tests/test_core.py index 7a09cc0..5c8cb53 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -71,6 +71,13 @@ class NumChoice(core.Choice): ] +class SeqChoice(core.Choice): + _alternatives = [ + ('one', CopySeq, {'tag_type': 'explicit', 'tag': 0}), + ('two', CopySeq, {'tag_type': 'implicit', 'tag': 1}), + ] + + @data_decorator class CoreTests(unittest.TestCase): @@ -273,6 +280,8 @@ class CoreTests(unittest.TestCase): def test_copy_mutable(self): a = CopySeq({'name': 'foo', 'pair': {'id': '1.2.3', 'value': 5}}) + # Cache the native representation so it is copied during the copy operation + a.native b = a.copy() self.assertNotEqual(id(a), id(b)) self.assertNotEqual(id(a['pair']), id(b['pair'])) @@ -283,6 +292,9 @@ class CoreTests(unittest.TestCase): a['pair']['value'] = 6 self.assertNotEqual(a['pair']['value'].native, b['pair']['value'].native) + a.native['pair']['value'] = 6 + self.assertNotEqual(a.native['pair']['value'], b.native['pair']['value']) + self.assertNotEqual(a.contents, b.contents) self.assertNotEqual(a.dump(), b.dump()) @@ -309,3 +321,14 @@ class CoreTests(unittest.TestCase): self.assertEqual(correct.dump(), choice.dump()) self.assertEqual(correct.tag_type, choice.chosen.tag_type) self.assertEqual(correct.explicit_tag, choice.chosen.explicit_tag) + + def test_copy_choice_mutate(self): + a = CopySeq({'name': 'foo', 'pair': {'id': '1.2.3', 'value': 5}}) + choice = SeqChoice( + name='one', + value=a + ) + choice.dump() + choice_copy = choice.copy() + choice.chosen['name'] = 'bar' + self.assertNotEqual(choice.chosen['name'], choice_copy.chosen['name']) -- cgit v1.2.3 From 8dff4cd7f70e4e79441680e3a59f5b07644adfc7 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 12:02:36 -0400 Subject: Parsing performance improvements - ~15% on Python 3 - ~5% on Python 2 - No observable difference with pypy --- asn1crypto/core.py | 128 +++++++++++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 58 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d77bacb..63b9167 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2646,7 +2646,7 @@ class Sequence(Asn1Value): field += 1 continue - if field_spec is None or (issubclass(field_spec, Any) and spec_override): + if field_spec is None or (spec_override and issubclass(field_spec, Any)): field_spec = value_spec spec_override = None @@ -3815,45 +3815,6 @@ def _build_id_tuple(params, spec): return (required_class, required_tag) -def _parse_id(encoded_data, pointer): - """ - Peeks ahead into a byte string and parses the ASN.1 header - - :param encoded_data: - A byte string - - :param pointer: - The index in the byte string to parse the header from - - :return: - A 4-element tuple of (class_, method, tag, number_of_bytes_consumed) - """ - - original_pointer = pointer - - first_octet = ord(encoded_data[pointer:pointer + 1]) - pointer += 1 - - class_ = first_octet >> 6 - method = (first_octet >> 5) & 1 - - tag = first_octet & 31 - # Base 128 length using 8th bit as continuation indicator - if tag == 31: - tag = 0 - while True: - num = ord(encoded_data[pointer:pointer + 1]) - pointer += 1 - tag *= 128 - tag += num & 127 - if num >> 7 == 0: - break - - num_bytes = pointer - original_pointer - - return (class_, method, tag, num_bytes) - - def _build(class_, method, tag, header, contents, trailer, spec=None, spec_params=None, nested_spec=None): """ Builds an Asn1Value object generically, or using a spec with optional params @@ -3905,7 +3866,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param else: value = spec(contents=contents) - if isinstance(value, Any): + if spec == Any: pass elif value.tag_type == 'explicit': @@ -4083,23 +4044,41 @@ def _parse(encoded_data, pointer=0): encoded_length = len(encoded_data) - def _slice(start, end): - if end > encoded_length: - raise ValueError(unwrap( - ''' - Insufficient data - %s bytes requested but only %s available - ''', - end, - encoded_length - )) - return encoded_data[start:end] - start = pointer - class_, method, tag, num_bytes = _parse_id(encoded_data, pointer) + id_pointer = pointer + first_octet = ord(encoded_data[id_pointer]) if py2 else encoded_data[id_pointer] + id_pointer += 1 + + class_ = first_octet >> 6 + method = (first_octet >> 5) & 1 + + tag = first_octet & 31 + # Base 128 length using 8th bit as continuation indicator + if tag == 31: + tag = 0 + while True: + num = ord(encoded_data[id_pointer]) if py2 else encoded_data[id_pointer] + id_pointer += 1 + tag *= 128 + tag += num & 127 + if num >> 7 == 0: + break + + num_bytes = id_pointer - start + pointer += num_bytes - length_octet = ord(_slice(pointer, pointer + 1)) + if pointer + 1 > encoded_length: + raise ValueError(unwrap( + ''' + Insufficient data - %s bytes requested but only %s available + ''', + pointer + 1, + encoded_length + )) + length_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] + pointer += 1 length_type = length_octet >> 7 if length_type == 1: @@ -4107,22 +4086,55 @@ def _parse(encoded_data, pointer=0): remaining_length_octets = length_octet & 127 while remaining_length_octets > 0: length *= 256 - length += ord(_slice(pointer, pointer + 1)) + if pointer + 1 > encoded_length: + raise ValueError(unwrap( + ''' + Insufficient data - %s bytes requested but only %s available + ''', + pointer + 1, + encoded_length + )) + length += ord(encoded_data[pointer]) if py2 else encoded_data[pointer] pointer += 1 remaining_length_octets -= 1 else: length = length_octet & 127 - header = _slice(start, pointer) + if pointer > encoded_length: + raise ValueError(unwrap( + ''' + Insufficient data - %s bytes requested but only %s available + ''', + pointer, + encoded_length + )) + header = encoded_data[start:pointer] # Indefinite length if length_type == 1 and length == 0: end_token = encoded_data.find(b'\x00\x00', pointer) - contents = _slice(pointer, end_token) + if end_token > encoded_length: + raise ValueError(unwrap( + ''' + Insufficient data - %s bytes requested but only %s available + ''', + end_token, + encoded_length + )) + contents = encoded_data[pointer:end_token] pointer = end_token + 2 trailer = b'\x00\x00' + else: - contents = _slice(pointer, pointer + length) + if pointer + length > encoded_length: + raise ValueError(unwrap( + ''' + Insufficient data - %s bytes requested but only %s available + ''', + pointer + length, + encoded_length + )) + contents = encoded_data[pointer:pointer + length] pointer += length trailer = b'' -- cgit v1.2.3 From 7c0b483a16c66849db9b3da375edd08d6ee4f46f Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 14:04:14 -0400 Subject: core.Choice.validate() now properly handles explicit tagging --- asn1crypto/core.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 63b9167..d4d43e9 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -790,7 +790,7 @@ class Choice(Asn1Value): return self.chosen.native - def validate(self, class_, tag): + def validate(self, class_, tag, contents): """ Ensures that the class and tag specified exist as an alternative @@ -800,12 +800,29 @@ class Choice(Asn1Value): :param tag: The integer tag from the encoded value header + :param contents: + A byte string of the contents of the value - used when the object + is explicitly tagged + :raises: ValueError - when value is not a valid alternative """ id_ = (class_, tag) + if self.tag_type == 'explicit': + if (self.explicit_class, self.explicit_tag) != id_: + raise ValueError(unwrap( + ''' + %s was explicitly tagged, but the value provided does not + match the class and tag + ''', + type_name(self) + )) + + ((class_, _, tag, _, _, _), _) = _parse(contents) + id_ = (class_, tag) + if id_ in self._id_map: self._choice = self._id_map[id_] return @@ -2553,7 +2570,7 @@ class Sequence(Asn1Value): )) if not isinstance(value, value_spec): wrapper = value_spec() - wrapper.validate(value.class_, value.tag) + wrapper.validate(value.class_, value.tag, value.contents) wrapper._parsed = value new_value = wrapper else: @@ -2633,7 +2650,7 @@ class Sequence(Asn1Value): if issubclass(field_spec, Choice): try: tester = field_spec(**field_params) - tester.validate(*id_) + tester.validate(id_[0], id_[1], parts[4]) choice_match = True except (ValueError): pass @@ -3007,7 +3024,7 @@ class SequenceOf(Asn1Value): )) if not isinstance(value, self._child_spec): wrapper = self._child_spec() - wrapper.validate(value.class_, value.tag) + wrapper.validate(value.class_, value.tag, value.contents) wrapper._parsed = value value = wrapper new_value = value @@ -3902,7 +3919,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param )) elif isinstance(value, Choice): - value.validate(class_, tag) + value.validate(class_, tag, contents) else: if class_ != value.class_: -- cgit v1.2.3 From 43099f43c96c00fe6a5de589e067a4ea52c96a6f Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 20 Oct 2015 14:04:45 -0400 Subject: Add type stack info to core.Sequence.__getitem__() --- asn1crypto/core.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d4d43e9..9276e78 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2321,7 +2321,13 @@ class Sequence(Asn1Value): type_name(self) )) - return self._lazy_child(key) + try: + return self._lazy_child(key) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args + raise e def __setitem__(self, key, value): """ -- cgit v1.2.3 From e47c0002115a628a07c827d1bf725e56d028fb49 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 01:47:27 -0400 Subject: Allow comparing core.NoValue objects --- asn1crypto/core.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 9276e78..ac71cb5 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -483,6 +483,17 @@ class NoValue(Asn1Value): property and .dump() method to be compatible with other value classes. """ + def __eq__(self, other): + """ + :param other: + The other Primitive to compare to + + :return: + A boolean + """ + + return other.__class__ == self.__class__ + def __len__(self): return 0 -- cgit v1.2.3 From babedc6b7491efcec34643d4f03ff106a8fbe00b Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 01:48:14 -0400 Subject: Handle x509.Certificate objects with CRL distribution points that do not have a name --- asn1crypto/x509.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 4c8a763..72e16d7 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -38,6 +38,7 @@ from .core import ( GeneralString, IA5String, Integer, + NoValue, Null, NumericString, ObjectIdentifier, @@ -2221,6 +2222,8 @@ class Certificate(Sequence): for distribution_point in crl_distribution_points: distribution_point_name = distribution_point['distribution_point'] + if distribution_point_name.__class__ == NoValue: + continue # RFC 5280 indicates conforming CA should not use the relative form if distribution_point_name.name == 'name_relative_to_crl_issuer': continue -- cgit v1.2.3 From 9cf04023fba193a602e9b9df9f0a26df0f5bfa1e Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 11:31:30 -0400 Subject: Performance: use a single NoValue object --- asn1crypto/core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ac71cb5..426024c 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -526,6 +526,9 @@ class NoValue(Asn1Value): return b'' +NO_VALUE = NoValue() + + class Any(Asn1Value): """ A value class that can contain any value, and allows for easy parsing of @@ -2435,7 +2438,7 @@ class Sequence(Asn1Value): )) if 'optional' in info[2]: - self.children[key] = NoValue() + self.children[key] = NO_VALUE if self._native is not None: self._native[info[0]] = None else: @@ -2634,7 +2637,7 @@ class Sequence(Asn1Value): if self._contents is None: if self._fields: - self.children = [NoValue()] * len(self._fields) + self.children = [NO_VALUE] * len(self._fields) for index, info in enumerate(self._fields): if len(info) > 2 and 'default' in info[2]: field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) @@ -2674,7 +2677,7 @@ class Sequence(Asn1Value): if not choice_match: if 'optional' in field_params: - self.children.append(NoValue()) + self.children.append(NO_VALUE) else: self.children.append(field_spec(**field_params)) field += 1 @@ -2737,7 +2740,7 @@ class Sequence(Asn1Value): if 'default' in field_params: self.children.append(field_spec(**field_params)) elif 'optional' in field_params: - self.children.append(NoValue()) + self.children.append(NO_VALUE) else: raise ValueError(unwrap( ''' @@ -3407,7 +3410,7 @@ class Set(Sequence): elif 'optional' not in field_info[2] and 'default' not in field_info[2]: missing = True elif 'optional' in field_info[2]: - child_map[index] = NoValue() + child_map[index] = NO_VALUE elif 'default' in field_info[2]: child_map[index] = field_info[1](**field_info[2]) @@ -3891,7 +3894,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param """ if header is None: - return NoValue() + return NO_VALUE # If an explicit specification was passed in, make sure it matches if spec is not None: -- cgit v1.2.3 From b7c1412bfd90e6505df6044fcab0061b2ab5d9c1 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 11:32:51 -0400 Subject: Performance: expand all field definitions to have an empty params dict --- asn1crypto/core.py | 87 +++++++++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 426024c..37e7e2a 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -691,8 +691,10 @@ class Choice(Asn1Value): cls._id_map = {} cls._name_map = {} for index, info in enumerate(cls._alternatives): - params = info[2] if len(info) > 2 else {} - id_ = _build_id_tuple(params, info[1]) + if len(info) < 3: + info = info + ({},) + cls._alternatives[index] = info + id_ = _build_id_tuple(info[2], info[1]) cls._id_map[id_] = index cls._name_map[info[0]] = index @@ -739,9 +741,7 @@ class Choice(Asn1Value): )) self._choice = self._name_map[name] - info = self._alternatives[self._choice] - spec = info[1] - params = {} if len(info) < 3 else info[2] + _, spec, params = self._alternatives[self._choice] if not isinstance(value, spec): value = spec(value, **params) @@ -776,9 +776,8 @@ class Choice(Asn1Value): return self._parsed try: - info = self._alternatives[self._choice] - params = info[2] if len(info) > 2 else {} - self._parsed, _ = _parse_build(self.contents, spec=info[1], spec_params=params) + _, 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 @@ -2426,21 +2425,21 @@ class Sequence(Asn1Value): )) key = self._field_map[key] - info = self._fields[key] - if len(info) < 3 or ('default' not in info[2] and 'optional' not in info[2]): + name, _, params = self._fields[key] + if not params or ('default' not in params and 'optional' not in params): raise ValueError(unwrap( ''' Can not delete the value for the field "%s" of %s since it is not optional or defaulted ''', - info[0], + name, type_name(self) )) - if 'optional' in info[2]: + if 'optional' in params: self.children[key] = NO_VALUE if self._native is not None: - self._native[info[0]] = None + self._native[name] = None else: self.__setitem__(key, None) self._mutated = True @@ -2480,7 +2479,7 @@ class Sequence(Asn1Value): else: child_dump = child.dump(force=force) # Skip values that are the same as the default - if len(info) > 2 and 'default' in info[2]: + if info[2] and 'default' in info[2]: default_value = info[1](**info[2]) if default_value.dump() == child_dump: continue @@ -2500,9 +2499,11 @@ class Sequence(Asn1Value): cls._field_map = {} cls._field_ids = [] for index, field in enumerate(cls._fields): + if len(field) < 3: + field = field + ({},) + cls._fields[index] = field cls._field_map[field[0]] = index - params = field[2] if len(field) > 2 else {} - cls._field_ids.append(_build_id_tuple(params, field[1])) + cls._field_ids.append(_build_id_tuple(field[2], field[1])) if cls._oid_pair is not None: cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) @@ -2523,15 +2524,12 @@ class Sequence(Asn1Value): - None or Asn1Value class indicating the value spec was derived fomr an OID or a spec callback """ - info = self._fields[index] - field_params = info[2] if len(info) > 2 else {} - - field_spec = info[1] + name, field_spec, field_params = self._fields[index] value_spec = field_spec spec_override = None - if self._spec_callbacks is not None and info[0] in self._spec_callbacks: - callback = self._spec_callbacks[info[0]] + if self._spec_callbacks is not None and name in self._spec_callbacks: + callback = self._spec_callbacks[name] spec_override = callback(self) if spec_override: # Allow a spec callback to specify both the base spec and @@ -2550,7 +2548,7 @@ class Sequence(Asn1Value): spec_override = self._oid_specs[oid] value_spec = spec_override - return (info[0], field_spec, value_spec, field_params, spec_override) + return (name, field_spec, value_spec, field_params, spec_override) def _make_value(self, field_name, field_spec, value_spec, field_params, value): """ @@ -2638,8 +2636,8 @@ class Sequence(Asn1Value): if self._contents is None: if self._fields: self.children = [NO_VALUE] * len(self._fields) - for index, info in enumerate(self._fields): - if len(info) > 2 and 'default' in info[2]: + for index, (_, _, params) in enumerate(self._fields): + if 'default' in params: field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) self.children[index] = self._make_value(field_name, field_spec, value_spec, field_params, None) return @@ -2650,10 +2648,11 @@ class Sequence(Asn1Value): child_pointer = 0 field = 0 seen_field = 1 + field_len = len(self._fields) while child_pointer < contents_length: parts, num_bytes = _parse(self._contents, pointer=child_pointer) - if field < len(self._fields): + if field < field_len: _, field_spec, value_spec, field_params, spec_override = self._determine_spec(field) # If the next value is optional or default, allow it to be absent @@ -2693,7 +2692,7 @@ class Sequence(Asn1Value): child = parts + (field_spec, field_params) # Handle situations where an optional or defaulted field definition is incorrect - elif len(self._fields) > 0 and seen_field <= len(self._fields): + elif field_len > 0 and seen_field <= field_len: missed_fields = [] prev_field = field - 1 while prev_field >= 0: @@ -2731,12 +2730,9 @@ class Sequence(Asn1Value): field += 1 seen_field += 1 - total_fields = len(self._fields) index = len(self.children) - while index < total_fields: - field_info = self._fields[index] - field_spec = field_info[1] - field_params = field_info[2] if len(field_info) > 2 else {} + while index < field_len: + name, field_spec, field_params = self._fields[index] if 'default' in field_params: self.children.append(field_spec(**field_params)) elif 'optional' in field_params: @@ -2746,7 +2742,7 @@ class Sequence(Asn1Value): ''' Field "%s" is missing from structure ''', - field_info[0] + name )) index += 1 @@ -3340,9 +3336,11 @@ class Set(Sequence): cls._field_map = {} cls._field_ids = {} for index, field in enumerate(cls._fields): + if len(field) < 3: + field = field + ({},) + cls._fields[index] = field cls._field_map[field[0]] = index - params = field[2] if len(field) > 2 else {} - cls._field_ids[_build_id_tuple(params, field[1])] = index + cls._field_ids[_build_id_tuple(field[2], field[1])] = index if cls._oid_pair is not None: cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) @@ -3370,10 +3368,7 @@ class Set(Sequence): id_ = (parts[0], parts[2]) field = self._field_ids[id_] - field_info = self._fields[field] - field_params = field_info[2] if len(field_info) > 2 else {} - - spec = field_info[1] + _, spec, field_params = self._fields[field] parse_as = None if self._oid_nums is not None and self._oid_nums[1] == field: @@ -3401,25 +3396,25 @@ class Set(Sequence): for index in range(0, total_fields): if index in child_map: continue - field_info = self._fields[index] + name, spec, field_params = self._fields[index] missing = False - if len(field_info) < 3: + if not field_params: missing = True - elif 'optional' not in field_info[2] and 'default' not in field_info[2]: + elif 'optional' not in field_params and 'default' not in field_params: missing = True - elif 'optional' in field_info[2]: + elif 'optional' in field_params: child_map[index] = NO_VALUE - elif 'default' in field_info[2]: - child_map[index] = field_info[1](**field_info[2]) + elif 'default' in field_params: + child_map[index] = spec(**field_params) if missing: raise ValueError(unwrap( ''' Missing required field "%s" from %s ''', - field_info[0], + name, type_name(self) )) -- cgit v1.2.3 From a2a81badbb45439b090f676abdd63b35fe79df5a Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 11:33:19 -0400 Subject: Performance: reduce isinstance() checks in _build() --- asn1crypto/core.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 37e7e2a..b954c8d 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3898,6 +3898,8 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param else: value = spec(contents=contents) + is_choice = isinstance(value, Choice) + if spec == Any: pass @@ -3933,7 +3935,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param tag )) - elif isinstance(value, Choice): + elif is_choice: value.validate(class_, tag, contents) else: @@ -3971,6 +3973,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # anyway a little further on elif spec_params and 'tag_type' in spec_params and spec_params['tag_type'] == 'explicit': value = Asn1Value(contents=contents, **spec_params) + is_choice = False # If no spec was specified, allow anything and just process what # is in the input data @@ -4018,6 +4021,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param spec = universal_specs[tag] value = spec(contents=contents, class_=class_) + is_choice = False value._header = header if trailer is not None and trailer != b'': @@ -4036,13 +4040,13 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class value.explicit_tag = original_value.explicit_tag - elif isinstance(value, Choice): + elif is_choice: value.contents = value._header + value.contents value._header = b'' try: # Force parsing the Choice now - if isinstance(value, Choice): + if is_choice: value.parse() if nested_spec: -- cgit v1.2.3 From cbbc80b73bce92853aafbc87f06cfbb57d68e204 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 11:33:58 -0400 Subject: Performance: reduce assignments in _parse() --- asn1crypto/core.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index b954c8d..04fdba8 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -4082,9 +4082,8 @@ def _parse(encoded_data, pointer=0): start = pointer - id_pointer = pointer - first_octet = ord(encoded_data[id_pointer]) if py2 else encoded_data[id_pointer] - id_pointer += 1 + first_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] + pointer += 1 class_ = first_octet >> 6 method = (first_octet >> 5) & 1 @@ -4094,17 +4093,13 @@ def _parse(encoded_data, pointer=0): if tag == 31: tag = 0 while True: - num = ord(encoded_data[id_pointer]) if py2 else encoded_data[id_pointer] - id_pointer += 1 + num = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] + pointer += 1 tag *= 128 tag += num & 127 if num >> 7 == 0: break - num_bytes = id_pointer - start - - pointer += num_bytes - if pointer + 1 > encoded_length: raise ValueError(unwrap( ''' @@ -4174,9 +4169,7 @@ def _parse(encoded_data, pointer=0): pointer += length trailer = b'' - num_bytes = pointer - start - - return ((class_, method, tag, header, contents, trailer), num_bytes) + return ((class_, method, tag, header, contents, trailer), pointer - start) def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None): -- cgit v1.2.3 From 093f9867650d164db4de32981bd2cfc3098ea922 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 11:54:37 -0400 Subject: Renamed core.NoValue() to core.Void(), document singleton core.VOID --- asn1crypto/core.py | 23 ++++++++++++----------- asn1crypto/x509.py | 4 ++-- docs/universal_types.md | 6 +++--- tests/test_x509.py | 12 ++++++------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 04fdba8..37c4b39 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -20,7 +20,6 @@ ASN.1 type classes for universal types. Exports the following items: - Integer() - IntegerBitString() - IntegerOctetString() - - NoValue() - Null() - NumericString() - ObjectDescriptor() @@ -40,6 +39,8 @@ ASN.1 type classes for universal types. Exports the following items: - UTF8String() - VideotexString() - VisibleString() + - VOID + - Void() Other type classes are defined that help compose the types listed above. """ @@ -477,7 +478,7 @@ class ValueMap(): cls._reverse_map[value] = key -class NoValue(Asn1Value): +class Void(Asn1Value): """ A representation of an optional value that is not present. Has .native property and .dump() method to be compatible with other value classes. @@ -526,7 +527,7 @@ class NoValue(Asn1Value): return b'' -NO_VALUE = NoValue() +VOID = Void() class Any(Asn1Value): @@ -2224,7 +2225,7 @@ class Sequence(Asn1Value): # been set for the field, then skip it if check_existing: index = self._field_map[key] - if index < len(self.children) and not isinstance(self.children[index], NoValue): + if index < len(self.children) and self.children[index] is not VOID: continue if key in value: @@ -2437,7 +2438,7 @@ class Sequence(Asn1Value): )) if 'optional' in params: - self.children[key] = NO_VALUE + self.children[key] = VOID if self._native is not None: self._native[name] = None else: @@ -2635,7 +2636,7 @@ class Sequence(Asn1Value): if self._contents is None: if self._fields: - self.children = [NO_VALUE] * len(self._fields) + self.children = [VOID] * len(self._fields) for index, (_, _, params) in enumerate(self._fields): if 'default' in params: field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) @@ -2676,7 +2677,7 @@ class Sequence(Asn1Value): if not choice_match: if 'optional' in field_params: - self.children.append(NO_VALUE) + self.children.append(VOID) else: self.children.append(field_spec(**field_params)) field += 1 @@ -2736,7 +2737,7 @@ class Sequence(Asn1Value): if 'default' in field_params: self.children.append(field_spec(**field_params)) elif 'optional' in field_params: - self.children.append(NO_VALUE) + self.children.append(VOID) else: raise ValueError(unwrap( ''' @@ -2861,7 +2862,7 @@ class Sequence(Asn1Value): _basic_debug(prefix, self) for field_name in self: child = self._lazy_child(self._field_map[field_name]) - if not isinstance(child, NoValue): + if child is not VOID: print('%s Field "%s"' % (prefix, field_name)) child.debug(nest_level + 3) @@ -3405,7 +3406,7 @@ class Set(Sequence): elif 'optional' not in field_params and 'default' not in field_params: missing = True elif 'optional' in field_params: - child_map[index] = NO_VALUE + child_map[index] = VOID elif 'default' in field_params: child_map[index] = spec(**field_params) @@ -3889,7 +3890,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param """ if header is None: - return NO_VALUE + return VOID # If an explicit specification was passed in, make sure it matches if spec is not None: diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 72e16d7..c670f56 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -38,7 +38,6 @@ from .core import ( GeneralString, IA5String, Integer, - NoValue, Null, NumericString, ObjectIdentifier, @@ -55,6 +54,7 @@ from .core import ( UTCTime, UTF8String, VisibleString, + VOID, ) from .keys import PublicKeyInfo from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton @@ -2222,7 +2222,7 @@ class Certificate(Sequence): for distribution_point in crl_distribution_points: distribution_point_name = distribution_point['distribution_point'] - if distribution_point_name.__class__ == NoValue: + if distribution_point_name is VOID: continue # RFC 5280 indicates conforming CA should not use the relative form if distribution_point_name.name == 'name_relative_to_crl_issuer': diff --git a/docs/universal_types.md b/docs/universal_types.md index b03c4a0..e89fcf4 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -219,9 +219,9 @@ way to construct the value out of a native Python data type. ### Optional Fields When a field is configured via the `optional` parameter, not present in the -`Sequence`, but accessed, an instance of the `NoValue` class will be returned. -This class is serialized to an empty byte string and returns `None` when -`.native` is accessed. +`Sequence`, but accessed, the `VOID` object will be returned. This is an object +that is serialized to an empty byte string and returns `None` when `.native` is +accessed. ## Set diff --git a/tests/test_x509.py b/tests/test_x509.py index d3539b4..aebda27 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -2609,7 +2609,7 @@ class X509Tests(unittest.TestCase): ) self.assertIsInstance( tbs_certificate['issuer_unique_id'], - core.NoValue + core.Void ) self.assertEqual( None, @@ -2617,7 +2617,7 @@ class X509Tests(unittest.TestCase): ) self.assertIsInstance( tbs_certificate['subject_unique_id'], - core.NoValue + core.Void ) self.maxDiff = None @@ -2759,7 +2759,7 @@ class X509Tests(unittest.TestCase): ) self.assertIsInstance( tbs_certificate['issuer_unique_id'], - core.NoValue + core.Void ) self.assertEqual( None, @@ -2767,7 +2767,7 @@ class X509Tests(unittest.TestCase): ) self.assertIsInstance( tbs_certificate['subject_unique_id'], - core.NoValue + core.Void ) self.maxDiff = None @@ -3006,7 +3006,7 @@ class X509Tests(unittest.TestCase): ) self.assertIsInstance( tbs_certificate['issuer_unique_id'], - core.NoValue + core.Void ) self.assertEqual( None, @@ -3014,7 +3014,7 @@ class X509Tests(unittest.TestCase): ) self.assertIsInstance( tbs_certificate['subject_unique_id'], - core.NoValue + core.Void ) self.maxDiff = None -- cgit v1.2.3 From 2eba0bd1a7673271feda8f449548521d65dcebaf Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 23:32:29 -0400 Subject: Implement core.Void.__nonzero__() --- asn1crypto/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 37c4b39..fff0c7a 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -495,6 +495,9 @@ class Void(Asn1Value): return other.__class__ == self.__class__ + def __nonzero__(self): + return False + def __len__(self): return 0 -- cgit v1.2.3 From ebd4dc77d6f07c3c06d34764f0156fe0c896f745 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 22 Oct 2015 23:32:58 -0400 Subject: Add .__contains__() to core.SequenceOf and core.SetOf --- asn1crypto/core.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index fff0c7a..6e3b3c0 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3149,6 +3149,35 @@ class SequenceOf(Asn1Value): for index in range(0, len(self.children)): yield self._lazy_child(index) + def __contains__(self, item): + """ + :param item: + An object of the type cls._child_spec + + :return: + A boolean if the item is contained in this SequenceOf + """ + + if item is None or item is VOID: + return False + + if not isinstance(item, self._child_spec): + raise TypeError(unwrap( + ''' + Checking membership in %s is only available for instances of + %s, not %s + ''', + type_name(self), + type_name(self._child_spec), + type_name(item) + )) + + for child in self: + if child == item: + return True + + return False + def append(self, value): """ Allows adding a child to the end of the sequence -- cgit v1.2.3 From 8fbe3f7993f2ea9c4eadc693868ad7dd4e41b2b4 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 23 Oct 2015 11:18:20 -0400 Subject: Added readmes for various fixtures subdirs --- tests/fixtures/geotrust_certs/readme.md | 1 + tests/fixtures/globalsign_example_keys/readme.md | 2 ++ tests/fixtures/keys/readme.md | 2 ++ tests/fixtures/lets_encrypt/readme.md | 1 + 4 files changed, 6 insertions(+) create mode 100644 tests/fixtures/geotrust_certs/readme.md create mode 100644 tests/fixtures/globalsign_example_keys/readme.md create mode 100644 tests/fixtures/keys/readme.md create mode 100644 tests/fixtures/lets_encrypt/readme.md diff --git a/tests/fixtures/geotrust_certs/readme.md b/tests/fixtures/geotrust_certs/readme.md new file mode 100644 index 0000000..37eeede --- /dev/null +++ b/tests/fixtures/geotrust_certs/readme.md @@ -0,0 +1 @@ +Example EV certificate and chain. diff --git a/tests/fixtures/globalsign_example_keys/readme.md b/tests/fixtures/globalsign_example_keys/readme.md new file mode 100644 index 0000000..8f98684 --- /dev/null +++ b/tests/fixtures/globalsign_example_keys/readme.md @@ -0,0 +1,2 @@ +Valid and invalid certificates using name constraints. From +https://cabforum.org/pipermail/public/2013-July/001839.html. diff --git a/tests/fixtures/keys/readme.md b/tests/fixtures/keys/readme.md new file mode 100644 index 0000000..956bf0b --- /dev/null +++ b/tests/fixtures/keys/readme.md @@ -0,0 +1,2 @@ +Certificates generated by hand with OpenSSL for testing various algorithms +and encodings diff --git a/tests/fixtures/lets_encrypt/readme.md b/tests/fixtures/lets_encrypt/readme.md new file mode 100644 index 0000000..ff9f749 --- /dev/null +++ b/tests/fixtures/lets_encrypt/readme.md @@ -0,0 +1 @@ +Certificates from https://letsencrypt.org/ -- cgit v1.2.3 From db495793ef54c6bdd07b11564025eb48a938d30c Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 23 Oct 2015 11:19:08 -0400 Subject: Added some X.509 examples from chromium --- tests/fixtures/chromium/ndn.ca.crt | 35 +++++++++++++++++++ tests/fixtures/chromium/punycodetest.pem | 19 ++++++++++ tests/fixtures/chromium/readme.md | 2 ++ .../chromium/subjectAltName_sanity_check.pem | 23 +++++++++++++ tests/test_x509.py | 40 ++++++++++++++++++++++ 5 files changed, 119 insertions(+) create mode 100644 tests/fixtures/chromium/ndn.ca.crt create mode 100644 tests/fixtures/chromium/punycodetest.pem create mode 100644 tests/fixtures/chromium/readme.md create mode 100644 tests/fixtures/chromium/subjectAltName_sanity_check.pem diff --git a/tests/fixtures/chromium/ndn.ca.crt b/tests/fixtures/chromium/ndn.ca.crt new file mode 100644 index 0000000..6da9fb2 --- /dev/null +++ b/tests/fixtures/chromium/ndn.ca.crt @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIGBjCCA+4CCQDbt8YGR683ojANBgkqhkiG9w0BAQUFADCBxDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMR8w +HQYDVQQKExZOZXcgRHJlYW0gTmV0d29yaywgTExDMREwDwYDVQQLEwhTZWN1cml0 +eTEwMC4GA1UEAxMnTmV3IERyZWFtIE5ldHdvcmsgQ2VydGlmaWNhdGUgQXV0aG9y +aXR5MSQwIgYJKoZIhvcNAQkBFhVzdXBwb3J0QGRyZWFtaG9zdC5jb20wHhcNMDYw +ODIyMjExMjU4WhcNMTYwODE5MjExMjU4WjCBxDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMR8wHQYDVQQKExZO +ZXcgRHJlYW0gTmV0d29yaywgTExDMREwDwYDVQQLEwhTZWN1cml0eTEwMC4GA1UE +AxMnTmV3IERyZWFtIE5ldHdvcmsgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MSQwIgYJ +KoZIhvcNAQkBFhVzdXBwb3J0QGRyZWFtaG9zdC5jb20wggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDHykL2vz70nbj827iARXxgBK41yXUy7oBcoG5TA+MD +GjXVqV+szQWgQz6C6uPPp+O4cUVxnAIfXvCo7VjsYYsDcmCWAlV27ZRN3Ek7HvIv +4rHaht0y/E/DgKOZiiy7kJMvqDZSuD9NxSlIOTDBS6aDyORNZKubWdg0l+Axk3Yn +YhmVhPtqupO8HzQjR0s2wgwKdz3m96RqjJDBng0wLEv/iCGN0ogBBV1TePuKcodF +TQivkjvVcITRVWIBKNfg0uDeM+cHIYGp44WM3gBX0W3AaG9JH/1tYWmgVrk/cmwK +oMq3PKVr/Usp7nR3PTyn1rpcJjPZDSNOleO+KilI4vbwFgnydbm97eTf/BFy548j +SPi9HINufNDSajXCRvyQ04CQVpAzH2mPVcykoL6++M2u2nNO5tLWa2ix96RNPrV/ +K00KbmIODPoqRVmoL7UvD4w2AM26l7Ol20cLqU8G4bZGe1DoKpF1CxwQlBzY6iJN +PiEY+eAII54w7cnHdAp2mOKvJBVUzCROc7g4W0n3/JCHEaXDnfXlqevCckAaDbes +AM7w9epOd7rUWpbSxOuKsgIyXvhM9VwxI7L5TdgULqKXq57jMAK8irsOalEt+5pJ +8RGFDOgJsbR+eJ3En//11OJzgz1bdW0lSUD72hoNdLyJtP7dn8+FrIfGBw9mwWMG +MwIDAQABMA0GCSqGSIb3DQEBBQUAA4ICAQAVUa0xn7/bDpyxdqJrr3np5HKDKaL6 +CL4KNgBS1hKHLIqxwKGTO+nCm1sVgK9fIh7hF5sG7f5hc5ZPp9usOeqwewB5drYZ +jPyk2e5WX0fjdB2pFPxhPS8vzNJRBtpPnUoiOuwXI4K12V7xj3SC/S4tqNfBM/p9 +sk15XfWiS8rMqDGg6oMFeM4BZ7YaZOXY/upqofg+MEbVdLBngwUPI/a1rR8OYUBr +NhsSZpXwg22IwDE45M7wxgRvj0hojRAAxVS75ogitx040tf2Lp8DJgtSGvOHtuW9 +87MXd8Ev8xf/7d9EXw4ghwKvyWglrw0Bpoh9OP9DOwoRFIzdBz5aUmAx6PNIvZ0Y +xQ+QRUxj+Kx2Xl6hpsk8URsfxKDHR2sZwcSqjD+JJGJee26An7btAst1/grAYw5X +tEfcXyXiDBHXPY6t0NZOzfQUggwIU0fiWhvT43Skob6pCc0dUBeEhFK0dpyDE3AM +4LwF/ECGfPn/UzzB/Lax4WaY2yGccBrikRfIkZARrJbvBCAxchH+HCeBV0B0BlOi +2R0RxwH0gKBEHxhMaq5J/rTaE7V5wd8OsxzcxUgW+te6hc0bbuhPiYhtvQ0N7+oF +z89JuuwocsTbNekeBwHB5wpsCPA29mYId8uvVl7LThvut9+szsWetmVua7P9t10h +GX3cdJCMZUwypA== +-----END CERTIFICATE----- diff --git a/tests/fixtures/chromium/punycodetest.pem b/tests/fixtures/chromium/punycodetest.pem new file mode 100644 index 0000000..58a3e51 --- /dev/null +++ b/tests/fixtures/chromium/punycodetest.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGzCCAgOgAwIBAgIJAPNKm+KwszDJMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV +BAMMEnhuLS13Z3Y3MWExMTllLmNvbTAeFw0xNDA4MjUyMzM1NTJaFw0yNDA4MjIy +MzM1NTJaMB0xGzAZBgNVBAMMEnhuLS13Z3Y3MWExMTllLmNvbTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAM3+IZdIMy92S+iPB0BBD2QPdymlha2GROuN +KSO1tVa0CfxZNf/Q3/4B9eXGxu6OM0X9/abUreB7z/1ajYnAh7y5XN0ZFtR9G0jA +KdZ//8dPIuk1Mu5+da2Z5q5z1vrb7Lp7E7JtXvsxY2p/gJtvqwL/3Y6o3fd092ce +51MHsrJVqGvfJFqxijXxW+qjKP7XU0RQxrSLLchtcUpDVwuBZoJl9ttLMkDpQzrg +qvV8Yth6fiHJD9qQESwUFmot/Y7zz0rtb0UjywRWlemekc+Tv+9sVxzAZDd2ejhd +lEwdeehjpIDqUfYoN9JvPHC5lzJ5whiUZvMZ86wPoC+gZp3TDjcCAwEAAaNeMFww +DwYDVR0TAQH/BAUwAwEB/zBJBgNVHREEQjBAghJ4bi0td2d2NzFhMTE5ZS5jb22C +FCoueG4tLXdndjcxYTExOWUuY29tghRibGFoYmxhaGJsYWhibGFoLmNvbTANBgkq +hkiG9w0BAQsFAAOCAQEAfSlwzuxgmTCCxLrDzHL4O7/jXA1FeiPVhanbqfT8EQ1Z +1T7wsKbbgt2IYdEXaqmnZLAL/bbW1bpNg3LIobVbo/sXeBSGWAktNR6oGXUlVTjl +jXQqxf0RlUNBp9gbULdaIDmOvQYoStbTKD0dRsxkmwseEshkGrW4b5lGeMrjwR8M +cms44n+GhxQLE9GbZFDUweY2XsBmKT3kYXvQ4pSPYNaRSjE28OZwdZ8C89aQahIQ +qDUOq3Cnxjoh58LstEIftUPrk7hg7pOjF6rjIyo0dJ2xkXiCj2uFbpSdAE9lhd7J +MOIibmB6xph+j3Y/LBgJS5GVU2dmwEQengqhwXGHng== +-----END CERTIFICATE----- diff --git a/tests/fixtures/chromium/readme.md b/tests/fixtures/chromium/readme.md new file mode 100644 index 0000000..54ff4f7 --- /dev/null +++ b/tests/fixtures/chromium/readme.md @@ -0,0 +1,2 @@ +Various certificates from +https://chromium.googlesource.com/chromium/src.git/+/master/net/data/ssl/certificates/. diff --git a/tests/fixtures/chromium/subjectAltName_sanity_check.pem b/tests/fixtures/chromium/subjectAltName_sanity_check.pem new file mode 100644 index 0000000..83dbaeb --- /dev/null +++ b/tests/fixtures/chromium/subjectAltName_sanity_check.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDyDCCArCgAwIBAgIJAITNX/V3KEBxMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV +BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBW +aWV3MRAwDgYDVQQKDAdUZXN0IENBMRIwEAYDVQQDDAkxMjcuMC4wLjEwHhcNMTQw +ODE5MjI1NTA2WhcNMjQwODE2MjI1NTA2WjBgMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwH +VGVzdCBDQTESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAvcYPQtxlkgsHAGCaTrCE8PH6C2WMxkXtvM6vpFxPeG8wTJdC +ZsytZkoRSrWKV/BYpFQf+ayCZ/h8VTbXwLFaBvolwgc2ZKdUwmJnLREwd+82O9kv +miwucSvXY/ca/em4yEqh0rnU3AbKaFLij2nvK6R7Hkzb5VKyF7bn9v2REfJPCNWd +f7YLHY//F15vK+97bqMsvKnQw/TxJmVKIx51Hq7Wb4htCqTCljTg1CbyLdMWJqM8 +rBq0vu/AhGqVTnHGvuPEFVVelMOoA0A0IXZIVh7kJgXAR2dEWQRHsjjgLWkR3ppr +GH9XVm98bNr/psXbsFaigd6uykugs2Yd95ZBMwIDAQABo4GEMIGBMA8GA1UdEwEB +/wQFMAMBAf8wbgYDVR0RBGcwZYcEfwAAAocQ/oAAAAAAAAAAAAAAAAAAAYIMdGVz +dC5leGFtcGxlgRF0ZXN0QHRlc3QuZXhhbXBsZaASBgMqAwSgCwwJaWdub3JlIG1l +pBYwFDESMBAGA1UEAwwJMTI3LjAuMC4zMA0GCSqGSIb3DQEBCwUAA4IBAQCcuKQm +iG8bvpNNP4nSy7i5eh62sGaK33mhN8O6khcZu07TSiHieMMnbErUEeC/kDi6mjz4 +VNGsVjJPsdFsuZK/mKXQVZH813R0Mz354vKXCjJbBeQu+UGmLCzcZ1iBZ7WEw/KU +nc4rgM5I7AgqAXhM9YwDK4cGHe01UwnH5QavO/psQqW+Ht3O+My/DsU0kXxMtS8H +tK92vLMixuvXsEcA5InvjP6++G4VH/ms3rc+3MLgZXzQi12QDmg+UStcdPD9ahRt +R6RF6OW6vqy4pa8PwKyZtwxw0rRTSlueOyEduq2ssQp5U8Od2dYfp9NfmZo13YUd +jq336kKdqfNHQxET +-----END CERTIFICATE----- diff --git a/tests/test_x509.py b/tests/test_x509.py index aebda27..a6036a7 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -357,6 +357,46 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(general_name_1, general_name_2) + def test_v1_cert(self): + cert = self._load_cert('chromium/ndn.ca.crt') + tbs_cert = cert['tbs_certificate'] + self.assertEqual('v1', tbs_cert['version'].native) + self.assertEqual(15832340745319036834, tbs_cert['serial_number'].native) + self.assertEqual( + 'Email Address: support@dreamhost.com; Common Name: New Dream Network Certificate Authority; ' + 'Organizational Unit: Security; Organization: New Dream Network, LLC; Locality: Los Angeles; ' + 'State/Province: California; Country: US', + tbs_cert['issuer'].human_friendly + ) + self.assertEqual( + 'Email Address: support@dreamhost.com; Common Name: New Dream Network Certificate Authority; ' + 'Organizational Unit: Security; Organization: New Dream Network, LLC; Locality: Los Angeles; ' + 'State/Province: California; Country: US', + tbs_cert['subject'].human_friendly + ) + + def test_subject_alt_name_variations(self): + cert = self._load_cert('chromium/subjectAltName_sanity_check.pem') + alt_names = cert.subject_alt_name_value + for general_name in alt_names: + self.assertIsInstance(general_name, x509.GeneralName) + self.assertIsInstance(alt_names[0].chosen, x509.IPAddress) + self.assertEqual(alt_names[0].chosen.native, '127.0.0.2') + self.assertIsInstance(alt_names[1].chosen, x509.IPAddress) + self.assertEqual(alt_names[1].chosen.native, 'fe80::1') + self.assertIsInstance(alt_names[2].chosen, x509.DNSName) + self.assertEqual(alt_names[2].chosen.native, 'test.example') + self.assertIsInstance(alt_names[3].chosen, x509.EmailAddress) + self.assertEqual(alt_names[3].chosen.native, 'test@test.example') + self.assertIsInstance(alt_names[4].chosen, x509.AnotherName) + self.assertEqual(alt_names[4].chosen.native, util.OrderedDict([('type_id', '1.2.3.4'), ('value', 'ignore me')])) + self.assertIsInstance(alt_names[5].chosen, x509.Name) + self.assertEqual(alt_names[5].chosen.native, util.OrderedDict([('common_name', '127.0.0.3')])) + + def test_punycode_common_name(self): + cert = self._load_cert('chromium/punycodetest.pem') + self.assertEqual('xn--wgv71a119e.com', cert['tbs_certificate']['subject'].native['common_name']) + @staticmethod def signature_algo_info(): return ( -- cgit v1.2.3 From 3298a822501fe02a340e0a182450fab3910592f1 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 24 Oct 2015 13:10:38 -0400 Subject: Added x509.Name.__len__() --- asn1crypto/x509.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index c670f56..6fc7853 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -982,6 +982,9 @@ class Name(Choice): return self.chosen.hashable + def __len__(self): + return len(self.chosen) + def __ne__(self, other): return not self == other -- cgit v1.2.3 From c0d290e4b8b43b409f51e2a3dcd538aa384901fc Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 24 Oct 2015 13:11:04 -0400 Subject: Added a link to csrbuilder --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 373ff86..4d7d7e7 100644 --- a/readme.md +++ b/readme.md @@ -24,6 +24,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: - [asn1crypto](https://github.com/wbond/asn1crypto) - [oscrypto](https://github.com/wbond/oscrypto) + - [csrbuilder](https://github.com/wbond/csrbuilder) - [certbuilder](https://github.com/wbond/certbuilder) - [crlbuilder](https://github.com/wbond/crlbuilder) -- cgit v1.2.3 From 74513021813325397ff29c268dee3a75dc2bad2e Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 24 Oct 2015 20:29:49 -0400 Subject: Typo in core.Choice error message --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 6e3b3c0..8e0b0d0 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -868,7 +868,7 @@ class Choice(Asn1Value): ''', asn1, type_name(self), - '. '.join(asn1s) + ', '.join(asn1s) )) def _format_class_tag(self, class_, tag): -- cgit v1.2.3 From a8f896aa65cb2d017181637341451c395d05350f Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 25 Oct 2015 23:08:06 -0400 Subject: Fix a bug copying Asn1Value object .class_ attributes --- asn1crypto/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8e0b0d0..ec785e7 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -303,6 +303,7 @@ class Asn1Value(object): new_obj = self.__class__() new_obj.tag_type = self.tag_type + new_obj.class_ = self.class_ new_obj.tag = self.tag new_obj.explicit_class = self.explicit_class new_obj.explicit_tag = self.explicit_tag -- cgit v1.2.3 From 4fbef30f5def608ff2fe0f52812a21a0391831d9 Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 25 Oct 2015 23:08:34 -0400 Subject: Fix calling the classmethod .load() on a core.Choice class --- asn1crypto/core.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ec785e7..bf7401c 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -687,6 +687,21 @@ class Choice(Asn1Value): # A dict that maps alternative names to an index in _alternatives _name_map = None + @classmethod + def load(cls, encoded_data, **kwargs): + """ + Loads a BER/DER-encoded byte string using the current class as the spec + + :param encoded_data: + A byte string of BER or DER encoded data + + :return: + A instance of the current class + """ + + value, _ = _parse_build(encoded_data, spec=cls, spec_params=kwargs) + return value + def _setup(self): """ Generates _id_map from _alternatives to allow validating contents -- cgit v1.2.3 From b8800e89f1ad22c522f0842d83c06d494cbf27a0 Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 25 Oct 2015 23:23:03 -0400 Subject: Fixed TSA -> TSP in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 110fd6b..60f52dd 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( description=( 'Fast ASN.1 parser and serializer with definitions for private keys, public keys, ' - 'certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSA' + 'certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP' ), long_description='Docs for this project are maintained at https://github.com/wbond/asn1crypto#readme.', -- cgit v1.2.3 From c6e1143d67dc56f386a355de67957a27cf18c040 Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 25 Oct 2015 23:23:36 -0400 Subject: Version 0.12.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 17 +++++++++++++++++ readme.md | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 39a97cf..f87131d 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.11.1' -__version_info__ = (0, 11, 1) +__version__ = '0.12.0' +__version_info__ = (0, 12, 0) diff --git a/changelog.md b/changelog.md index ea83499..fcf8c08 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,22 @@ # changelog +## 0.12.0 + + - Backwards Compatiblity Break: `core.NoValue` was renamed to `core.Void` and + a singleton was added as `core.VOID` + - 20-30% improvement in parsing performance + - `core.Void` now implements `__nonzero__` + - `core.Asn1Value.copy()` now performs a deep copy + - All `core` value classes are now compatible with the `copy` module + - `core.SequenceOf` and `core.SetOf` now implement `__contains__` + - Added `x509.Name.__len__()` + - Fixed a bug where `core.Choice.validate()` would not properly account for + explicit tagging + - `core.Choice.load()` now properly passes itself as the spec when parsing + - `x509.Certificate.crl_distribution_points` no longer throws an exception if + the `DistributionPoint` does not have a value for the `distribution_point` + field + ## 0.11.1 - Corrected `core.UTCTime` to interpret year <= 49 as 20xx and >= 50 as 19xx diff --git a/readme.md b/readme.md index 4d7d7e7..115d13f 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ required.* ## Version -0.11.1 - [changelog](changelog.md) +0.12.0 - [changelog](changelog.md) ## Installation -- cgit v1.2.3 From df88dca0ca84bd97b27988648d8669075d8fbd36 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 26 Oct 2015 10:24:22 -0400 Subject: Fix a bug on Python 2 where x509.URI.dump() would fail due to mixing bytes and unicode --- asn1crypto/x509.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 6fc7853..1a72943 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -179,7 +179,10 @@ class URI(IA5String): if path is None: path = '' - return urlunsplit((scheme, netloc, path, query, fragment)) + output = urlunsplit((scheme, netloc, path, query, fragment)) + if isinstance(output, str_cls): + output = output.encode('latin1') + return output def __unicode__(self): """ -- cgit v1.2.3 From b5c80a89b62e457be8ca35ba2cf7d7025e1448a0 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 26 Oct 2015 10:27:51 -0400 Subject: Version 0.12.1 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 4 ++++ readme.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index f87131d..7bf2703 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.12.0' -__version_info__ = (0, 12, 0) +__version__ = '0.12.1' +__version_info__ = (0, 12, 1) diff --git a/changelog.md b/changelog.md index fcf8c08..5e27e67 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # changelog +## 0.12.1 + + - Fixed a `unicode`/`bytes` bug with `x509.URI.dump()` on Python 2 + ## 0.12.0 - Backwards Compatiblity Break: `core.NoValue` was renamed to `core.Void` and diff --git a/readme.md b/readme.md index 115d13f..497f2e5 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ required.* ## Version -0.12.0 - [changelog](changelog.md) +0.12.1 - [changelog](changelog.md) ## Installation -- cgit v1.2.3 From 1bdf79a6a2e02a84f8b36a390644ca27bc5ec10b Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 26 Oct 2015 12:21:28 -0400 Subject: When setting None to an optional field in core.Sequence, construct core.VOID --- asn1crypto/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index bf7401c..bbd8138 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -485,6 +485,8 @@ class Void(Asn1Value): property and .dump() method to be compatible with other value classes. """ + contents = b'' + def __eq__(self, other): """ :param other: @@ -2593,6 +2595,9 @@ class Sequence(Asn1Value): An instance of a child class of Asn1Value """ + if value is None and 'optional' in field_params: + return VOID + specs_different = field_spec != value_spec is_any = issubclass(field_spec, Any) -- cgit v1.2.3 From 9d1da4d30c8399923d1b413e24f44c0a57fe909a Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 27 Oct 2015 00:03:27 -0400 Subject: Version 0.12.2 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 5 +++++ readme.md | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 7bf2703..1b3bf11 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.12.1' -__version_info__ = (0, 12, 1) +__version__ = '0.12.2' +__version_info__ = (0, 12, 2) diff --git a/changelog.md b/changelog.md index 5e27e67..26974d2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # changelog +## 0.12.2 + + - Correct `core.Sequence.__setitem__()` so set `core.VOID` to an optional + field when `None` is set + ## 0.12.1 - Fixed a `unicode`/`bytes` bug with `x509.URI.dump()` on Python 2 diff --git a/readme.md b/readme.md index 497f2e5..42ca3b3 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ required.* ## Version -0.12.1 - [changelog](changelog.md) +0.12.2 - [changelog](changelog.md) ## Installation -- cgit v1.2.3 From 76ee263891eb723ff44856f1fcc774af0e0f8179 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 27 Oct 2015 01:03:09 -0400 Subject: Add a link to ocspbuilder --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 42ca3b3..424631b 100644 --- a/readme.md +++ b/readme.md @@ -27,6 +27,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: - [csrbuilder](https://github.com/wbond/csrbuilder) - [certbuilder](https://github.com/wbond/certbuilder) - [crlbuilder](https://github.com/wbond/crlbuilder) + - [ocspbuilder](https://github.com/wbond/ocspbuilder) ## License -- cgit v1.2.3 From fbdd581e4d50eec277bce4092038f2df5d67ee33 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 30 Oct 2015 19:59:23 -0400 Subject: Add ocsp.OCSPResponse.basic_ocsp_response and ocsp.OCSPResponse.response_data --- asn1crypto/ocsp.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index cd2c5c9..bcdf1eb 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -625,3 +625,25 @@ class OCSPResponse(Sequence): if self._processed_extensions is False: self._set_extensions() return self._extended_revoke_value + + @property + def basic_ocsp_response(self): + """ + A shortcut into the BasicOCSPResponse sequence + + :return: + None or an asn1crypto.ocsp.BasicOCSPResponse object + """ + + return self['response_bytes']['response'].parsed + + @property + def response_data(self): + """ + A shortcut into the parsed, ResponseData sequence + + :return: + None or an asn1crypto.ocsp.ResponseData object + """ + + return self['response_bytes']['response'].parsed['tbs_response_data'] -- cgit v1.2.3 From aeaebcc45e4c1cae95fce16a286efa0b546979e4 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 30 Oct 2015 20:02:28 -0400 Subject: Update x509.Name.human_friendly to handle multiple values per x509.NameTypeAndValue type --- asn1crypto/x509.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 1a72943..e93df05 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1043,7 +1043,16 @@ class Name(Choice): for key in data: value = data[key] if isinstance(value, list): - value = ', '.join([sub_value.native for sub_value in value]) + native_sub_values = [] + for sub_value in value: + if isinstance(sub_value, list): + native_sub_value = ', '.join( + reversed([sub_sub_value.native for sub_sub_value in sub_value]) + ) + else: + native_sub_value = sub_value.native + native_sub_values.append(native_sub_value) + value = ', '.join(reversed(native_sub_values)) else: value = value.native to_join.append('%s: %s' % (key, value)) -- cgit v1.2.3 From fda656d4e08d6209cb9ccfe8aaf1533f5db2364a Mon Sep 17 00:00:00 2001 From: eukaryote Date: Mon, 2 Nov 2015 10:04:37 -0800 Subject: Support AES Key Wrap for CMS Key Encryption Adds KeyEncryptionAlgorithmId oids for aes key wrap (rfc3394) and aes key wrap with padding (rfc5649). --- asn1crypto/cms.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 2fbb646..252703e 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -419,7 +419,13 @@ class RecipientIdentifier(Choice): class KeyEncryptionAlgorithmId(ObjectIdentifier): _map = { - '1.2.840.113549.1.1.1': 'rsa' + '1.2.840.113549.1.1.1': 'rsa', + '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', + '2.16.840.1.101.3.4.1.28': 'aes192_wrap_pad', + '2.16.840.1.101.3.4.1.45': 'aes256_wrap', + '2.16.840.1.101.3.4.1.48': 'aes256_wrap_pad', } -- cgit v1.2.3 From a7bbfc9e9cc68f06a99883edac26852a6f544ea5 Mon Sep 17 00:00:00 2001 From: eukaryote Date: Mon, 2 Nov 2015 10:19:01 -0800 Subject: EncryptionAlgorithmId block mode changes/additions The OID mappings for aes128, aes192, and aes256 are actually for AES in CBC mode, with other modes having different identifiers. I added mappings for the other modes described in the referenced NIST doc, and changed the three existing ones to map to aes128_cbc, aes192_cbc, and aes256_cbc so that they're consistent with the newly added mappings --- asn1crypto/algos.py | 28 +++++++++++++++++++++++++--- tests/test_cms.py | 2 +- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 5c62329..7c0799c 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -405,9 +405,31 @@ class EncryptionAlgorithmId(ObjectIdentifier): '1.2.840.113549.3.7': 'tripledes_3key', '1.2.840.113549.3.2': 'rc2', '1.2.840.113549.3.9': 'rc5', - '2.16.840.1.101.3.4.1.2': 'aes128', - '2.16.840.1.101.3.4.1.22': 'aes192', - '2.16.840.1.101.3.4.1.42': 'aes256', + # 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', + '2.16.840.1.101.3.4.1.2': 'aes128_cbc', + '2.16.840.1.101.3.4.1.3': 'aes128_ofb', + '2.16.840.1.101.3.4.1.4': 'aes128_cfb', + '2.16.840.1.101.3.4.1.5': 'aes128_wrap', + '2.16.840.1.101.3.4.1.6': 'aes128_gcm', + '2.16.840.1.101.3.4.1.7': 'aes128_ccm', + '2.16.840.1.101.3.4.1.8': 'aes128_wrap_pad', + '2.16.840.1.101.3.4.1.21': 'aes192_ecb', + '2.16.840.1.101.3.4.1.22': 'aes192_cbc', + '2.16.840.1.101.3.4.1.23': 'aes192_ofb', + '2.16.840.1.101.3.4.1.24': 'aes192_cfb', + '2.16.840.1.101.3.4.1.25': 'aes192_wrap', + '2.16.840.1.101.3.4.1.26': 'aes192_gcm', + '2.16.840.1.101.3.4.1.27': 'aes192_ccm', + '2.16.840.1.101.3.4.1.28': 'aes192_wrap_pad', + '2.16.840.1.101.3.4.1.41': 'aes256_ecb', + '2.16.840.1.101.3.4.1.42': 'aes256_cbc', + '2.16.840.1.101.3.4.1.43': 'aes256_ofb', + '2.16.840.1.101.3.4.1.44': 'aes256_cfb', + '2.16.840.1.101.3.4.1.45': 'aes256_wrap', + '2.16.840.1.101.3.4.1.46': 'aes256_gcm', + '2.16.840.1.101.3.4.1.47': 'aes256_ccm', + '2.16.840.1.101.3.4.1.48': 'aes256_wrap_pad', # From PKCS#5 '1.2.840.113549.1.5.13': 'pbes2', '1.2.840.113549.1.5.1': 'pbes1_md2_des', diff --git a/tests/test_cms.py b/tests/test_cms.py index 6adb15d..adc97b3 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -121,7 +121,7 @@ class CMSTests(unittest.TestCase): encrypted_content_info['content_type'].native ) self.assertEqual( - 'aes128', + 'aes128_cbc', encrypted_content_info['content_encryption_algorithm']['algorithm'].native ) self.assertEqual( -- cgit v1.2.3 From 277d76d3699ca93084c3e25965b6a7dd965e68ea Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 2 Nov 2015 17:16:46 -0500 Subject: Updated algos.EncryptionAlgorithm property methods and specs for new AES OID format --- asn1crypto/algos.py | 92 ++++++++++++++++++++++++++++++++++++++++------------- tests/test_cms.py | 32 +++++++++++++++++++ 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 7c0799c..49f192a 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -463,9 +463,12 @@ class EncryptionAlgorithm(Sequence): 'tripledes_3key': OctetString, 'rc2': Rc2Params, 'rc5': Rc5Params, - 'aes128': OctetString, - 'aes192': OctetString, - 'aes256': OctetString, + 'aes128_cbc': OctetString, + 'aes192_cbc': OctetString, + 'aes256_cbc': OctetString, + 'aes128_ofb': OctetString, + 'aes192_ofb': OctetString, + 'aes256_ofb': OctetString, # From PKCS#5 'pbes1_md2_des': Pbes1Params, 'pbes1_md5_des': Pbes1Params, @@ -658,12 +661,16 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native + if encryption_algo[0:3] == 'aes': + return { + 'aes128_': 16, + 'aes192_': 24, + 'aes256_': 32, + }[encryption_algo[0:7]] + cipher_lengths = { 'des': 8, 'tripledes_3key': 24, - 'aes128': 16, - 'aes192': 24, - 'aes256': 32, } if encryption_algo in cipher_lengths: @@ -732,6 +739,40 @@ class EncryptionAlgorithm(Sequence): encryption_algo )) + @property + def encryption_mode(self): + """ + Returns the name of the encryption mode to use. + + :return: + A unicode string from one of the following: "cbc", "ecb", "ofb", + "cfb", "wrap", "gcm", "ccm", "wrap_pad" + """ + + encryption_algo = self['algorithm'].native + + if encryption_algo[0:7] in set(['aes128_', 'aes192_', 'aes256_']): + return encryption_algo[7:] + + if encryption_algo[0:6] == 'pbes1_': + return 'cbc' + + if encryption_algo[0:7] == 'pkcs12_': + return 'cbc' + + if encryption_algo in set(['des', 'tripledes_3key', 'rc2', 'rc5']): + return 'cbc' + + if encryption_algo == 'pbes2': + return self['parameters']['encryption_scheme'].encryption_mode + + raise ValueError(unwrap( + ''' + Unrecognized encryption algorithm "%s" + ''', + encryption_algo + )) + @property def encryption_cipher(self): """ @@ -746,17 +787,14 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native - cipher_map = { - 'des': 'des', - 'tripledes_3key': 'tripledes', - 'aes128': 'aes', - 'aes192': 'aes', - 'aes256': 'aes', - 'rc2': 'rc2', - 'rc5': 'rc5', - } - if encryption_algo in cipher_map: - return cipher_map[encryption_algo] + if encryption_algo[0:7] in set(['aes128_', 'aes192_', 'aes256_']): + return 'aes' + + if encryption_algo in set(['des', 'rc2', 'rc5']): + return encryption_algo + + if encryption_algo == 'tripledes_3key': + return 'tripledes' if encryption_algo == 'pbes2': return self['parameters']['encryption_scheme'].encryption_cipher @@ -795,12 +833,12 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native + if encryption_algo[0:7] in set(['aes128_', 'aes192_', 'aes256_']): + return 16 + cipher_map = { 'des': 8, 'tripledes_3key': 8, - 'aes128': 16, - 'aes192': 16, - 'aes256': 16, 'rc2': 8, } if encryption_algo in cipher_map: @@ -848,11 +886,21 @@ class EncryptionAlgorithm(Sequence): encryption_algo = self['algorithm'].native - if encryption_algo in ('rc2', 'rc5'): + if encryption_algo in set(['rc2', 'rc5']): return self['parameters'].parsed['iv'].native # For DES/Triple DES and AES the IV is the entirety of the parameters - if encryption_algo in ('des', 'tripledes_3key', 'aes128', 'aes192', 'aes256'): + octet_string_iv_oids = set([ + 'des', + 'tripledes_3key', + 'aes128_cbc', + 'aes192_cbc', + 'aes256_cbc', + 'aes128_ofb', + 'aes192_ofb', + 'aes256_ofb', + ]) + if encryption_algo in octet_string_iv_oids: return self['parameters'].native if encryption_algo == 'pbes2': diff --git a/tests/test_cms.py b/tests/test_cms.py index adc97b3..19df0bc 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -124,6 +124,22 @@ class CMSTests(unittest.TestCase): 'aes128_cbc', encrypted_content_info['content_encryption_algorithm']['algorithm'].native ) + self.assertEqual( + 'aes', + encrypted_content_info['content_encryption_algorithm'].encryption_cipher + ) + self.assertEqual( + 'cbc', + encrypted_content_info['content_encryption_algorithm'].encryption_mode + ) + self.assertEqual( + 16, + encrypted_content_info['content_encryption_algorithm'].key_length + ) + self.assertEqual( + 16, + encrypted_content_info['content_encryption_algorithm'].encryption_block_size + ) self.assertEqual( b'\x1F\x34\x54\x9F\x7F\xB7\x06\xBD\x81\x57\x68\x84\x79\xB5\x2F\x6F', encrypted_content_info['content_encryption_algorithm']['parameters'].native @@ -214,6 +230,22 @@ class CMSTests(unittest.TestCase): 'tripledes_3key', encrypted_content_info['content_encryption_algorithm']['algorithm'].native ) + self.assertEqual( + 'tripledes', + encrypted_content_info['content_encryption_algorithm'].encryption_cipher + ) + self.assertEqual( + 'cbc', + encrypted_content_info['content_encryption_algorithm'].encryption_mode + ) + self.assertEqual( + 24, + encrypted_content_info['content_encryption_algorithm'].key_length + ) + self.assertEqual( + 8, + encrypted_content_info['content_encryption_algorithm'].encryption_block_size + ) self.assertEqual( b'\x52\x50\x98\xFA\x33\x88\xC7\x3C', encrypted_content_info['content_encryption_algorithm']['parameters'].native -- cgit v1.2.3 From 801ce8636654d8efab71b079c46765e66e0359f1 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 4 Nov 2015 23:47:34 -0500 Subject: Add "ci" task and Travis CI config --- .travis.yml | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dev/coverage.py | 12 +++++++++-- dev/lint.py | 15 +++++++++++-- dev/tests.py | 14 +++++++++++- run.py | 20 ++++++++++++++--- 5 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6c870ed --- /dev/null +++ b/.travis.yml @@ -0,0 +1,67 @@ +sudo: false +language: c +matrix: + include: + - os: osx + python: "2.7" + env: TRAVIS_PYTHON_VERSION=2.7 + - os: osx + python: "3.5" + env: TRAVIS_PYTHON_VERSION=3.5 + - os: osx + python: "pypy" + env: TRAVIS_PYTHON_VERSION=pypy + - os: linux + language: python + python: "2.6" + - os: linux + language: python + python: "2.7" + - os: linux + language: python + python: "3.2" + - os: linux + language: python + python: "3.3" + - os: linux + language: python + python: "3.4" + - os: linux + language: python + python: "3.5" + - os: linux + language: python + python: "pypy" + - os: linux + language: python + python: "pypy3" +install: + - if [ "$TRAVIS_OS_NAME" == "osx" ]; then + if [ "$TRAVIS_PYTHON_VERSION" == "3.5" ]; then + brew install python3; + /usr/local/bin/pip3 install flake8; + else + if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then + brew install pypy; + /usr/local/bin/pip_pypy install flake8; + else + sudo /usr/bin/easy_install-2.7 flake8; + fi + fi + else + pip install flake8; + fi +script: + - if [ "$TRAVIS_OS_NAME" == "osx" ]; then + if [ "$TRAVIS_PYTHON_VERSION" == "3.5" ]; then + /usr/local/bin/python3 run.py ci; + else + if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then + /usr/local/bin/pypy run.py ci; + else + /usr/bin/python2.7 run.py ci; + fi + fi + else + python run.py ci; + fi diff --git a/dev/coverage.py b/dev/coverage.py index 8961f6f..8f836c5 100644 --- a/dev/coverage.py +++ b/dev/coverage.py @@ -4,16 +4,24 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import coverage - def run(): + """ + Runs the tests while measuring coverage + + :return: + A bool - if the tests ran successfully + """ + cov = coverage.Coverage(include='asn1crypto/*.py') cov.start() from .tests import run as run_tests - run_tests() + result = run_tests() print() cov.stop() cov.save() cov.report(show_missing=False) + + return result diff --git a/dev/lint.py b/dev/lint.py index ec35573..2ac8159 100644 --- a/dev/lint.py +++ b/dev/lint.py @@ -11,7 +11,14 @@ config_file = os.path.join(cur_dir, '..', '.pep8') def run(): - print('Running flake8...') + """ + Runs flake8 lint + + :return: + A bool - if flake8 did not find any errors + """ + + print('Running flake8') flake8_style = get_style_guide(config_file=config_file) @@ -21,4 +28,8 @@ def run(): if not filename.endswith('.py'): continue paths.append(os.path.join(root, filename)) - flake8_style.check_files(paths) + report = flake8_style.check_files(paths) + success = report.total_errors == 0 + if success: + print('OK') + return success diff --git a/dev/tests.py b/dev/tests.py index 553e27f..b9c54b5 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -19,6 +19,17 @@ test_classes = [CMSTests, CRLTests, CSRTests, KeysTests, OCSPTests, PEMTests, TS def run(matcher=None): + """ + Runs the tests + + :param matcher: + A unicode string containing a regular expression to use to filter test + names by. A value of None will cause no filtering. + + :return: + A bool - if the tests succeeded + """ + suite = unittest.TestSuite() loader = unittest.TestLoader() for test_class in test_classes: @@ -30,4 +41,5 @@ def run(matcher=None): else: suite.addTest(loader.loadTestsFromTestCase(test_class)) verbosity = 2 if matcher else 1 - unittest.TextTestRunner(verbosity=verbosity).run(suite) + result = unittest.TextTestRunner(verbosity=verbosity).run(suite) + return result.wasSuccessful() diff --git a/run.py b/run.py index 65f6be6..41c93b9 100644 --- a/run.py +++ b/run.py @@ -11,7 +11,7 @@ else: def show_usage(): - print('Usage: run.py (lint | tests [regex] | coverage)', file=sys.stderr) + print('Usage: run.py (lint | tests [regex] | coverage | ci)', file=sys.stderr) sys.exit(1) @@ -29,7 +29,7 @@ if len(sys.argv) < 2 or len(sys.argv) > 3: task = get_arg(1) -if task not in ('lint', 'tests', 'coverage'): +if task not in set(['lint', 'tests', 'coverage', 'ci']): show_usage() if task != 'tests' and len(sys.argv) == 3: @@ -48,4 +48,18 @@ elif task == 'tests': elif task == 'coverage': from dev.coverage import run -run(*params) +elif task == 'ci': + from dev.tests import run as run_tests + from dev.lint import run as run_lint + + def run(): + print('Python ' + sys.version.replace('\n', '')) + print('') + lint_result = run_lint() + print('\nRunning tests') + tests_result = run_tests() + + return lint_result and tests_result + +result = run(*params) +sys.exit(int(not result)) -- cgit v1.2.3 From 3e5fa468eb1bc43e9f742fcf100eff07f31f6ef0 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 5 Nov 2015 02:28:05 -0500 Subject: Add explicit appveyor.yml --- appveyor.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..365a90b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,5 @@ +install: +- cmd: C:\Python27\Scripts\pip.exe install flake8 +build: off +test_script: +- cmd: python run.py ci -- cgit v1.2.3 From 413f237d7517091674889651685406ccf7014ff8 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 5 Nov 2015 09:00:20 -0500 Subject: Run AppVeyor on multiple versions of Python --- appveyor.yml | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- dev/ci.py | 18 +++++++++++++++++ readme.md | 5 +++++ run.py | 12 +---------- 4 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 dev/ci.py diff --git a/appveyor.yml b/appveyor.yml index 365a90b..0976bac 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,67 @@ +version: "{build}" +environment: + matrix: + - PYTHON: "C:\\Python26" + PYTHON_ID: "26" + PYTHON_EXE: python + - PYTHON: "C:\\Python26-x64" + PYTHON_ID: "26-x64" + PYTHON_EXE: python + - PYTHON: "C:\\Python27" + PYTHON_ID: "27" + PYTHON_EXE: python + - PYTHON: "C:\\Python27-x64" + PYTHON_ID: "27-x64" + PYTHON_EXE: python + - PYTHON: "C:\\Python33" + PYTHON_ID: "33" + PYTHON_EXE: python + - PYTHON: "C:\\Python33-x64" + PYTHON_ID: "33-x64" + PYTHON_EXE: python + - PYTHON: "C:\\pypy-4.0.0-win32" + PYTHON_ID: "pypy" + PYTHON_EXE: pypy install: -- cmd: C:\Python27\Scripts\pip.exe install flake8 + - ps: + $env:PYTMP = "${env:TMP}\py"; + if (!(Test-Path "$env:PYTMP")) { + New-Item -ItemType directory -Path "$env:PYTMP" | Out-Null; + } + + if ("${env:PYTHON_ID}" -eq "26") { + if (!(Test-Path "${env:PYTMP}\python-2.6.6.msi")) { + (New-Object Net.WebClient).DownloadFile('https://www.python.org/ftp/python/2.6.6/python-2.6.6.msi', "${env:PYTMP}\python-2.6.6.msi"); + } + Start-Process -FilePath msiexec.exe -ArgumentList ('/qn', '/i', "${env:PYTMP}\python-2.6.6.msi", "TARGETDIR=${env:PYTHON}") -Wait; + } elseif ("${env:PYTHON_ID}" -eq "26-x64") { + if (!(Test-Path "${env:PYTMP}\python-2.6.6.amd64.msi")) { + (New-Object Net.WebClient).DownloadFile('https://www.python.org/ftp/python/2.6.6/python-2.6.6.amd64.msi', "${env:PYTMP}\python-2.6.6.amd64.msi"); + } + Start-Process -FilePath msiexec.exe -ArgumentList ('/qn', '/i', "${env:PYTMP}\python-2.6.6.amd64.msi", "TARGETDIR=${env:PYTHON}") -Wait; + } elseif ("${env:PYTHON_ID}" -eq "pypy") { + if (!(Test-Path "${env:PYTMP}\pypy-4.0.0-win32.zip")) { + (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy-4.0.0-win32.zip', "${env:PYTMP}\pypy-4.0.0-win32.zip"); + } + 7z x -y "${env:PYTMP}\pypy-4.0.0-win32.zip" -oC:\ | Out-Null; + } + + if (!(Test-Path "${env:PYTMP}\get-pip.py")) { + (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); + } + + if ("${env:PYTHON_ID}" -eq "pypy") { + & "${env:PYTHON}\pypy.exe" "${env:PYTMP}\get-pip.py"; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8; + } elseif ("${env:PYTHON_ID}" -eq "26" -or "${env:PYTHON_ID}" -eq "26-x64") { + & "${env:PYTHON}\python.exe" -W ignore "${env:PYTMP}\get-pip.py"; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8'])"; + } else { + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8; + } + - "SET PATH=%PYTHON%;%PATH%" +cache: + - '%TMP%\py\' build: off test_script: -- cmd: python run.py ci + - cmd: "%PYTHON_EXE% run.py ci" diff --git a/dev/ci.py b/dev/ci.py new file mode 100644 index 0000000..d2144ba --- /dev/null +++ b/dev/ci.py @@ -0,0 +1,18 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +from .tests import run as run_tests +from .lint import run as run_lint + + +def run(): + print('Python ' + sys.version.replace('\n', '')) + print('') + lint_result = run_lint() + print('\nRunning tests') + sys.stdout.flush() + tests_result = run_tests() + + return lint_result and tests_result diff --git a/readme.md b/readme.md index 424631b..0dad563 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,11 @@ required.* 0.12.2 - [changelog](changelog.md) +## Continuous Integration + + - [Windows](https://ci.appveyor.com/project/wbond/asn1crypto/history) via AppVeyor + - [OS X & Linux](https://travis-ci.org/wbond/package_control_channel/builds) via Travis CI + ## Installation ```bash diff --git a/run.py b/run.py index 41c93b9..61d68a2 100644 --- a/run.py +++ b/run.py @@ -49,17 +49,7 @@ elif task == 'coverage': from dev.coverage import run elif task == 'ci': - from dev.tests import run as run_tests - from dev.lint import run as run_lint - - def run(): - print('Python ' + sys.version.replace('\n', '')) - print('') - lint_result = run_lint() - print('\nRunning tests') - tests_result = run_tests() - - return lint_result and tests_result + from dev.ci import run result = run(*params) sys.exit(int(not result)) -- cgit v1.2.3 From d3bbc0e7a4545fda2248b7099fa9eb06967e8c3d Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 5 Nov 2015 13:17:02 -0500 Subject: Add OS X/Python 2.6 to Travis CI matrix --- .travis.yml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c870ed..d5aab8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ sudo: false language: c matrix: include: + - os: osx + python: "2.6" + env: TRAVIS_PYTHON_VERSION=2.6 - os: osx python: "2.7" env: TRAVIS_PYTHON_VERSION=2.7 @@ -37,31 +40,28 @@ matrix: python: "pypy3" install: - if [ "$TRAVIS_OS_NAME" == "osx" ]; then - if [ "$TRAVIS_PYTHON_VERSION" == "3.5" ]; then - brew install python3; - /usr/local/bin/pip3 install flake8; + if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then + brew install pypy; + /usr/local/bin/pip_pypy install flake8; + export PYTHON_BIN=/usr/local/bin/pypy; else - if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then - brew install pypy; - /usr/local/bin/pip_pypy install flake8; + if [ "$TRAVIS_PYTHON_VERSION" == "3.5" ]; then + brew install python3; + /usr/local/bin/pip3 install flake8; + export PYTHON_BIN=/usr/local/bin/python3; else - sudo /usr/bin/easy_install-2.7 flake8; + if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then + sudo /usr/bin/easy_install-2.7 flake8; + export PYTHON_BIN=/usr/bin/python2.7; + else + sudo /usr/bin/easy_install-2.6 flake8; + export PYTHON_BIN=/usr/bin/python2.6; + fi fi fi else pip install flake8; + export PYTHON_BIN=python; fi script: - - if [ "$TRAVIS_OS_NAME" == "osx" ]; then - if [ "$TRAVIS_PYTHON_VERSION" == "3.5" ]; then - /usr/local/bin/python3 run.py ci; - else - if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then - /usr/local/bin/pypy run.py ci; - else - /usr/bin/python2.7 run.py ci; - fi - fi - else - python run.py ci; - fi + - $PYTHON_BIN run.py ci -- cgit v1.2.3 From af89af5cf461afd3d553aecab5988362c26e0e3c Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 5 Nov 2015 13:31:07 -0500 Subject: Fix link to Travis CI in readme --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0dad563..9f3d715 100644 --- a/readme.md +++ b/readme.md @@ -46,7 +46,7 @@ required.* ## Continuous Integration - [Windows](https://ci.appveyor.com/project/wbond/asn1crypto/history) via AppVeyor - - [OS X & Linux](https://travis-ci.org/wbond/package_control_channel/builds) via Travis CI + - [OS X & Linux](https://travis-ci.org/wbond/asn1crypto/builds) via Travis CI ## Installation -- cgit v1.2.3 From 0054ee774f5ff4f8d08b728e17c0a94e25c4e18b Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 5 Nov 2015 16:08:47 -0500 Subject: Fix a bug in Python 3 related to parsing dates with timezone offsets --- asn1crypto/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index bbd8138..8ac68aa 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -47,7 +47,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 +from datetime import datetime, timedelta import binascii import copy import math @@ -63,7 +63,6 @@ from .util import int_to_bytes, int_from_bytes, timezone # Python 2 if sys.version_info <= (3,): from cStringIO import StringIO as BytesIO - from datetime import timedelta range = xrange # noqa py2 = True -- cgit v1.2.3 From 532ee835ac82de4cd2c39949b4e062ef91a8fce8 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 5 Nov 2015 16:09:26 -0500 Subject: Allow attributes field of csr.CertificationRequestInfo() to be optional --- asn1crypto/csr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/csr.py b/asn1crypto/csr.py index ce6835a..051f510 100644 --- a/asn1crypto/csr.py +++ b/asn1crypto/csr.py @@ -84,7 +84,7 @@ class CertificationRequestInfo(Sequence): ('version', Version), ('subject', Name), ('subject_pk_info', PublicKeyInfo), - ('attributes', CRIAttributes, {'tag_type': 'implicit', 'tag': 0}), + ('attributes', CRIAttributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), ] -- cgit v1.2.3 From 796cff11c44b50442411d76f2a1897d14c3d5ad1 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 9 Nov 2015 17:26:31 -0500 Subject: Use Python 2.6 from new version of AppVeyor workers --- appveyor.yml | 25 ++++++------------------- dev/ci.py | 7 +++++++ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 0976bac..25b9ae4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -29,33 +29,20 @@ install: New-Item -ItemType directory -Path "$env:PYTMP" | Out-Null; } - if ("${env:PYTHON_ID}" -eq "26") { - if (!(Test-Path "${env:PYTMP}\python-2.6.6.msi")) { - (New-Object Net.WebClient).DownloadFile('https://www.python.org/ftp/python/2.6.6/python-2.6.6.msi', "${env:PYTMP}\python-2.6.6.msi"); - } - Start-Process -FilePath msiexec.exe -ArgumentList ('/qn', '/i', "${env:PYTMP}\python-2.6.6.msi", "TARGETDIR=${env:PYTHON}") -Wait; - } elseif ("${env:PYTHON_ID}" -eq "26-x64") { - if (!(Test-Path "${env:PYTMP}\python-2.6.6.amd64.msi")) { - (New-Object Net.WebClient).DownloadFile('https://www.python.org/ftp/python/2.6.6/python-2.6.6.amd64.msi', "${env:PYTMP}\python-2.6.6.amd64.msi"); - } - Start-Process -FilePath msiexec.exe -ArgumentList ('/qn', '/i', "${env:PYTMP}\python-2.6.6.amd64.msi", "TARGETDIR=${env:PYTHON}") -Wait; - } elseif ("${env:PYTHON_ID}" -eq "pypy") { + if ("${env:PYTHON_ID}" -eq "pypy") { if (!(Test-Path "${env:PYTMP}\pypy-4.0.0-win32.zip")) { (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy-4.0.0-win32.zip', "${env:PYTMP}\pypy-4.0.0-win32.zip"); } 7z x -y "${env:PYTMP}\pypy-4.0.0-win32.zip" -oC:\ | Out-Null; - } - - if (!(Test-Path "${env:PYTMP}\get-pip.py")) { - (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); - } - - if ("${env:PYTHON_ID}" -eq "pypy") { + if (!(Test-Path "${env:PYTMP}\get-pip.py")) { + (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); + } & "${env:PYTHON}\pypy.exe" "${env:PYTMP}\get-pip.py"; & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8; + } elseif ("${env:PYTHON_ID}" -eq "26" -or "${env:PYTHON_ID}" -eq "26-x64") { - & "${env:PYTHON}\python.exe" -W ignore "${env:PYTMP}\get-pip.py"; & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8'])"; + } else { & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8; } diff --git a/dev/ci.py b/dev/ci.py index d2144ba..a40ba3a 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -8,6 +8,13 @@ from .lint import run as run_lint def run(): + """ + Runs the linter and tests + + :return: + A bool - if the linter and tests ran successfully + """ + print('Python ' + sys.version.replace('\n', '')) print('') lint_result = run_lint() -- cgit v1.2.3 From ce3939e1d85827ef25886e4345c8fd4bf83cff7b Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 10 Nov 2015 11:29:10 -0500 Subject: Version 0.13.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 16 ++++++++++++++++ readme.md | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 1b3bf11..42725e6 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.12.2' -__version_info__ = (0, 12, 2) +__version__ = '0.13.0' +__version_info__ = (0, 13, 0) diff --git a/changelog.md b/changelog.md index 26974d2..e54bf21 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,21 @@ # changelog +## 0.13.0 + + - Backwards compatibility break: the native representation of some + `algos.EncryptionAlgorithmId` values changed. `aes128` became `aes128_cbc`, + `aes192` became `aes192_cbc` and `aes256` became `aes256_cbc`. + - Added more OIDs to `algos.EncryptionAlgorithmId` + - Added more OIDs to `cms.KeyEncryptionAlgorithmId` + - `x509.Name.human_friendly` now properly supports multiple values per + `x509.NameTypeAndValue` object + - Added `ocsp.OCSPResponse.basic_ocsp_response` and + `ocsp.OCSPResponse.response_data` properties + - Added `algos.EncryptionAlgorithm.encryption_mode` property + - Fixed a bug with parsing times containing timezone offsets in Python 3 + - The `attributes` field of `csr.CertificationRequestInfo` is now optional, + for compatibility with other ASN.1 parsers + ## 0.12.2 - Correct `core.Sequence.__setitem__()` so set `core.VOID` to an optional diff --git a/readme.md b/readme.md index 9f3d715..157eed2 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ required.* ## Version -0.12.2 - [changelog](changelog.md) +0.13.0 - [changelog](changelog.md) ## Continuous Integration -- cgit v1.2.3 From 1215269c7dfb66663c57c16890cf99860073aeb6 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 13 Nov 2015 02:24:10 -0500 Subject: Added x509.Certificate.sha1_fingerprint --- asn1crypto/x509.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index e93df05..d0c3f3f 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2382,13 +2382,23 @@ class Certificate(Sequence): def sha1(self): """ :return: - The SHA1 hash of the DER-encoded bytes of this complete certificate + The SHA-1 hash of the DER-encoded bytes of this complete certificate """ if self._sha1 is None: self._sha1 = hashlib.sha1(self.dump()).digest() return self._sha1 + @property + def sha1_fingerprint(self): + """ + :return: + A unicode string of the SHA-1 hash, formatted using hex encoding + with a space between each pair of characters, all uppercase + """ + + return ' '.join('%02X' % c for c in self.sha1) + @property def sha256(self): """ -- cgit v1.2.3 From 1c5eecc832d9bee282fb69fb4aaa0b90e4cb6995 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 13 Nov 2015 13:53:09 -0500 Subject: Refactored readme, added TOC --- readme.md | 188 +++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 105 insertions(+), 83 deletions(-) diff --git a/readme.md b/readme.md index 157eed2..1009ecc 100644 --- a/readme.md +++ b/readme.md @@ -1,25 +1,96 @@ # asn1crypto -A fast, pure Python library for parsing and serializing ASN.1 structures. In -addition to an ASN.1 BER/DER decoder and DER serializer, the project includes +A fast, pure Python library for parsing and serializing ASN.1 structures. + + - [Features](#features) + - [Why Another Python ASN.1 Library?](#why-another-python-asn1-library) + - [Related Crypto Libraries](#related-crypto-libraries) + - [Current Release](#current-release) + - [Dependencies](#dependencies) + - [Installation](#installation) + - [License](#license) + - [Documentation](#documentation) + - [Continuous Integration](#continuous-integration) + - [Testing](#testing) + - [Development](#development) + +## Features + +In addition to an ASN.1 BER/DER decoder and DER serializer, the project includes a bunch of ASN.1 structures for use with various common cryptography standards: | Standard | Module | Source | | ---------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| X.509 | [`asn1crypto.x509`](asn1crypto/x509.py) | [RFC 5280](https://tools.ietf.org/html/rfc5280) | -| CRL | [`asn1crypto.crl`](asn1crypto/crl.py) | [RFC 5280](https://tools.ietf.org/html/rfc5280) | -| CSR | [`asn1crypto.csr`](asn1crypto/csr.py) | [RFC 2986](https://tools.ietf.org/html/rfc2986), [RFC 2985](https://tools.ietf.org/html/rfc2985) | -| OCSP | [`asn1crypto.ocsp`](asn1crypto/ocsp.py) | [RFC 6960](https://tools.ietf.org/html/rfc6960) | -| PKCS#12 | [`asn1crypto.pkcs12`](asn1crypto/pkcs12.py) | [RFC 7292](https://tools.ietf.org/html/rfc7292) | -| PKCS#8 | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 5208](https://tools.ietf.org/html/rfc5208) | -| PKCS#1 v2.1 (RSA keys) | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3447](https://tools.ietf.org/html/rfc3447) | -| DSA keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3279](https://tools.ietf.org/html/rfc3279) | +| X.509 | [`asn1crypto.x509`](asn1crypto/x509.py) | [RFC 5280](https://tools.ietf.org/html/rfc5280) | +| CRL | [`asn1crypto.crl`](asn1crypto/crl.py) | [RFC 5280](https://tools.ietf.org/html/rfc5280) | +| CSR | [`asn1crypto.csr`](asn1crypto/csr.py) | [RFC 2986](https://tools.ietf.org/html/rfc2986), [RFC 2985](https://tools.ietf.org/html/rfc2985) | +| OCSP | [`asn1crypto.ocsp`](asn1crypto/ocsp.py) | [RFC 6960](https://tools.ietf.org/html/rfc6960) | +| PKCS#12 | [`asn1crypto.pkcs12`](asn1crypto/pkcs12.py) | [RFC 7292](https://tools.ietf.org/html/rfc7292) | +| PKCS#8 | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 5208](https://tools.ietf.org/html/rfc5208) | +| PKCS#1 v2.1 (RSA keys) | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3447](https://tools.ietf.org/html/rfc3447) | +| DSA keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3279](https://tools.ietf.org/html/rfc3279) | | Elliptic curve keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [SECG SEC1 V2](http://www.secg.org/sec1-v2.pdf) | | PKCS#5 v2.1 | [`asn1crypto.algos`](asn1crypto/algos.py) | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | -| CMS (and PKCS#7) | [`asn1crypto.cms`](asn1crypto/cms.py) | [RFC 5652](https://tools.ietf.org/html/rfc5652), [RFC 2315](https://tools.ietf.org/html/rfc2315) | -| TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC 3161](https://tools.ietf.org/html/rfc3161) | +| CMS (and PKCS#7) | [`asn1crypto.cms`](asn1crypto/cms.py) | [RFC 5652](https://tools.ietf.org/html/rfc5652), [RFC 2315](https://tools.ietf.org/html/rfc2315) | +| TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC 3161](https://tools.ietf.org/html/rfc3161) | | PDF signatures | [`asn1crypto.pdf`](asn1crypto/pdf.py) | [PDF 1.7](http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf) | +## Why Another Python ASN.1 Library? + +Python has long had the [pyasn1](https://pypi.python.org/pypi/pyasn1) and +[pyasn1_modules](https://pypi.python.org/pypi/pyasn1-modules) available for +parsing and serializing ASN.1 structures. While the project does include a +comprehensive set of tools for parsing and serializing, the performance of the +library can be very poor, especially when dealing with bit fields and parsing +large structures such as CRLs. + +After spending extensive time using *pyasn1*, the following issues were +identified: + + 1. Poor performance + 2. Verbose, non-pythonic API + 3. Out-dated and incomplete definitions in *pyasn1-modules* + 4. No simple way to map data to native Python data structures + 5. No mechanism for overridden universal ASN.1 types + +The *pyasn1* API is largely method driven, and uses extensive configuration +objects and lowerCamelCase names. There were no consistent options for +converting types of native Python data structures. Since the project supports +out-dated versions of Python, many newer language features are unavailable +for use. + +Time was spent trying to profile issues with the performance, however the +architecture made it hard to pin down the primary source of the poor +performance. Attempts were made to improve performance by utilizing unreleased +patches and delaying parsing using the `Any` type. Even with such changes, the +performance was still unacceptably slow. + +Finally, a number of structures in the cryptographic space use universal data +types such as `BitString` and `OctetString`, but interpret the data as other +types. For instance, signatures are really byte strings, but are encoded as +`BitString`. Elliptic curve keys use both `BitString` and `OctetString` to +represent integers. Parsing these structures as the base universal types and +then re-interpreting them wastes computation. + +*asn1crypto* uses the following techniques to improve performance, especially +when extracting one or two fields from large, complex structures: + + - Delayed parsing of byte string values + - Persistence of original ASN.1 encoded data until a value is changed + - Lazy loading of child fields + - Utilization of high-level Python stdlib modules + +While there is no extensive performance test suite, the +`CRLTests.test_parse_crl` test case was used to parse a 21MB CRL file on a +late 2013 rMBP. *asn1crypto* parsed the certificate serial numbers in just +under 8 seconds. With *pyasn1*, using definitions from *pyasn1-modules*, the +same parsing took over 4,100 seconds. + +For smaller structures the performance difference can range from a few times +faster to an order of magnitude of more. + +## Related Crypto Libraries + *asn1crypto* is part of the modularcrypto family of Python packages: - [asn1crypto](https://github.com/wbond/asn1crypto) @@ -29,31 +100,26 @@ a bunch of ASN.1 structures for use with various common cryptography standards: - [crlbuilder](https://github.com/wbond/crlbuilder) - [ocspbuilder](https://github.com/wbond/ocspbuilder) -## License +## Current Release -*asn1crypto* is licensed under the terms of the MIT license. See the -[LICENSE](LICENSE) file for the exact license text. +0.13.0 - [changelog](changelog.md) ## Dependencies Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, pypy or pypy3. *No third-party packages required.* -## Version - -0.13.0 - [changelog](changelog.md) - -## Continuous Integration - - - [Windows](https://ci.appveyor.com/project/wbond/asn1crypto/history) via AppVeyor - - [OS X & Linux](https://travis-ci.org/wbond/asn1crypto/builds) via Travis CI - ## Installation ```bash pip install asn1crypto ``` +## License + +*asn1crypto* is licensed under the terms of the MIT license. See the +[LICENSE](LICENSE) file for the exact license text. + ## Documentation The documentation for *asn1crypto* is composed of tutorials on basic usage and @@ -78,20 +144,17 @@ links to the source for the various pre-defined type classes. - [Time stamp protocol (TSP)](asn1crypto/tsp.py), `asn1crypto.tsp` - [PDF signatures](asn1crypto/pdf.py), `asn1crypto.pdf` -## Development +## Continuous Integration -To install required development dependencies, execute: + - [Windows](https://ci.appveyor.com/project/wbond/asn1crypto/history) via AppVeyor + - [OS X & Linux](https://travis-ci.org/wbond/asn1crypto/builds) via Travis CI -```bash -pip install -r dev-requirements.txt -``` +## Testing -The following commands will run the test suite, linter and test coverage: +Tests are written using `unittest` and require no third-party packages: ```bash python run.py tests -python run.py lint -python run.py coverage ``` To run only some tests, pass a regular expression as a parameter to `tests`. @@ -100,58 +163,17 @@ To run only some tests, pass a regular expression as a parameter to `tests`. python run.py tests ocsp ``` -## Why Another Python ASN.1 Library? - -Python has long had the [pyasn1](https://pypi.python.org/pypi/pyasn1) and -[pyasn1_modules](https://pypi.python.org/pypi/pyasn1-modules) available for -parsing and serializing ASN.1 structures. While the project does include a -comprehensive set of tools for parsing and serializing, the performance of the -library can be very poor, especially when dealing with bit fields and parsing -large structures such as CRLs. - -After spending extensive time using *pyasn1*, the following issues were -identified: - - 1. Poor performance - 2. Verbose, non-pythonic API - 3. Out-dated and incomplete definitions in *pyasn1-modules* - 4. No simple way to map data to native Python data structures - 5. No mechanism for overriden universal ASN.1 types - -The *pyasn1* API is largely method driven, and uses extensive configuration -objects and lowerCamelCase names. There were no consistent options for -converting types of native Python data structures. Since the project supports -out-dated versions of Python, many newer language features are unavailable -for use. - -Time was spent trying to profile issues with the performance, however the -architecture made it hard to pin down the primary source of the poor -performance. Attempts were made to improve performance by utilizing unreleased -patches and delaying parsing using the `Any` type. Even with such changes, the -performance was still unacceptably slow. - -Finally, a number of structures in the cryptographic space use universal data -types such as `BitString` and `OctetString`, but interpret the data as other -types. For instance, signatures are really byte strings, but are encoded as -`BitString`. Elliptic curve keys use both `BitString` and `OctetString` to -represent integers. Parsing these structures as the base universal types and -then re-interpreting them wastes computation. - -*asn1crypto* uses the following techniques to improve performance, especially -when extracting one or two fields from large, complex structures: - - - Delayed parsing of byte string values - - Persistence of original ASN.1 encoded data until a value is changed - - Lazy loading of child fields - - Utilization of high-level Python stdlib modules +## Development -While there is no extensive performance test suite, the -`CRLTests.test_parse_crl` test case was used to parse a 21MB CRL file on a -late 2013 rMBP. *asn1crypto* parsed the certificate serial numbers in just -under 8 seconds. With *pyasn1*, using definitions from *pyasn1-modules*, the -same parsing took over 4,100 seconds. +To install required development dependencies, execute: -For smaller structures the performance difference can range from a few times -faster to an order of magnitude of more. +```bash +pip install -r dev-requirements.txt +``` +The following commands will run the linter and test coverage: +```bash +python run.py lint +python run.py coverage +``` -- cgit v1.2.3 From 74dd8691e490df82f434a6de974a5547909d1f60 Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 15 Nov 2015 11:54:49 -0500 Subject: Add pypi keywords --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 60f52dd..f96302c 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ setup( 'Topic :: Security :: Cryptography', ], - keywords='asn1 crypto pki', + keywords='asn1 crypto pki x509 certificate rsa dsa ec', packages=find_packages(exclude=['tests*', 'dev*']), -- cgit v1.2.3 From 1c875b62872a85593a20637206c209fb72e8735b Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Nov 2015 14:57:40 -0500 Subject: Version 0.14.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 4 ++++ readme.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 42725e6..d39c349 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.13.0' -__version_info__ = (0, 13, 0) +__version__ = '0.14.0' +__version_info__ = (0, 14, 0) diff --git a/changelog.md b/changelog.md index e54bf21..2e00e58 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # changelog +## 0.14.0 + + - Added the `x509.Certificate.sha1_fingerprint` attribute + ## 0.13.0 - Backwards compatibility break: the native representation of some diff --git a/readme.md b/readme.md index 1009ecc..aad391e 100644 --- a/readme.md +++ b/readme.md @@ -102,7 +102,7 @@ faster to an order of magnitude of more. ## Current Release -0.13.0 - [changelog](changelog.md) +0.14.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 1be9c1bd92cc6eb0f5d061d9dba1f1c99671e6ed Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Nov 2015 15:06:44 -0500 Subject: Add link to certvalidator --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index aad391e..86aaf87 100644 --- a/readme.md +++ b/readme.md @@ -99,6 +99,7 @@ faster to an order of magnitude of more. - [certbuilder](https://github.com/wbond/certbuilder) - [crlbuilder](https://github.com/wbond/crlbuilder) - [ocspbuilder](https://github.com/wbond/ocspbuilder) + - [certvalidator](https://github.com/wbond/certvalidator) ## Current Release -- cgit v1.2.3 From 4c518b23bb9afbd8404b2db39830a6a2f0968443 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Nov 2015 15:15:38 -0500 Subject: Fix x509.Certificate.sha1_fingerprint on Python 2 --- asn1crypto/x509.py | 2 +- tests/test_x509.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index d0c3f3f..4e846ee 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2397,7 +2397,7 @@ class Certificate(Sequence): with a space between each pair of characters, all uppercase """ - return ' '.join('%02X' % c for c in self.sha1) + return ' '.join('%02X' % c for c in bytes_to_list(self.sha1)) @property def sha256(self): diff --git a/tests/test_x509.py b/tests/test_x509.py index a6036a7..cefa9f0 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -393,6 +393,10 @@ class X509Tests(unittest.TestCase): self.assertIsInstance(alt_names[5].chosen, x509.Name) self.assertEqual(alt_names[5].chosen.native, util.OrderedDict([('common_name', '127.0.0.3')])) + def test_sha1_fingerprint(self): + cert = self._load_cert('geotrust_certs/codex.crt') + self.assertEqual('78 1C 9F 87 59 93 52 08 D2 21 FA 70 6C C5 F9 76 12 C9 6D 8B', cert.sha1_fingerprint) + def test_punycode_common_name(self): cert = self._load_cert('chromium/punycodetest.pem') self.assertEqual('xn--wgv71a119e.com', cert['tbs_certificate']['subject'].native['common_name']) -- cgit v1.2.3 From 6755ee85c485b81907bca7e6c5585ed3d410d78c Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 18 Nov 2015 02:47:10 -0500 Subject: Version 0.14.1 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 4 ++++ readme.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index d39c349..71e7d21 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.14.0' -__version_info__ = (0, 14, 0) +__version__ = '0.14.1' +__version_info__ = (0, 14, 1) diff --git a/changelog.md b/changelog.md index 2e00e58..debe4d0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # changelog +## 0.14.1 + + - Fixed a bug generating `x509.Certificate.sha1_fingerprint` on Python 2 + ## 0.14.0 - Added the `x509.Certificate.sha1_fingerprint` attribute diff --git a/readme.md b/readme.md index 86aaf87..1194bd0 100644 --- a/readme.md +++ b/readme.md @@ -103,7 +103,7 @@ faster to an order of magnitude of more. ## Current Release -0.14.0 - [changelog](changelog.md) +0.14.1 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From ea2781891a85868026069e36804af557a6fc4e2d Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 23 Nov 2015 13:55:29 -0500 Subject: Added TLS Feature x509 extension OID and property from RFC 7633 --- asn1crypto/x509.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 4e846ee..7eb060e 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1632,6 +1632,11 @@ class SubjectInfoAccessSyntax(SequenceOf): _child_spec = AccessDescription +# https://tools.ietf.org/html/rfc7633 +class Features(SequenceOf): + _child_spec = Integer + + class EntrustVersionInfo(Sequence): _fields = [ ('entrust_vers', GeneralString), @@ -1672,6 +1677,8 @@ class ExtensionId(ObjectIdentifier): '2.5.29.54': 'inhibit_any_policy', '1.3.6.1.5.5.7.1.1': 'authority_information_access', '1.3.6.1.5.5.7.1.11': 'subject_information_access', + # https://tools.ietf.org/html/rfc7633 + '1.3.6.1.5.5.7.1.24': 'tls_feature', '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', '1.2.840.113533.7.65.0': 'entrust_version_extension', '2.16.840.1.113730.1.1': 'netscape_certificate_type', @@ -1705,6 +1712,7 @@ class Extension(Sequence): 'inhibit_any_policy': Integer, 'authority_information_access': AuthorityInfoAccessSyntax, 'subject_information_access': SubjectInfoAccessSyntax, + 'tls_feature': Features, 'ocsp_no_check': Null, 'entrust_version_extension': EntrustVersionInfo, 'netscape_certificate_type': NetscapeCertificateType, @@ -1764,6 +1772,7 @@ class Certificate(Sequence): _extended_key_usage_value = None _authority_information_access_value = None _subject_information_access_value = None + _tls_feature_value = None _ocsp_no_check_value = None _issuer_serial = None _authority_issuer_serial = False @@ -2052,6 +2061,20 @@ class Certificate(Sequence): self._set_extensions() return self._subject_information_access_value + @property + def tls_feature_value(self): + """ + This extension is used to list the TLS features a server must respond + with if a client initiates a request supporting them. + + :return: + None or a Features object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._tls_feature_value + @property def ocsp_no_check_value(self): """ -- cgit v1.2.3 From 965ea21b7a7ec2d8bbeaee79359fbd05e0228210 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 30 Nov 2015 15:54:01 -0500 Subject: Add the ability too force x509.Name.build() to use core.PrintableString --- asn1crypto/x509.py | 16 +++++++++++++--- tests/test_x509.py | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 7eb060e..a55da3d 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -932,7 +932,7 @@ class Name(Choice): _sha256 = None @classmethod - def build(cls, name_dict): + def build(cls, name_dict, use_printable=False): """ Creates a Name object from a dict of unicode string keys and values. The keys should be from NameType._map, or a dotted-integer OID unicode @@ -942,11 +942,21 @@ class Name(Choice): A dict of name information, e.g. {"common_name": "Will Bond", "country_name": "US", "organization": "Codex Non Sufficit LC"} + :param use_printable: + A bool - if PrintableString should be used for encoding instead of + UTF8String. This is for backwards compatiblity with old software. + :return: An x509.Name object """ attributes = [] + if not use_printable: + encoding_name = 'utf8_string' + encoding_class = UTF8String + else: + encoding_name = 'printable_string' + encoding_class = PrintableString for attribute_name in NameType.preferred_order: if attribute_name not in name_dict: @@ -963,8 +973,8 @@ class Name(Choice): ) else: value = DirectoryString( - name='utf8_string', - value=UTF8String(name_dict[attribute_name]) + name=encoding_name, + value=encoding_class(name_dict[attribute_name]) ) attributes.append(NameTypeAndValue({ diff --git a/tests/test_x509.py b/tests/test_x509.py index cefa9f0..500b4f9 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -357,6 +357,27 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(general_name_1, general_name_2) + def test_build_name_printable(self): + utf8_name = x509.Name.build( + { + 'country_name': 'US', + 'state_or_province_name': 'Massachusetts', + 'common_name': 'Will Bond' + } + ) + self.assertIsInstance(utf8_name.chosen[0][2]['value'].chosen, core.UTF8String) + self.assertEqual('common_name', utf8_name.chosen[0][2]['type'].native) + printable_name = x509.Name.build( + { + 'country_name': 'US', + 'state_or_province_name': 'Massachusetts', + 'common_name': 'Will Bond' + }, + use_printable=True + ) + self.assertIsInstance(printable_name.chosen[0][2]['value'].chosen, core.PrintableString) + self.assertEqual('common_name', printable_name.chosen[0][2]['type'].native) + def test_v1_cert(self): cert = self._load_cert('chromium/ndn.ca.crt') tbs_cert = cert['tbs_certificate'] -- cgit v1.2.3 From 2b31d956328f381ad6cf3b0221e0255e24cad4f6 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 30 Nov 2015 12:54:03 -0500 Subject: Added util.uri_to_iri() and util.iri_to_uri() --- asn1crypto/_iri.py | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++++ asn1crypto/util.py | 3 + asn1crypto/x509.py | 236 +++------------------------------------------- 3 files changed, 286 insertions(+), 224 deletions(-) create mode 100644 asn1crypto/_iri.py diff --git a/asn1crypto/_iri.py b/asn1crypto/_iri.py new file mode 100644 index 0000000..57342d3 --- /dev/null +++ b/asn1crypto/_iri.py @@ -0,0 +1,271 @@ +# coding: utf-8 + +""" +Functions to convert unicode IRIs into ASCII byte string URIs and back. Exports +the following items: + + - iri_to_uri() + - uri_to_iri() +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +from encodings import idna # noqa +import codecs +import re +import sys + +if sys.version_info < (3,): + from urlparse import urlsplit, urlunsplit + from urllib import ( + quote as urlquote, + unquote as unquote_to_bytes, + ) + + bytes_to_list = lambda byte_string: [ord(b) for b in byte_string] + +else: + from urllib.parse import ( + quote as urlquote, + unquote_to_bytes, + urlsplit, + urlunsplit, + ) + + bytes_to_list = list + +from ._errors import unwrap +from ._types import byte_cls, str_cls, type_name + + +def iri_to_uri(value): + """ + Normalizes and encodes a unicode IRI into an ASCII byte string URI + + :param value: + A unicode string of an IRI + + :return: + A byte string of the ASCII-encoded URI + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + value must be a unicode string, not %s + ''', + type_name(value) + )) + + parsed = urlsplit(value) + + scheme = _urlquote(parsed.scheme) + hostname = parsed.hostname + if hostname is not None: + hostname = hostname.encode('idna') + username = _urlquote(parsed.username) + password = _urlquote(parsed.password) + port = parsed.port + if port is not None: + port = str_cls(port).encode('ascii') + + netloc = b'' + if username is not None: + netloc += username + if password: + netloc += b':' + password + netloc += b'@' + if hostname is not None: + netloc += hostname + 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: + netloc += b':' + port + + path = _urlquote(parsed.path, safe='/') + query = _urlquote(parsed.query, safe='&=') + fragment = _urlquote(parsed.fragment) + + if query is None and fragment is None and path == b'/': + path = None + + # Python 2.7 compat + if path is None: + path = '' + + output = urlunsplit((scheme, netloc, path, query, fragment)) + if isinstance(output, str_cls): + output = output.encode('latin1') + return output + + +def uri_to_iri(value): + """ + Converts an ASCII URI byte string into a unicode IRI + + :param value: + An ASCII-encoded byte string of the URI + + :return: + A unicode string of the IRI + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + value must be a byte string, not %s + ''', + type_name(value) + )) + + parsed = urlsplit(value) + + scheme = parsed.scheme + if scheme is not None: + scheme = scheme.decode('ascii') + + username = _urlunquote(parsed.username, remap=[':', '@']) + password = _urlunquote(parsed.password, remap=[':', '@']) + hostname = parsed.hostname + if hostname: + hostname = hostname.decode('idna') + port = parsed.port + if port: + port = port.decode('ascii') + + netloc = '' + if username is not None: + netloc += username + if password: + netloc += ':' + password + netloc += '@' + if hostname is not None: + netloc += hostname + if port is not None: + netloc += ':' + port + + path = _urlunquote(parsed.path, remap=['/'], preserve=True) + query = _urlunquote(parsed.query, remap=['&', '='], preserve=True) + fragment = _urlunquote(parsed.fragment) + + return urlunsplit((scheme, netloc, path, query, fragment)) + + +def _iri_utf8_errors_handler(exc): + """ + Error handler for decoding UTF-8 parts of a URI into an IRI. Leaves byte + sequences encoded in %XX format, but as part of a unicode string. + + :param exc: + The UnicodeDecodeError exception + + :return: + A 2-element tuple of (replacement unicode string, integer index to + resume at) + """ + + bytes_as_ints = bytes_to_list(exc.object[exc.start:exc.end]) + replacements = ['%%%02x' % num for num in bytes_as_ints] + return (''.join(replacements), exc.end) + + +codecs.register_error('iriutf8', _iri_utf8_errors_handler) + + +def _urlquote(string, safe=''): + """ + Quotes a unicode string for use in a URL + + :param string: + A unicode string + + :param safe: + A unicode string of character to not encode + + :return: + None (if string is None) or an ASCII byte string of the quoted string + """ + + if string is None or string == '': + return None + + # Anything already hex quoted is pulled out of the URL and unquoted if + # possible + escapes = [] + if re.search('%[0-9a-fA-F]{2}', string): + # Try to unquote any percent values, restoring them if they are not + # valid UTF-8. Also, requote any safe chars since encoded versions of + # those are functionally different than the unquoted ones. + def _try_unescape(match): + byte_string = unquote_to_bytes(match.group(0)) + unicode_string = byte_string.decode('utf-8', 'iriutf8') + for safe_char in list(safe): + unicode_string = unicode_string.replace(safe_char, '%%%02x' % ord(safe_char)) + return unicode_string + string = re.sub('(?:%[0-9a-fA-F]{2})+', _try_unescape, string) + + # Once we have the minimal set of hex quoted values, removed them from + # the string so that they are not double quoted + def _extract_escape(match): + escapes.append(match.group(0).encode('ascii')) + return '\x00' + string = re.sub('%[0-9a-fA-F]{2}', _extract_escape, string) + + output = urlquote(string.encode('utf-8'), safe=safe.encode('utf-8')) + if not isinstance(output, byte_cls): + output = output.encode('ascii') + + # Restore the existing quoted values that we extracted + if len(escapes) > 0: + def _return_escape(_): + return escapes.pop(0) + output = re.sub(b'%00', _return_escape, output) + + return output + + +def _urlunquote(byte_string, remap=None, preserve=None): + """ + Unquotes a URI portion from a byte string into unicode using UTF-8 + + :param byte_string: + A byte string of the data to unquote + + :param remap: + A list of characters (as unicode) that should be re-mapped to a + %XX encoding. This is used when characters are not valid in part of a + URL. + + :param preserve: + A bool - indicates that the chars to be remapped if they occur in + non-hex form, should be preserved. E.g. / for URL path. + + :return: + A unicode string + """ + + if byte_string is None: + return byte_string + + byte_string = unquote_to_bytes(byte_string) + + if preserve: + replacements = ['\x1A', '\x1C', '\x1D', '\x1E', '\x1F'] + preserve_unmap = {} + for char in remap: + replacement = replacements.pop(0) + preserve_unmap[replacement] = char + byte_string = byte_string.replace(char.encode('ascii'), replacement.encode('ascii')) + + if remap: + for char in remap: + byte_string = byte_string.replace(char.encode('ascii'), ('%%%02x' % ord(char)).encode('ascii')) + + output = byte_string.decode('utf-8', 'iriutf8') + + if preserve: + for replacement, original in preserve_unmap.items(): + output = output.replace(replacement, original) + + return output diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 750064d..c7af62d 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -10,6 +10,8 @@ from bytes and UTC timezone. Exports the following items: - timezone.utc - inet_ntop() - inet_pton() + - uri_to_iri() + - iri_to_uri() """ from __future__ import unicode_literals, division, absolute_import, print_function @@ -17,6 +19,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math import sys +from ._iri import iri_to_uri, uri_to_iri # noqa from ._ordereddict import OrderedDict # noqa if sys.platform == 'win32': diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index a55da3d..5aa7ca7 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -16,7 +16,6 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function from encodings import idna # noqa -import codecs import hashlib import re import socket @@ -25,8 +24,9 @@ import sys import unicodedata from ._errors import unwrap +from ._iri import iri_to_uri, uri_to_iri from ._ordereddict import OrderedDict -from ._types import type_name, str_cls, byte_cls +from ._types import type_name, str_cls from .algos import SignedDigestAlgorithm from .core import ( Any, @@ -60,22 +60,9 @@ from .keys import PublicKeyInfo from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton if sys.version_info < (3,): - from urlparse import urlsplit, urlunsplit - from urllib import ( - quote as urlquote, - unquote as unquote_to_bytes, - ) - bytes_to_list = lambda byte_string: [ord(b) for b in byte_string] else: - from urllib.parse import ( - quote as urlquote, - unquote_to_bytes, - urlsplit, - urlunsplit, - ) - bytes_to_list = list @@ -129,99 +116,11 @@ class URI(IA5String): self._normalized = True self._native = value - self.contents = self._normalize(value) + self.contents = iri_to_uri(value) self._header = None if self._trailer != b'': self._trailer = b'' - def _normalize(self, value): - """ - Normalizes and encodes a unicode IRI into an ASCII byte string - - :param value: - - """ - - parsed = urlsplit(value) - - scheme = _urlquote(parsed.scheme) - hostname = parsed.hostname - if hostname is not None: - hostname = hostname.encode('idna') - username = _urlquote(parsed.username) - password = _urlquote(parsed.password) - port = parsed.port - if port is not None: - port = str_cls(port).encode('ascii') - - netloc = b'' - if username is not None: - netloc += username - if password: - netloc += b':' + password - netloc += b'@' - if hostname is not None: - netloc += hostname - 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: - netloc += b':' + port - - path = _urlquote(parsed.path, safe='/') - query = _urlquote(parsed.query, safe='&=') - fragment = _urlquote(parsed.fragment) - - if query is None and fragment is None and path == b'/': - path = None - - # Python 2.7 compat - if path is None: - path = '' - - output = urlunsplit((scheme, netloc, path, query, fragment)) - if isinstance(output, str_cls): - output = output.encode('latin1') - return output - - def __unicode__(self): - """ - :return: - A unicode string - """ - - parsed = urlsplit(self.contents) - - scheme = parsed.scheme - if scheme is not None: - scheme = scheme.decode('ascii') - - username = _urlunquote(parsed.username, remap=[':', '@']) - password = _urlunquote(parsed.password, remap=[':', '@']) - hostname = parsed.hostname - if hostname: - hostname = hostname.decode('idna') - port = parsed.port - if port: - port = port.decode('ascii') - - netloc = '' - if username is not None: - netloc += username - if password: - netloc += ':' + password - netloc += '@' - if hostname is not None: - netloc += hostname - if port is not None: - netloc += ':' + port - - path = _urlunquote(parsed.path, remap=['/'], preserve=True) - query = _urlunquote(parsed.query, remap=['&', '='], preserve=True) - fragment = _urlunquote(parsed.fragment) - - return urlunsplit((scheme, netloc, path, query, fragment)) - def __ne__(self, other): return not self == other @@ -239,7 +138,15 @@ class URI(IA5String): if not isinstance(other, URI): return False - return self._normalize(self.native) == self._normalize(other.native) + return iri_to_uri(self.native) == iri_to_uri(other.native) + + def __unicode__(self): + """ + :return: + A unicode string + """ + + return uri_to_iri(self.contents) class EmailAddress(IA5String): @@ -2579,122 +2486,3 @@ class Certificate(Sequence): return True return False - - -def _iri_utf8_errors_handler(exc): - """ - Error handler for decoding UTF-8 parts of a URI into an IRI. Leaves byte - sequences encoded in %XX format, but as part of a unicode string. - - :param exc: - The UnicodeDecodeError exception - - :return: - A 2-element tuple of (replacement unicode string, integer index to - resume at) - """ - - bytes_as_ints = bytes_to_list(exc.object[exc.start:exc.end]) - replacements = ['%%%02x' % num for num in bytes_as_ints] - return (''.join(replacements), exc.end) - - -codecs.register_error('iriutf8', _iri_utf8_errors_handler) - - -def _urlquote(string, safe=''): - """ - Quotes a unicode string for use in a URL - - :param string: - A unicode string - - :param safe: - A unicode string of character to not encode - - :return: - None (if string is None) or an ASCII byte string of the quoted string - """ - - if string is None or string == '': - return None - - # Anything already hex quoted is pulled out of the URL and unquoted if - # possible - escapes = [] - if re.search('%[0-9a-fA-F]{2}', string): - # Try to unquote any percent values, restoring them if they are not - # valid UTF-8. Also, requote any safe chars since encoded versions of - # those are functionally different than the unquoted ones. - def _try_unescape(match): - byte_string = unquote_to_bytes(match.group(0)) - unicode_string = byte_string.decode('utf-8', 'iriutf8') - for safe_char in list(safe): - unicode_string = unicode_string.replace(safe_char, '%%%02x' % ord(safe_char)) - return unicode_string - string = re.sub('(?:%[0-9a-fA-F]{2})+', _try_unescape, string) - - # Once we have the minimal set of hex quoted values, removed them from - # the string so that they are not double quoted - def _extract_escape(match): - escapes.append(match.group(0).encode('ascii')) - return '\x00' - string = re.sub('%[0-9a-fA-F]{2}', _extract_escape, string) - - output = urlquote(string.encode('utf-8'), safe=safe.encode('utf-8')) - if not isinstance(output, byte_cls): - output = output.encode('ascii') - - # Restore the existing quoted values that we extracted - if len(escapes) > 0: - def _return_escape(_): - return escapes.pop(0) - output = re.sub(b'%00', _return_escape, output) - - return output - - -def _urlunquote(byte_string, remap=None, preserve=None): - """ - Unquotes a URI portion from a byte string into unicode using UTF-8 - - :param byte_string: - A byte string of the data to unquote - - :param remap: - A list of characters (as unicode) that should be re-mapped to a - %XX encoding. This is used when characters are not valid in part of a - URL. - - :param preserve: - A bool - indicates that the chars to be remapped if they occur in - non-hex form, should be preserved. E.g. / for URL path. - - :return: - A unicode string - """ - - if byte_string is None: - return byte_string - - byte_string = unquote_to_bytes(byte_string) - - if preserve: - replacements = ['\x1A', '\x1C', '\x1D', '\x1E', '\x1F'] - preserve_unmap = {} - for char in remap: - replacement = replacements.pop(0) - preserve_unmap[replacement] = char - byte_string = byte_string.replace(char.encode('ascii'), replacement.encode('ascii')) - - if remap: - for char in remap: - byte_string = byte_string.replace(char.encode('ascii'), ('%%%02x' % ord(char)).encode('ascii')) - - output = byte_string.decode('utf-8', 'iriutf8') - - if preserve: - for replacement, original in preserve_unmap.items(): - output = output.replace(replacement, original) - - return output -- cgit v1.2.3 From c7b710f39444780b64b9bf5920d598a1a2a21341 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 30 Nov 2015 16:50:53 -0500 Subject: Configure preferred OIDs for algos.SignedDigestAlgorithmId --- asn1crypto/algos.py | 22 ++++++++++++++++++++++ asn1crypto/core.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 49f192a..9692d26 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -187,6 +187,28 @@ class SignedDigestAlgorithmId(ObjectIdentifier): '1.2.840.10045.4': 'ecdsa', } + _reverse_map = { + 'dsa': '1.2.840.10040.4.1', + 'ecdsa': '1.2.840.10045.4', + 'md2_rsa': '1.2.840.113549.1.1.2', + 'md5_rsa': '1.2.840.113549.1.1.4', + 'rsassa_pkcs1v15': '1.2.840.113549.1.1.1', + 'rsassa_pss': '1.2.840.113549.1.1.10', + 'sha1_dsa': '1.2.840.10040.4.3', + 'sha1_ecdsa': '1.2.840.10045.4.1', + 'sha1_rsa': '1.2.840.113549.1.1.5', + 'sha224_dsa': '2.16.840.1.101.3.4.3.1', + 'sha224_ecdsa': '1.2.840.10045.4.3.1', + 'sha224_rsa': '1.2.840.113549.1.1.14', + 'sha256_dsa': '2.16.840.1.101.3.4.3.2', + 'sha256_ecdsa': '1.2.840.10045.4.3.2', + 'sha256_rsa': '1.2.840.113549.1.1.11', + 'sha384_ecdsa': '1.2.840.10045.4.3.3', + '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', + } + class SignedDigestAlgorithm(Sequence): _fields = [ diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8ac68aa..11613e0 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -471,7 +471,7 @@ class ValueMap(): """ cls = self.__class__ - if cls._map is None: + if cls._map is None or cls._reverse_map is not None: return cls._reverse_map = {} for key, value in cls._map.items(): -- cgit v1.2.3 From eb4b979e0b1ff8d9cb36d3bef182ce79f3462eaf Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 1 Dec 2015 14:05:55 -0500 Subject: Version 0.15.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 11 +++++++++++ readme.md | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 71e7d21..256adae 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.14.1' -__version_info__ = (0, 14, 1) +__version__ = '0.15.0' +__version_info__ = (0, 15, 0) diff --git a/changelog.md b/changelog.md index debe4d0..2363cd0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # changelog +## 0.15.0 + + - Added support for the TLS feature extension from RFC 7633 + - `x509.Name.build()` now accepts a keyword parameter `use_printable` to force + string encoding to be `core.PrintableString` instead of `core.UTF8String` + - Added the functions `util.uri_to_iri()` and `util.iri_to_uri()` + - Changed `algos.SignedDigestAlgorithmId` to use the preferred OIDs when + mapping a unicode string name to an OID. Previously there were multiple OIDs + for some algorithms, and different OIDs would sometimes be selected due to + the fact that the `_map` `dict` is not ordered. + ## 0.14.1 - Fixed a bug generating `x509.Certificate.sha1_fingerprint` on Python 2 diff --git a/readme.md b/readme.md index 1194bd0..6f49fa0 100644 --- a/readme.md +++ b/readme.md @@ -103,7 +103,7 @@ faster to an order of magnitude of more. ## Current Release -0.14.1 - [changelog](changelog.md) +0.15.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From f87d4855fadc1d6edc95ce110ba63c13ecee6292 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 3 Dec 2015 00:02:35 -0500 Subject: Fix cms.CMSAttributes() to be a SetOf instead of a SequenceOf --- asn1crypto/cms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 252703e..5895783 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -140,7 +140,7 @@ class CMSAttribute(Sequence): _oid_specs = {} -class CMSAttributes(SequenceOf): +class CMSAttributes(SetOf): _child_spec = CMSAttribute -- cgit v1.2.3 From a6bb0ef3b1e4a2e99c7728c5c1d1bf03286aafad Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 3 Dec 2015 01:07:52 -0500 Subject: Add ValueError() when parsing a Set and an unknown field is encountered --- asn1crypto/core.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 11613e0..46c0a08 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3415,12 +3415,24 @@ class Set(Sequence): child_map = {} contents_length = len(self.contents) child_pointer = 0 + seen_field = 0 while child_pointer < contents_length: parts, num_bytes = _parse(self.contents, pointer=child_pointer) id_ = (parts[0], parts[2]) - field = self._field_ids[id_] + field = self._field_ids.get(id_) + if field is None: + raise ValueError(unwrap( + ''' + Data for field %s (%s class, %s method, tag %s) does + not match any of the field definitions + ''', + seen_field, + CLASS_NUM_TO_NAME_MAP.get(parts[0]), + METHOD_NUM_TO_NAME_MAP.get(parts[1]), + parts[2], + )) _, spec, field_params = self._fields[field] parse_as = None @@ -3443,6 +3455,7 @@ class Set(Sequence): child_map[field] = child child_pointer += num_bytes + seen_field += 1 total_fields = len(self._fields) -- cgit v1.2.3 From a07a51abc4764a658a13f7f972c1dc864d0a8517 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 3 Dec 2015 01:09:07 -0500 Subject: Fix cms.CMSAttribute() to be able to parse unknown attribute constructs --- asn1crypto/cms.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 5895783..5f37ac9 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -130,15 +130,25 @@ class SetOfTime(SetOf): _child_spec = Time +class SetOfAny(SetOf): + _child_spec = Any + + class CMSAttribute(Sequence): _fields = [ ('type', CMSAttributeType), - ('values', Any), + ('values', None), ] - _oid_pair = ('type', 'values') _oid_specs = {} + def _values_spec(self): + return self._oid_specs.get(self['type'].native, SetOfAny) + + _spec_callbacks = { + 'values': _values_spec + } + class CMSAttributes(SetOf): _child_spec = CMSAttribute -- cgit v1.2.3 From 20ec1190e7fdaa9d8cbcfb7398b13c2ce74d3026 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 4 Dec 2015 10:31:49 -0500 Subject: Fix x509.PolicyMapping to use x509.PolicyIdentifier for field types --- asn1crypto/x509.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5aa7ca7..7e56052 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1489,8 +1489,8 @@ class CertificatePolicies(SequenceOf): class PolicyMapping(Sequence): _fields = [ - ('issuer_domain_policy', ObjectIdentifier), - ('subject_domain_policy', ObjectIdentifier), + ('issuer_domain_policy', PolicyIdentifier), + ('subject_domain_policy', PolicyIdentifier), ] -- cgit v1.2.3 From 5d119641ed83d72d625f019c12dc5a19151879e3 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 4 Dec 2015 11:29:20 -0500 Subject: Fixed pdf.RevocationInfoArchival so that all fields are core.SequenceOf --- asn1crypto/pdf.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index 3c3a826..7673894 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -15,6 +15,7 @@ from .core import ( ObjectIdentifier, OctetString, Sequence, + SequenceOf, SetOf, ) from .crl import CertificateList @@ -48,11 +49,23 @@ class OtherRevInfo(Sequence): ] +class SequenceOfCertificateList(SequenceOf): + _child_spec = CertificateList + + +class SequenceOfOCSPResponse(SequenceOf): + _child_spec = OCSPResponse + + +class SequenceOfOtherRevInfo(SequenceOf): + _child_spec = OtherRevInfo + + class RevocationInfoArchival(Sequence): _fields = [ - ('crl', CertificateList, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('ocsp', OCSPResponse, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), - ('other_rev_info', OtherRevInfo, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ('crl', SequenceOfCertificateList, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('ocsp', SequenceOfOCSPResponse, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('other_rev_info', SequenceOfOtherRevInfo, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), ] -- cgit v1.2.3 From 0c0050fa7dd8b9980ed6a8ce2b3bec94f153f661 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 7 Dec 2015 10:22:08 -0500 Subject: Add the name_distinguisher OID for x509.Name --- asn1crypto/x509.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 7e56052..e9695a6 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -455,6 +455,8 @@ class NameType(ObjectIdentifier): '1.3.6.1.4.1.311.60.2.1.3': 'incorporation_country', # https://tools.ietf.org/html/rfc2247#section-4 '0.9.2342.19200300.100.1.25': 'domain_component', + # http://www.alvestrand.no/objectid/0.2.262.1.10.7.20.html + '0.2.262.1.10.7.20': 'name_distinguisher', } # This order is largely based on observed order seen in EV certs from -- cgit v1.2.3 From 558e1aed19772f01b2465d28956da6fa5335055b Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 7 Dec 2015 10:57:14 -0500 Subject: Allow parsing broken encodings of x509.NameTypeAndValue where the type is 'domain_component' --- asn1crypto/core.py | 6 +++++- asn1crypto/x509.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 46c0a08..9ca5f0a 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -126,6 +126,10 @@ class Asn1Value(object): # An integer 1 or greater indicating the tag number tag = None + # An alternate tag allowed for this type - used for handling broken + # structures where a string value is encoded using an incorrect tag + _bad_tag = None + # A unicode string or None - "explicit" or "implicit" for # tagged values, None for normal tag_type = None @@ -4024,7 +4028,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param METHOD_NUM_TO_NAME_MAP.get(value.method), METHOD_NUM_TO_NAME_MAP.get(method, method) )) - if tag != value.tag: + if tag != value.tag and tag != value._bad_tag: raise ValueError(unwrap( ''' Error parsing %s - tag should have been %s, but %s was found diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index e9695a6..56a7271 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -74,6 +74,7 @@ else: class DNSName(IA5String): _encoding = 'idna' + _bad_tag = 19 def __ne__(self, other): return not self == other -- cgit v1.2.3 From 526dd5420c63de6ba9d4a0ad5063cd3559c10b8f Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 7 Dec 2015 10:58:06 -0500 Subject: Add parsing of more x509.Name OIDs --- asn1crypto/x509.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 56a7271..2ae5180 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -442,12 +442,14 @@ class NameType(ObjectIdentifier): '2.5.4.12': 'title', '2.5.4.15': 'business_category', '2.5.4.17': 'postal_code', + '2.5.4.20': 'telephone_number', '2.5.4.41': 'name', '2.5.4.42': 'given_name', '2.5.4.43': 'initials', '2.5.4.44': 'generation_qualifier', '2.5.4.46': 'dn_qualifier', '2.5.4.65': 'pseudonym', + '2.5.4.97': 'organization_identifier', # https://tools.ietf.org/html/rfc2985#page-26 '1.2.840.113549.1.9.1': 'email_address', # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf @@ -485,8 +487,11 @@ class NameType(ObjectIdentifier): 'name', 'pseudonym', 'dn_qualifier', + 'telephone_number', 'email_address', 'domain_component', + 'name_distinguisher', + 'organization_identifier', ] @property @@ -509,6 +514,7 @@ class NameType(ObjectIdentifier): 'title': 'Title', 'business_category': 'Business Category', 'postal_code': 'Postal Code', + 'telephone_number': 'Telephone Number', 'name': 'Name', 'given_name': 'Given Name', 'initials': 'Initials', @@ -520,6 +526,8 @@ class NameType(ObjectIdentifier): 'incorporation_state_or_province': 'Incorporation State/Province', 'incorporation_country': 'Incorporation Country', 'domain_component': 'Domain Component', + 'name_distinguisher': 'Name Distinguisher', + 'organization_identifier': 'Organization Identifier', }[self.native] @@ -543,6 +551,7 @@ class NameTypeAndValue(Sequence): 'title': DirectoryString, 'business_category': DirectoryString, 'postal_code': DirectoryString, + 'telephone_number': PrintableString, 'name': DirectoryString, 'given_name': DirectoryString, 'initials': DirectoryString, @@ -556,6 +565,8 @@ class NameTypeAndValue(Sequence): 'incorporation_state_or_province': DirectoryString, 'incorporation_country': DirectoryString, 'domain_component': DNSName, + 'name_distinguisher': DirectoryString, + 'organization_identifier': DirectoryString, } _prepped = None -- cgit v1.2.3 From ff8b0346b9d5c292aaffeccdd7ca926817b66c16 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 7 Dec 2015 10:58:49 -0500 Subject: Fix a bug in x509.NameType.human_friendly that would raise an exception on an unknown OID --- asn1crypto/x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 2ae5180..4ae6460 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -528,7 +528,7 @@ class NameType(ObjectIdentifier): 'domain_component': 'Domain Component', 'name_distinguisher': 'Name Distinguisher', 'organization_identifier': 'Organization Identifier', - }[self.native] + }.get(self.native, self.native) class NameTypeAndValue(Sequence): -- cgit v1.2.3 From 8cbf404f6f34b3e75a5c8f286ae3c53e031d64e4 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 7 Dec 2015 10:59:42 -0500 Subject: For x509.Name objects that are encoded in the opposite order (Country last), reverse them for display --- asn1crypto/x509.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 4ae6460..fd4e26f 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -962,16 +962,21 @@ class Name(Choice): if self._human_friendly is None: data = OrderedDict() + last_field = None for rdn in self.chosen: for type_val in rdn: field_name = type_val['type'].human_friendly + last_field = field_name if field_name in data: data[field_name] = [data[field_name]] data[field_name].append(type_val['value']) else: data[field_name] = type_val['value'] to_join = [] - for key in data: + keys = data.keys() + if last_field == 'Country': + keys = reversed(list(keys)) + for key in keys: value = data[key] if isinstance(value, list): native_sub_values = [] -- cgit v1.2.3 From 7dac7a931c6b1e05f76c25101c22608cd31d9ad1 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 7 Dec 2015 11:00:07 -0500 Subject: Handle x509.Name objects with multiple levels of value nesting --- asn1crypto/x509.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index fd4e26f..7a56eb5 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -978,20 +978,8 @@ class Name(Choice): keys = reversed(list(keys)) for key in keys: value = data[key] - if isinstance(value, list): - native_sub_values = [] - for sub_value in value: - if isinstance(sub_value, list): - native_sub_value = ', '.join( - reversed([sub_sub_value.native for sub_sub_value in sub_value]) - ) - else: - native_sub_value = sub_value.native - native_sub_values.append(native_sub_value) - value = ', '.join(reversed(native_sub_values)) - else: - value = value.native - to_join.append('%s: %s' % (key, value)) + native_value = self._recursive_humanize(value) + to_join.append('%s: %s' % (key, native_value)) has_comma = False for element in to_join: @@ -1004,6 +992,23 @@ class Name(Choice): return self._human_friendly + def _recursive_humanize(self, value): + """ + Recursively serializes data compiled from the RDNSequence + + :param value: + An Asn1Value object, or a list of Asn1Value objects + + :return: + A unicode string + """ + + if isinstance(value, list): + return', '.join( + reversed([self._recursive_humanize(sub_value) for sub_value in value]) + ) + return value.native + @property def sha1(self): """ -- cgit v1.2.3 From 4ae81c9683701c6b3465d71126e757a0ca971b00 Mon Sep 17 00:00:00 2001 From: Alexander Duryagin Date: Tue, 15 Dec 2015 14:20:15 +0300 Subject: proper handling of multiple fields --- asn1crypto/x509.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 7a56eb5..d20c27f 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -947,8 +947,10 @@ class Name(Choice): for type_val in rdn: field_name = type_val['type'] if field_name in self._native: - self._native[field_name] = [self._native[field_name]] - self._native[field_name].append(type_val['value']) + existing = self._native[field_name] + if not isinstance(existing, list): + existing = self._native[field_name] = [existing] + existing.append(type_val['value']) else: self._native[field_name] = type_val['value'] return self._native -- cgit v1.2.3 From 35fb7791c4709a1f436fe1e7bcceace45acc9967 Mon Sep 17 00:00:00 2001 From: Alexander Duryagin Date: Wed, 16 Dec 2015 20:44:57 +0300 Subject: proper handling of repeated certificate subject fields - test case --- tests/fixtures/self-signed-repeated-subject-fields.der | Bin 0 -> 985 bytes tests/test_x509.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/fixtures/self-signed-repeated-subject-fields.der diff --git a/tests/fixtures/self-signed-repeated-subject-fields.der b/tests/fixtures/self-signed-repeated-subject-fields.der new file mode 100644 index 0000000..dc12751 Binary files /dev/null and b/tests/fixtures/self-signed-repeated-subject-fields.der differ diff --git a/tests/test_x509.py b/tests/test_x509.py index 500b4f9..0a638f6 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -3124,3 +3124,19 @@ class X509Tests(unittest.TestCase): ], extensions.native ) + + def test_repeated_subject_fields(self): + cert = self._load_cert('self-signed-repeated-subject-fields.der') + self.assertEqual( + cert.subject.native, + util.OrderedDict([ + ('country_name', 'RU'), + ('state_or_province_name', 'Some'), + ('locality_name', 'Any'), + ('organization_name', 'Org'), + ('organizational_unit_name', 'OrgUnit'), + ('common_name', 'zzz.yyy.domain.tld'), + ('email_address', 'no@email'), + ('domain_component', ['zzz', 'yyy', 'domain', 'tld']) + ]) + ) -- cgit v1.2.3 From e48c75a71cac1f1b9aaf824e30387ad7c9816afc Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 17 Dec 2015 11:25:11 -0500 Subject: Version 0.15.1 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 20 ++++++++++++++++++++ readme.md | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 256adae..c0cad19 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.15.0' -__version_info__ = (0, 15, 0) +__version__ = '0.15.1' +__version_info__ = (0, 15, 1) diff --git a/changelog.md b/changelog.md index 2363cd0..bd78732 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,25 @@ # changelog +## 0.15.1 + + - Fixed `cms.CMSAttributes` to be a `core.SetOf` instead of `core.SequenceOf` + - `cms.CMSAttribute` can now parse unknown attribute contrustruct without an + exception being raised + - `x509.PolicyMapping` now uses `x509.PolicyIdentifier` for field types + - Fixed `pdf.RevocationInfoArchival` so that all fields are now of the type + `core.SequenceOf` instead of a single value + - Added support for the `name_distinguisher`, `telephone_number` and + `organization_identifier` OIDs to `x509.Name` + - Fixed `x509.Name.native` to not accidentally create nested lists when three + of more values for a single type are part of the name + - `x509.Name.human_friendly` now reverses the order of fields when the data + in an `x509.Name` was encoded in most-specific to least-specific order, which + is the opposite of the standard way of least-specific to most-specific. + - `x509.NameType.human_friendly` no longer raises an exception when an + unknown OID is encountered + - Raise a `ValueError` when parsing a `core.Set` and an unknown field is + encountered + ## 0.15.0 - Added support for the TLS feature extension from RFC 7633 diff --git a/readme.md b/readme.md index 6f49fa0..1e3e787 100644 --- a/readme.md +++ b/readme.md @@ -103,7 +103,7 @@ faster to an order of magnitude of more. ## Current Release -0.15.0 - [changelog](changelog.md) +0.15.1 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From bcb422986b160f8fd65d00f866174d29a3596bde Mon Sep 17 00:00:00 2001 From: Peter Sagerson Date: Thu, 21 Jan 2016 17:37:09 -0800 Subject: Add .public_key_info accessor to PrivateKeyInfo. --- asn1crypto/keys.py | 34 +++++++++++++++++++--------------- tests/test_keys.py | 9 +++++++++ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 2f79fad..32b1cfc 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -802,6 +802,16 @@ class PrivateKeyInfo(Sequence): return self._public_key + @property + def public_key_info(self): + """ + :return: + A PublicKeyInfo object derived from this private key. + """ + params = self['private_key_algorithm']['parameters'] + + return PublicKeyInfo.wrap(self.public_key, self.algorithm, params) + @property def fingerprint(self): """ @@ -943,15 +953,19 @@ class PublicKeyInfo(Sequence): _sha256 = None @classmethod - def wrap(cls, public_key, algorithm): + def wrap(cls, public_key, algorithm, parameters=Null()): """ Wraps a public key in a PublicKeyInfo structure :param public_key: - A byte string or Asn1Value object of the public key + An RSAPublicKey, Integer, or ECPointBitString, according to the + algorithm. :param algorithm: - A unicode string of "rsa" + A unicode string of "rsa", "dsa", or "ec". + + :param parameters: + An Asn1Value object for the algorithm parameters, if applicable. :return: A PublicKeyInfo object @@ -965,23 +979,13 @@ class PublicKeyInfo(Sequence): type_name(public_key) )) - if algorithm != 'rsa': - raise ValueError(unwrap( - ''' - algorithm must "rsa", not %s - ''', - repr(algorithm) - )) - algo = PublicKeyAlgorithm() algo['algorithm'] = PublicKeyAlgorithmId(algorithm) - algo['parameters'] = Null() + algo['parameters'] = parameters container = cls() container['algorithm'] = algo - if isinstance(public_key, Asn1Value): - public_key = public_key.untag().dump() - container['public_key'] = ParsableOctetBitString(public_key) + container['public_key'] = public_key return container diff --git a/tests/test_keys.py b/tests/test_keys.py index 70b0ae2..98f9d30 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -403,6 +403,15 @@ class KeysTests(unittest.TestCase): self.assertEqual(public_key['public_key'].native, private_key.public_key.native) + @data('key_pairs', True) + def public_key_info_property(self, private_key_file, public_key_file, *_): + with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: + private_key = keys.PrivateKeyInfo.load(f.read()) + with open(os.path.join(fixtures_dir, public_key_file), 'rb') as f: + public_key = keys.PublicKeyInfo.load(f.read()) + + self.assertEqual(public_key.dump(), private_key.public_key_info.dump()) + @data('key_pairs', True) def algorithm_name(self, private_key_file, public_key_file, algorithm, _): with open(os.path.join(fixtures_dir, private_key_file), 'rb') as f: -- cgit v1.2.3 From 59960eab74aae03548fd45f07f78c9f2b2a17205 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 14 Mar 2016 21:46:39 -0400 Subject: Fix compatibility with newer versions of flake8 --- asn1crypto/_iri.py | 10 +++------- asn1crypto/_types.py | 5 +++++ asn1crypto/x509.py | 8 +------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/asn1crypto/_iri.py b/asn1crypto/_iri.py index 57342d3..884c2a7 100644 --- a/asn1crypto/_iri.py +++ b/asn1crypto/_iri.py @@ -15,6 +15,9 @@ import codecs import re import sys +from ._errors import unwrap +from ._types import byte_cls, str_cls, type_name, bytes_to_list + if sys.version_info < (3,): from urlparse import urlsplit, urlunsplit from urllib import ( @@ -22,8 +25,6 @@ if sys.version_info < (3,): unquote as unquote_to_bytes, ) - bytes_to_list = lambda byte_string: [ord(b) for b in byte_string] - else: from urllib.parse import ( quote as urlquote, @@ -32,11 +33,6 @@ else: urlunsplit, ) - bytes_to_list = list - -from ._errors import unwrap -from ._types import byte_cls, str_cls, type_name - def iri_to_uri(value): """ diff --git a/asn1crypto/_types.py b/asn1crypto/_types.py index a82a9e9..93d485e 100644 --- a/asn1crypto/_types.py +++ b/asn1crypto/_types.py @@ -10,11 +10,16 @@ if sys.version_info < (3,): byte_cls = str int_types = (int, long) # noqa + def bytes_to_list(byte_string): + return [ord(b) for b in byte_string] + else: str_cls = str byte_cls = bytes int_types = int + bytes_to_list = list + def type_name(value): """ diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index d20c27f..2386753 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -26,7 +26,7 @@ import unicodedata from ._errors import unwrap from ._iri import iri_to_uri, uri_to_iri from ._ordereddict import OrderedDict -from ._types import type_name, str_cls +from ._types import type_name, str_cls, bytes_to_list from .algos import SignedDigestAlgorithm from .core import ( Any, @@ -59,12 +59,6 @@ from .core import ( from .keys import PublicKeyInfo from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton -if sys.version_info < (3,): - bytes_to_list = lambda byte_string: [ord(b) for b in byte_string] - -else: - bytes_to_list = list - # The structures in this file are taken from https://tools.ietf.org/html/rfc5280 # and a few other supplementary sources, mostly due to extra supported -- cgit v1.2.3 From 88ba5ee8b5e5627648f736a27516cf176b7e4e61 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 17 Mar 2016 11:39:22 -0400 Subject: Add algos.KeyExchangeAlgorithm,/lgos.DHParameters, DH key support to keys.PublicKeyAlgorithm --- asn1crypto/algos.py | 30 ++++++++++++++++++++++++++++++ asn1crypto/keys.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 9692d26..fbf26ce 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -336,6 +336,36 @@ class KdfAlgorithm(Sequence): } +class DHParameters(Sequence): + """ + Original Name: DHParameter + Source: ftp://ftp.rsasecurity.com/pub/pkcs/ascii/pkcs-3.asc section 9 + """ + + _fields = [ + ('p', Integer), + ('g', Integer), + ('private_value_length', Integer, {'optional': True}), + ] + + +class KeyExchangeAlgorithmId(ObjectIdentifier): + _map = { + '1.2.840.113549.1.3.1': 'dh', + } + + +class KeyExchangeAlgorithm(Sequence): + _fields = [ + ('algorithm', KeyExchangeAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'dh': DHParameters, + } + + class Rc2Params(Sequence): _fields = [ ('rc2_parameter_version', Integer, {'optional': True}), diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 2f79fad..5dad1fc 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -34,6 +34,7 @@ from .algos import DigestAlgorithm, EncryptionAlgorithm from .core import ( Any, Asn1Value, + BitString, Choice, Integer, IntegerOctetString, @@ -876,6 +877,31 @@ class EncryptedPrivateKeyInfo(Sequence): # These structures are from https://tools.ietf.org/html/rfc3279 +class ValidationParms(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3279#page-10 + """ + + _fields = [ + ('seed', BitString), + ('pgen_counter', Integer), + ] + + +class DomainParameters(Sequence): + """ + Source: https://tools.ietf.org/html/rfc3279#page-10 + """ + + _fields = [ + ('p', Integer), + ('g', Integer), + ('q', Integer), + ('j', Integer, {'optional': True}), + ('validation_params', ValidationParms, {'optional': True}), + ] + + class PublicKeyAlgorithmId(ObjectIdentifier): """ Original Name: None @@ -889,6 +915,8 @@ class PublicKeyAlgorithmId(ObjectIdentifier): '1.2.840.10040.4.1': 'dsa', # https://tools.ietf.org/html/rfc3279#page-13 '1.2.840.10045.2.1': 'ec', + # https://tools.ietf.org/html/rfc3279#page-10 + '1.2.840.10046.2.1': 'dh', } @@ -908,6 +936,7 @@ class PublicKeyAlgorithm(Sequence): 'rsa': Null, 'dsa': DSAParams, 'ec': ECDomainParameters, + 'dh': DomainParameters, } @@ -930,6 +959,7 @@ class PublicKeyInfo(Sequence): # We override the field spec with ECPoint so that users can easily # decompose the byte string into the constituent X and Y coords 'ec': (ECPointBitString, None), + 'dh': Integer, }[algorithm] _spec_callbacks = { -- cgit v1.2.3 From 50f78762e26c816382577ef4ec922f6144c14940 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 17 Mar 2016 11:49:22 -0400 Subject: Version 0.16.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 8 ++++++++ readme.md | 3 ++- setup.py | 7 ++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index c0cad19..029d07b 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.15.1' -__version_info__ = (0, 15, 1) +__version__ = '0.16.0' +__version_info__ = (0, 16, 0) diff --git a/changelog.md b/changelog.md index bd78732..c3723f5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # changelog +## 0.16.0 + + - Added DH key exchange structures: `algos.KeyExchangeAlgorithm`, + `algos.KeyExchangeAlgorithmId` and `algos.DHParameters`. + - Added DH public key support to `keys.PublicKeyInfo`, + `keys.PublicKeyAlgorithm` and `keys.PublicKeyAlgorithmId`. New structures + include `keys.DomainParameters` and `keys.ValidationParms`. + ## 0.15.1 - Fixed `cms.CMSAttributes` to be a `core.SetOf` instead of `core.SequenceOf` diff --git a/readme.md b/readme.md index 1e3e787..4806147 100644 --- a/readme.md +++ b/readme.md @@ -30,6 +30,7 @@ a bunch of ASN.1 structures for use with various common cryptography standards: | PKCS#1 v2.1 (RSA keys) | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3447](https://tools.ietf.org/html/rfc3447) | | DSA keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [RFC 3279](https://tools.ietf.org/html/rfc3279) | | Elliptic curve keys | [`asn1crypto.keys`](asn1crypto/keys.py) | [SECG SEC1 V2](http://www.secg.org/sec1-v2.pdf) | +| PKCS#3 v1.4 | [`asn1crypto.algos`](asn1crypto/algos.py) | [PKCS#3 v1.4](ftp://ftp.rsasecurity.com/pub/pkcs/ascii/pkcs-3.asc) | | PKCS#5 v2.1 | [`asn1crypto.algos`](asn1crypto/algos.py) | [PKCS#5 v2.1](http://www.emc.com/collateral/white-papers/h11302-pkcs5v2-1-password-based-cryptography-standard-wp.pdf) | | CMS (and PKCS#7) | [`asn1crypto.cms`](asn1crypto/cms.py) | [RFC 5652](https://tools.ietf.org/html/rfc5652), [RFC 2315](https://tools.ietf.org/html/rfc2315) | | TSP | [`asn1crypto.tsp`](asn1crypto/tsp.py) | [RFC 3161](https://tools.ietf.org/html/rfc3161) | @@ -103,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.15.1 - [changelog](changelog.md) +0.16.0 - [changelog](changelog.md) ## Dependencies diff --git a/setup.py b/setup.py index f96302c..85f04e9 100644 --- a/setup.py +++ b/setup.py @@ -35,8 +35,9 @@ setup( version=asn1crypto.__version__, description=( - 'Fast ASN.1 parser and serializer with definitions for private keys, public keys, ' - 'certificates, CRL, OCSP, CMS, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP' + 'Fast ASN.1 parser and serializer with definitions for private keys, ' + 'public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, ' + 'PKCS#12, PKCS#5, X.509 and TSP' ), long_description='Docs for this project are maintained at https://github.com/wbond/asn1crypto#readme.', @@ -65,7 +66,7 @@ setup( 'Topic :: Security :: Cryptography', ], - keywords='asn1 crypto pki x509 certificate rsa dsa ec', + keywords='asn1 crypto pki x509 certificate rsa dsa ec dh', packages=find_packages(exclude=['tests*', 'dev*']), -- cgit v1.2.3 From 0a94e28dbd10413b803067711ebdd32fb341fef3 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 17 Mar 2016 11:54:44 -0400 Subject: Add "release" task to run.py --- dev/release.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 8 ++++++++ run.py | 7 +++++-- 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 dev/release.py diff --git a/dev/release.py b/dev/release.py new file mode 100644 index 0000000..65981f4 --- /dev/null +++ b/dev/release.py @@ -0,0 +1,61 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import os +import subprocess +import sys + +import setuptools.sandbox +import twine.cli + + +base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +setup_file = os.path.join(base_dir, 'setup.py') + + +def run(): + """ + Creates a sdist .tar.gz and a bdist_wheel --univeral .whl and uploads + them to pypi + + :return: + A bool - if the packaging and upload process was successful + """ + + git_wc_proc = subprocess.Popen( + ['git', 'status', '--porcelain', '-uno'], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=base_dir + ) + git_wc_status, _ = git_wc_proc.communicate() + + if len(git_wc_status) > 0: + print(git_wc_status.decode('utf-8').rstrip(), file=sys.stderr) + print('Unable to perform release since working copy is not clean', file=sys.stderr) + return False + + git_tag_proc = subprocess.Popen( + ['git', 'tag', '-l', '--contains', 'HEAD'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=base_dir + ) + tag, tag_error = git_tag_proc.communicate() + + if len(tag_error) > 0: + print(tag_error.decode('utf-8').rstrip(), file=sys.stderr) + print('Error looking for current git tag', file=sys.stderr) + return False + + if len(tag) == 0: + print('No git tag found on HEAD', file=sys.stderr) + return False + + tag = tag.decode('ascii') + + setuptools.sandbox.run_setup( + setup_file, + ['sdist', 'bdist_wheel', '--universal'] + ) + twine.cli.dispatch(['upload', 'dist/asn1crypto-%s*' % tag]) diff --git a/readme.md b/readme.md index 4806147..b790514 100644 --- a/readme.md +++ b/readme.md @@ -179,3 +179,11 @@ The following commands will run the linter and test coverage: python run.py lint python run.py coverage ``` + +After creating a [semver](http://semver.org/) git tag, a `.tar.gz` and `.whl` +of the package can be created and uploaded to +[PyPi](https://pypi.python.org/pypi/asn1crypto) by executing: + +```bash +python run.py release +``` diff --git a/run.py b/run.py index 61d68a2..1c35d09 100644 --- a/run.py +++ b/run.py @@ -11,7 +11,7 @@ else: def show_usage(): - print('Usage: run.py (lint | tests [regex] | coverage | ci)', file=sys.stderr) + print('Usage: run.py (lint | tests [regex] | coverage | ci | release)', file=sys.stderr) sys.exit(1) @@ -29,7 +29,7 @@ if len(sys.argv) < 2 or len(sys.argv) > 3: task = get_arg(1) -if task not in set(['lint', 'tests', 'coverage', 'ci']): +if task not in set(['lint', 'tests', 'coverage', 'ci', 'release']): show_usage() if task != 'tests' and len(sys.argv) == 3: @@ -51,5 +51,8 @@ elif task == 'coverage': elif task == 'ci': from dev.ci import run +elif task == 'release': + from dev.release import run + result = run(*params) sys.exit(int(not result)) -- cgit v1.2.3 From 99459d335027cca6adfd978dabae5fc1105faa57 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 17 Mar 2016 11:58:20 -0400 Subject: Have "release" task run "setup.py cleanup" when done --- dev/release.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev/release.py b/dev/release.py index 65981f4..c3a4bbd 100644 --- a/dev/release.py +++ b/dev/release.py @@ -58,4 +58,10 @@ def run(): setup_file, ['sdist', 'bdist_wheel', '--universal'] ) + twine.cli.dispatch(['upload', 'dist/asn1crypto-%s*' % tag]) + + setuptools.sandbox.run_setup( + setup_file, + ['clean'] + ) -- cgit v1.2.3 From 54dc6851163e68147c78f79f77f40792a276f1d1 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 18 Mar 2016 12:24:41 -0400 Subject: Added x509.TrustedCertificate() to support OpenSSL key usage info --- asn1crypto/core.py | 268 ++++++++++++++++++++++++++++++++ asn1crypto/x509.py | 29 +++- tests/fixtures/sender_dummycorp.com.crt | 33 ++++ tests/test_x509.py | 32 ++++ 4 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/sender_dummycorp.com.crt diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 9ca5f0a..dd886c0 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -948,6 +948,274 @@ class Choice(Asn1Value): return self._header + self.contents +class Concat(object): + """ + A class that contains two or more encoded child values concatentated + together. THIS IS NOT PART OF THE ASN.1 SPECIFICATION! This exists to handle + the x509.TrustedCertificate() class for OpenSSL certificates containing + extra information. + """ + + # A list of the specs of the concatenated values + _child_specs = None + + _children = None + + @classmethod + def load(cls, encoded_data): + """ + Loads a BER/DER-encoded byte string using the current class as the spec + + :param encoded_data: + A byte string of BER or DER encoded data + + :return: + A Concat object + """ + + return cls(contents=encoded_data) + + def __init__(self, contents=None): + """ + :param contents: + A byte string of the encoded contents of the value + + :raises: + ValueError - when an error occurs with one of the children + TypeError - when an error occurs with one of the children + """ + + try: + contents_len = len(contents) + self._children = [] + + offset = 0 + for spec in self._child_specs: + if offset < contents_len: + value, bytes_read = _parse_build(contents[offset:], spec=spec) + offset += bytes_read + else: + value = spec() + self._children.append(value) + + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + def __str__(self): + """ + Since str is differnt in Python 2 and 3, this calls the appropriate + method, __unicode__() or __bytes__() + + :return: + A unicode string + """ + + if py2: + return self.__bytes__() + else: + return self.__unicode__() + + def __bytes__(self): + """ + A byte string of the DER-encoded contents + """ + + return self.dump() + + def __unicode__(self): + """ + :return: + A unicode string + """ + + return repr(self) + + def __repr__(self): + """ + :return: + A unicode string + """ + + return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump())) + + def __copy__(self): + """ + Implements the copy.copy() interface + + :return: + A new shallow copy of the Concat object + """ + + new_obj = self.__class__() + new_obj._copy(self, copy.copy) + return new_obj + + def __deepcopy__(self, memo): + """ + Implements the copy.deepcopy() interface + + :param memo: + A dict for memoization + + :return: + A new deep copy of the Concat object and all child objects + """ + + new_obj = self.__class__() + memo[id(self)] = new_obj + new_obj._copy(self, copy.deepcopy) + return new_obj + + def copy(self): + """ + Copies the object + + :return: + A Concat object + """ + + return copy.deepcopy(self) + + def _copy(self, other, copy_func): + """ + Copies the contents of another Concat 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 + """ + + if self.__class__ != other.__class__: + raise TypeError(unwrap( + ''' + Can not copy values from %s object to %s object + ''', + type_name(other), + type_name(self) + )) + + self._children = copy_func(other._children) + + def debug(self, nest_level=1): + """ + Show the binary data and parsed data in a tree structure + """ + + prefix = ' ' * nest_level + print('%s%s Object #%s' % (prefix, type_name(self), id(self))) + print('%s Children:' % (prefix,)) + for child in self._children: + child.debug(nest_level + 2) + + def dump(self, force=False): + """ + Encodes the value using DER + + :param force: + If the encoded contents already exist, clear them and regenerate + to ensure they are in DER format instead of BER format + + :return: + A byte string of the DER-encoded value + """ + + contents = b'' + for child in self._children: + contents += child.dump(force=force) + return contents + + @property + def contents(self): + """ + :return: + A byte string of the DER-encoded contents of the children + """ + + return self.dump() + + def __len__(self): + """ + :return: + Integer + """ + + return len(self._children) + + def __getitem__(self, key): + """ + Allows accessing children by index + + :param key: + An integer of the child index + + :raises: + KeyError - when an index is invalid + + :return: + The Asn1Value object of the child specified + """ + + if key > len(self._child_specs) - 1 or key < 0: + raise KeyError(unwrap( + ''' + No child is definition for position %d of %s + ''', + key, + type_name(self) + )) + + return self._children[key] + + def __setitem__(self, key, value): + """ + Allows settings children by index + + :param key: + An integer of the child index + + :param value: + An Asn1Value object to set the child to + + :raises: + KeyError - when an index is invalid + ValueError - when the value is not an instance of Asn1Value + """ + + if key > len(self._child_specs) - 1 or key < 0: + raise KeyError(unwrap( + ''' + No child is definition for position %d of %s + ''', + key, + type_name(self) + )) + + if not isinstance(value, Asn1Value): + raise ValueError(unwrap( + ''' + Value for child %s of %s is not an instance of + asn1crypto.core.Asn1Value + ''', + key, + type_name(self) + )) + + self._children[key] = value + + def __iter__(self): + """ + :return: + An iterator of child values + """ + + return iter(self._children) + + class Primitive(Asn1Value): """ Sets the class_ and method attributes for primitive, universal values diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 2386753..5e847d4 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -27,13 +27,14 @@ from ._errors import unwrap from ._iri import iri_to_uri, uri_to_iri from ._ordereddict import OrderedDict from ._types import type_name, str_cls, bytes_to_list -from .algos import SignedDigestAlgorithm +from .algos import AlgorithmIdentifier, SignedDigestAlgorithm from .core import ( Any, BitString, BMPString, Boolean, Choice, + Concat, GeneralizedTime, GeneralString, IA5String, @@ -2506,3 +2507,29 @@ class Certificate(Sequence): return True return False + + +# The structures are taken from the OpenSSL source file x_x509a.c, and specify +# extra information that is added to X.509 certificates to store trust +# information about the certificate. + +class KeyPurposeIdentifiers(SequenceOf): + _child_spec = KeyPurposeId + + +class SequenceOfAlgorithmIdentifiers(SequenceOf): + _child_spec = AlgorithmIdentifier + + +class CertificateAux(Sequence): + _fields = [ + ('trust', KeyPurposeIdentifiers, {'optional': True}), + ('reject', KeyPurposeIdentifiers, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('alias', UTF8String, {'optional': True}), + ('keyid', OctetString, {'optional': True}), + ('other', SequenceOfAlgorithmIdentifiers, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ] + + +class TrustedCertificate(Concat): + _child_specs = [Certificate, CertificateAux] diff --git a/tests/fixtures/sender_dummycorp.com.crt b/tests/fixtures/sender_dummycorp.com.crt new file mode 100644 index 0000000..fe13daf --- /dev/null +++ b/tests/fixtures/sender_dummycorp.com.crt @@ -0,0 +1,33 @@ +-----BEGIN TRUSTED CERTIFICATE----- +MIIFijCCA3ICAQEwDQYJKoZIhvcNAQEFBQAwgYoxCzAJBgNVBAYTAlVTMQswCQYD +VQQIEwJWQTEQMA4GA1UEBxMHSGVybmRvbjEhMB8GA1UEChMYSW50ZXJuZXQgV2lk +Z2l0cyBQdHkgTHRkMRQwEgYDVQQDEwtKb2huIERvZWJveTEjMCEGCSqGSIb3DQEJ +ARYUam9obmRvZWJveUBnbWFpbC5jb20wHhcNMTUxMjA0MTgzMjMxWhcNMTYxMjAz +MTgzMjMxWjCBijELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlZBMRAwDgYDVQQHEwdI +ZXJuZG9uMSEwHwYDVQQKExhJbnRlcm5ldCBHYWRnZXRzIFB0eSBMdGQxFDASBgNV +BAMTC0Zha2UgU2VuZGVyMSMwIQYJKoZIhvcNAQkBFhRzZW5kZXJAZHVtbXljb3Jw +LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL8ySfdlfwAoUHhX +OLcNxpWDfn0uQ88uY/RDk+XMbS3t+UrGzdDMoe2gk7zjnMRpdHsg705DkEm24B+8 +vqideZD2CqWuF6oLnfvwQFaQplDVO4BFmYNNkaLfXO69KUgSa4EBc1FhLfELs8zV +3YfE1lAtzvCRwedCvFd2IqO4XjJO2u/DSpgt4zZVW6QyR15PMxF56qmqk8YjsBns +8dzKQRtxsY6KOVfHMjGLnmRrTsf+iaWMOfXEAS267xx9RMrna4P/TYiPOMo4Pvmn +LuJwlWiBDoGsew7QU1HLIUbgiZZ6XptUMVzGloXx/M3HkMoCYmwtQYK7j2JDwoIR +bhHdnuL5+jaZcVAkYBHx5xrMB4zUJcORtFxYJaF9NEHXfF1ompNZwp0Arq3+B3ua +26dcQoIz+ghVWS9y8cHR7Slsc/e6eJVinrRy86/GMbMUU5cisE/PXFe5pUMyWsAD +L8M+xrtgcXuZEXsutPk2BA2mF78Y79C6mJQ7jIBhO5bbUSd8TtmtX7nJl06SKee9 +QmpGhsJd1BYbqRjXlvFG1a27Hghznt46vs18ER/KbIyLKlCZ9OdAoFCflsZgZ5AU +lmSsLzT+V2X/yuAIDhWWMBzpIo8RigkNJ/CfqLtNnHS8Oz10k4oJ38L2uTkuup0i +EgV7gvERJ6dbwjnXJdUP+e58og87AgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAG1T +od75s5B97E4gXm2CpHYZ70by6hM8DE5aK/dJLydB1yqvp68El1GLmKpSu7ua2+WD +R0Cf5K2ibddihWkGLl+ku2LPPpIBEfMuOrEKkSp1pmVpaN72wvTR/u1nJNXfMM2q +6Txj5O0k3vuPnNiiQVwwVzJRNY/IAa5QJkqgvih+FkWmjf+PTuAj+8Qh8Gk0/YEk +Y2A8V0q3mRBEjYQzKdhKBywxjOBlxBeTse/0xXn/dds8B30C3iEeeYRhDWOPoC99 +dbcdkBqANNdpuVG3BcEQarLWKFfK74g+s133PuRIHPCn1TDqcuuhH3PQRhcOd/CO +9a8/Lx92aye+XkIHr2KQKGyQHS+dh5o+ceRZxuhJAn6R3yNDTaVwk4OGG0zSHcQg +zP/7HX3JmPSFpmEcx/Pd6F0goi02ltL2gX7wKC1J495kHy1D/GWXOxcephGAY/y7 +khP4ocxzz7EXk9ArDObEO46+1y+EHw1ilIc9LFedq2MfajtSmXRD92PVbOn6TKCr +39GZ8QWVz2kB4T7OtfzqEbI6ITOUkuA7b9q3Kszv3lLo5LlWKzkv3P3jBzFBZEMS +ITSPevLCily+YS54/psiZb3aIz+IttVhoaclUaIDpTck67oKHKh8gDSxPm8GjrmL +XBq+WcY1hW02CrrvE4q+KOcZBhVZqr+BDD0nferFMDUwCgYIKwYBBQUHAwSgFAYI +KwYBBQUHAwIGCCsGAQUFBwMBDBFTZWxmIFNpZ25lZCBTTUlNRQ== +-----END TRUSTED CERTIFICATE----- diff --git a/tests/test_x509.py b/tests/test_x509.py index 0a638f6..5526406 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -3140,3 +3140,35 @@ class X509Tests(unittest.TestCase): ('domain_component', ['zzz', 'yyy', 'domain', 'tld']) ]) ) + + def test_trusted_certificate(self): + with open(os.path.join(fixtures_dir, 'sender_dummycorp.com.crt'), 'rb') as f: + cert_bytes = f.read() + if pem.detect(cert_bytes): + _, _, cert_bytes = pem.unarmor(cert_bytes) + trusted_cert = x509.TrustedCertificate.load(cert_bytes) + + cert = trusted_cert[0] + aux = trusted_cert[1] + + self.assertEqual( + cert.subject.native, + util.OrderedDict([ + ('country_name', 'US'), + ('state_or_province_name', 'VA'), + ('locality_name', 'Herndon'), + ('organization_name', 'Internet Gadgets Pty Ltd'), + ('common_name', 'Fake Sender'), + ('email_address', 'sender@dummycorp.com'), + ]) + ) + + self.assertEqual( + aux['trust'].native, + ['email_protection'] + ) + + self.assertEqual( + aux['reject'].native, + ['client_auth', 'server_auth'] + ) -- cgit v1.2.3 From a316f276474e2c87f6f1c0e8e71ea767541830b8 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 18 Mar 2016 12:25:29 -0400 Subject: Fixed raises documentation on core.Sequence --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index dd886c0..ad62d02 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2597,7 +2597,7 @@ class Sequence(Asn1Value): A unicode string of the field name, or an integer of the field index :raises: - ValueError - when a field name or index is invalid + KeyError - when a field name or index is invalid :return: The Asn1Value object of the field specified -- cgit v1.2.3 From d17a77d6760b9fdc9e6a6d159c6f85f51b5f2949 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 18 Mar 2016 12:26:14 -0400 Subject: Added many OIDs to x509.KeyPurposeId(), changed "wireless_access_points" to "capwap_wtp" --- asn1crypto/x509.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5e847d4..5da3dce 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1528,6 +1528,7 @@ class PolicyConstraints(Sequence): class KeyPurposeId(ObjectIdentifier): _map = { + # https://tools.ietf.org/html/rfc5280#page-45 '2.5.29.37.0': 'any_extended_key_usage', '1.3.6.1.5.5.7.3.1': 'server_auth', '1.3.6.1.5.5.7.3.2': 'client_auth', @@ -1538,7 +1539,53 @@ class KeyPurposeId(ObjectIdentifier): '1.3.6.1.5.5.7.3.7': 'ipsec_user', '1.3.6.1.5.5.7.3.8': 'time_stamping', '1.3.6.1.5.5.7.3.9': 'ocsp_signing', - '1.3.6.1.5.5.7.3.19': 'wireless_access_points', + # http://tools.ietf.org/html/rfc3029.html#page-9 + '1.3.6.1.5.5.7.3.10': 'dvcs', + # http://tools.ietf.org/html/rfc6268.html#page-16 + '1.3.6.1.5.5.7.3.13': 'eap_over_ppp', + '1.3.6.1.5.5.7.3.14': 'eap_over_lan', + # https://tools.ietf.org/html/rfc5055#page-76 + '1.3.6.1.5.5.7.3.15': 'scvp_server', + '1.3.6.1.5.5.7.3.16': 'scvp_client', + # https://tools.ietf.org/html/rfc4945#page-31 + '1.3.6.1.5.5.7.3.17': 'ipsec_ike', + # https://tools.ietf.org/html/rfc5415#page-38 + '1.3.6.1.5.5.7.3.18': 'capwap_ac', + '1.3.6.1.5.5.7.3.19': 'capwap_wtp', + # https://tools.ietf.org/html/rfc5924#page-8 + '1.3.6.1.5.5.7.3.20': 'sip_domain', + # https://tools.ietf.org/html/rfc6187#page-7 + '1.3.6.1.5.5.7.3.21': 'secure_shell_client', + '1.3.6.1.5.5.7.3.22': 'secure_shell_server', + # https://tools.ietf.org/html/rfc6494#page-7 + '1.3.6.1.5.5.7.3.23': 'send_router', + '1.3.6.1.5.5.7.3.24': 'send_proxied_router', + '1.3.6.1.5.5.7.3.25': 'send_owner', + '1.3.6.1.5.5.7.3.26': 'send_proxied_owner', + # https://tools.ietf.org/html/rfc6402#page-10 + '1.3.6.1.5.5.7.3.27': 'cmc_ca', + '1.3.6.1.5.5.7.3.28': 'cmc_ra', + '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://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', + '1.3.6.1.4.1.311.10.3.2': 'microsoft_time_stamp_signing', + '1.3.6.1.4.1.311.10.3.3': 'microsoft_server_gated', + '1.3.6.1.4.1.311.10.3.3.1': 'microsoft_serialized', + '1.3.6.1.4.1.311.10.3.4': 'microsoft_efs', + '1.3.6.1.4.1.311.10.3.4.1': 'microsoft_efs_recovery', + '1.3.6.1.4.1.311.10.3.5': 'microsoft_whql', + '1.3.6.1.4.1.311.10.3.6': 'microsoft_nt5', + '1.3.6.1.4.1.311.10.3.7': 'microsoft_oem_whql', + '1.3.6.1.4.1.311.10.3.8': 'microsoft_embedded_nt', + '1.3.6.1.4.1.311.10.3.9': 'microsoft_root_list_signer', + '1.3.6.1.4.1.311.10.3.10': 'microsoft_qualified_subordination', + '1.3.6.1.4.1.311.10.3.11': 'microsoft_key_recovery', + '1.3.6.1.4.1.311.10.3.12': 'microsoft_document_signing', + '1.3.6.1.4.1.311.10.3.13': 'microsoft_lifetime_signing', + '1.3.6.1.4.1.311.10.3.14': 'microsoft_mobile_device_software', } -- cgit v1.2.3 From 5024f071e54054079c7628d719715907715dc800 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 18 Mar 2016 14:14:10 -0400 Subject: Add support for "broken" X.509 certificates that use acore.IA5String where a509.DirectoryString should be used instead --- asn1crypto/x509.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5da3dce..dfa7d49 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -420,6 +420,8 @@ class DirectoryString(Choice): ('universal_string', UniversalString), ('utf8_string', UTF8String), ('bmp_string', BMPString), + # This is an invalid/bad alternative, but some broken certs use it + ('ia5_string', IA5String), ] -- cgit v1.2.3 From b4f2adf875c489e27bce57c081c56883ddbe32b4 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 18 Mar 2016 14:19:03 -0400 Subject: Version 0.17.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 10 ++++++++++ readme.md | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 029d07b..b7e069e 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.16.0' -__version_info__ = (0, 16, 0) +__version__ = '0.17.0' +__version_info__ = (0, 17, 0) diff --git a/changelog.md b/changelog.md index c3723f5..d741c1d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # changelog +## 0.17.0 + + - Added `x509.TrustedCertificate` for handling OpenSSL auxiliary certificate + information appended after a certificate + - Added `core.Concat` class for situations such as `x509.TrustedCertificate` + - Allow "broken" X.509 certificates to use `core.IA5String` where an + `x509.DirectoryString` should be used instead + - Added `keys.PrivateKeyInfo.public_key_info` attribute + - Added a bunch of OIDs to `x509.KeyPurposeId` + ## 0.16.0 - Added DH key exchange structures: `algos.KeyExchangeAlgorithm`, diff --git a/readme.md b/readme.md index b790514..6a6ad85 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.16.0 - [changelog](changelog.md) +0.17.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From fd2b7cbfbfe0b1ee95c2b1b82e045d0ce81333fc Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 18 Mar 2016 14:25:48 -0400 Subject: Fix a bug uploading via twine with the "release" task --- dev/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/release.py b/dev/release.py index c3a4bbd..316d75c 100644 --- a/dev/release.py +++ b/dev/release.py @@ -52,7 +52,7 @@ def run(): print('No git tag found on HEAD', file=sys.stderr) return False - tag = tag.decode('ascii') + tag = tag.decode('ascii').strip() setuptools.sandbox.run_setup( setup_file, -- cgit v1.2.3 From fdd90c68885f73f44b453fc766e2786b13573c80 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 29 Mar 2016 05:19:31 -0400 Subject: Add tox.ini for those so inclined --- .pep8 | 2 -- dev/lint.py | 2 +- tox.ini | 12 ++++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) delete mode 100644 .pep8 create mode 100644 tox.ini diff --git a/.pep8 b/.pep8 deleted file mode 100644 index 6deafc2..0000000 --- a/.pep8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 120 diff --git a/dev/lint.py b/dev/lint.py index 2ac8159..bc9dddc 100644 --- a/dev/lint.py +++ b/dev/lint.py @@ -7,7 +7,7 @@ from flake8.engine import get_style_guide cur_dir = os.path.dirname(__file__) -config_file = os.path.join(cur_dir, '..', '.pep8') +config_file = os.path.join(cur_dir, '..', 'tox.ini') def run(): diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5b59a06 --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +[tox] +envlist = py26,py27,py32,py33,py34,py35,pypy,pypy3 + +[testenv] +deps = flake8 +commands = {envpython} run.py ci + +[pep8] +max-line-length = 120 + +[flake8] +max-line-length = 120 -- cgit v1.2.3 From db9a86b532b6ffa8a6846d9ef7a56e0afdea0e82 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 29 Mar 2016 05:23:40 -0400 Subject: Ignore .tox temp dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2dc6a99..b6cd5bd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ tests/output/ .DS_Store tmp/ .coverage +.tox -- cgit v1.2.3 From 0ca5d03ac1b867ba07e54ee60c64bc6a8cf9f3eb Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 29 Mar 2016 09:30:19 -0400 Subject: Fix type issue parsing IRI with explicit port on Python 3 --- asn1crypto/_iri.py | 6 +++--- tests/fixtures/admin.ch.crt | Bin 0 -> 1800 bytes tests/test_x509.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/admin.ch.crt diff --git a/asn1crypto/_iri.py b/asn1crypto/_iri.py index 884c2a7..d6e3013 100644 --- a/asn1crypto/_iri.py +++ b/asn1crypto/_iri.py @@ -16,7 +16,7 @@ import re import sys from ._errors import unwrap -from ._types import byte_cls, str_cls, type_name, bytes_to_list +from ._types import byte_cls, str_cls, type_name, bytes_to_list, int_types if sys.version_info < (3,): from urlparse import urlsplit, urlunsplit @@ -127,7 +127,7 @@ def uri_to_iri(value): if hostname: hostname = hostname.decode('idna') port = parsed.port - if port: + if port and not isinstance(port, int_types): port = port.decode('ascii') netloc = '' @@ -139,7 +139,7 @@ def uri_to_iri(value): if hostname is not None: netloc += hostname if port is not None: - netloc += ':' + port + netloc += ':' + str_cls(port) path = _urlunquote(parsed.path, remap=['/'], preserve=True) query = _urlunquote(parsed.query, remap=['&', '='], preserve=True) diff --git a/tests/fixtures/admin.ch.crt b/tests/fixtures/admin.ch.crt new file mode 100644 index 0000000..2ec1edf Binary files /dev/null and b/tests/fixtures/admin.ch.crt differ diff --git a/tests/test_x509.py b/tests/test_x509.py index 5526406..007b979 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -3172,3 +3172,36 @@ class X509Tests(unittest.TestCase): aux['reject'].native, ['client_auth', 'server_auth'] ) + + def test_iri_with_port(self): + with open(os.path.join(fixtures_dir, 'admin.ch.crt'), 'rb') as f: + cert_bytes = f.read() + if pem.detect(cert_bytes): + _, _, cert_bytes = pem.unarmor(cert_bytes) + cert = x509.Certificate.load(cert_bytes) + + self.assertEqual( + [dp.native for dp in cert.crl_distribution_points], + [ + util.OrderedDict([ + ('distribution_point', ['http://www.pki.admin.ch/crl/SSLCA01.crl']), + ('reasons', None), + ('crl_issuer', None) + ]), + util.OrderedDict([ + ( + 'distribution_point', + [ + 'ldap://admindir.admin.ch:389/' + 'cn=Swiss Government SSL CA 01,' + 'ou=Certification Authorities,' + 'ou=Services,' + 'o=Admin,' + 'c=CH' + ] + ), + ('reasons', None), + ('crl_issuer', None) + ]) + ] + ) -- cgit v1.2.3 From 601e6b47ffa9dc83d12011ccaf8e0457291a4b44 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 29 Mar 2016 09:33:25 -0400 Subject: Version 0.17.1 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 5 +++++ readme.md | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index b7e069e..554ed86 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.17.0' -__version_info__ = (0, 17, 0) +__version__ = '0.17.1' +__version_info__ = (0, 17, 1) diff --git a/changelog.md b/changelog.md index d741c1d..0ee1d2a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # changelog +## 0.17.1 + + - Fix a bug in `x509.URI` parsing IRIs containing explicit port numbers on + Python 3.x + ## 0.17.0 - Added `x509.TrustedCertificate` for handling OpenSSL auxiliary certificate diff --git a/readme.md b/readme.md index 6a6ad85..9394284 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.17.0 - [changelog](changelog.md) +0.17.1 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 72017002a032ea9eb7b0969e7e5aa607e7da0bbf Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 31 Mar 2016 05:09:48 -0400 Subject: Configure CI to skip tags [ci skip] --- .travis.yml | 3 +++ appveyor.yml | 1 + 2 files changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index d5aab8a..172bc19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ sudo: false language: c +branches: + except: + - /^[0-9]+\.[0-9]+\.[0-9]$/ matrix: include: - os: osx diff --git a/appveyor.yml b/appveyor.yml index 25b9ae4..db8d102 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,5 @@ version: "{build}" +skip_tags: true environment: matrix: - PYTHON: "C:\\Python26" -- cgit v1.2.3 From 6cceac23c8bfa0812878c4cf3db7f977db5c68eb Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 31 Mar 2016 05:10:56 -0400 Subject: Add test for keys.ECPointBitString.to_coords() --- tests/test_x509.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_x509.py b/tests/test_x509.py index 007b979..da97572 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -3065,6 +3065,13 @@ class X509Tests(unittest.TestCase): b'=WB\xdfx\x03\x0b\xcc1\xc6\x03\xd7\x9f`\x01', subject_public_key.native ) + self.assertEqual( + ( + 63036330335395236932063564494857090016633168203412940864166337576590847793152, + 66640105439272245186116058015235631147470323594355535909132387303736913911809 + ), + subject_public_key.to_coords() + ) self.assertEqual( None, tbs_certificate['issuer_unique_id'].native -- cgit v1.2.3 From d7a440cb48e90da5e606daaffb4ea15a1ad0f799 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 31 Mar 2016 05:19:12 -0400 Subject: Make "setup.py test" work [ci skip] --- dev/tests.py | 17 +++++++++++++++++ setup.py | 2 ++ 2 files changed, 19 insertions(+) diff --git a/dev/tests.py b/dev/tests.py index b9c54b5..14119e2 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -18,6 +18,23 @@ from tests.test_core import CoreTests test_classes = [CMSTests, CRLTests, CSRTests, KeysTests, OCSPTests, PEMTests, TSPTests, X509Tests, CoreTests] +def make_suite(): + """ + Constructs a unittest.TestSuite() of all tests for the package. For use + with setuptools. + + :return: + A unittest.TestSuite() object + """ + + loader = unittest.TestLoader() + suite = unittest.TestSuite() + for test_class in test_classes: + tests = loader.loadTestsFromTestCase(test_class) + suite.addTests(tests) + return suite + + def run(matcher=None): """ Runs the tests diff --git a/setup.py b/setup.py index 85f04e9..3cff195 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,8 @@ setup( packages=find_packages(exclude=['tests*', 'dev*']), + test_suite='dev.tests.make_suite', + cmdclass={ 'clean': CleanCommand, } -- cgit v1.2.3 From ffb857a165243823d877610e33b0b6fb57e1f4bc Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 31 Mar 2016 10:59:08 -0400 Subject: Fixed a bug with x509.Certificate.issuer_alt_name_value if it is the first extension accessed --- asn1crypto/x509.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index dfa7d49..f115d39 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1873,8 +1873,8 @@ class Certificate(Sequence): None or an x509.GeneralNames object """ - if self._processed_extensions is False: - self._processed_extensions() + if not self._processed_extensions: + self._set_extensions() return self._issuer_alt_name_value @property -- cgit v1.2.3 From b24ef050180e8640fb8d205772cc3bcacf23bf03 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 28 Apr 2016 10:41:38 -0400 Subject: Add support for Windows XP --- asn1crypto/_inet.py | 170 ++++++++++++++++++++++++++++++++++++++ asn1crypto/_win/__init__.py | 0 asn1crypto/_win/_ws2_32.py | 80 ------------------ asn1crypto/_win/_ws2_32_cffi.py | 36 -------- asn1crypto/_win/_ws2_32_ctypes.py | 33 -------- asn1crypto/util.py | 2 +- 6 files changed, 171 insertions(+), 150 deletions(-) create mode 100644 asn1crypto/_inet.py delete mode 100644 asn1crypto/_win/__init__.py delete mode 100644 asn1crypto/_win/_ws2_32.py delete mode 100644 asn1crypto/_win/_ws2_32_cffi.py delete mode 100644 asn1crypto/_win/_ws2_32_ctypes.py diff --git a/asn1crypto/_inet.py b/asn1crypto/_inet.py new file mode 100644 index 0000000..0d78aa4 --- /dev/null +++ b/asn1crypto/_inet.py @@ -0,0 +1,170 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import socket +import struct + +from ._errors import unwrap +from ._types import byte_cls, bytes_to_list, str_cls, type_name + + +def inet_ntop(address_family, packed_ip): + """ + Windows compatiblity shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param packed_ip: + A byte string of the network form of an IP address + + :return: + A unicode string of the IP address + """ + + if address_family not in set([socket.AF_INET, socket.AF_INET6]): + raise ValueError(unwrap( + ''' + address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), + not %s + ''', + repr(socket.AF_INET), + repr(socket.AF_INET6), + repr(address_family) + )) + + if not isinstance(packed_ip, byte_cls): + raise TypeError(unwrap( + ''' + packed_ip must be a byte string, not %s + ''', + type_name(packed_ip) + )) + + required_len = 4 if address_family == socket.AF_INET else 16 + if len(packed_ip) != required_len: + raise ValueError(unwrap( + ''' + packed_ip must be %d bytes long - is %d + ''', + required_len, + len(packed_ip) + )) + + if address_family == socket.AF_INET: + return '%d.%d.%d.%d' % tuple(bytes_to_list(packed_ip)) + + octets = struct.unpack(b'!HHHHHHHH', packed_ip) + + runs_of_zero = {} + longest_run = 0 + zero_index = None + for i, octet in enumerate(octets + (-1,)): + if octet != 0: + if zero_index is not None: + length = i - zero_index + if length not in runs_of_zero: + runs_of_zero[length] = zero_index + longest_run = max(longest_run, length) + zero_index = None + elif zero_index is None: + zero_index = i + + hexed = [hex(o)[2:] for o in octets] + + if longest_run < 2: + return ':'.join(hexed) + + zero_start = runs_of_zero[longest_run] + zero_end = zero_start + longest_run + + return ':'.join(hexed[:zero_start]) + '::' + ':'.join(hexed[zero_end:]) + + +def inet_pton(address_family, ip_string): + """ + Windows compatiblity shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param ip_string: + A unicode string of an IP address + + :return: + A byte string of the network form of the IP address + """ + + if address_family not in set([socket.AF_INET, socket.AF_INET6]): + raise ValueError(unwrap( + ''' + address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), + not %s + ''', + repr(socket.AF_INET), + repr(socket.AF_INET6), + repr(address_family) + )) + + if not isinstance(ip_string, str_cls): + raise TypeError(unwrap( + ''' + ip_string must be a unicode string, not %s + ''', + type_name(ip_string) + )) + + if address_family == socket.AF_INET: + octets = ip_string.split('.') + error = len(octets) != 4 + if not error: + ints = [] + for o in octets: + o = int(o) + if o > 255 or o < 0: + error = True + break + ints.append(o) + + if error: + raise ValueError(unwrap( + ''' + ip_string must be a dotted string with four integers in the + range of 0 to 255, got %s + ''', + repr(ip_string) + )) + + return struct.pack(b'!BBBB', *ints) + + error = False + omitted = ip_string.count('::') + if omitted > 1: + error = True + elif omitted == 0: + octets = ip_string.split(':') + error = len(octets) != 8 + else: + begin, end = ip_string.split('::') + begin_octets = begin.split(':') + end_octets = end.split(':') + missing = 8 - len(begin_octets) - len(end_octets) + octets = begin_octets + (['0'] * missing) + end_octets + + if not error: + ints = [] + for o in octets: + o = int(o, 16) + if o > 65535 or o < 0: + error = True + break + ints.append(o) + + return struct.pack(b'!HHHHHHHH', *ints) + + raise ValueError(unwrap( + ''' + ip_string must be a valid ipv6 string, got %s + ''', + repr(ip_string) + )) diff --git a/asn1crypto/_win/__init__.py b/asn1crypto/_win/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/asn1crypto/_win/_ws2_32.py b/asn1crypto/_win/_ws2_32.py deleted file mode 100644 index ffef85e..0000000 --- a/asn1crypto/_win/_ws2_32.py +++ /dev/null @@ -1,80 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals, division, absolute_import, print_function - -import socket - -from .._ffi import ( - buffer_from_bytes, - bytes_from_buffer, - cast_void_p, - FFIEngineError, - is_null, - string_from_buffer, - unicode_buffer, -) - -try: - from ._ws2_32_cffi import ws2_32 -except (FFIEngineError): - from ._ws2_32_ctypes import ws2_32 - - -AF_INET = 2 -AF_INET6 = 23 - - -def inet_ntop(address_family, packed_ip): - """ - Windows compatiblity shim for socket.inet_ntop(). - - :param address_family: - socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 - - :param packed_ip: - A byte string of the network form of an IP address - - :return: - A unicode string of the IP address - """ - - family = { - socket.AF_INET: AF_INET, - socket.AF_INET6: AF_INET6, - }[address_family] - - buffer_size = 46 if family == AF_INET6 else 16 - buffer = unicode_buffer(buffer_size) - packed_ip_buffer = buffer_from_bytes(packed_ip) - result = ws2_32.InetNtopW(family, cast_void_p(packed_ip_buffer), buffer, buffer_size) - if is_null(result): - raise OSError('Windows error %s calling InetNtop' % ws2_32.WSAGetLastError()) - - return string_from_buffer(buffer) - - -def inet_pton(address_family, ip_string): - """ - Windows compatiblity shim for socket.inet_ntop(). - - :param address_family: - socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 - - :param ip_string: - A unicode string of an IP address - - :return: - A byte string of the network form of the IP address - """ - - family = { - socket.AF_INET: AF_INET, - socket.AF_INET6: AF_INET6, - }[address_family] - - buffer_size = 16 if family == AF_INET6 else 4 - buffer = buffer_from_bytes(buffer_size) - result = ws2_32.InetPtonW(family, ip_string, buffer) - if result != 1: - raise OSError('Windows error %s calling InetPtob' % ws2_32.WSAGetLastError()) - - return bytes_from_buffer(buffer, buffer_size) diff --git a/asn1crypto/_win/_ws2_32_cffi.py b/asn1crypto/_win/_ws2_32_cffi.py deleted file mode 100644 index 55c9565..0000000 --- a/asn1crypto/_win/_ws2_32_cffi.py +++ /dev/null @@ -1,36 +0,0 @@ -# coding: utf-8 - -""" -cffi interface for IP translation functions in Windows. Exports the -following items: - - - ws2_32 - - InetNtop() - - InetPton() - - WSAGetLastError() -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -from .._ffi import FFIEngineError - -try: - from cffi import FFI - -except (ImportError): - raise FFIEngineError('Error importing cffi') - - -try: - ffi = FFI() - ffi.set_unicode(True) - ffi.cdef(""" - LPCWSTR InetNtopW(INT Family, void *pAddr, LPWSTR pStringBuf, size_t StringBufSize); - INT InetPtonW(INT Family, LPCWSTR pszAddrString, void *pAddrBuf); - int WSAGetLastError(void); - """) - - ws2_32 = ffi.dlopen('ws2_32.dll') - -except (AttributeError): - raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/_win/_ws2_32_ctypes.py b/asn1crypto/_win/_ws2_32_ctypes.py deleted file mode 100644 index 2745818..0000000 --- a/asn1crypto/_win/_ws2_32_ctypes.py +++ /dev/null @@ -1,33 +0,0 @@ -# coding: utf-8 - -""" -ctypes interface for IP translation functions in Windows. Exports the -following items: - - - ws2_32 - - InetNtop() - - InetPton() - - WSAGetLastError() -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -from ctypes import windll, wintypes, c_size_t, c_void_p, c_int - -from .._ffi import FFIEngineError - - -try: - ws2_32 = windll.ws2_32 - - ws2_32.InetNtopW.argtypes = [wintypes.INT, c_void_p, wintypes.LPWSTR, c_size_t] - ws2_32.InetNtopW.restype = wintypes.LPCWSTR - - ws2_32.InetPtonW.argtypes = [wintypes.INT, wintypes.LPCWSTR, c_void_p] - ws2_32.InetPtonW.restype = wintypes.INT - - ws2_32.WSAGetLastError.argtypes = [] - ws2_32.WSAGetLastError.restype = c_int - -except (AttributeError): - raise FFIEngineError('Error initializing ctypes') diff --git a/asn1crypto/util.py b/asn1crypto/util.py index c7af62d..59ecdcb 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -23,7 +23,7 @@ from ._iri import iri_to_uri, uri_to_iri # noqa from ._ordereddict import OrderedDict # noqa if sys.platform == 'win32': - from ._win._ws2_32 import inet_ntop, inet_pton + from ._inet import inet_ntop, inet_pton else: from socket import inet_ntop, inet_pton # noqa -- cgit v1.2.3 From d6aa5a07682bf49660a686c0c51fd00bc67ffe5e Mon Sep 17 00:00:00 2001 From: Walter Franzini Date: Thu, 12 May 2016 12:51:42 +0200 Subject: test file that let _parse_children raise an exception --- tests/fixtures/cms-signed-indefinite-length.der | 1414 +++++++++++++++++++++++ tests/test_cms.py | 5 + 2 files changed, 1419 insertions(+) create mode 100644 tests/fixtures/cms-signed-indefinite-length.der diff --git a/tests/fixtures/cms-signed-indefinite-length.der b/tests/fixtures/cms-signed-indefinite-length.der new file mode 100644 index 0000000..97153ba --- /dev/null +++ b/tests/fixtures/cms-signed-indefinite-length.der @@ -0,0 +1,1414 @@ +0 *H 01 0  `He0 *H  + + + 62ede6fd-5994-4fd5-8811-942ffa58c2a6 + referti-661148800 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxxxxxxxxxxxxxx + referti-661148800.rtf + + + 19494945-5ebb-4e80-9865-9125dd417105 + referti-235588799 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxxxxxxxxxxxxxx + referti-235588799.rtf + + + cc9c5e86-fad1-423d-83ab-87d8e26fdae6 + referti-552878798 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxxxxxxxxxxxxxx + referti-552878798.rtf + + + d0cbf95f-509a-4023-ac00-7b574a9b86c9 + referti-109428797 + radiologico + 2014-10-15 + Prova6 + Prova6 + PRVPV609P41C553B + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-109428797.rtf + + + 35f09d3c-8867-4fc6-920a-239e8b07e82d + referti-630498796 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-630498796.rtf + + + 76407911-06fa-4dee-b1c9-88d8dc3cf567 + referti-440568795 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-440568795.rtf + + + 66dc238e-3bca-4847-9ae2-c0a240f999b7 + referti-110088794 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-110088794.rtf + + + 25bc9613-cc6c-411c-a192-e76437734921 + referti-444868793 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-444868793.rtf + + + 7c64b012-5866-4f5f-9b99-a0db9737854f + referti-152408792 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-152408792.rtf + + + d91faafd-0c5a-4198-ab18-88d989ad3a1a + referti-517228791 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-517228791.rtf + + + eb779d2d-07d8-47b7-8576-953d7912a90d + referti-370388790 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-370388790.rtf + + + 495867e3-6906-41fd-b138-9785d35126d6 + referti-521608789 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-521608789.rtf + + + bc46bbcd-be0d-4ef8-a0d9-fa99d2fc9b5f + referti-020608788 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-020608788.rtf + + + 6ab12ed6-f23d-4799-a4d9-8f7691de9fb5 + referti-398958787 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-398958787.rtf + + + fc866f2b-0e2a-4f91-afae-c378ba08fc35 + referti-156228786 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-156228786.rtf + + + 27727aef-ec8c-429b-9ac3-55cb40ae650a + referti-873788785 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-873788785.rtf + + + c8480482-eeef-49f9-8232-536bcd8856e2 + referti-532518784 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-532518784.rtf + + + 805d6ce6-6abe-4534-88ce-f03757f56955 + referti-897268783 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-897268783.rtf + + + 47bab41d-f305-4d2b-8638-c9164b3447ad + referti-769098782 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-769098782.rtf + + + ad2a1a7f-8957-4142-8b85-31c61ae6a489 + referti-166518781 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-166518781.rtf + + + dde33d3e-c70c-4004-90ab-2513e9e6031b + referti-190628780 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-190628780.rtf + + + 6be378de-ee7d-4f8e-a8a7-26997cb8ecf3 + referti-390568779 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-390568779.rtf + + + 3e886dda-4a7b-483b-b057-bfd79b5a11b8 + referti-316778778 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-316778778.rtf + + + 69308574-93c6-47e1-b98d-468f1997d22e + referti-221808777 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-221808777.rtf + + + c2d4215f-2dab-4086-b586-7312c4309c87 + referti-468068776 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-468068776.rtf + + + 30a513db-ff1a-41b6-b4e2-3b50bc2b6b44 + referti-859498775 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-859498775.rtf + + + 7cb39aea-29e0-47a1-9a2c-f9e47df45423 + referti-004108774 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-004108774.rtf + + + 9b9709ac-58e0-46d3-bfc8-eaea3fe19c34 + referti-225178773 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-225178773.rtf + + + 36d5e7fa-e3b5-4b9c-84ef-d9f3dd56abba + referti-493808772 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-493808772.rtf + + + f5de82c0-6588-47c1-98dd-70a55efdc319 + referti-219068771 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-219068771.rtf + + + 5cba491c-a9bf-4004-8ea9-25db1fe79ddc + referti-275238770 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-275238770.rtf + + + 4f313ec7-1723-4d72-b680-6db635e37d78 + referti-715628769 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-715628769.rtf + + + 5be1b3c9-aaf6-4a7f-80b0-b76782bcdf17 + referti-908078768 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-908078768.rtf + + + 0ef7efd0-d536-44dd-946b-ed5316b240ac + referti-176518767 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-176518767.rtf + + + 325f1b16-b71e-43d3-9426-e6e5e810a409 + referti-870108766 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-870108766.rtf + + + 1ec52ea9-43da-48dc-94b5-710829ccede1 + referti-031108765 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-031108765.rtf + + + 4a306f42-24ff-49e7-b345-1ed8ac25130e + referti-167618764 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-167618764.rtf + + + 315af6a7-658a-4c52-a787-304806f79563 + referti-349608763 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-349608763.rtf + + + 47da2fbd-5277-48a7-9cc3-d45e19f780c4 + referti-451848762 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-451848762.rtf + + + 3c67953d-cf34-4e76-815b-66f30845ec95 + referti-385388761 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-385388761.rtf + + + 9ce4203f-b318-4ed2-9bb5-8b5cb8f5e6e7 + referti-237208760 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-237208760.rtf + + + fb1136ab-6c42-4ad3-bd7a-09260d6ba290 + referti-228728759 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-228728759.rtf + + + f206bc17-fc72-42a2-b7c3-f516f5f5ce4f + referti-219058758 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-219058758.rtf + + + d275b0a0-7d9e-4a26-ac59-696d9937b761 + referti-819308757 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-819308757.rtf + + + e56b8c19-f3fa-4966-b132-0189a4a4c074 + referti-820568756 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-820568756.rtf + + + 9fce3e64-fc5c-4945-8585-89c78c20b540 + referti-574168755 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-574168755.rtf + + + 6d3cc452-498d-4fb7-bc77-92551f3d8912 + referti-231048754 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-231048754.rtf + + + 7ccd4176-9177-4a74-903b-ba6a8c65947d + referti-082458753 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-082458753.rtf + + + b5298b48-61eb-4488-b7be-ee78f4076b2d + referti-421618752 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-421618752.rtf + + + 84dbd362-2da8-42bd-b03a-7c8495f7573c + referti-813398751 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-813398751.rtf + + + 8edb071e-2a45-4a67-a56d-14f6ceb8d0b8 + referti-831578750 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-831578750.rtf + + + abe1c7ac-8a8a-4c0e-975d-bf6ccf70afb8 + referti-149978749 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-149978749.rtf + + + 4b391d68-137a-4f99-9a70-5649c455a885 + referti-078958748 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-078958748.rtf + + + 84f1790a-9d60-4f26-b072-8df0d3c2a931 + referti-416148747 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-416148747.rtf + + + eb5799d8-9d38-43f5-a484-b55f0e10ceb7 + referti-173908746 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-173908746.rtf + + + 2d24cc23-c873-4266-8eb1-5be0fbe1761f + referti-670138745 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-670138745.rtf + + + b8846eb4-9b5e-4572-be80-f3add1dd2809 + referti-788608744 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-788608744.rtf + + + 37a0c26b-ecdc-4cba-bd11-bbbd770a1602 + referti-697888743 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-697888743.rtf + + + 50297573-dd4b-4c9d-b382-87555df696e9 + referti-673838742 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-673838742.rtf + + + 824228e4-552f-4ddc-98d9-f7c3d7857fc3 + referti-182208741 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-182208741.rtf + + + e6ee07c3-3e41-43e4-a4be-c899418bdd69 + referti-806938740 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-806938740.rtf + + + d88ae7b4-cd15-4ace-9eae-761b70b0b4a2 + referti-451448739 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-451448739.rtf + + + 2bd12a1c-df19-4af7-bb20-6ab14f3332c8 + referti-870028738 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-870028738.rtf + + + 52accf52-9bf2-4a5b-b1a4-d41d6afd40fe + referti-629738737 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-629738737.rtf + + + 9778f50a-5eae-4818-9619-3a2defc12fd3 + referti-462728736 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-462728736.rtf + + + fe7f5358-d01c-4d5f-b0d5-b4f3cd8d7302 + referti-459798735 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-459798735.rtf + + + 87398882-fa6b-4ece-8c01-a6748b8a1d21 + referti-517488734 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-517488734.rtf + + + 9c10bc89-724e-43f1-b3d5-3bc2a443d226 + referti-875258733 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-875258733.rtf + + + 685de4cf-7f3e-441d-a49a-061d4b4cfedb + referti-458208732 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-458208732.rtf + + + 7c43fdb5-4001-437b-8882-53bc158a69ea + referti-626758731 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-626758731.rtf + + + f0adf2ba-0649-456d-94d1-94f8e4af68af + referti-204468730 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-204468730.rtf + + + ab8451a4-8657-473a-a9ca-7cda5a0b532c + referti-474968729 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-474968729.rtf + + + 8befa776-9c9f-4875-8dde-58811a76a33c + referti-538618728 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-538618728.rtf + + + 6a3afdde-54d6-4e0c-a7f2-995ada66ee9c + referti-203608727 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-203608727.rtf + + + 2a15822a-49a0-4a8c-8231-bbc32cd8df87 + referti-919808726 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-919808726.rtf + + + 9bceb04b-b065-4386-b76a-472f705d5b6b + referti-493238725 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-493238725.rtf + + + c27aa95b-67da-46e5-b560-6c2cc7c7aab1 + referti-879388724 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-879388724.rtf + + + 5649d464-d05d-4371-9508-ee9cf7582b69 + referti-001238723 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-001238723.rtf + + + d740db69-bc94-4cdf-a4e1-ccd98ee020f6 + referti-566698722 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-566698722.rtf + + + 952e1c52-e929-43e2-9837-f03adc3754d8 + referti-157168721 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-157168721.rtf + + + d5e30ccf-69a7-4d7f-afd9-cb3d0a86289b + referti-298668720 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-298668720.rtf + + + ca3db245-b545-452e-8937-9478ca9a7084 + referti-572018719 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-572018719.rtf + + + 20e736c2-d6f5-4c9e-9a99-e8d9879a805e + referti-289688718 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-289688718.rtf + + + aea1f611-c21f-4530-a3c1-ac20cdf6f421 + referti-834918717 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-834918717.rtf + + + c4d2a6ef-1617-4bbc-9dcd-091b0a6097d4 + referti-719978716 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-719978716.rtf + + + f089d513-c6cc-4761-bbc9-df557bee6a16 + referti-765048715 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-765048715.rtf + + + 1b2e4bfa-e03a-49ed-b8af-223a6e03f783 + referti-272918714 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-272918714.rtf + + + daadc617-37c4-48bc-8589-8c995d94d9a0 + referti-154588713 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-154588713.rtf + + + 3e9044e1-8a12-41b3-a48e-368ba588cdfb + referti-881718712 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-881718712.rtf + + + 0a08de57-3869-4f69-bcff-f65ffa6a42f2 + referti-216428711 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-216428711.rtf + + + 3e929e2a-8988-4d02-8829-367479f8f559 + referti-408448710 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-408448710.rtf + + + e5d04243-d016-4040-b41b-6fb57d1a5fe1 + referti-935318709 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-935318709.rtf + + + a6dda134-2e0d-448e-9efd-7be36e940dfc + referti-456878708 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-456878708.rtf + + + b782b02d-a98b-4b23-97c0-8a2af14cd486 + referti-050918707 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-050918707.rtf + + + 58c2f33c-2234-4700-85ef-9a52e776544b + referti-062248706 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-062248706.rtf + + + 934371e9-616e-498a-8c14-33e58a7e20df + referti-406148705 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-406148705.rtf + + + 9c73bf68-0f20-4404-b49f-725eba2f9171 + referti-114928704 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-114928704.rtf + + + 41778171-5f06-475f-b5e5-689b747611d0 + referti-632428703 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-632428703.rtf + + + ea5b05f0-0c6f-4190-802c-5609cab81844 + referti-149018702 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-149018702.rtf + + + 02d51b9c-4ab5-4171-92ae-01f35fe63272 + referti-127268701 + radiologico + 2014-10-15 + Prova6 + Prova6 + XXXXXXXXXXXXXXXX + XXXXXXX + XXXXXXX + 111111111111111 + xxxxxxxx.xxxxxxx.isi + referti-127268701.rtf + + +000  *H  0`1 0 UIT10U + Postecom S.p.A.1 0U Certification Authority10U Postecom CA30 150810082556Z 180810082556Z01 0 UIT1$0"U + POSTECOM S.P.A./0583884100410U SENSIDONI FABIO10UIT:SNSFBA64A26H501V10 U* FABIO10U SENSIDONI10U.36166201'0%U RESP CONSERVAZIONE SOSTITUTIVA0"0  *H 0 +@I^ *%x [ڎ0D +b*#m7 U@VJ.)=uG+Hq&/Dq\gbyJ9_\dz *GDY>uil=Ofgnǡרs7r:.V/K̤DJ\jRӱԤLc +vN>1먝*Y\Sry٦ͦD^aR |hJz +^@\*a{100U 00+L 00+0 Il presente certificato è valido solo per firme apposte con procedura automatica./This certificate may only be used for unattended/automated digital signatures.0#+http://www.postecert.it0/+#0!0F0 F0F0:+.0,0*+0http://postecert.poste.it/ocsp0U@0U#04\&] 1K70?U8060420.http://postecert.poste.it/postecomca3/crl3.crl0UM.|<ui]j0  *H  c{"qLG M2y*<-20q"c'p4Utץjq ~]<^.\oB,l  (C.J.:ӌ'#MiCq;LzqGjp/WIj<ӆˠOv|EK̍?M?HEqPiS;(;*PD_9kT`0k0db0`1 0 UIT10U + Postecom S.p.A.1 0U Certification Authority10U Postecom CA30  *H }2 7tULN0/jKdz|6-6k׾MuDG+ Mcu'Jrgg+jҜC,f#VdhBj +1()t9-cK9VV.KZ(J2F}7&*le{J7g=sV}@ lE\G @^Bup?$7YpPUP47 diff --git a/tests/test_cms.py b/tests/test_cms.py index 19df0bc..9abb22d 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -537,6 +537,11 @@ class CMSTests(unittest.TestCase): signer['signature'].native ) + def test_parse_cms_signed_date_indefinite_length(self): + with open(os.path.join(fixtures_dir, 'cms-signed-indefinite-length.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + signed_data = info['content'] + def test_parse_content_info_cms_signed_digested_data(self): with open(os.path.join(fixtures_dir, 'cms-signed-digested.der'), 'rb') as f: info = cms.ContentInfo.load(f.read()) -- cgit v1.2.3 From 69d96d8892841a37790cd895e85a21bd1739888a Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 15 Jun 2016 06:02:18 -0400 Subject: keys.PublicKeyInfo.bit_size and keys.PrivateKeyInfo.bit_size always round up to closest multiple of 8 now --- asn1crypto/keys.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 4c51ae3..8e8212f 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -772,6 +772,9 @@ class PrivateKeyInfo(Sequence): elif self.algorithm == 'ec': prime = self['private_key'].parsed['private_key'].native self._bit_size = int(math.ceil(math.log(prime, 2))) + modulus = self._bit_size % 8 + if modulus != 0: + self._bit_size += 8 - modulus return self._bit_size @property @@ -1145,6 +1148,9 @@ class PublicKeyInfo(Sequence): elif self.algorithm == 'dsa': prime = self['algorithm']['parameters']['p'].native self._bit_size = int(math.ceil(math.log(prime, 2))) + modulus = self._bit_size % 8 + if modulus != 0: + self._bit_size += 8 - modulus return self._bit_size -- cgit v1.2.3 From ace45a1a22ba6e3bafd59abd2906b88e669faa2b Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 16 Jun 2016 05:32:48 -0400 Subject: Add Apple OIDs to x509.KeyPurposeId --- asn1crypto/x509.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index f115d39..d725065 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1588,6 +1588,41 @@ class KeyPurposeId(ObjectIdentifier): '1.3.6.1.4.1.311.10.3.12': 'microsoft_document_signing', '1.3.6.1.4.1.311.10.3.13': 'microsoft_lifetime_signing', '1.3.6.1.4.1.311.10.3.14': 'microsoft_mobile_device_software', + # https://opensource.apple.com/source + # - /Security/Security-57031.40.6/Security/libsecurity_keychain/lib/SecPolicy.cpp + # - /libsecurity_cssm/libsecurity_cssm-36064/lib/oidsalg.c + '1.2.840.113635.100.1.2': 'apple_x509_basic', + '1.2.840.113635.100.1.3': 'apple_ssl', + '1.2.840.113635.100.1.4': 'apple_local_cert_gen', + '1.2.840.113635.100.1.5': 'apple_csr_gen', + '1.2.840.113635.100.1.6': 'apple_revocation_crl', + '1.2.840.113635.100.1.7': 'apple_revocation_ocsp', + '1.2.840.113635.100.1.8': 'apple_smime', + '1.2.840.113635.100.1.9': 'apple_eap', + '1.2.840.113635.100.1.10': 'apple_software_update_signing', + '1.2.840.113635.100.1.11': 'apple_ipsec', + '1.2.840.113635.100.1.12': 'apple_ichat', + '1.2.840.113635.100.1.13': 'apple_resource_signing', + '1.2.840.113635.100.1.14': 'apple_pkinit_client', + '1.2.840.113635.100.1.15': 'apple_pkinit_server', + '1.2.840.113635.100.1.16': 'apple_code_signing', + '1.2.840.113635.100.1.17': 'apple_package_signing', + '1.2.840.113635.100.1.18': 'apple_id_validation', + '1.2.840.113635.100.1.20': 'apple_time_stamping', + '1.2.840.113635.100.1.21': 'apple_revocation', + '1.2.840.113635.100.1.22': 'apple_passbook_signing', + '1.2.840.113635.100.1.23': 'apple_mobile_store', + '1.2.840.113635.100.1.24': 'apple_escrow_service', + '1.2.840.113635.100.1.25': 'apple_profile_signer', + '1.2.840.113635.100.1.26': 'apple_qa_profile_signer', + '1.2.840.113635.100.1.27': 'apple_test_mobile_store', + '1.2.840.113635.100.1.28': 'apple_otapki_signer', + '1.2.840.113635.100.1.29': 'apple_test_otapki_signer', + '1.2.840.113625.100.1.30': 'apple_id_validation_record_signing_policy', + '1.2.840.113625.100.1.31': 'apple_smp_encryption', + '1.2.840.113625.100.1.32': 'apple_test_smp_encryption', + '1.2.840.113635.100.1.33': 'apple_server_authentication', + '1.2.840.113635.100.1.34': 'apple_pcs_escrow_service', } -- cgit v1.2.3 From 4365ed6aced5d81f85041d5b14de4ecc5f5448bb Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 16 Jun 2016 05:33:32 -0400 Subject: Raise ValueError() when instantiating a PrimePoint() that is invalid --- asn1crypto/_elliptic_curve.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index 7e41cc5..0ecab2d 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -116,10 +116,12 @@ class PrimePoint(): # self.curve is allowed to be None only for INFINITY: if self.curve: - assert self.curve.contains(self) + if not self.curve.contains(self): + raise ValueError('Invalid EC point') if self.order: - assert self * self.order == INFINITY + if self * self.order != INFINITY: + raise ValueError('Invalid EC point') def __cmp__(self, other): """ -- cgit v1.2.3 From 13df68b9a161e8d3d8615145d8b32aaeed1e947a Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 16 Jun 2016 05:50:48 -0400 Subject: Allow --all flag to be passed to setup.py clean [ci skip] --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3cff195..d8825d8 100644 --- a/setup.py +++ b/setup.py @@ -7,10 +7,12 @@ import asn1crypto class CleanCommand(Command): - user_options = [] + user_options = [ + ('all', None, '(Compatibility with original clean command)') + ] def initialize_options(self): - pass + self.all = False def finalize_options(self): pass -- cgit v1.2.3 From 3534063c27b99f34a6049e8dc2743385d7921ee6 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 16 Jun 2016 07:26:20 -0400 Subject: Fix parsing of nested indefinite length values --- asn1crypto/core.py | 99 ++++++++++++++++++++++++++++++++++++------------------ tests/test_cms.py | 1 + 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ad62d02..6b2a658 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -4397,9 +4397,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param return value -def _parse(encoded_data, pointer=0): +def _parse_header(encoded_data, pointer): """ - Parses a byte string into component parts + Parses the header of an ASN.1 encoded value :param encoded_data: A byte string that contains BER-encoded data @@ -4408,17 +4408,15 @@ def _parse(encoded_data, pointer=0): The index in the byte string to parse from :return: - A 2-element tuple: - - 0: A tuple of (class_, method, tag, header, content, trailer) - - 1: An integer indicating how many bytes were consumed + A 4-element tuple: + - 0: A byte string of the header + - 1: The integer length of the contents + - 2: A boolean if the value was indefinite length + - 3: A tuple of (class_, method, tag) """ - if len(encoded_data) == 0: - return ((None, None, None, None, None, None), 0) - - encoded_length = len(encoded_data) - start = pointer + encoded_length = len(encoded_data) first_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] pointer += 1 @@ -4448,6 +4446,9 @@ def _parse(encoded_data, pointer=0): )) length_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] + if first_octet == 0 and length_octet == 0: + return (b'\x00\x00', 0, False, (0, 0, 0)) + pointer += 1 length_type = length_octet >> 7 if length_type == 1: @@ -4479,33 +4480,67 @@ def _parse(encoded_data, pointer=0): )) header = encoded_data[start:pointer] - # Indefinite length + indefinite = False + if length_type == 1 and length == 0: - end_token = encoded_data.find(b'\x00\x00', pointer) - if end_token > encoded_length: - raise ValueError(unwrap( - ''' - Insufficient data - %s bytes requested but only %s available - ''', - end_token, - encoded_length - )) - contents = encoded_data[pointer:end_token] - pointer = end_token + 2 - trailer = b'\x00\x00' + indefinite = True + # To properly parse indefinite length values, we need to scan forward + # parsing headers until we find a value with a length of zero. If we + # just scanned looking for \x00\x00, nested indefinite length values + # would not work. + end_token_pointer = pointer + while end_token_pointer < encoded_length: + sub_header, sub_length, _, _ = _parse_header(encoded_data, end_token_pointer) + end_token_pointer += len(sub_header) + sub_length + if sub_length == 0: + break + length = end_token_pointer - pointer + + return (header, length, indefinite, (class_, method, tag)) + + +def _parse(encoded_data, pointer=0): + """ + Parses a byte string into component parts + + :param encoded_data: + A byte string that contains BER-encoded data + :param pointer: + The index in the byte string to parse from + + :return: + A 2-element tuple: + - 0: A tuple of (class_, method, tag, header, content, trailer) + - 1: An integer indicating how many bytes were consumed + """ + + if len(encoded_data) == 0: + return ((None, None, None, None, None, None), 0) + + encoded_length = len(encoded_data) + + start = pointer + + header, length, indefinite, info = _parse_header(encoded_data, pointer) + class_, method, tag = info + pointer += len(header) + + if pointer + length > encoded_length: + raise ValueError(unwrap( + ''' + Insufficient data - %s bytes requested but only %s available + ''', + pointer + length, + encoded_length + )) + if indefinite: + contents = encoded_data[pointer:pointer + length - 2] + trailer = b'\x00\x00' else: - if pointer + length > encoded_length: - raise ValueError(unwrap( - ''' - Insufficient data - %s bytes requested but only %s available - ''', - pointer + length, - encoded_length - )) contents = encoded_data[pointer:pointer + length] - pointer += length trailer = b'' + pointer += length return ((class_, method, tag, header, contents, trailer), pointer - start) diff --git a/tests/test_cms.py b/tests/test_cms.py index 9abb22d..858b83c 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -541,6 +541,7 @@ class CMSTests(unittest.TestCase): with open(os.path.join(fixtures_dir, 'cms-signed-indefinite-length.der'), 'rb') as f: info = cms.ContentInfo.load(f.read()) signed_data = info['content'] + self.assertIsInstance(signed_data.native, util.OrderedDict) def test_parse_content_info_cms_signed_digested_data(self): with open(os.path.join(fixtures_dir, 'cms-signed-digested.der'), 'rb') as f: -- cgit v1.2.3 From 0375311ad023546a6d7c041eb42d342011e5c0c9 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 16 Jun 2016 09:23:20 -0400 Subject: Added core.ObjectIdentifier().dotted to get the non-mapped version of an OID --- asn1crypto/core.py | 50 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 6b2a658..a816b1e 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2219,6 +2219,9 @@ class ObjectIdentifier(Primitive, ValueMap): tag = 6 + # A unicode string of the dotted form of the object identifier + _dotted = None + def set(self, value): """ Sets the value of the object @@ -2274,24 +2277,37 @@ class ObjectIdentifier(Primitive, ValueMap): :return: A unicode string """ - output = [] - part = 0 - for byte in self.contents: - if py2: - byte = ord(byte) - part = part * 128 - part += byte & 127 - # Last byte in subidentifier has the eighth bit set to 0 - if byte & 0x80 == 0: - if len(output) == 0: - output.append(str_cls(part // 40)) - output.append(str_cls(part % 40)) - else: - output.append(str_cls(part)) - part = 0 + return self.dotted - return '.'.join(output) + @property + def dotted(self): + """ + :return: + A unicode string of the object identifier in dotted notation, thus + ignoring any mapped value + """ + + if self._dotted is None: + output = [] + + part = 0 + for byte in self.contents: + if py2: + byte = ord(byte) + part = part * 128 + part += byte & 127 + # Last byte in subidentifier has the eighth bit set to 0 + if byte & 0x80 == 0: + if len(output) == 0: + output.append(str_cls(part // 40)) + output.append(str_cls(part % 40)) + else: + output.append(str_cls(part)) + part = 0 + + self._dotted = '.'.join(output) + return self._dotted @property def native(self): @@ -2308,7 +2324,7 @@ class ObjectIdentifier(Primitive, ValueMap): return None if self._native is None: - self._native = self.__unicode__() + self._native = self.dotted if self._map is not None and self._native in self._map: self._native = self._map[self._native] return self._native -- cgit v1.2.3 From 52c0b754de87f3d0c6a6ac28eec279bdd6d63cfe Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 18 Jun 2016 15:10:46 -0400 Subject: Allow native Python values to passed to core.Concat.__init__() instead of having to set values via __setitem__() --- asn1crypto/core.py | 46 ++++++++++++++++++++++++++++------------------ tests/test_core.py | 18 ++++++++++++++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index a816b1e..990e85a 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -975,8 +975,11 @@ class Concat(object): return cls(contents=encoded_data) - def __init__(self, contents=None): + def __init__(self, value=None, contents=None): """ + :param value: + A native Python datatype to initialize the object value with + :param contents: A byte string of the encoded contents of the value @@ -985,23 +988,30 @@ class Concat(object): TypeError - when an error occurs with one of the children """ - try: - contents_len = len(contents) - self._children = [] - - offset = 0 - for spec in self._child_specs: - if offset < contents_len: - value, bytes_read = _parse_build(contents[offset:], spec=spec) - offset += bytes_read - else: - value = spec() - self._children.append(value) + if contents is not None: + try: + contents_len = len(contents) + self._children = [] + + offset = 0 + for spec in self._child_specs: + if offset < contents_len: + child_value, bytes_read = _parse_build(contents[offset:], spec=spec) + offset += bytes_read + else: + child_value = spec() + self._children.append(child_value) - except (ValueError, TypeError) as e: - args = e.args[1:] - e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args - raise e + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args + raise e + + if value is not None: + if self._children is None: + self._children = [None] * len(self._child_specs) + for index, data in enumerate(value): + self.__setitem__(index, data) def __str__(self): """ @@ -1189,7 +1199,7 @@ class Concat(object): if key > len(self._child_specs) - 1 or key < 0: raise KeyError(unwrap( ''' - No child is definition for position %d of %s + No child is defined for position %d of %s ''', key, type_name(self) diff --git a/tests/test_core.py b/tests/test_core.py index 5c8cb53..0cd05ed 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -78,6 +78,10 @@ class SeqChoice(core.Choice): ] +class ConcatTest(core.Concat): + _child_specs = [Seq, core.Integer] + + @data_decorator class CoreTests(unittest.TestCase): @@ -332,3 +336,17 @@ class CoreTests(unittest.TestCase): choice_copy = choice.copy() choice.chosen['name'] = 'bar' self.assertNotEqual(choice.chosen['name'], choice_copy.chosen['name']) + + def test_concat(self): + child1 = Seq({ + 'id': '1.2.3', + 'value': 1 + }) + child2 = core.Integer(0) + parent = ConcatTest([ + child1, + child2 + ]) + self.assertEqual(child1, parent[0]) + self.assertEqual(child2, parent[1]) + self.assertEqual(child1.dump() + child2.dump(), parent.dump()) -- cgit v1.2.3 From a5f9fe7fc5c0fd359ee142fcc019676132f1012e Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 18 Jun 2016 16:52:41 -0400 Subject: Added core.ObjectIdentifier.map() and core.ObjectIdentifier.unmap() --- asn1crypto/core.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++--- tests/test_core.py | 18 ++++++++++ 2 files changed, 110 insertions(+), 5 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 990e85a..fd345be 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -105,6 +105,9 @@ METHOD_NUM_TO_NAME_MAP = { } +_OID_RE = re.compile('^\d+(\.\d+)*$') + + # A global tracker to ensure that _setup() is called for every class, even # if is has been called for a parent class. This allows different _fields # definitions for child classes. Without such a construct, the child classes @@ -1576,7 +1579,7 @@ class BitString(Primitive, ValueMap, object): if self._map is None: raise ValueError(unwrap( ''' - %s _map has not been defined + %s._map has not been defined ''', type_name(self) )) @@ -1673,7 +1676,7 @@ class BitString(Primitive, ValueMap, object): if not isinstance(self._map, dict): raise ValueError(unwrap( ''' - %s _map has not been defined + %s._map has not been defined ''', type_name(self) )) @@ -1681,7 +1684,7 @@ class BitString(Primitive, ValueMap, object): if key not in self._reverse_map: raise ValueError(unwrap( ''' - %s _map does not contain an entry for "%s" + %s._map does not contain an entry for "%s" ''', type_name(self), key @@ -1719,7 +1722,7 @@ class BitString(Primitive, ValueMap, object): if self._map is None: raise ValueError(unwrap( ''' - %s _map has not been defined + %s._map has not been defined ''', type_name(self) )) @@ -1727,7 +1730,7 @@ class BitString(Primitive, ValueMap, object): if key not in self._reverse_map: raise ValueError(unwrap( ''' - %s _map does not contain an entry for "%s" + %s._map does not contain an entry for "%s" ''', type_name(self), key @@ -2232,6 +2235,90 @@ class ObjectIdentifier(Primitive, ValueMap): # A unicode string of the dotted form of the object identifier _dotted = None + @classmethod + def map(cls, value): + """ + Converts a dotted unicode string OID into a mapped unicode string + + :param value: + A dotted unicode string OID + + :raises: + ValueError - when no _map dict has been defined on the class + TypeError - when value is not a unicode string + + :return: + A mapped unicode string + """ + + if cls._map is None: + raise ValueError(unwrap( + ''' + %s._map has not been defined + ''', + type_name(cls) + )) + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + value must be a unicode string, not %s + ''', + type_name(value) + )) + + return cls._map.get(value, value) + + @classmethod + def unmap(cls, value): + """ + Converts a mapped unicode string value into a dotted unicode string OID + + :param value: + A mapped unicode string OR dotted unicode string OID + + :raises: + ValueError - when no _map dict has been defined on the class or the value can't be unmapped + TypeError - when value is not a unicode string + + :return: + A dotted unicode string OID + """ + + if cls not in _SETUP_CLASSES: + cls()._setup() + _SETUP_CLASSES[cls] = True + + if cls._map is None: + raise ValueError(unwrap( + ''' + %s._map has not been defined + ''', + type_name(cls) + )) + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + value must be a unicode string, not %s + ''', + type_name(value) + )) + + if value in cls._reverse_map: + return cls._reverse_map[value] + + if not _OID_RE.match(value): + raise ValueError(unwrap( + ''' + %s._map does not contain an entry for "%s" + ''', + type_name(cls), + value + )) + + return value + def set(self, value): """ Sets the value of the object diff --git a/tests/test_core.py b/tests/test_core.py index 0cd05ed..a094eb8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -82,6 +82,13 @@ class ConcatTest(core.Concat): _child_specs = [Seq, core.Integer] +class MyOids(core.ObjectIdentifier): + _map = { + '1.2.3': 'abc', + '4.5.6': 'def', + } + + @data_decorator class CoreTests(unittest.TestCase): @@ -350,3 +357,14 @@ class CoreTests(unittest.TestCase): self.assertEqual(child1, parent[0]) self.assertEqual(child2, parent[1]) self.assertEqual(child1.dump() + child2.dump(), parent.dump()) + + def test_oid_map_unmap(self): + self.assertEqual('abc', MyOids.map('1.2.3')) + self.assertEqual('def', MyOids.map('4.5.6')) + self.assertEqual('7.8.9', MyOids.map('7.8.9')) + self.assertEqual('1.2.3', MyOids.unmap('abc')) + self.assertEqual('4.5.6', MyOids.unmap('def')) + self.assertEqual('7.8.9', MyOids.unmap('7.8.9')) + + with self.assertRaises(ValueError): + MyOids.unmap('no_such_mapping') -- cgit v1.2.3 From ff3971146543bb87428c4a4f961fc650d91f34f7 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jun 2016 07:13:03 -0400 Subject: Improve parsing performance --- asn1crypto/core.py | 394 +++++++++++++++++++++++++---------------------------- 1 file changed, 187 insertions(+), 207 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index fd345be..e5ab805 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -861,7 +861,7 @@ class Choice(Asn1Value): type_name(self) )) - ((class_, _, tag, _, _, _), _) = _parse(contents) + ((class_, _, tag, _, _, _), _) = _parse(contents, len(contents)) id_ = (class_, tag) if id_ in self._id_map: @@ -999,8 +999,7 @@ class Concat(object): offset = 0 for spec in self._child_specs: if offset < contents_len: - child_value, bytes_read = _parse_build(contents[offset:], spec=spec) - offset += bytes_read + child_value, offset = _parse_build(contents, pointer=offset, spec=spec) else: child_value = spec() self._children.append(child_value) @@ -1595,7 +1594,7 @@ class BitString(Primitive, ValueMap, object): value = ''.join(map(str_cls, bits)) - elif isinstance(value, tuple): + elif value.__class__ == tuple: if self._map is None: self._native = value else: @@ -2591,6 +2590,9 @@ class Sequence(Asn1Value): # A 2-element tuple of the indexes in _fields of the OID and value fields _oid_nums = None + # Predetermined field specs to optimize away calls to _determine_spec() + _precomputed_specs = None + def __init__(self, value=None, default=None, **kwargs): """ Allows setting field values before passing everything else along to @@ -2686,9 +2688,8 @@ class Sequence(Asn1Value): """ child = self.children[index] - if isinstance(child, tuple): - child = _build(*child) - self.children[index] = child + if child.__class__ == tuple: + child = self.children[index] = _build(*child) return child def __len__(self): @@ -2877,7 +2878,7 @@ class Sequence(Asn1Value): child = self.children[index] if child is None: child_dump = b'' - elif isinstance(child, tuple): + elif child.__class__ == tuple: if force: child_dump = self._lazy_child(index).dump(force=force) else: @@ -2904,6 +2905,7 @@ class Sequence(Asn1Value): cls = self.__class__ cls._field_map = {} cls._field_ids = [] + cls._precomputed_specs = [] for index, field in enumerate(cls._fields): if len(field) < 3: field = field + ({},) @@ -2914,6 +2916,14 @@ class Sequence(Asn1Value): if cls._oid_pair is not None: cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) + for index, field in enumerate(cls._fields): + has_callback = cls._spec_callbacks is not None and field[0] in cls._spec_callbacks + is_mapped_oid = cls._oid_nums is not None and cls._oid_nums[1] == index + if has_callback or is_mapped_oid: + cls._precomputed_specs.append(None) + else: + cls._precomputed_specs.append((field[0], field[1], field[1], field[2], None)) + def _determine_spec(self, index): """ Determine how a value for a field should be constructed @@ -2940,7 +2950,7 @@ class Sequence(Asn1Value): if spec_override: # Allow a spec callback to specify both the base spec and # the override, for situations such as OctetString and parse_as - if isinstance(spec_override, tuple) and len(spec_override) == 2: + if spec_override.__class__ == tuple and len(spec_override) == 2: field_spec, value_spec = spec_override if value_spec is None: value_spec = field_spec @@ -3042,12 +3052,16 @@ class Sequence(Asn1Value): ValueError - when an error occurs parsing child objects """ + cls = self.__class__ if self._contents is None: if self._fields: self.children = [VOID] * len(self._fields) for index, (_, _, params) in enumerate(self._fields): if 'default' in params: - field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) + if cls._precomputed_specs[field]: + field_name, field_spec, value_spec, field_params, _ = cls._precomputed_specs[index] + else: + field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) self.children[index] = self._make_value(field_name, field_spec, value_spec, field_params, None) return @@ -3056,21 +3070,20 @@ class Sequence(Asn1Value): contents_length = len(self._contents) child_pointer = 0 field = 0 - seen_field = 1 field_len = len(self._fields) - while child_pointer < contents_length: - parts, num_bytes = _parse(self._contents, pointer=child_pointer) + parts = None + again = child_pointer < contents_length + while again: + if parts is None: + parts, child_pointer = _parse(self._contents, contents_length, pointer=child_pointer) + again = child_pointer < contents_length if field < field_len: - _, field_spec, value_spec, field_params, spec_override = self._determine_spec(field) + _, field_spec, value_spec, field_params, spec_override = cls._precomputed_specs[field] or self._determine_spec(field) # If the next value is optional or default, allow it to be absent - if 'optional' in field_params or 'default' in field_params: - id_ = (parts[0], parts[2]) - - no_id_match = self._field_ids[field] != id_ - not_any = field_spec != Any - if no_id_match and not_any: + if field_params and ('optional' in field_params or 'default' in field_params): + if self._field_ids[field] != (parts[0], parts[2]) and field_spec != Any: # See if the value is a valid choice before assuming # that we have a missing optional or default value @@ -3078,7 +3091,7 @@ class Sequence(Asn1Value): if issubclass(field_spec, Choice): try: tester = field_spec(**field_params) - tester.validate(id_[0], id_[1], parts[4]) + tester.validate(parts[0], parts[2], parts[4]) choice_match = True except (ValueError): pass @@ -3089,6 +3102,7 @@ class Sequence(Asn1Value): else: self.children.append(field_spec(**field_params)) field += 1 + again = True continue if field_spec is None or (spec_override and issubclass(field_spec, Any)): @@ -3101,7 +3115,7 @@ class Sequence(Asn1Value): child = parts + (field_spec, field_params) # Handle situations where an optional or defaulted field definition is incorrect - elif field_len > 0 and seen_field <= field_len: + elif field_len > 0 and field + 1 <= field_len: missed_fields = [] prev_field = field - 1 while prev_field >= 0: @@ -3118,7 +3132,7 @@ class Sequence(Asn1Value): Data for field %s (%s class, %s method, tag %s) does not match the field definition%s of %s ''', - seen_field, + field + 1, CLASS_NUM_TO_NAME_MAP.get(parts[0]), METHOD_NUM_TO_NAME_MAP.get(parts[1]), parts[2], @@ -3135,9 +3149,8 @@ class Sequence(Asn1Value): child._parse_children(recurse=True) self.children.append(child) - child_pointer += num_bytes field += 1 - seen_field += 1 + parts = None index = len(self.children) while index < field_len: @@ -3217,7 +3230,7 @@ class Sequence(Asn1Value): self._parse_children(recurse=True) self._native = OrderedDict() for index, child in enumerate(self.children): - if isinstance(child, tuple): + if child.__class__ == tuple: child = _build(*child) self.children[index] = child try: @@ -3253,7 +3266,7 @@ class Sequence(Asn1Value): if self.children is not None: self.children = [] for child in other.children: - if isinstance(child, tuple): + if child.__class__ == tuple: self.children.append(child) else: self.children.append(child.copy()) @@ -3404,7 +3417,7 @@ class SequenceOf(Asn1Value): """ child = self.children[index] - if isinstance(child, tuple): + if child.__class__ == tuple: child = _build(*child) self.children[index] = child return child @@ -3643,7 +3656,7 @@ class SequenceOf(Asn1Value): contents_length = len(self._contents) child_pointer = 0 while child_pointer < contents_length: - parts, num_bytes = _parse(self._contents, pointer=child_pointer) + parts, child_pointer = _parse(self._contents, contents_length, pointer=child_pointer) if self._child_spec: child = parts + (self._child_spec,) else: @@ -3653,7 +3666,6 @@ class SequenceOf(Asn1Value): if isinstance(child, (Sequence, SequenceOf)): child._parse_children(recurse=True) self.children.append(child) - child_pointer += num_bytes except (ValueError, TypeError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args @@ -3715,7 +3727,7 @@ class SequenceOf(Asn1Value): if self.children is not None: self.children = [] for child in other.children: - if isinstance(child, tuple): + if child.__class__ == tuple: self.children.append(child) else: self.children.append(child.copy()) @@ -3802,7 +3814,7 @@ class Set(Sequence): child_pointer = 0 seen_field = 0 while child_pointer < contents_length: - parts, num_bytes = _parse(self.contents, pointer=child_pointer) + parts, child_pointer = _parse(self.contents, contents_length, pointer=child_pointer) id_ = (parts[0], parts[2]) @@ -3839,7 +3851,6 @@ class Set(Sequence): child._parse_children(recurse=True) child_map[field] = child - child_pointer += num_bytes seen_field += 1 total_fields = len(self._fields) @@ -4298,6 +4309,38 @@ def _build_id_tuple(params, spec): return (required_class, required_tag) +_UNIVERSAL_SPECS = { + 1: Boolean, + 2: Integer, + 3: BitString, + 4: OctetString, + 5: Null, + 6: ObjectIdentifier, + 7: ObjectDescriptor, + 8: InstanceOf, + 9: Real, + 10: Enumerated, + 11: EmbeddedPdv, + 12: UTF8String, + 13: RelativeOid, + 16: Sequence, + 17: Set, + 18: NumericString, + 19: PrintableString, + 20: TeletexString, + 21: VideotexString, + 22: IA5String, + 23: UTCTime, + 24: GeneralizedTime, + 25: GraphicString, + 26: VisibleString, + 27: GeneralString, + 28: UniversalString, + 29: CharacterString, + 30: BMPString +} + + def _build(class_, method, tag, header, contents, trailer, spec=None, spec_params=None, nested_spec=None): """ Builds an Asn1Value object generically, or using a spec with optional params @@ -4349,9 +4392,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param else: value = spec(contents=contents) - is_choice = isinstance(value, Choice) - - if spec == Any: + if spec is Any: pass elif value.tag_type == 'explicit': @@ -4385,9 +4426,26 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value.explicit_tag, tag )) - - elif is_choice: + original_value = value + info, _ = _parse(contents, len(contents)) + value = _build(*info, spec=spec) + value._header = header + info[3] + value._trailer += trailer or b'' + value.tag_type = 'explicit' + value.explicit_class = original_value.explicit_class + value.explicit_tag = original_value.explicit_tag + + elif isinstance(value, Choice): value.validate(class_, tag, contents) + try: + # Force parsing the Choice now + value.contents = header + value.contents + header = b'' + value.parse() + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args + raise e else: if class_ != value.class_: @@ -4423,43 +4481,19 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # since we will be parsing the contents and discarding the outer object # anyway a little further on elif spec_params and 'tag_type' in spec_params and spec_params['tag_type'] == 'explicit': - value = Asn1Value(contents=contents, **spec_params) - is_choice = False + original_value = Asn1Value(contents=contents, **spec_params) + info, _ = _parse(contents, len(contents)) + value = _build(*info, spec=spec) + value._header = header + info[3] + value._trailer += trailer or b'' + value.tag_type = 'explicit' + value.explicit_class = original_value.explicit_class + value.explicit_tag = original_value.explicit_tag # If no spec was specified, allow anything and just process what # is in the input data else: - universal_specs = { - 1: Boolean, - 2: Integer, - 3: BitString, - 4: OctetString, - 5: Null, - 6: ObjectIdentifier, - 7: ObjectDescriptor, - 8: InstanceOf, - 9: Real, - 10: Enumerated, - 11: EmbeddedPdv, - 12: UTF8String, - 13: RelativeOid, - 16: Sequence, - 17: Set, - 18: NumericString, - 19: PrintableString, - 20: TeletexString, - 21: VideotexString, - 22: IA5String, - 23: UTCTime, - 24: GeneralizedTime, - 25: GraphicString, - 26: VisibleString, - 27: GeneralString, - 28: UniversalString, - 29: CharacterString, - 30: BMPString - } - if tag not in universal_specs: + if tag not in _UNIVERSAL_SPECS: raise ValueError(unwrap( ''' Unknown element - %s class, %s method, tag %s @@ -4469,74 +4503,61 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param tag )) - spec = universal_specs[tag] + spec = _UNIVERSAL_SPECS[tag] value = spec(contents=contents, class_=class_) - is_choice = False value._header = header - if trailer is not None and trailer != b'': - value._trailer = trailer + value._trailer = trailer or b'' # Destroy any default value that our contents have overwritten value._native = None - # For explicitly tagged values, parse the inner value and pull it out - if value.tag_type == 'explicit': - original_value = value - (class_, method, tag, header, contents, trailer), _ = _parse(value.contents) - value = _build(class_, method, tag, header, contents, trailer, spec=spec) - value._header = original_value._header + header - value._trailer += original_value._trailer - value.tag_type = 'explicit' - value.explicit_class = original_value.explicit_class - value.explicit_tag = original_value.explicit_tag - elif is_choice: - value.contents = value._header + value.contents - value._header = b'' - - try: - # Force parsing the Choice now - if is_choice: - value.parse() - - if nested_spec: + if nested_spec: + try: value.parse(nested_spec) - except (ValueError, TypeError) as e: - args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args - raise e + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args + raise e return value -def _parse_header(encoded_data, pointer): +_MEMOIZED_PARSINGS = {} + + +def _parse(encoded_data, data_len, pointer=0, lengths_only=False): """ - Parses the header of an ASN.1 encoded value + Parses a byte string into component parts :param encoded_data: A byte string that contains BER-encoded data + :param data_len: + The integer length of the encoded data + :param pointer: The index in the byte string to parse from + :param lengths_only: + A boolean to cause the call to return a 2-element tuple of the integer + number of bytes in the header and the integer number of bytes in the + contents. Internal use only. + :return: - A 4-element tuple: - - 0: A byte string of the header - - 1: The integer length of the contents - - 2: A boolean if the value was indefinite length - - 3: A tuple of (class_, method, tag) + A 2-element tuple: + - 0: A tuple of (class_, method, tag, header, content, trailer) + - 1: An integer indicating how many bytes were consumed """ - start = pointer - encoded_length = len(encoded_data) + if data_len == 0: + return ((None, None, None, None, None, None), pointer) + start = pointer first_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] pointer += 1 - class_ = first_octet >> 6 - method = (first_octet >> 5) & 1 - tag = first_octet & 31 # Base 128 length using 8th bit as continuation indicator if tag == 31: @@ -4549,113 +4570,73 @@ def _parse_header(encoded_data, pointer): if num >> 7 == 0: break - if pointer + 1 > encoded_length: - raise ValueError(unwrap( - ''' - Insufficient data - %s bytes requested but only %s available - ''', - pointer + 1, - encoded_length - )) length_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] + pointer += 1 - if first_octet == 0 and length_octet == 0: - return (b'\x00\x00', 0, False, (0, 0, 0)) + if length_octet >> 7 == 0: + if lengths_only: + return (pointer, pointer + (length_octet & 127)) + contents_end = pointer + (length_octet & 127) - pointer += 1 - length_type = length_octet >> 7 - if length_type == 1: - length = 0 - remaining_length_octets = length_octet & 127 - while remaining_length_octets > 0: - length *= 256 - if pointer + 1 > encoded_length: + else: + length_octets = length_octet & 127 + if length_octets: + pointer += length_octets + contents_end = pointer + int_from_bytes(encoded_data[pointer - length_octets:pointer], signed=False) + if lengths_only: + return (pointer, contents_end) + + else: + # To properly parse indefinite length values, we need to scan forward + # parsing headers until we find a value with a length of zero. If we + # just scanned looking for \x00\x00, nested indefinite length values + # would not work. + contents_end = pointer + 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: + break + if lengths_only: + return (pointer, contents_end) + if contents_end > data_len: raise ValueError(unwrap( ''' Insufficient data - %s bytes requested but only %s available ''', - pointer + 1, - encoded_length + contents_end, + data_len )) - length += ord(encoded_data[pointer]) if py2 else encoded_data[pointer] - pointer += 1 - remaining_length_octets -= 1 - else: - length = length_octet & 127 - - if pointer > encoded_length: - raise ValueError(unwrap( - ''' - Insufficient data - %s bytes requested but only %s available - ''', - pointer, - encoded_length - )) - header = encoded_data[start:pointer] - - indefinite = False - - if length_type == 1 and length == 0: - indefinite = True - # To properly parse indefinite length values, we need to scan forward - # parsing headers until we find a value with a length of zero. If we - # just scanned looking for \x00\x00, nested indefinite length values - # would not work. - end_token_pointer = pointer - while end_token_pointer < encoded_length: - sub_header, sub_length, _, _ = _parse_header(encoded_data, end_token_pointer) - end_token_pointer += len(sub_header) + sub_length - if sub_length == 0: - break - length = end_token_pointer - pointer - - return (header, length, indefinite, (class_, method, tag)) - - -def _parse(encoded_data, pointer=0): - """ - Parses a byte string into component parts - - :param encoded_data: - A byte string that contains BER-encoded data - - :param pointer: - The index in the byte string to parse from - - :return: - A 2-element tuple: - - 0: A tuple of (class_, method, tag, header, content, trailer) - - 1: An integer indicating how many bytes were consumed - """ - - if len(encoded_data) == 0: - return ((None, None, None, None, None, None), 0) - - encoded_length = len(encoded_data) - - start = pointer - - header, length, indefinite, info = _parse_header(encoded_data, pointer) - class_, method, tag = info - pointer += len(header) + return ( + ( + first_octet >> 6, + (first_octet >> 5) & 1, + tag, + encoded_data[start:pointer], + encoded_data[pointer:contents_end - 2], + b'\x00\x00' + ), + contents_end + ) - if pointer + length > encoded_length: + if contents_end > data_len: raise ValueError(unwrap( ''' Insufficient data - %s bytes requested but only %s available ''', - pointer + length, - encoded_length + contents_end, + data_len )) - if indefinite: - contents = encoded_data[pointer:pointer + length - 2] - trailer = b'\x00\x00' - else: - contents = encoded_data[pointer:pointer + length] - trailer = b'' - pointer += length - - return ((class_, method, tag, header, contents, trailer), pointer - start) + return ( + ( + first_octet >> 6, + (first_octet >> 5) & 1, + tag, + encoded_data[start:pointer], + encoded_data[pointer:contents_end], + b'' + ), + contents_end + ) def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None): @@ -4684,6 +4665,5 @@ def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None): - 1: An integer indicating how many bytes were consumed """ - (class_, method, tag, header, contents, trailer), num_bytes = _parse(encoded_data, pointer) - value = _build(class_, method, tag, header, contents, trailer, spec=spec, spec_params=spec_params) - return (value, num_bytes) + info, new_pointer = _parse(encoded_data, len(encoded_data), pointer) + return (_build(*info, spec=spec, spec_params=spec_params), new_pointer) -- cgit v1.2.3 From ba6a822b97649c0c7982674eb345f36ca0a63db7 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Jun 2016 07:20:55 -0400 Subject: Fix lint errors --- asn1crypto/core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index e5ab805..6d62b0f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3058,7 +3058,7 @@ class Sequence(Asn1Value): self.children = [VOID] * len(self._fields) for index, (_, _, params) in enumerate(self._fields): if 'default' in params: - if cls._precomputed_specs[field]: + if cls._precomputed_specs[index]: field_name, field_spec, value_spec, field_params, _ = cls._precomputed_specs[index] else: field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) @@ -3079,7 +3079,8 @@ class Sequence(Asn1Value): again = child_pointer < contents_length if field < field_len: - _, field_spec, value_spec, field_params, spec_override = cls._precomputed_specs[field] or self._determine_spec(field) + _, field_spec, value_spec, field_params, spec_override = ( + cls._precomputed_specs[field] or self._determine_spec(field)) # If the next value is optional or default, allow it to be absent if field_params and ('optional' in field_params or 'default' in field_params): -- cgit v1.2.3 From c96c57da7f5a7265d5bc7372b6cdd1f38eb8330c Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 24 Jun 2016 14:47:51 -0400 Subject: Add docs for new core.ObjectIdentifier() attribute and class methods --- docs/universal_types.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/universal_types.md b/docs/universal_types.md index e89fcf4..26a89e7 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -334,6 +334,12 @@ The mapping of OID strings to name strings is configured via the `_map` property, which is a `dict` object with keys being unicode OID string and the values being a unicode string. +The `.dotted` attribute will always return a unicode string of the dotted +integer form of the OID. + +The class methods `.map()` and `.unmap()` will convert a dotted integer unicode +string to the user-friendly name, and vice-versa. + ```python from asn1crypto.core import ObjectIdentifier @@ -346,8 +352,17 @@ class MyType(ObjectIdentifier): # Will print: "value_name" print(MyType('1.8.2.1.23').native) +# Will print: "1.8.2.1.23" +print(MyType('1.8.2.1.23').dotted) + # Will print: "1.8.2.1.25" print(MyType('1.8.2.1.25').native) + +# Will print "value_name" +print(MyType.map('1.8.2.1.23')) + +# Will print "1.8.2.1.23" +print(MyType.unmap('value_name')) ``` ## BitString -- cgit v1.2.3 From 43dfc1f9b3369510266fd2190b2638591173a37e Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 24 Jun 2016 14:54:39 -0400 Subject: Version 0.18.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 16 ++++++++++++++++ readme.md | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 554ed86..c1c57de 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.17.1' -__version_info__ = (0, 17, 1) +__version__ = '0.18.0' +__version_info__ = (0, 18, 0) diff --git a/changelog.md b/changelog.md index 0ee1d2a..4450e3c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,21 @@ # changelog +## 0.18.0 + + - Improved general parsing performance by 10-15% + - Add support for Windows XP + - Added `core.ObjectIdentifier.dotted` attribute to always return dotted + integer unicode string + - Added `core.ObjectIdentifier.map()` and `core.ObjectIdentifier.unmap()` + class methods to map dotted integer unicode strings to user-friendly unicode + strings and back + - Added various Apple OIDs to `x509.KeyPurposeId` + - Fixed a bug parsing nested indefinite-length-encoded values + - Fixed a bug with `x509.Certificate.issuer_alt_name_value` if it is the first + extension queried + - `keys.PublicKeyInfo.bit_size` and `keys.PrivateKeyInfo.bit_size` values are + now rounded up to the next closest multiple of 8 + ## 0.17.1 - Fix a bug in `x509.URI` parsing IRIs containing explicit port numbers on diff --git a/readme.md b/readme.md index 9394284..82d32b9 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.17.1 - [changelog](changelog.md) +0.18.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 2ebcfea4cd8de2654bc1ee422b847233d1e776bc Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 10 Jul 2016 21:41:24 -0400 Subject: Fix the definition of pdf.AdobeTimestamp() to make the requires_auth field optional, default false --- asn1crypto/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index 7673894..6bf325c 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -38,7 +38,7 @@ class AdobeTimestamp(Sequence): _fields = [ ('version', Integer), ('location', GeneralName), - ('requires_auth', Boolean), + ('requires_auth', Boolean, {'optional': True, 'default': False}), ] -- cgit v1.2.3 From 107a11d8fbab4c015098ee970525a77170cd396d Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 11 Jul 2016 05:44:39 -0400 Subject: Pin flake8 version in TravisCI config since easy_install will grab v3.0.0 beta --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 172bc19..a8d39c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,10 +54,10 @@ install: export PYTHON_BIN=/usr/local/bin/python3; else if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then - sudo /usr/bin/easy_install-2.7 flake8; + sudo /usr/bin/easy_install-2.7 flake8==2.6.2; export PYTHON_BIN=/usr/bin/python2.7; else - sudo /usr/bin/easy_install-2.6 flake8; + sudo /usr/bin/easy_install-2.6 flake8==2.6.2; export PYTHON_BIN=/usr/bin/python2.6; fi fi -- cgit v1.2.3 From ed347b929e37faa8ea0f854be731ee8db8be7ff1 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 05:57:47 -0400 Subject: Improve exceptions to include spec names when an error occurs accessing .native --- asn1crypto/core.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 6d62b0f..a1f6e0e 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3227,18 +3227,23 @@ class Sequence(Asn1Value): return None if self._native is None: - if self.children is None: - self._parse_children(recurse=True) - self._native = OrderedDict() - for index, child in enumerate(self.children): - if child.__class__ == tuple: - child = _build(*child) - self.children[index] = child - try: - name = self._fields[index][0] - except (IndexError): - name = str_cls(index) - self._native[name] = child.native + try: + if self.children is None: + self._parse_children(recurse=True) + self._native = OrderedDict() + for index, child in enumerate(self.children): + if child.__class__ == tuple: + child = _build(*child) + self.children[index] = child + try: + name = self._fields[index][0] + except (IndexError): + name = str_cls(index) + self._native[name] = child.native + 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._native def _copy(self, other, copy_func): @@ -3697,9 +3702,14 @@ class SequenceOf(Asn1Value): return None if self._native is None: - if self.children is None: - self._parse_children(recurse=True) - self._native = [child.native for child in self] + try: + if self.children is None: + self._parse_children(recurse=True) + self._native = [child.native for child in self] + 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._native def _copy(self, other, copy_func): -- cgit v1.2.3 From c57878c24487e923018fbead0a9ef086fba4a22b Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 06:02:01 -0400 Subject: Added OID 1.2.840.113549.1.9.16.2.14 to cms.CMSAttributeType --- asn1crypto/cms.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 5f37ac9..b706dce 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -94,6 +94,8 @@ class CMSAttributeType(ObjectIdentifier): '1.2.840.113549.1.9.4': 'message_digest', '1.2.840.113549.1.9.5': 'signing_time', '1.2.840.113549.1.9.6': 'counter_signature', + # https://tools.ietf.org/html/rfc3161#page-20 + '1.2.840.113549.1.9.16.2.14': 'signature_time_stamp_token', } @@ -298,6 +300,10 @@ class ContentInfo(Sequence): _oid_specs = {} +class SetOfContentInfo(SetOf): + _child_spec = ContentInfo + + class EncapsulatedContentInfo(Sequence): _fields = [ ('content_type', ContentType), @@ -707,4 +713,5 @@ CMSAttribute._oid_specs = { 'message_digest': SetOfOctetString, 'signing_time': SetOfTime, 'counter_signature': SignerInfos, + 'signature_time_stamp_token': SetOfContentInfo, } -- cgit v1.2.3 From 4762fc80173499cdf2c5972fe5a389855833f666 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 06:06:04 -0400 Subject: Fix exception spec info when accessing .native attribute to not double some spec names --- asn1crypto/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index a1f6e0e..7eb9b96 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3227,9 +3227,9 @@ class Sequence(Asn1Value): return None if self._native is None: + if self.children is None: + self._parse_children(recurse=True) try: - if self.children is None: - self._parse_children(recurse=True) self._native = OrderedDict() for index, child in enumerate(self.children): if child.__class__ == tuple: @@ -3702,9 +3702,9 @@ class SequenceOf(Asn1Value): return None if self._native is None: + if self.children is None: + self._parse_children(recurse=True) try: - if self.children is None: - self._parse_children(recurse=True) self._native = [child.native for child in self] except (ValueError, TypeError) as e: args = e.args[1:] -- cgit v1.2.3 From 6a8cf2a1442715e49b908dbd96141553f5996fe0 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 07:17:44 -0400 Subject: Add a bunch of attribute types to cms.AttributeCertificateV2 --- asn1crypto/cms.py | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 2 deletions(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index b706dce..6058ee4 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -33,6 +33,7 @@ from .algos import ( ) from .core import ( Any, + BitString, Choice, Enumerated, GeneralizedTime, @@ -45,11 +46,12 @@ from .core import ( SequenceOf, SetOf, UTCTime, + UTF8String, ) from .crl import CertificateList from .keys import PublicKeyInfo from .ocsp import OCSPResponse -from .x509 import Attributes, Certificate, Extensions, GeneralNames, Name +from .x509 import Attributes, Certificate, Extensions, GeneralName, GeneralNames, Name # These structures are taken from @@ -247,6 +249,176 @@ class AttCertIssuer(Choice): ] +class IetfAttrValue(Choice): + _alternatives = [ + ('octets', OctetString), + ('oid', ObjectIdentifier), + ('string', UTF8String), + ] + + +class IetfAttrValues(SequenceOf): + _child_spec = IetfAttrValue + + +class IetfAttrSyntax(Sequence): + _fields = [ + ('policy_authority', GeneralNames, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('values', IetfAttrValues), + ] + + +class SetOfIetfAttrSyntax(SetOf): + _child_spec = IetfAttrSyntax + + +class SvceAuthInfo(Sequence): + _fields = [ + ('service', GeneralName), + ('ident', GeneralName), + ('auth_info', OctetString, {'optional': True}), + ] + + +class SetOfSvceAuthInfo(SetOf): + _child_spec = SvceAuthInfo + + +class RoleSyntax(Sequence): + _fields = [ + ('role_authority', GeneralNames, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('role_name', GeneralName, {'tag_type': 'implicit', 'tag': 1}), + ] + + +class SetOfRoleSyntax(SetOf): + _child_spec = RoleSyntax + + +class ClassList(BitString): + _map = { + 0: 'unmarked', + 1: 'unclassified', + 2: 'restricted', + 3: 'confidential', + 4: 'secret', + 5: 'top_secret', + } + + +class SecurityCategory(Sequence): + _fields = [ + ('type', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 0}), + ('value', Any, {'tag_type': 'implicit', 'tag': 1}), + ] + + +class SetOfSecurityCategory(SetOf): + _child_spec = SecurityCategory + + +class Clearance(Sequence): + _fields = [ + ('policy_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 0}), + ('class_list', ClassList, {'tag_type': 'implicit', 'tag': 1, 'default': 'unclassified'}), + ('security_categories', SetOfSecurityCategory, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ] + + +class SetOfClearance(SetOf): + _child_spec = Clearance + + +class BigTime(Sequence): + _fields = [ + ('major', Integer), + ('fractional_seconds', Integer), + ('sign', Integer, {'optional': True}), + ] + + +class LeapData(Sequence): + _fields = [ + ('leap_time', BigTime), + ('action', Integer), + ] + + +class SetOfLeapData(SetOf): + _child_spec = LeapData + + +class TimingMetrics(Sequence): + _fields = [ + ('ntp_time', BigTime), + ('offset', BigTime), + ('delay', BigTime), + ('expiration', BigTime), + ('leap_event', SetOfLeapData, {'optional': True}), + ] + + +class SetOfTimingMetrics(SetOf): + _child_spec = TimingMetrics + + +class TimingPolicy(Sequence): + _fields = [ + ('policy_id', SequenceOf, {'spec': ObjectIdentifier}), + ('max_offset', BigTime, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('max_delay', BigTime, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ] + + +class SetOfTimingPolicy(SetOf): + _child_spec = TimingPolicy + + +class AttCertAttributeType(ObjectIdentifier): + _map = { + '1.3.6.1.5.5.7.10.1': 'authentication_info', + '1.3.6.1.5.5.7.10.2': 'access_identity', + '1.3.6.1.5.5.7.10.3': 'charging_identity', + '1.3.6.1.5.5.7.10.4': 'group', + '2.5.4.72': 'role', + '2.5.4.55': 'clearance', + '1.3.6.1.4.1.601.10.4.1': 'timing_metrics', + '1.3.6.1.4.1.601.10.4.2': 'timing_policy', + } + + +class AttCertAttribute(Sequence): + _fields = [ + ('type', AttCertAttributeType), + ('values', None), + ] + + _oid_specs = { + 'authentication_info': SetOfSvceAuthInfo, + 'access_identity': SetOfSvceAuthInfo, + 'charging_identity': SetOfIetfAttrSyntax, + 'group': SetOfIetfAttrSyntax, + 'role': SetOfRoleSyntax, + 'clearance': SetOfClearance, + 'timing_metrics': SetOfTimingMetrics, + 'timing_policy': SetOfTimingPolicy, + } + + def _values_spec(self): + spec = self._oid_specs.get(self['type'].native, SetOfAny) + if spec == SetOfAny: + print('UNKNOWN OID: %s' % self['type'].native) + return spec + + _spec_callbacks = { + 'values': _values_spec + } + + +class AttCertAttributes(SequenceOf): + _child_spec = AttCertAttribute + + class AttributeCertificateInfoV2(Sequence): _fields = [ ('version', AttCertVersion), @@ -255,7 +427,7 @@ class AttributeCertificateInfoV2(Sequence): ('signature', SignedDigestAlgorithm), ('serial_number', Integer), ('att_cert_validity_period', AttCertValidityPeriod), - ('attributes', Attributes), + ('attributes', AttCertAttributes), ('issuer_unique_id', OctetBitString, {'optional': True}), ('extensions', Extensions, {'optional': True}), ] -- cgit v1.2.3 From 97cdd2f3332d9771a64f19548f6721d4251198cf Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 11:58:41 -0400 Subject: Add a workaround for improperly-encoded cms.AttributeCertificateV2 sequences --- asn1crypto/cms.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 6058ee4..cd18a4b 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -434,6 +434,9 @@ class AttributeCertificateInfoV2(Sequence): class AttributeCertificateV2(Sequence): + # Handle the situation where a V2 cert is encoded as V1 + _bad_tag = 1 + _fields = [ ('ac_info', AttributeCertificateInfoV2), ('signature_algorithm', SignedDigestAlgorithm), @@ -457,6 +460,31 @@ class CertificateChoices(Choice): ('other', OtherCertificateFormat, {'tag_type': 'implicit', 'tag': 3}), ] + def validate(self, class_, tag, contents): + """ + Ensures that the class and tag specified exist as an alternative. This + custom version fixes parsing broken encodings there a V2 attribute + # certificate is encoded as a V1 + + :param class_: + The integer class_ from the encoded value header + + :param tag: + The integer tag from the encoded value header + + :param contents: + A byte string of the contents of the value - used when the object + is explicitly tagged + + :raises: + ValueError - when value is not a valid alternative + """ + + super(CertificateChoices, self).validate(class_, tag, contents) + if self._choice == 2: + if AttCertVersion.load(Sequence.load(contents)[0].dump()).native == 'v2': + self._choice = 3 + class CertificateSet(SetOf): _child_spec = CertificateChoices -- cgit v1.2.3 From cfdd1d2113b33268bcd33ba6f1999fcb0949168a Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 21:20:07 -0400 Subject: Don't lint on Python 2.6 since pip, flake8 and pycodestyle are deprecating support --- .travis.yml | 4 ++-- appveyor.yml | 2 +- dev/ci.py | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index a8d39c9..2809d77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,10 +54,10 @@ install: export PYTHON_BIN=/usr/local/bin/python3; else if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then - sudo /usr/bin/easy_install-2.7 flake8==2.6.2; + curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8'])"; export PYTHON_BIN=/usr/bin/python2.7; else - sudo /usr/bin/easy_install-2.6 flake8==2.6.2; export PYTHON_BIN=/usr/bin/python2.6; fi fi diff --git a/appveyor.yml b/appveyor.yml index db8d102..e27cd9f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -42,7 +42,7 @@ install: & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8; } elseif ("${env:PYTHON_ID}" -eq "26" -or "${env:PYTHON_ID}" -eq "26-x64") { - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8'])"; + # Skip flake8 for 2.6 since pip, flake8 and pycodestyle have all deprecated support for it } else { & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8; diff --git a/dev/ci.py b/dev/ci.py index a40ba3a..82d0182 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -4,7 +4,8 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys from .tests import run as run_tests -from .lint import run as run_lint +if sys.version_info > (2, 6): + from .lint import run as run_lint def run(): @@ -16,8 +17,11 @@ def run(): """ print('Python ' + sys.version.replace('\n', '')) - print('') - lint_result = run_lint() + if sys.version_info > (2, 6): + print('') + lint_result = run_lint() + else: + lint_result = True print('\nRunning tests') sys.stdout.flush() tests_result = run_tests() -- cgit v1.2.3 From 89ee81d607b9fe24ed53516a98509f1528a0b640 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 21:21:22 -0400 Subject: Fix appveyor.yml --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index e27cd9f..8227f8b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,7 +24,7 @@ environment: PYTHON_ID: "pypy" PYTHON_EXE: pypy install: - - ps: + - ps: |- $env:PYTMP = "${env:TMP}\py"; if (!(Test-Path "$env:PYTMP")) { New-Item -ItemType directory -Path "$env:PYTMP" | Out-Null; -- cgit v1.2.3 From cf4debe97d48d42ac28fb8e2d328a8583e81a007 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 21:23:13 -0400 Subject: Fix version_info comparison --- dev/ci.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/ci.py b/dev/ci.py index 82d0182..b3ab848 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys from .tests import run as run_tests -if sys.version_info > (2, 6): +if sys.version_info >= (2, 7): from .lint import run as run_lint @@ -17,7 +17,7 @@ def run(): """ print('Python ' + sys.version.replace('\n', '')) - if sys.version_info > (2, 6): + if sys.version_info >= (2, 7): print('') lint_result = run_lint() else: -- cgit v1.2.3 From 242958c89c5dca7ea3c9a555d673355334b11adc Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Jul 2016 21:55:19 -0400 Subject: Rip out cffi support and skip performance tweaks for PyPy due to segfaults --- asn1crypto/_ffi.py | 59 +++++------------------------------ asn1crypto/_int.py | 15 ++++----- asn1crypto/_perf/_big_num_cffi.py | 65 --------------------------------------- 3 files changed, 16 insertions(+), 123 deletions(-) delete mode 100644 asn1crypto/_perf/_big_num_cffi.py diff --git a/asn1crypto/_ffi.py b/asn1crypto/_ffi.py index 2fc196f..2a4f5bf 100644 --- a/asn1crypto/_ffi.py +++ b/asn1crypto/_ffi.py @@ -1,8 +1,7 @@ # coding: utf-8 """ -Exceptions for help trying to use cffi, then ctypes for shared library access. -Also includes helper compatibility functions. Exports the following items: +FFI helper compatibility functions. Exports the following items: - LibraryNotFoundError - FFIEngineError @@ -13,61 +12,19 @@ Also includes helper compatibility functions. Exports the following items: from __future__ import unicode_literals, division, absolute_import, print_function +from ctypes import create_string_buffer -try: - import cffi - ffi = cffi.FFI() +def buffer_from_bytes(initializer): + return create_string_buffer(initializer) - def buffer_from_bytes(initializer): - return ffi.new('char[]', initializer) - def unicode_buffer(initializer): - return ffi.new('wchar_t[]', initializer) +def bytes_from_buffer(buffer, maxlen=None): + return buffer.raw - def bytes_from_buffer(buffer, maxlen=None): - return ffi.buffer(buffer, maxlen)[:] - def null(): - return ffi.NULL - - def cast_void_p(value): - return ffi.cast('void *', value) - - def is_null(point): - if point == ffi.NULL: - return True - if point[0] == ffi.NULL: - return True - return False - - def string_from_buffer(buffer): - return ffi.string(buffer) - -except (ImportError): - - from ctypes import create_string_buffer, create_unicode_buffer, cast, c_void_p - - def buffer_from_bytes(initializer): - return create_string_buffer(initializer) - - def unicode_buffer(initializer): - return create_unicode_buffer(initializer) - - def bytes_from_buffer(buffer, maxlen=None): - return buffer.raw - - def null(): - return None - - def cast_void_p(value): - return cast(value, c_void_p) - - def is_null(point): - return not bool(point) - - def string_from_buffer(buffer): - return buffer.value +def null(): + return None class LibraryNotFoundError(Exception): diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index a50b667..bf1ec3a 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -34,6 +34,7 @@ THE SOFTWARE. from __future__ import unicode_literals, division, absolute_import, print_function import math +import platform from ._ffi import ( buffer_from_bytes, @@ -44,13 +45,13 @@ from ._ffi import ( ) from .util import int_to_bytes, int_from_bytes - -# First try to use ctypes or cffi with OpenSSL for better performance +# First try to use ctypes with OpenSSL for better performance try: - try: - from ._perf._big_num_cffi import libcrypto - except (FFIEngineError) as e: - from ._perf._big_num_ctypes import libcrypto + # Some versions of PyPy have segfault issues, so we just punt on PyPy + if platform.python_implementation() == 'PyPy': + raise EnvironmentError() + + from ._perf._big_num_ctypes import libcrypto def inverse_mod(a, p): """ @@ -97,7 +98,7 @@ try: return result # If there was an issue using OpenSSL, we fall back to pure python -except (LibraryNotFoundError, FFIEngineError): +except (LibraryNotFoundError, FFIEngineError, EnvironmentError): def inverse_mod(a, p): """ diff --git a/asn1crypto/_perf/_big_num_cffi.py b/asn1crypto/_perf/_big_num_cffi.py deleted file mode 100644 index 0c70f46..0000000 --- a/asn1crypto/_perf/_big_num_cffi.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding: utf-8 - -""" -cffi 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 cffi is not instaled -or there is an error interfacing with libcrypto. -""" - -from __future__ import unicode_literals, division, absolute_import, print_function - -from ctypes.util import find_library - -from .._ffi import LibraryNotFoundError, FFIEngineError - -try: - from cffi import FFI - -except (ImportError): - raise FFIEngineError('Error importing cffi') - - -try: - ffi = FFI() - ffi.cdef(""" - typedef ... BIGNUM; - typedef ... BN_CTX; - - BIGNUM *BN_new(void); - - int BN_bn2bin(const BIGNUM *a, unsigned char *to); - BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret); - - void BN_set_negative(BIGNUM *a, int n); - - int BN_num_bits(const BIGNUM *a); - - void BN_free(BIGNUM *a); - - BN_CTX *BN_CTX_new(void); - void BN_CTX_free(BN_CTX *c); - - BIGNUM *BN_mod_inverse(BIGNUM *r, BIGNUM *a, const BIGNUM *n, BN_CTX *ctx); - """) - - libcrypto_path = find_library('crypto') - if not libcrypto_path: - raise LibraryNotFoundError('The library libcrypto could not be found') - - libcrypto = ffi.dlopen(libcrypto_path) - -except (AttributeError): - raise FFIEngineError('Error initializing ctypes') -- cgit v1.2.3 From bfbb410615608880a32de49d3c99b5c26324fe92 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 21 Jul 2016 15:53:40 -0400 Subject: Ensure that algos.SignedDigestAlgorithm() sets parameters field to Null when a SHA-2 RSA algorithm, per RFC 4055 --- asn1crypto/algos.py | 38 +++++++++++++++++++++++++++++++++++--- dev/tests.py | 14 +++++++++++++- tests/test_algos.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 tests/test_algos.py diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index fbf26ce..20dde15 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -23,9 +23,11 @@ from .core import ( Any, Choice, Integer, + Null, ObjectIdentifier, OctetString, Sequence, + Void, ) @@ -216,11 +218,41 @@ class SignedDigestAlgorithm(Sequence): ('parameters', Any, {'optional': True}), ] - _oid_pair = ('algorithm', 'parameters') - _oid_specs = { - 'rsa_pss': RSASSAPSSParams, + # The following attribute, plus the parameters spec callback and custom + # __setitem__ are all to handle a situation where parameters should not be + # optional and must be Null for certain OIDs. More info at + # https://tools.ietf.org/html/rfc4055#page-15 + _null_algos = set(['sha224_rsa', 'sha256_rsa', 'sha384_rsa', 'sha512_rsa']) + + def _parameters_spec(self): + algo = self['algorithm'].native + if algo == 'rsassa_pss': + return RSASSAPSSParams + if algo in self._null_algos: + return Null + return None + + _spec_callbacks = { + 'parameters': _parameters_spec } + # We have to override this since the spec callback uses the value of + # algorithm to determine the parameter spec, however default values are + # assigned before setting a field, so a default value can't be based on + # another field value (unless it is a default also). Thus we have to + # manually check to see if the algorithm was set and parameters is unset, + # and then fix the value as appropriate. + def __setitem__(self, key, value): + res = super(SignedDigestAlgorithm, self).__setitem__(key, value) + if key != 'algorithm': + return res + if self['algorithm'].native not in self._null_algos: + return res + if self['parameters'].__class__ != Void: + return res + self['parameters'] = Null() + return res + @property def signature_algo(self): """ diff --git a/dev/tests.py b/dev/tests.py index 14119e2..b9fdd4d 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import unittest import re +from tests.test_algos import AlgoTests from tests.test_cms import CMSTests from tests.test_crl import CRLTests from tests.test_csr import CSRTests @@ -15,7 +16,18 @@ from tests.test_x509 import X509Tests from tests.test_core import CoreTests -test_classes = [CMSTests, CRLTests, CSRTests, KeysTests, OCSPTests, PEMTests, TSPTests, X509Tests, CoreTests] +test_classes = [ + AlgoTests, + CMSTests, + CRLTests, + CSRTests, + KeysTests, + OCSPTests, + PEMTests, + TSPTests, + X509Tests, + CoreTests +] def make_suite(): diff --git a/tests/test_algos.py b/tests/test_algos.py new file mode 100644 index 0000000..d3aea6a --- /dev/null +++ b/tests/test_algos.py @@ -0,0 +1,29 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import unittest +import sys +import os + +from asn1crypto import algos, core +from ._unittest_compat import patch + +patch() + +if sys.version_info < (3,): + byte_cls = str + num_cls = long # noqa +else: + byte_cls = bytes + num_cls = int + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + +class AlgoTests(unittest.TestCase): + + def test_signed_digest_parameters(self): + sha256_rsa = algos.SignedDigestAlgorithm({'algorithm': 'sha256_rsa'}) + self.assertEqual(core.Null,sha256_rsa['parameters'].__class__) -- cgit v1.2.3 From 3eeb96af94eaf528c1a947f300d014f243ed0b73 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 21 Jul 2016 21:25:47 -0400 Subject: Fix DER encoding of core.Set and core.SetOf --- asn1crypto/core.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test_core.py | 19 +++++++++++++ 2 files changed, 97 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 7eb9b96..8c05ed7 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3796,6 +3796,7 @@ class Set(Sequence): cls = self.__class__ cls._field_map = {} cls._field_ids = {} + cls._precomputed_specs = [] for index, field in enumerate(cls._fields): if len(field) < 3: field = field + ({},) @@ -3806,6 +3807,14 @@ class Set(Sequence): if cls._oid_pair is not None: cls._oid_nums = (cls._field_map[cls._oid_pair[0]], cls._field_map[cls._oid_pair[1]]) + for index, field in enumerate(cls._fields): + has_callback = cls._spec_callbacks is not None and field[0] in cls._spec_callbacks + is_mapped_oid = cls._oid_nums is not None and cls._oid_nums[1] == index + if has_callback or is_mapped_oid: + cls._precomputed_specs.append(None) + else: + cls._precomputed_specs.append((field[0], field[1], field[1], field[2], None)) + def _parse_children(self, recurse=False): """ Parses the contents and generates Asn1Value objects based on the @@ -3819,6 +3828,18 @@ class Set(Sequence): ValueError - when an error occurs parsing child objects """ + if self._contents is None: + if self._fields: + self.children = [VOID] * len(self._fields) + for index, (_, _, params) in enumerate(self._fields): + if 'default' in params: + if cls._precomputed_specs[index]: + field_name, field_spec, value_spec, field_params, _ = cls._precomputed_specs[index] + else: + field_name, field_spec, value_spec, field_params, _ = self._determine_spec(index) + self.children[index] = self._make_value(field_name, field_spec, value_spec, field_params, None) + return + try: child_map = {} contents_length = len(self.contents) @@ -3900,6 +3921,39 @@ class Set(Sequence): e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args raise e + def _set_contents(self, force=False): + """ + Encodes all child objects into the contents for this object. + + This method is overridden because a Set needs to be encoded by + removing defaulted fields and then sorting the fields by tag. + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data + """ + + if self.children is None: + self._parse_children() + + child_tag_encodings = [] + for index, child in enumerate(self.children): + child_encoding = child.dump(force=force) + + # Skip encoding defaulted children + name, spec, field_params = self._fields[index] + if 'default' in field_params: + if spec(**field_params).dump() == child_encoding: + continue + + child_tag_encodings.append((child.tag, child_encoding)) + child_tag_encodings.sort(key=lambda ct: ct[0]) + + self._contents = b''.join([ct[1] for ct in child_tag_encodings]) + self._header = None + if self._trailer != b'': + self._trailer = b'' + class SetOf(SequenceOf): """ @@ -3909,6 +3963,30 @@ class SetOf(SequenceOf): tag = 17 + def _set_contents(self, force=False): + """ + Encodes all child objects into the contents for this object. + + This method is overridden because a SetOf needs to be encoded by + sorting the child encodings. + + :param force: + Ensure all contents are in DER format instead of possibly using + cached BER-encoded data + """ + + if self.children is None: + self._parse_children() + + child_encodings = [] + for child in self: + child_encodings.append(child.dump(force=force)) + + self._contents = b''.join(sorted(child_encodings)) + self._header = None + if self._trailer != b'': + self._trailer = b'' + class EmbeddedPdv(Sequence): """ diff --git a/tests/test_core.py b/tests/test_core.py index a094eb8..32e4b2e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -78,6 +78,17 @@ class SeqChoice(core.Choice): ] +class SetTest(core.Set): + _fields = [ + ('two', core.Integer, {'tag_type': 'implicit', 'tag': 2}), + ('one', core.Integer, {'tag_type': 'implicit', 'tag': 1}), + ] + + +class SetOfTest(core.SetOf): + _child_spec = core.Integer + + class ConcatTest(core.Concat): _child_specs = [Seq, core.Integer] @@ -368,3 +379,11 @@ class CoreTests(unittest.TestCase): with self.assertRaises(ValueError): MyOids.unmap('no_such_mapping') + + def test_dump_set(self): + st = SetTest({'two': 2, 'one': 1}) + self.assertEqual(b'1\x06\x81\x01\x01\x82\x01\x02', st.dump()) + + def test_dump_set_of(self): + st = SetOfTest([3, 2, 1]) + self.assertEqual(b'1\x09\x02\x01\x01\x02\x01\x02\x02\x01\x03', st.dump()) -- cgit v1.2.3 From e1fa6cd5de4f533a1f15a0e20d643695b395325d Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 21 Jul 2016 21:46:25 -0400 Subject: Fix x509.Name.build() to generate a Sstructure with multiple x509.RelativeDistinguishedName() which each contain a single x509.NameTypeAndValue() --- asn1crypto/x509.py | 16 +++++++------- tests/test_x509.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index d725065..e296f51 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -868,7 +868,7 @@ class Name(Choice): An x509.Name object """ - attributes = [] + rdns = [] if not use_printable: encoding_name = 'utf8_string' encoding_class = UTF8String @@ -895,14 +895,14 @@ class Name(Choice): value=encoding_class(name_dict[attribute_name]) ) - attributes.append(NameTypeAndValue({ - 'type': attribute_name, - 'value': value - })) + rdns.append(RelativeDistinguishedName([ + NameTypeAndValue({ + 'type': attribute_name, + 'value': value + }) + ])) - rdn = RelativeDistinguishedName(attributes) - sequence = RDNSequence([rdn]) - return cls(name='', value=sequence) + return cls(name='', value=RDNSequence(rdns)) @property def hashable(self): diff --git a/tests/test_x509.py b/tests/test_x509.py index da97572..4ef53a5 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -365,8 +365,8 @@ class X509Tests(unittest.TestCase): 'common_name': 'Will Bond' } ) - self.assertIsInstance(utf8_name.chosen[0][2]['value'].chosen, core.UTF8String) - self.assertEqual('common_name', utf8_name.chosen[0][2]['type'].native) + self.assertIsInstance(utf8_name.chosen[2][0]['value'].chosen, core.UTF8String) + self.assertEqual('common_name', utf8_name.chosen[2][0]['type'].native) printable_name = x509.Name.build( { 'country_name': 'US', @@ -375,8 +375,8 @@ class X509Tests(unittest.TestCase): }, use_printable=True ) - self.assertIsInstance(printable_name.chosen[0][2]['value'].chosen, core.PrintableString) - self.assertEqual('common_name', printable_name.chosen[0][2]['type'].native) + self.assertIsInstance(printable_name.chosen[2][0]['value'].chosen, core.PrintableString) + self.assertEqual('common_name', printable_name.chosen[2][0]['type'].native) def test_v1_cert(self): cert = self._load_cert('chromium/ndn.ca.crt') @@ -2591,6 +2591,62 @@ class X509Tests(unittest.TestCase): cert = self._load_cert(relative_path) self.assertEqual(self_signed, cert.self_signed) + @staticmethod + def cert_list(): + return ( + ( + 'keys/test-der.crt', + ), + ( + 'keys/test-inter-der.crt', + ), + ( + 'keys/test-third-der.crt', + ), + ( + 'geotrust_certs/GeoTrust_Universal_CA.crt', + ), + ( + 'geotrust_certs/GeoTrust_Primary_CA.crt', + ), + ( + 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', + ), + ( + 'geotrust_certs/codex.crt', + ), + ( + 'lets_encrypt/isrgrootx1.pem', + ), + ( + 'lets_encrypt/letsencryptauthorityx1.pem', + ), + ( + 'lets_encrypt/letsencryptauthorityx2.pem', + ), + ( + 'globalsign_example_keys/IssuingCA-der.cer', + ), + ( + 'globalsign_example_keys/rootCA.cer', + ), + ( + 'globalsign_example_keys/SSL1.cer', + ), + ( + 'globalsign_example_keys/SSL2.cer', + ), + ( + 'globalsign_example_keys/SSL3.cer', + ), + ) + + @data('cert_list') + def name_is_rdn_squence_of_single_child_sets(self, relative_path): + cert = self._load_cert(relative_path) + for child in cert.subject.chosen: + self.assertEqual(1, len(child)) + def test_parse_certificate(self): cert = self._load_cert('keys/test-der.crt') -- cgit v1.2.3 From fc934bc357fcbd6dd6d2873727a3da9ef3ca924e Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 21 Jul 2016 22:06:23 -0400 Subject: Fix core.Set._parse_children() to properly support _spec_callbacks --- asn1crypto/core.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8c05ed7..1edecd7 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3828,6 +3828,7 @@ class Set(Sequence): ValueError - when an error occurs parsing child objects """ + cls = self.__class__ if self._contents is None: if self._fields: self.children = [VOID] * len(self._fields) @@ -3862,20 +3863,18 @@ class Set(Sequence): METHOD_NUM_TO_NAME_MAP.get(parts[1]), parts[2], )) - _, spec, field_params = self._fields[field] - parse_as = None - if self._oid_nums is not None and self._oid_nums[1] == field: - oid = self.children[self._oid_nums[0]].native - if isinstance(spec, Any): - spec = self._oid_specs[oid] - else: - parse_as = self._oid_specs[oid] + _, field_spec, value_spec, field_params, spec_override = ( + cls._precomputed_specs[field] or self._determine_spec(field)) + + if field_spec is None or (spec_override and issubclass(field_spec, Any)): + field_spec = value_spec + spec_override = None - if parse_as: - child = parts + (spec, field_params, parse_as) + if spec_override: + child = parts + (field_spec, field_params, value_spec) else: - child = parts + (spec, field_params) + child = parts + (field_spec, field_params) if recurse: child = _build(*child) @@ -3890,7 +3889,13 @@ class Set(Sequence): for index in range(0, total_fields): if index in child_map: continue - name, spec, field_params = self._fields[index] + + name, field_spec, value_spec, field_params, spec_override = ( + cls._precomputed_specs[index] or self._determine_spec(index)) + + if field_spec is None or (spec_override and issubclass(field_spec, Any)): + field_spec = value_spec + spec_override = None missing = False @@ -3901,7 +3906,7 @@ class Set(Sequence): elif 'optional' in field_params: child_map[index] = VOID elif 'default' in field_params: - child_map[index] = spec(**field_params) + child_map[index] = field_spec(**field_params) if missing: raise ValueError(unwrap( -- cgit v1.2.3 From f625cac0a49bafc96403f5b34c2e138f8d2cfbea Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 26 Jul 2016 06:29:55 -0400 Subject: Add support for flake8 3.0 --- dev/lint.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dev/lint.py b/dev/lint.py index bc9dddc..f2cfc7d 100644 --- a/dev/lint.py +++ b/dev/lint.py @@ -3,7 +3,11 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import os -from flake8.engine import get_style_guide +import flake8 +if flake8.__version_info__ < (3,): + from flake8.engine import get_style_guide +else: + from flake8.api.legacy import get_style_guide cur_dir = os.path.dirname(__file__) -- cgit v1.2.3 From e7c78f26abf26157965583fd508f0c32bad3b350 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 26 Jul 2016 06:39:02 -0400 Subject: Ensure Travis homebrew environment is up-to-date --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2809d77..24cf8e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,11 +44,13 @@ matrix: install: - if [ "$TRAVIS_OS_NAME" == "osx" ]; then if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then + brew update; brew install pypy; /usr/local/bin/pip_pypy install flake8; export PYTHON_BIN=/usr/local/bin/pypy; else if [ "$TRAVIS_PYTHON_VERSION" == "3.5" ]; then + brew update; brew install python3; /usr/local/bin/pip3 install flake8; export PYTHON_BIN=/usr/local/bin/python3; -- cgit v1.2.3 From cf24c6166e9428ee5e07740a8597ea21db24bd49 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 26 Jul 2016 08:00:01 -0400 Subject: Help flake8 3.0 work on Windows --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 5b59a06..7f8c068 100644 --- a/tox.ini +++ b/tox.ini @@ -10,3 +10,4 @@ max-line-length = 120 [flake8] max-line-length = 120 +jobs = 1 -- cgit v1.2.3 From 2b06d62d2722ba259757454b18333f114ab7e256 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 26 Jul 2016 08:13:39 -0400 Subject: Update version of Pypy used on AppVeyor --- appveyor.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 8227f8b..86778af 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,7 +20,7 @@ environment: - PYTHON: "C:\\Python33-x64" PYTHON_ID: "33-x64" PYTHON_EXE: python - - PYTHON: "C:\\pypy-4.0.0-win32" + - PYTHON: "C:\\pypy2-v5.3.1-win32" PYTHON_ID: "pypy" PYTHON_EXE: pypy install: @@ -31,10 +31,10 @@ install: } if ("${env:PYTHON_ID}" -eq "pypy") { - if (!(Test-Path "${env:PYTMP}\pypy-4.0.0-win32.zip")) { - (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy-4.0.0-win32.zip', "${env:PYTMP}\pypy-4.0.0-win32.zip"); + if (!(Test-Path "${env:PYTMP}\pypy2-v5.3.1-win32.zip")) { + (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.3.1-win32.zip', "${env:PYTMP}\pypy2-v5.3.1-win32.zip"); } - 7z x -y "${env:PYTMP}\pypy-4.0.0-win32.zip" -oC:\ | Out-Null; + 7z x -y "${env:PYTMP}\pypy2-v5.3.1-win32.zip" -oC:\ | Out-Null; if (!(Test-Path "${env:PYTMP}\get-pip.py")) { (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); } -- cgit v1.2.3 From 71ff361ba342c27f1daa56e1d1883ffad15f3457 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 26 Jul 2016 15:59:50 -0400 Subject: Remove pypy3 support since it is... not good --- .travis.yml | 3 --- readme.md | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 24cf8e1..6a5c6e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,9 +38,6 @@ matrix: - os: linux language: python python: "pypy" - - os: linux - language: python - python: "pypy3" install: - if [ "$TRAVIS_OS_NAME" == "osx" ]; then if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then diff --git a/readme.md b/readme.md index 82d32b9..669eb98 100644 --- a/readme.md +++ b/readme.md @@ -108,7 +108,7 @@ faster to an order of magnitude of more. ## Dependencies -Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, pypy or pypy3. *No third-party packages +Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 or pypy. *No third-party packages required.* ## Installation -- cgit v1.2.3 From 01897dd46b067b1ffe2cf90691f9fe72f48c8ede Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 26 Jul 2016 06:15:49 -0400 Subject: Version 0.18.1 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 17 +++++++++++++++++ readme.md | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index c1c57de..980037d 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.18.0' -__version_info__ = (0, 18, 0) +__version__ = '0.18.1' +__version_info__ = (0, 18, 1) diff --git a/changelog.md b/changelog.md index 4450e3c..f5a9149 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,22 @@ # changelog +## 0.18.1 + + - Fixed DER encoding of `core.Set` and `core.SetOf` + - Fixed a bug in `x509.Name.build()` that could generate invalid DER encoding + - Improved exception messages when parsing nested structures via the `.native` + attribute + - `algos.SignedDigestAlgorithm` now ensures the `parameters` are set to + `Null` when `algorithm` is `sha224_rsa`, `sha256_rsa`, `sha384_rsa` or + `sha512_rsa`, per RFC 4055 + - Corrected the definition of `pdf.AdobeTimestamp` to mark the + `requires_auth` field as optional + - Add support for the OID `1.2.840.113549.1.9.16.2.14` to + `cms.CMSAttributeType` + - Improve attribute support for `cms.AttributeCertificateV2` + - Handle `cms.AttributeCertificateV2` when incorrectly tagged as + `cms.AttributeCertificateV1` in `cms.CertificateChoices` + ## 0.18.0 - Improved general parsing performance by 10-15% diff --git a/readme.md b/readme.md index 669eb98..9cffa03 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.18.0 - [changelog](changelog.md) +0.18.1 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 3607220ae359b5752507033f219dd6fd25198d91 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 26 Jul 2016 17:50:16 -0400 Subject: Remove pypy3 from tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7f8c068..9e16787 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33,py34,py35,pypy,pypy3 +envlist = py26,py27,py32,py33,py34,py35,pypy [testenv] deps = flake8 -- cgit v1.2.3 From fb9c136968c9c116efcb38354e2618814d024547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Fri, 29 Jul 2016 03:59:02 +0200 Subject: Rectify CMS docs --- asn1crypto/cms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index cd18a4b..af8f6fe 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -8,9 +8,9 @@ compatible with PKCS#7. Exports the following items: - AuthEnvelopedData() - CompressedData() - ContentInfo() + - DigestedData() - EncryptedData() - EnvelopedData() - - EnvelopedData() - SignedAndEnvelopedData() - SignedData() -- cgit v1.2.3 From 1dba6292a33be56d470b85c9fb94307f4deb2ba3 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 29 Jul 2016 11:34:19 -0400 Subject: Allow _perf/ to be removed --- asn1crypto/_int.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index bf1ec3a..f8af877 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -98,7 +98,7 @@ try: return result # If there was an issue using OpenSSL, we fall back to pure python -except (LibraryNotFoundError, FFIEngineError, EnvironmentError): +except (LibraryNotFoundError, FFIEngineError, EnvironmentError, ImportError): def inverse_mod(a, p): """ -- cgit v1.2.3 From 0ae3e884162ac338335e779220b0e465a14e1a04 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 29 Jul 2016 13:49:35 -0400 Subject: Version 0.18.2 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 4 ++++ readme.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 980037d..f7d299d 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.18.1' -__version_info__ = (0, 18, 1) +__version__ = '0.18.2' +__version_info__ = (0, 18, 2) diff --git a/changelog.md b/changelog.md index f5a9149..31ae97d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # changelog +## 0.18.2 + + - Allow `_perf` submodule to be removed from source tree when embedding + ## 0.18.1 - Fixed DER encoding of `core.Set` and `core.SetOf` diff --git a/readme.md b/readme.md index 9cffa03..4a56f5b 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.18.1 - [changelog](changelog.md) +0.18.2 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From e492a23f98ea2d3edd13d390e009c08c15b31753 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 5 Aug 2016 06:32:37 -0400 Subject: core.IntegerOctetString is for positive values only --- asn1crypto/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 1edecd7..10a553f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1982,8 +1982,7 @@ class IntegerOctetString(Primitive): )) self._native = value - # Set the unused bits to 0 - self.contents = int_to_bytes(value, signed=True) + self.contents = int_to_bytes(value, signed=False) self._header = None if self._trailer != b'': self._trailer = b'' -- cgit v1.2.3 From 88eb8483ca9a01f0173ac4c8968dfeb32eadb990 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 5 Aug 2016 06:33:27 -0400 Subject: Fix a bug with keys.PrivateKeyInfo.wrap() and EC keys that could lead to side effects --- asn1crypto/keys.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 8e8212f..bb875e8 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -557,6 +557,8 @@ class PrivateKeyInfo(Sequence): elif algorithm == 'ec': if not isinstance(private_key, ECPrivateKey): private_key = ECPrivateKey.load(private_key) + else: + private_key = private_key.copy() params = private_key['parameters'] del private_key['parameters'] else: -- cgit v1.2.3 From 38a2985cb1a7225acd62d32a16a2fb31c178d03f Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 5 Aug 2016 07:15:23 -0400 Subject: Fix DER encoding of core.BitString when a _map is specified, i.e. a named bit list --- asn1crypto/core.py | 11 ++++++++++- tests/test_core.py | 6 +++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 10a553f..ebfe1ee 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1626,7 +1626,16 @@ class BitString(Primitive, ValueMap, object): size, len(value) )) - value += '0' * (size - len(value)) + # A NamedBitList must have trailing zero bit truncated. See + # https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf + # section 11.2, + # https://tools.ietf.org/html/rfc5280#page-134 and + # https://www.ietf.org/mail-archive/web/pkix/current/msg10443.html + value = value.rstrip('0') + size = len(value) + if size == 0: + value = '0' + size = 1 else: size = len(value) diff --git a/tests/test_core.py b/tests/test_core.py index 32e4b2e..4945016 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -213,17 +213,17 @@ class CoreTests(unittest.TestCase): return ( ( (0, 1, 1), - b'\x03\x02\x00\x60', + b'\x03\x02\x05\x60', set(['one', 'two']) ), ( (0,), - b'\x03\x02\x00\x00', + b'\x03\x02\x07\x00', set() ), ( set(['one', 'two']), - b'\x03\x02\x00\x60', + b'\x03\x02\x05\x60', set(['one', 'two']) ) ) -- cgit v1.2.3 From de9e492a1a563af606ebc3ae5e39c2454cbac1b8 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 5 Aug 2016 11:24:59 -0400 Subject: Fix for core.BitString with _map and no 1 bits --- asn1crypto/core.py | 12 +++--------- tests/test_core.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ebfe1ee..eea1026 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1616,14 +1616,13 @@ class BitString(Primitive, ValueMap, object): )) if self._map is not None: - size = self._size - if len(value) > size: + if len(value) > self._size: raise ValueError(unwrap( ''' %s value must be at most %s bits long, specified was %s long ''', type_name(self), - size, + self._size, len(value) )) # A NamedBitList must have trailing zero bit truncated. See @@ -1632,12 +1631,7 @@ class BitString(Primitive, ValueMap, object): # https://tools.ietf.org/html/rfc5280#page-134 and # https://www.ietf.org/mail-archive/web/pkix/current/msg10443.html value = value.rstrip('0') - size = len(value) - if size == 0: - value = '0' - size = 1 - else: - size = len(value) + size = len(value) size_mod = size % 8 extra_bits = 0 diff --git a/tests/test_core.py b/tests/test_core.py index 4945016..cdc4547 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -218,7 +218,7 @@ class CoreTests(unittest.TestCase): ), ( (0,), - b'\x03\x02\x07\x00', + b'\x03\x01\x00', set() ), ( -- cgit v1.2.3 From 9ddb2db296d47250036251821d584b719d96ada0 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 5 Aug 2016 11:30:11 -0400 Subject: Fix util.int_from_bytes() on Python 2 to return 0 from an empty byte string --- asn1crypto/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 59ecdcb..13989bb 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -92,6 +92,9 @@ if sys.version_info <= (3,): An integer """ + if value == b'': + return 0 + num = long(value.encode("hex"), 16) # noqa if not signed: -- cgit v1.2.3 From 1b0ef84d39aa3935bec1bb677327902715266a2d Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 5 Aug 2016 11:37:03 -0400 Subject: Version 0.18.3 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 13 +++++++++++++ readme.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index f7d299d..b58d72c 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.18.2' -__version_info__ = (0, 18, 2) +__version__ = '0.18.3' +__version_info__ = (0, 18, 3) diff --git a/changelog.md b/changelog.md index 31ae97d..08567ed 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # changelog +## 0.18.3 + + - Fixed DER encoding of `core.BitString` when a `_map` is specified (i.e. a + "named bit list") to omit trailing zero bits. This fixes compliance of + various `x509` structures with RFC 5280. + - Corrected a side effect in `keys.PrivateKeyInfo.wrap()` that would cause the + original `keys.ECPrivateKey` structure to become corrupt + - `core.IntegerOctetString` now correctly encodes the integer as an unsigned + value when converting to bytes. Previously decoding was unsigned, but + encoding was signed. + - Fix `util.int_from_bytes()` on Python 2 to return `0` from an empty byte + string + ## 0.18.2 - Allow `_perf` submodule to be removed from source tree when embedding diff --git a/readme.md b/readme.md index 4a56f5b..d05b748 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.18.2 - [changelog](changelog.md) +0.18.3 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From e7be3512dce0afaa6c30148e5d26406147457238 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 27 Aug 2016 15:29:29 -0400 Subject: Update license copyright years --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 6352e34..9172400 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 Will Bond +Copyright (c) 2015-2016 Will Bond 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 -- cgit v1.2.3 From afd3cac4b33d56aaf8ddcce2c333b60828350c3d Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 27 Aug 2016 15:30:39 -0400 Subject: Prevent a UnicodeDecodeError on Python 2 when debugging core.OctetString --- asn1crypto/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index eea1026..5d58671 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -421,7 +421,10 @@ class Asn1Value(object): elif hasattr(self, 'chosen'): self.chosen.debug(nest_level + 2) else: - print('%s Native: %s' % (prefix, self.native)) + if py2 and isinstance(self.native, byte_cls): + print('%s Native: b%s' % (prefix, repr(self.native))) + else: + print('%s Native: %s' % (prefix, self.native)) def dump(self, force=False): """ -- cgit v1.2.3 From be66d27618c73310e3de653367fed2d77fa8b9b8 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 27 Aug 2016 16:00:08 -0400 Subject: Fix default value of hash_algorithm field in tsp.ESSCertIDv2 --- asn1crypto/tsp.py | 2 +- tests/test_algos.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py index 8f2f463..ac73821 100644 --- a/asn1crypto/tsp.py +++ b/asn1crypto/tsp.py @@ -278,7 +278,7 @@ class SetOfSigningCertificates(SetOf): class ESSCertIDv2(Sequence): _fields = [ - ('hash_algorithm', DigestAlgorithm, {'default': 'sha256'}), + ('hash_algorithm', DigestAlgorithm, {'default': {'algorithm': 'sha256'}}), ('cert_hash', OctetString), ('issuer_serial', IssuerSerial, {'optional': True}), ] diff --git a/tests/test_algos.py b/tests/test_algos.py index d3aea6a..43eb642 100644 --- a/tests/test_algos.py +++ b/tests/test_algos.py @@ -26,4 +26,4 @@ class AlgoTests(unittest.TestCase): def test_signed_digest_parameters(self): sha256_rsa = algos.SignedDigestAlgorithm({'algorithm': 'sha256_rsa'}) - self.assertEqual(core.Null,sha256_rsa['parameters'].__class__) + self.assertEqual(core.Null, sha256_rsa['parameters'].__class__) -- cgit v1.2.3 From 2260ee300972ae3e0cd7d31710d21432466773e0 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 27 Aug 2016 16:02:06 -0400 Subject: Have core.Sequence raise a ValueError if an unknown field is provided to the constructor --- asn1crypto/core.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 5d58671..31a4040 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2629,8 +2629,10 @@ class Sequence(Asn1Value): # before the OID field, resulting in invalid value object creation. if self._fields: keys = [info[0] for info in self._fields] + unused_keys = set(value.keys()) else: keys = value.keys() + unused_keys = set(keys) for key in keys: # If we are setting defaults, but a real value has already @@ -2638,10 +2640,23 @@ class Sequence(Asn1Value): if check_existing: index = self._field_map[key] if index < len(self.children) and self.children[index] is not VOID: + if key in unused_keys: + unused_keys.remove(key) continue if key in value: self.__setitem__(key, value[key]) + unused_keys.remove(key) + + if len(unused_keys): + raise ValueError(unwrap( + ''' + One or more unknown fields was passed to the constructor + of %s: %s + ''', + type_name(self), + ', '.join(sorted(list(unused_keys))) + )) except (ValueError, TypeError) as e: args = e.args[1:] -- cgit v1.2.3 From 3aa0dca1adfc01af78dc2a2692b1b4b98ab8c800 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 29 Aug 2016 13:42:31 -0400 Subject: Fix a bug with constructing a cms.SignedData() object --- asn1crypto/core.py | 5 +++++ tests/test_cms.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 31a4040..cb1a854 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -2975,6 +2975,11 @@ class Sequence(Asn1Value): if value_spec is None: value_spec = field_spec spec_override = None + # When no field spec is specified, use a single return value as that + elif field_spec is None: + field_spec = spec_override + value_spec = field_spec + spec_override = None else: value_spec = spec_override diff --git a/tests/test_cms.py b/tests/test_cms.py index 858b83c..c8a8a69 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -16,6 +16,48 @@ fixtures_dir = os.path.join(tests_root, 'fixtures') class CMSTests(unittest.TestCase): + def test_create_content_info_data(self): + data = cms.SignedData({ + 'version': 'v1', + 'encap_content_info': { + 'content_type': 'data', + 'content': b'Hello', + } + }) + info = data['encap_content_info'] + + self.assertEqual('v1', data['version'].native) + self.assertEqual( + 'data', + info['content_type'].native + ) + self.assertEqual( + b'Hello', + info['content'].native + ) + self.assertIsInstance(info, cms.ContentInfo) + + def test_create_content_info_data_v2(self): + data = cms.SignedData({ + 'version': 'v2', + 'encap_content_info': { + 'content_type': 'data', + 'content': b'Hello', + } + }) + info = data['encap_content_info'] + + self.assertEqual('v2', data['version'].native) + self.assertEqual( + 'data', + info['content_type'].native + ) + self.assertEqual( + b'Hello', + info['content'].native + ) + self.assertIsInstance(info, cms.EncapsulatedContentInfo) + def test_parse_content_info_data(self): with open(os.path.join(fixtures_dir, 'message.der'), 'rb') as f: info = cms.ContentInfo.load(f.read()) -- cgit v1.2.3 From 381a4daca23807a2e8e24ba1ffe093bceba2ea66 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 30 Aug 2016 11:39:54 -0400 Subject: Ensure RSA OIDs have core.Null parameters across various structures --- asn1crypto/algos.py | 91 ++++++++++++++++++++++++++++++++--------------------- asn1crypto/cms.py | 3 +- asn1crypto/keys.py | 8 ++--- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 20dde15..36ea83c 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -43,6 +43,57 @@ class AlgorithmIdentifier(Sequence): ] +class _ForceNullParameters(object): + """ + Various structures based on AlgorithmIdentifier require that the parameters + field be core.Null() for certain OIDs. This mixin ensures that happens. + """ + + # The following attribute, plus the parameters spec callback and custom + # __setitem__ are all to handle a situation where parameters should not be + # optional and must be Null for certain OIDs. More info at + # https://tools.ietf.org/html/rfc4055#page-15 + _null_algos = set([ + '1.2.840.113549.1.1.1', # rsassa_pkcs1v15 / rsaes_pkcs1v15 / rsa + '1.2.840.113549.1.1.11', # sha256_rsa + '1.2.840.113549.1.1.12', # sha384_rsa + '1.2.840.113549.1.1.13', # sha512_rsa + '1.2.840.113549.1.1.14', # sha224_rsa + ]) + + def _parameters_spec(self): + if self._oid_pair == ('algorithm', 'parameters'): + algo = self['algorithm'].native + if algo in self._oid_specs: + return self._oid_specs[algo] + + if self['algorithm'].dotted in self._null_algos: + return Null + + return None + + _spec_callbacks = { + 'parameters': _parameters_spec + } + + # We have to override this since the spec callback uses the value of + # algorithm to determine the parameter spec, however default values are + # assigned before setting a field, so a default value can't be based on + # another field value (unless it is a default also). Thus we have to + # manually check to see if the algorithm was set and parameters is unset, + # and then fix the value as appropriate. + def __setitem__(self, key, value): + res = super(_ForceNullParameters, self).__setitem__(key, value) + if key != 'algorithm': + return res + if self['algorithm'].dotted not in self._null_algos: + return res + if self['parameters'].__class__ != Void: + return res + self['parameters'] = Null() + return res + + class HmacAlgorithmId(ObjectIdentifier): _map = { '1.3.14.3.2.10': 'des_mac', @@ -212,47 +263,17 @@ class SignedDigestAlgorithmId(ObjectIdentifier): } -class SignedDigestAlgorithm(Sequence): +class SignedDigestAlgorithm(_ForceNullParameters, Sequence): _fields = [ ('algorithm', SignedDigestAlgorithmId), ('parameters', Any, {'optional': True}), ] - # The following attribute, plus the parameters spec callback and custom - # __setitem__ are all to handle a situation where parameters should not be - # optional and must be Null for certain OIDs. More info at - # https://tools.ietf.org/html/rfc4055#page-15 - _null_algos = set(['sha224_rsa', 'sha256_rsa', 'sha384_rsa', 'sha512_rsa']) - - def _parameters_spec(self): - algo = self['algorithm'].native - if algo == 'rsassa_pss': - return RSASSAPSSParams - if algo in self._null_algos: - return Null - return None - - _spec_callbacks = { - 'parameters': _parameters_spec + _oid_pair = ('algorithm', 'parameters') + _oid_specs = { + 'rsassa_pss': RSASSAPSSParams, } - # We have to override this since the spec callback uses the value of - # algorithm to determine the parameter spec, however default values are - # assigned before setting a field, so a default value can't be based on - # another field value (unless it is a default also). Thus we have to - # manually check to see if the algorithm was set and parameters is unset, - # and then fix the value as appropriate. - def __setitem__(self, key, value): - res = super(SignedDigestAlgorithm, self).__setitem__(key, value) - if key != 'algorithm': - return res - if self['algorithm'].native not in self._null_algos: - return res - if self['parameters'].__class__ != Void: - return res - self['parameters'] = Null() - return res - @property def signature_algo(self): """ @@ -535,7 +556,7 @@ class EncryptionAlgorithmId(ObjectIdentifier): } -class EncryptionAlgorithm(Sequence): +class EncryptionAlgorithm(_ForceNullParameters, Sequence): _fields = [ ('algorithm', EncryptionAlgorithmId), ('parameters', Any, {'optional': True}), diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index af8f6fe..d66e931 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -25,6 +25,7 @@ except (ImportError): zlib = None from .algos import ( + _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm, HmacAlgorithm, @@ -645,7 +646,7 @@ class KeyEncryptionAlgorithmId(ObjectIdentifier): } -class KeyEncryptionAlgorithm(Sequence): +class KeyEncryptionAlgorithm(_ForceNullParameters, Sequence): _fields = [ ('algorithm', KeyEncryptionAlgorithmId), ('parameters', Any, {'optional': True}), diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index bb875e8..903725f 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -30,7 +30,7 @@ from ._elliptic_curve import ( ) from ._errors import unwrap from ._types import type_name, str_cls, byte_cls -from .algos import DigestAlgorithm, EncryptionAlgorithm +from .algos import _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm from .core import ( Any, Asn1Value, @@ -470,7 +470,7 @@ class PrivateKeyAlgorithmId(ObjectIdentifier): } -class PrivateKeyAlgorithm(Sequence): +class PrivateKeyAlgorithm(_ForceNullParameters, Sequence): """ Original Name: PrivateKeyAlgorithmIdentifier Source: https://tools.ietf.org/html/rfc5208#page-3 @@ -483,7 +483,6 @@ class PrivateKeyAlgorithm(Sequence): _oid_pair = ('algorithm', 'parameters') _oid_specs = { - 'rsa': Null, 'dsa': DSAParams, 'ec': ECDomainParameters, } @@ -940,7 +939,7 @@ class PublicKeyAlgorithmId(ObjectIdentifier): } -class PublicKeyAlgorithm(Sequence): +class PublicKeyAlgorithm(_ForceNullParameters, Sequence): """ Original Name: AlgorithmIdentifier Source: https://tools.ietf.org/html/rfc5280#page-18 @@ -953,7 +952,6 @@ class PublicKeyAlgorithm(Sequence): _oid_pair = ('algorithm', 'parameters') _oid_specs = { - 'rsa': Null, 'dsa': DSAParams, 'ec': ECDomainParameters, 'dh': DomainParameters, -- cgit v1.2.3 From 2ffc6481e2e6c82d6df53381599d65a9223dcfbb Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 13 Sep 2016 15:19:11 -0400 Subject: Version 0.18.4 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 11 +++++++++++ readme.md | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index b58d72c..6ab6984 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.18.3' -__version_info__ = (0, 18, 3) +__version__ = '0.18.4' +__version_info__ = (0, 18, 4) diff --git a/changelog.md b/changelog.md index 08567ed..7a80f6f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # changelog +## 0.18.4 + + - `core.Sequence` will now raise an exception when an unknown field is provided + - Prevent `UnicodeDecodeError` on Python 2 when calling + `core.OctetString.debug()` + - Corrected the default value for the `hash_algorithm` field of + `tsp.ESSCertIDv2` + - Fixed a bug constructing a `cms.SignedData` object + - Ensure that specific RSA OIDs are always paired with `parameters` set to + `core.Null` + ## 0.18.3 - Fixed DER encoding of `core.BitString` when a `_map` is specified (i.e. a diff --git a/readme.md b/readme.md index d05b748..508b996 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.18.3 - [changelog](changelog.md) +0.18.4 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 1e69e99bf94ae5db0f6cdf40fc4fff0f8a23244d Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 13 Sep 2016 16:42:27 -0400 Subject: Don't require ctypes --- asn1crypto/_int.py | 90 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index f8af877..f62a3a4 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -36,69 +36,73 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math import platform -from ._ffi import ( - buffer_from_bytes, - bytes_from_buffer, - FFIEngineError, - LibraryNotFoundError, - null, -) 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() - from ._perf._big_num_ctypes import libcrypto + try: + from ._perf._big_num_ctypes import libcrypto - def inverse_mod(a, p): - """ - Compute the modular inverse of a (mod p) + def inverse_mod(a, p): + """ + Compute the modular inverse of a (mod p) - :param a: - An integer + :param a: + An integer - :param p: - An integer + :param p: + An integer - :return: - An integer - """ + :return: + An integer + """ - ctx = libcrypto.BN_CTX_new() + ctx = libcrypto.BN_CTX_new() - a_bytes = int_to_bytes(abs(a)) - p_bytes = int_to_bytes(abs(p)) + 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) + 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) + 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) + 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) + libcrypto.BN_free(a_bn) + libcrypto.BN_free(p_bn) + libcrypto.BN_free(r_bn) + libcrypto.BN_CTX_free(ctx) - return result + return result + except (LibraryNotFoundError, FFIEngineError): + raise EnvironmentError() -# If there was an issue using OpenSSL, we fall back to pure python -except (LibraryNotFoundError, FFIEngineError, EnvironmentError, ImportError): +# If there was an issue using ctypes or OpenSSL, we fall back to pure python +except (EnvironmentError, ImportError): def inverse_mod(a, p): """ -- cgit v1.2.3 From f836db19d35dc923f9b165337cbe6a26aa1d1015 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 13 Sep 2016 16:44:10 -0400 Subject: Version 0.18.5 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 4 ++++ readme.md | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 6ab6984..2b05133 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.18.4' -__version_info__ = (0, 18, 4) +__version__ = '0.18.5' +__version_info__ = (0, 18, 5) diff --git a/changelog.md b/changelog.md index 7a80f6f..1f47297 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # changelog +## 0.18.5 + + - Don't fail importing if `ctypes` or `_ctypes` is not available + ## 0.18.4 - `core.Sequence` will now raise an exception when an unknown field is provided diff --git a/readme.md b/readme.md index 508b996..30c5833 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.18.4 - [changelog](changelog.md) +0.18.5 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From c429d608702f5318e1f6a47c38c2e00bfff193df Mon Sep 17 00:00:00 2001 From: Peter Sagerson Date: Fri, 21 Oct 2016 12:02:35 -0700 Subject: Support OIDs in x509.Name.build(). As documented. --- asn1crypto/x509.py | 37 ++++++++++++++++++++++++++++++------- tests/test_x509.py | 10 ++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index e296f51..6eb4d46 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -491,6 +491,24 @@ class NameType(ObjectIdentifier): 'organization_identifier', ] + @classmethod + def preferred_ordinal(cls, attr_name): + """ + Returns an ordering value for a particular attribute key. + + Unrecognized attributes and OIDs will be sorted lexically at the end. + + :return: + An orderable value. + + """ + if attr_name in cls.preferred_order: + ordinal = cls.preferred_order.index(attr_name) + else: + ordinal = len(cls.preferred_order) + + return (ordinal, attr_name) + @property def human_friendly(self): """ @@ -876,23 +894,28 @@ class Name(Choice): encoding_name = 'printable_string' encoding_class = PrintableString - for attribute_name in NameType.preferred_order: - if attribute_name not in name_dict: - continue + # Sort the attributes according to NameType.preferred_order. + name_dict = OrderedDict( + sorted( + name_dict.items(), + key=lambda item: NameType.preferred_ordinal(item[0]) + ) + ) + for attribute_name, attribute_value in name_dict.items(): if attribute_name == 'email_address': - value = EmailAddress(name_dict[attribute_name]) + value = EmailAddress(attribute_value) elif attribute_name == 'domain_component': - value = DNSName(name_dict[attribute_name]) + value = DNSName(attribute_value) elif attribute_name in set(['dn_qualifier', 'country_name', 'serial_number']): value = DirectoryString( name='printable_string', - value=PrintableString(name_dict[attribute_name]) + value=PrintableString(attribute_value) ) else: value = DirectoryString( name=encoding_name, - value=encoding_class(name_dict[attribute_name]) + value=encoding_class(attribute_value) ) rdns.append(RelativeDistinguishedName([ diff --git a/tests/test_x509.py b/tests/test_x509.py index 4ef53a5..4432af4 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -336,6 +336,16 @@ class X509Tests(unittest.TestCase): 'common_name': 'Will Bond \U0001D173\U000E007F' }) ), + ( + False, + x509.Name.build({ + 'common_name': 'Will Bond', + '0.9.2342.19200300.100.1.1': 'wbond' + }), + x509.Name.build({ + 'common_name': 'Will Bond', + }), + ), ( False, x509.Name.build({ -- cgit v1.2.3 From 0939833d739e167d410091a514d7453165f42557 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 06:20:53 -0500 Subject: Enforce core.Null() be used as parameters with certain algos.DigestAlgorithm() algorithms This is for compatibility with software tested only against OpenSSL --- asn1crypto/algos.py | 10 ++++++++-- tests/test_algos.py | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 36ea83c..746b074 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -52,13 +52,19 @@ class _ForceNullParameters(object): # The following attribute, plus the parameters spec callback and custom # __setitem__ are all to handle a situation where parameters should not be # optional and must be Null for certain OIDs. More info at - # https://tools.ietf.org/html/rfc4055#page-15 + # https://tools.ietf.org/html/rfc4055#page-15 and + # https://tools.ietf.org/html/rfc4055#section-2.1 _null_algos = set([ '1.2.840.113549.1.1.1', # rsassa_pkcs1v15 / rsaes_pkcs1v15 / rsa '1.2.840.113549.1.1.11', # sha256_rsa '1.2.840.113549.1.1.12', # sha384_rsa '1.2.840.113549.1.1.13', # sha512_rsa '1.2.840.113549.1.1.14', # sha224_rsa + '1.3.14.3.2.26', # sha1 + '2.16.840.1.101.3.4.2.4', # sha224 + '2.16.840.1.101.3.4.2.1', # sha256 + '2.16.840.1.101.3.4.2.2', # sha384 + '2.16.840.1.101.3.4.2.3', # sha512 ]) def _parameters_spec(self): @@ -128,7 +134,7 @@ class DigestAlgorithmId(ObjectIdentifier): } -class DigestAlgorithm(Sequence): +class DigestAlgorithm(_ForceNullParameters, Sequence): _fields = [ ('algorithm', DigestAlgorithmId), ('parameters', Any, {'optional': True}), diff --git a/tests/test_algos.py b/tests/test_algos.py index 43eb642..a3550af 100644 --- a/tests/test_algos.py +++ b/tests/test_algos.py @@ -27,3 +27,7 @@ class AlgoTests(unittest.TestCase): def test_signed_digest_parameters(self): sha256_rsa = algos.SignedDigestAlgorithm({'algorithm': 'sha256_rsa'}) self.assertEqual(core.Null, sha256_rsa['parameters'].__class__) + + def test_digest_parameters(self): + sha1 = algos.DigestAlgorithm({'algorithm': 'sha1'}) + self.assertEqual(core.Null, sha1['parameters'].__class__) -- cgit v1.2.3 From a01911d8880b744c5267aee7e7ed8b2ddbe3ac46 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 06:54:42 -0500 Subject: Ensure x509.Name.build() accepts dotted form of well-known OIDs also --- asn1crypto/x509.py | 5 ++++- tests/test_x509.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 6eb4d46..8782fce 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -502,6 +502,8 @@ class NameType(ObjectIdentifier): An orderable value. """ + + attr_name = cls.map(attr_name) if attr_name in cls.preferred_order: ordinal = cls.preferred_order.index(attr_name) else: @@ -894,7 +896,7 @@ class Name(Choice): encoding_name = 'printable_string' encoding_class = PrintableString - # Sort the attributes according to NameType.preferred_order. + # Sort the attributes according to NameType.preferred_order name_dict = OrderedDict( sorted( name_dict.items(), @@ -903,6 +905,7 @@ class Name(Choice): ) for attribute_name, attribute_value in name_dict.items(): + attribute_name = NameType.map(attribute_name) if attribute_name == 'email_address': value = EmailAddress(attribute_value) elif attribute_name == 'domain_component': diff --git a/tests/test_x509.py b/tests/test_x509.py index 4432af4..1cef8cf 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -336,6 +336,26 @@ class X509Tests(unittest.TestCase): 'common_name': 'Will Bond \U0001D173\U000E007F' }) ), + ( + True, + x509.Name.build({ + '2.5.4.3': 'Will Bond', + }), + x509.Name.build({ + 'common_name': 'Will Bond', + }), + ), + ( + True, + x509.Name.build({ + '2.5.4.6': 'US', + 'common_name': 'Will Bond' + }), + x509.Name.build({ + 'country_name': 'US', + 'common_name': 'Will Bond' + }) + ), ( False, x509.Name.build({ -- cgit v1.2.3 From 36e6bec77a085a639070d89a59adebb077f80fff Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 09:33:53 -0500 Subject: Fix lint error --- asn1crypto/algos.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 746b074..1fe10ff 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -55,16 +55,16 @@ class _ForceNullParameters(object): # https://tools.ietf.org/html/rfc4055#page-15 and # https://tools.ietf.org/html/rfc4055#section-2.1 _null_algos = set([ - '1.2.840.113549.1.1.1', # rsassa_pkcs1v15 / rsaes_pkcs1v15 / rsa - '1.2.840.113549.1.1.11', # sha256_rsa - '1.2.840.113549.1.1.12', # sha384_rsa - '1.2.840.113549.1.1.13', # sha512_rsa - '1.2.840.113549.1.1.14', # sha224_rsa - '1.3.14.3.2.26', # sha1 - '2.16.840.1.101.3.4.2.4', # sha224 - '2.16.840.1.101.3.4.2.1', # sha256 - '2.16.840.1.101.3.4.2.2', # sha384 - '2.16.840.1.101.3.4.2.3', # sha512 + '1.2.840.113549.1.1.1', # rsassa_pkcs1v15 / rsaes_pkcs1v15 / rsa + '1.2.840.113549.1.1.11', # sha256_rsa + '1.2.840.113549.1.1.12', # sha384_rsa + '1.2.840.113549.1.1.13', # sha512_rsa + '1.2.840.113549.1.1.14', # sha224_rsa + '1.3.14.3.2.26', # sha1 + '2.16.840.1.101.3.4.2.4', # sha224 + '2.16.840.1.101.3.4.2.1', # sha256 + '2.16.840.1.101.3.4.2.2', # sha384 + '2.16.840.1.101.3.4.2.3', # sha512 ]) def _parameters_spec(self): -- cgit v1.2.3 From 0cde8d63c78529cc27a94d68eb7c482c2b959437 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 09:34:46 -0500 Subject: Call .dump() from .__repr__() so that non-parsed objects have output --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index cb1a854..52c94ef 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -297,7 +297,7 @@ class Asn1Value(object): A unicode string """ - return '<%s %s %s>' % (type_name(self), id(self), repr(self.contents or b'')) + return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump())) def _new_instance(self): """ -- cgit v1.2.3 From 0bbbb96ee768c5c6f32e541869f259652a054d97 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 09:35:28 -0500 Subject: Fix x509.Name.native to work if object was not parsed from a byte string --- asn1crypto/x509.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 8782fce..ba0311a 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -962,8 +962,6 @@ class Name(Choice): @property def native(self): - if self.contents is None: - return None if self._native is None: self._native = OrderedDict() for rdn in self.chosen.native: -- cgit v1.2.3 From 13c875d634ce5994b3e188e889846ba9ee8e6f39 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 09:44:38 -0500 Subject: Improve usability by providing default __bytes__() and __unicode__() methods for all core classes --- asn1crypto/core.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 52c94ef..3a3570d 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -297,7 +297,30 @@ class Asn1Value(object): A unicode string """ - return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump())) + if py2: + return '<%s %s b%s>' % (type_name(self), id(self), repr(self.dump())) + else: + return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump())) + + def __bytes__(self): + """ + A fall-back method for print() in Python 2 + + :return: + A byte string of the output of repr() + """ + + return self.__repr__().encode('utf-8') + + def __unicode__(self): + """ + A fall-back method for print() in Python 3 + + :return: + A unicode string of the output of repr() + """ + + return self.__repr__() def _new_instance(self): """ -- cgit v1.2.3 From 9d6ca0c8b7651b69797fa9c1a564f0999f519fa1 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 10:12:40 -0500 Subject: Allow a core.Choice() to be constructed with a single key/value dict or a two-element tuple --- asn1crypto/core.py | 32 +++++++++++++++++++++++++++++++- tests/test_core.py | 22 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 3a3570d..79aa2af 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -758,7 +758,10 @@ class Choice(Asn1Value): incompatible with Choice, then forwards on to Asn1Value.__init__() :param name: - The name of the alternative to be set - used with value + 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. :param value: The alternative value to set - used with name @@ -784,6 +787,33 @@ class Choice(Asn1Value): )) if name is not None: + if isinstance(name, dict): + if len(name) != 1: + raise ValueError(unwrap( + ''' + When passing a dict as the "name" argument to %s, + it must have a single key/value - however %d were + present + ''', + type_name(self), + len(name) + )) + name, value = list(name.items())[0] + + if isinstance(name, tuple): + if len(name) != 2: + raise ValueError(unwrap( + ''' + When passing a tuple as the "name" argument to %s, + it must have two elements, the name and value - + however %d were present + ''', + type_name(self), + len(name) + )) + value = name[1] + name = name[0] + if name not in self._name_map: raise ValueError(unwrap( ''' diff --git a/tests/test_core.py b/tests/test_core.py index cdc4547..f0846f6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -334,6 +334,28 @@ class CoreTests(unittest.TestCase): self.assertEqual(a.contents, b.contents) self.assertNotEqual(a.dump(), b.dump()) + def test_choice_dict_name(self): + a = CopySeq({'name': 'foo', 'pair': {'id': '1.2.3', 'value': 5}}) + choice = SeqChoice({'one': a}) + self.assertEqual('one', choice.name) + + with self.assertRaises(ValueError): + SeqChoice({}) + + with self.assertRaises(ValueError): + SeqChoice({'one': a, 'two': a}) + + def test_choice_tuple_name(self): + a = CopySeq({'name': 'foo', 'pair': {'id': '1.2.3', 'value': 5}}) + choice = SeqChoice(('one', a)) + self.assertEqual('one', choice.name) + + with self.assertRaises(ValueError): + SeqChoice(('one',)) + + with self.assertRaises(ValueError): + SeqChoice(('one', a, None)) + def test_fix_tagging_choice(self): correct = core.Integer(200, tag_type='explicit', tag=2) choice = NumChoice( -- cgit v1.2.3 From d08dae573ea92f915cd3f43200a20da305bc9fde Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 11:02:12 -0500 Subject: Allow parsing BER-encoded indefinite-length values when nested inside of another structure --- asn1crypto/core.py | 20 ++++++++++++-------- tests/fixtures/meca2_compressed.der | Bin 0 -> 9639 bytes tests/test_cms.py | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 47 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/meca2_compressed.der diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 79aa2af..ad870fb 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -4631,14 +4631,18 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param CLASS_NUM_TO_NAME_MAP.get(class_, class_) )) if method != value.method: - raise ValueError(unwrap( - ''' - Error parsing %s - method should have been %s, but %s was found - ''', - type_name(value), - METHOD_NUM_TO_NAME_MAP.get(value.method), - METHOD_NUM_TO_NAME_MAP.get(method, method) - )) + # Allow parsing a primitive method as constructed if the value + # is indefinite length. This is to allow parsing BER. + ber_indef = method == 1 and value.method == 0 and trailer == b'\x00\x00' + if not ber_indef: + raise ValueError(unwrap( + ''' + Error parsing %s - method should have been %s, but %s was found + ''', + type_name(value), + METHOD_NUM_TO_NAME_MAP.get(value.method), + METHOD_NUM_TO_NAME_MAP.get(method, method) + )) if tag != value.tag and tag != value._bad_tag: raise ValueError(unwrap( ''' diff --git a/tests/fixtures/meca2_compressed.der b/tests/fixtures/meca2_compressed.der new file mode 100644 index 0000000..464d31e Binary files /dev/null and b/tests/fixtures/meca2_compressed.der differ diff --git a/tests/test_cms.py b/tests/test_cms.py index c8a8a69..ae2954d 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -5,7 +5,7 @@ import unittest import os from datetime import datetime -from asn1crypto import cms, util +from asn1crypto import cms, util, core from ._unittest_compat import patch patch() @@ -108,6 +108,40 @@ class CMSTests(unittest.TestCase): compressed_data.decompressed ) + def test_parse_content_info_compressed_data2(self): + with open(os.path.join(fixtures_dir, 'meca2_compressed.der'), 'rb') as f: + info = cms.ContentInfo.load(f.read()) + + compressed_data = info['content'] + + self.assertEqual( + 'compressed_data', + info['content_type'].native + ) + self.assertEqual( + 'v0', + compressed_data['version'].native + ) + self.assertEqual( + 'zlib', + compressed_data['compression_algorithm']['algorithm'].native + ) + self.assertEqual( + None, + compressed_data['compression_algorithm']['parameters'].native + ) + self.assertEqual( + 'data', + compressed_data['encap_content_info']['content_type'].native + ) + encap_data = compressed_data['encap_content_info']['content'].native + read = 0 + chunks = 0 + while read < len(encap_data): + value, read = core._parse_build(encap_data, read) + chunks += 1 + self.assertEqual(10, chunks) + def test_parse_content_info_digested_data(self): with open(os.path.join(fixtures_dir, 'cms-digested.der'), 'rb') as f: info = cms.ContentInfo.load(f.read()) -- cgit v1.2.3 From 0dcc8b1ad12ed13a027bc596a68be97085d46d9a Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 23 Nov 2016 11:17:35 -0500 Subject: Version 0.19.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 13 +++++++++++++ readme.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 2b05133..d7bfc68 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.18.5' -__version_info__ = (0, 18, 5) +__version__ = '0.19.0' +__version_info__ = (0, 19, 0) diff --git a/changelog.md b/changelog.md index 1f47297..346d4da 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # changelog +## 0.19.0 + + - Force `algos.DigestAlgorithm` to encoding `parameters` as `Null` when the + `algorithm` is `sha1`, `sha224`, `sha256`, `sha384` or `sha512` per RFC 4055 + - Resolved an issue where a BER-encoded indefinite-length value could not be + properly parsed when embedded inside of a `core.Sequence` or `core.Set` + - Fix `x509.Name.build()` to properly handle dotted OID type values + - `core.Choice` can now be constructed from a single-element `dict` or a + two-element `tuple` to allow for better usability when constructing values + from native Python values + - All `core` objects can now be passed to `print()` with an exception being + raised + ## 0.18.5 - Don't fail importing if `ctypes` or `_ctypes` is not available diff --git a/readme.md b/readme.md index 30c5833..f63cd2a 100644 --- a/readme.md +++ b/readme.md @@ -104,7 +104,7 @@ faster to an order of magnitude of more. ## Current Release -0.18.5 - [changelog](changelog.md) +0.19.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 258adf26609c866129b1e05a421d39c9293d9870 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 29 Nov 2016 06:02:21 -0500 Subject: Correct test of supposed compressed CMS data --- tests/test_cms.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_cms.py b/tests/test_cms.py index ae2954d..5d3c782 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import unittest import os +import zlib +import sys from datetime import datetime from asn1crypto import cms, util, core @@ -10,6 +12,11 @@ from ._unittest_compat import patch patch() +if sys.version_info < (3,): + byte_cls = str +else: + byte_cls = bytes + tests_root = os.path.dirname(__file__) fixtures_dir = os.path.join(tests_root, 'fixtures') @@ -108,7 +115,12 @@ class CMSTests(unittest.TestCase): compressed_data.decompressed ) - def test_parse_content_info_compressed_data2(self): + def test_parse_content_info_jibberish(self): + # This DER data isn't really DER, but BER because it uses indefinite + # length encoding. Additionally, the CompressedData isn't actually + # real CompressedData, but a number of OctetString values concatenated + # together that can have their native values concatenated and then + # run through zlib. with open(os.path.join(fixtures_dir, 'meca2_compressed.der'), 'rb') as f: info = cms.ContentInfo.load(f.read()) @@ -137,10 +149,13 @@ class CMSTests(unittest.TestCase): encap_data = compressed_data['encap_content_info']['content'].native read = 0 chunks = 0 + data = b'' while read < len(encap_data): value, read = core._parse_build(encap_data, read) + data += value.native chunks += 1 self.assertEqual(10, chunks) + self.assertIsInstance(zlib.decompress(data), byte_cls) def test_parse_content_info_digested_data(self): with open(os.path.join(fixtures_dir, 'cms-digested.der'), 'rb') as f: -- cgit v1.2.3 From ac7fefd3a2058e2e9773d7e458f8fad6112fc33c Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 29 Nov 2016 06:02:38 -0500 Subject: Fix support for flake8 v2 --- dev/lint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/lint.py b/dev/lint.py index f2cfc7d..39513b3 100644 --- a/dev/lint.py +++ b/dev/lint.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import os import flake8 -if flake8.__version_info__ < (3,): +if not hasattr(flake8, '__version_info__') or flake8.__version_info__ < (3,): from flake8.engine import get_style_guide else: from flake8.api.legacy import get_style_guide -- cgit v1.2.3 From d9d3d85b702470b928c29e808d9d8412beefc7fd Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 5 Dec 2016 06:50:12 -0500 Subject: Add unique identifier OID to x509.NameType and related classes --- asn1crypto/x509.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index ba0311a..efe6635 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -444,6 +444,7 @@ class NameType(ObjectIdentifier): '2.5.4.42': 'given_name', '2.5.4.43': 'initials', '2.5.4.44': 'generation_qualifier', + '2.5.4.45': 'unique_identifier', '2.5.4.46': 'dn_qualifier', '2.5.4.65': 'pseudonym', '2.5.4.97': 'organization_identifier', @@ -536,6 +537,7 @@ class NameType(ObjectIdentifier): 'given_name': 'Given Name', 'initials': 'Initials', 'generation_qualifier': 'Generation Qualifier', + 'unique_identifier': 'Unique Identifier', 'dn_qualifier': 'DN Qualifier', 'pseudonym': 'Pseudonym', 'email_address': 'Email Address', @@ -573,6 +575,7 @@ class NameTypeAndValue(Sequence): 'given_name': DirectoryString, 'initials': DirectoryString, 'generation_qualifier': DirectoryString, + 'unique_identifier': OctetBitString, 'dn_qualifier': DirectoryString, 'pseudonym': DirectoryString, # https://tools.ietf.org/html/rfc2985#page-26 -- cgit v1.2.3 From c05d375fab32a4897c5ab4481a1470868752397b Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 5 Dec 2016 07:29:07 -0500 Subject: Fix native representation of core.BitString containing leading null bytes --- asn1crypto/core.py | 9 ++++++++- tests/test_core.py | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index ad870fb..68d6884 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -1838,12 +1838,19 @@ class BitString(Primitive, ValueMap, object): if self._native is None: 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 = len(bit_string) % 8 + 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: diff --git a/tests/test_core.py b/tests/test_core.py index f0846f6..0f95c69 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -193,6 +193,8 @@ class CoreTests(unittest.TestCase): return ( ((0, 1, 1), b'\x03\x02\x05\x60'), ((0, 1, 1, 0, 0, 0, 0, 0), b'\x03\x02\x00\x60'), + ((0, 0, 0, 0, 0, 0, 0, 0), b'\x03\x02\x00\x00'), + ((0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1), b'\x03\x03\x00\x00\x01'), ) @data('bit_string_info') -- cgit v1.2.3 From 34d34c5ac67d00d6c5c5146ae9e932d835e5ba03 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 5 Dec 2016 09:36:42 -0500 Subject: Add a .cast() method to classes that have variants, e.g. core.BitString and core.OctetBitString --- asn1crypto/core.py | 54 +++++++++++++++++++++++++++++++++++++++++++------ docs/universal_types.md | 27 +++++++++++++++++++++++++ tests/test_core.py | 18 +++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 68d6884..df1eb2e 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -511,6 +511,48 @@ class ValueMap(): cls._reverse_map[value] = key +class Castable(): + """ + A mixin to handle converting an object between different classes that + represent the same encoded value, but with different rules for converting + to and from native Python values + """ + + def cast(self, other_class): + """ + Converts the current object into an object of a different class. The + new class must use the ASN.1 encoding for the value. + + :param other_class: + The class to instantiate the new object from + + :return: + An instance of the type other_class + """ + + if other_class.tag != self.__class__.tag: + raise TypeError(unwrap( + ''' + Can not covert a value from %s object to %s object since they + use different tags: %d versus %d + ''', + type_name(other_class), + type_name(self), + other_class.tag, + self.__class__.tag + )) + + new_obj = other_class() + new_obj.tag_type = self.tag_type + new_obj.class_ = self.class_ + new_obj.explicit_class = self.explicit_class + new_obj.explicit_tag = self.explicit_tag + new_obj._header = self._header + new_obj.contents = self.contents + new_obj._trailer = self._trailer + return new_obj + + class Void(Asn1Value): """ A representation of an optional value that is not present. Has .native @@ -1599,7 +1641,7 @@ class Integer(Primitive, ValueMap): return self._native -class BitString(Primitive, ValueMap, object): +class BitString(Castable, Primitive, ValueMap, object): """ Represents a bit string from ASN.1 as a Python tuple of 1s and 0s """ @@ -1868,7 +1910,7 @@ class BitString(Primitive, ValueMap, object): return self._native -class OctetBitString(Primitive): +class OctetBitString(Castable, Primitive): """ Represents a bit string in ASN.1 as a Python byte string """ @@ -1931,7 +1973,7 @@ class OctetBitString(Primitive): return self._native -class IntegerBitString(Primitive): +class IntegerBitString(Castable, Primitive): """ Represents a bit string in ASN.1 as a Python integer """ @@ -1988,7 +2030,7 @@ class IntegerBitString(Primitive): return self._native -class OctetString(Primitive): +class OctetString(Castable, Primitive): """ Represents a byte string in both ASN.1 and Python """ @@ -2020,7 +2062,7 @@ class OctetString(Primitive): return self._native -class IntegerOctetString(Primitive): +class IntegerOctetString(Castable, Primitive): """ Represents a byte string in ASN.1 as a Python integer """ @@ -2070,7 +2112,7 @@ class IntegerOctetString(Primitive): return self._native -class ParsableOctetString(Primitive): +class ParsableOctetString(Castable, Primitive): tag = 4 diff --git a/docs/universal_types.md b/docs/universal_types.md index 26a89e7..c58a323 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -92,6 +92,33 @@ function the same as `OctetString` and `OctetBitString`, however they also have an attribute `.parsed` and a method `.parse()` that allows for parsing the content as ASN.1 structures. +All of these overrides can be used with the `cast()` method to convert between +them. The only requirement is that the class being casted to has the same tag +as the original class. No re-encoding is done, rather the contents are simply +re-interpreted. + +```python +from asn1crypto.core import BitString, OctetBitString, IntegerBitString + +bit = BitString({ + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 1, 0, +}) + +# Will print (0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0) +print(bit.native) + +octet = bit.cast(OctetBitString) + +# Will print b'\x01\x02' +print(octet.native) + +i = bit.cast(IntegerBitString) + +# Will print 258 +print(i.native) +``` + ## Basic Usage All of the universal types implement four methods, a class method `.load()` and diff --git a/tests/test_core.py b/tests/test_core.py index 0f95c69..a91d45d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -203,6 +203,24 @@ class CoreTests(unittest.TestCase): self.assertEqual(der_bytes, bs.dump()) self.assertEqual(native, core.BitString.load(der_bytes).native) + def test_cast(self): + a = core.OctetBitString(b'\x00\x01\x02\x03') + self.assertEqual(b'\x00\x01\x02\x03', a.native) + b = a.cast(core.BitString) + self.assertIsInstance(b, core.BitString) + self.assertEqual( + ( + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 1, 1 + ), + b.native + ) + c = a.cast(core.IntegerBitString) + self.assertIsInstance(c, core.IntegerBitString) + self.assertEqual(66051, c.native) + def test_bit_string_item_access(self): named = core.BitString() named[0] = True -- cgit v1.2.3 From 457b6b0a8bf7b58761536843919bc8011534573f Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 12:22:53 -0500 Subject: Ignore coverage.xml for codecov.io --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b6cd5bd..a5bf810 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ tests/output/ tmp/ .coverage .tox +coverage.xml -- cgit v1.2.3 From 64b946ccbaba04d8ecf6836414826911ef7d091e Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 13:06:45 -0500 Subject: Add codecov.io integration --- .travis.yml | 45 ++++++++++++++++++++++++++++++++++++++------- appveyor.yml | 18 ++++++++++++++++++ dev/ci.py | 4 ++++ dev/coverage.py | 4 +++- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6a5c6e0..069f97d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,52 +7,81 @@ matrix: include: - os: osx python: "2.6" - env: TRAVIS_PYTHON_VERSION=2.6 + env: + PYTHON_VERSION=2.6 + PLATFORM=osx - os: osx python: "2.7" - env: TRAVIS_PYTHON_VERSION=2.7 + env: + PYTHON_VERSION=2.7 + PLATFORM=osx - os: osx python: "3.5" - env: TRAVIS_PYTHON_VERSION=3.5 + env: + PYTHON_VERSION=3.5 + PLATFORM=osx - os: osx python: "pypy" - env: TRAVIS_PYTHON_VERSION=pypy + env: + PYTHON_VERSION=pypy + PLATFORM=osx - os: linux language: python python: "2.6" + env: + PYTHON_VERSION=2.6 + PLATFORM=linux - os: linux language: python python: "2.7" + env: + PYTHON_VERSION=2.7 + PLATFORM=linux - os: linux language: python python: "3.2" + env: + PYTHON_VERSION=3.2 + PLATFORM=linux - os: linux language: python python: "3.3" + env: + PYTHON_VERSION=3.3 + PLATFORM=linux - os: linux language: python python: "3.4" + env: + PYTHON_VERSION=3.4 + PLATFORM=linux - os: linux language: python python: "3.5" + env: + PYTHON_VERSION=3.5 + PLATFORM=linux - os: linux language: python python: "pypy" + env: + PYTHON_VERSION=pypy + PLATFORM=linux install: - if [ "$TRAVIS_OS_NAME" == "osx" ]; then - if [ "$TRAVIS_PYTHON_VERSION" == "pypy" ]; then + if [ "$PYTHON_VERSION" == "pypy" ]; then brew update; brew install pypy; /usr/local/bin/pip_pypy install flake8; export PYTHON_BIN=/usr/local/bin/pypy; else - if [ "$TRAVIS_PYTHON_VERSION" == "3.5" ]; then + if [ "$PYTHON_VERSION" == "3.5" ]; then brew update; brew install python3; /usr/local/bin/pip3 install flake8; export PYTHON_BIN=/usr/local/bin/python3; else - if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then + if [ "$PYTHON_VERSION" == "2.7" ]; then curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8'])"; export PYTHON_BIN=/usr/bin/python2.7; @@ -67,3 +96,5 @@ install: fi script: - $PYTHON_BIN run.py ci +after_success: + - bash <(curl -s https://codecov.io/bash) -X gcov -X xcode -X search -f coverage.xml -e PYTHON_VERSION,PLATFORM diff --git a/appveyor.yml b/appveyor.yml index 86778af..c199f82 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,24 +5,38 @@ environment: - PYTHON: "C:\\Python26" PYTHON_ID: "26" PYTHON_EXE: python + PYTHON_VERSION: "2.6" + PLATFORM: win-x32 - PYTHON: "C:\\Python26-x64" PYTHON_ID: "26-x64" PYTHON_EXE: python + PYTHON_VERSION: "2.6" + PLATFORM: win-x64 - PYTHON: "C:\\Python27" PYTHON_ID: "27" PYTHON_EXE: python + PYTHON_VERSION: "2.7" + PLATFORM: win-x32 - PYTHON: "C:\\Python27-x64" PYTHON_ID: "27-x64" PYTHON_EXE: python + PYTHON_VERSION: "2.7" + PLATFORM: win-x64 - PYTHON: "C:\\Python33" PYTHON_ID: "33" PYTHON_EXE: python + PYTHON_VERSION: "3.3" + PLATFORM: win-x32 - PYTHON: "C:\\Python33-x64" PYTHON_ID: "33-x64" PYTHON_EXE: python + PYTHON_VERSION: "3.3" + PLATFORM: win-x64 - PYTHON: "C:\\pypy2-v5.3.1-win32" PYTHON_ID: "pypy" PYTHON_EXE: pypy + PYTHON_VERSION: "pypy" + PLATFORM: win-x32 install: - ps: |- $env:PYTMP = "${env:TMP}\py"; @@ -47,9 +61,13 @@ install: } else { & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8; } + + & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install codecov; - "SET PATH=%PYTHON%;%PATH%" cache: - '%TMP%\py\' build: off test_script: - cmd: "%PYTHON_EXE% run.py ci" +after_test: + - C:\Python27-x64\Scripts\codecov -X search -f coverage.xml -e PYTHON_VERSION,PLATFORM diff --git a/dev/ci.py b/dev/ci.py index b3ab848..335d101 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -6,6 +6,7 @@ import sys from .tests import run as run_tests if sys.version_info >= (2, 7): from .lint import run as run_lint +from .coverage import run as run_coverage def run(): @@ -26,4 +27,7 @@ def run(): sys.stdout.flush() tests_result = run_tests() + print('\nRunning coverage.py') + run_coverage(write_xml=True) + return lint_result and tests_result diff --git a/dev/coverage.py b/dev/coverage.py index 8f836c5..1e781e7 100644 --- a/dev/coverage.py +++ b/dev/coverage.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import coverage -def run(): +def run(write_xml=False): """ Runs the tests while measuring coverage @@ -23,5 +23,7 @@ def run(): cov.save() cov.report(show_missing=False) + if write_xml: + cov.xml_report() return result -- cgit v1.2.3 From 1d6209bdd7cda231003ee20895c9feefd38a42a0 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 13:17:59 -0500 Subject: Install coverage.py on CI --- .travis.yml | 10 ++++++---- appveyor.yml | 11 ++++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 069f97d..83f8792 100644 --- a/.travis.yml +++ b/.travis.yml @@ -72,26 +72,28 @@ install: if [ "$PYTHON_VERSION" == "pypy" ]; then brew update; brew install pypy; - /usr/local/bin/pip_pypy install flake8; + /usr/local/bin/pip_pypy install flake8 coverage; export PYTHON_BIN=/usr/local/bin/pypy; else if [ "$PYTHON_VERSION" == "3.5" ]; then brew update; brew install python3; - /usr/local/bin/pip3 install flake8; + /usr/local/bin/pip3 install flake8 coverage; export PYTHON_BIN=/usr/local/bin/python3; else if [ "$PYTHON_VERSION" == "2.7" ]; then curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8'])"; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8', 'coverage'])"; export PYTHON_BIN=/usr/bin/python2.7; else + curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; + sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; export PYTHON_BIN=/usr/bin/python2.6; fi fi fi else - pip install flake8; + pip install flake8 coverage; export PYTHON_BIN=python; fi script: diff --git a/appveyor.yml b/appveyor.yml index c199f82..c292dca 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -53,13 +53,18 @@ install: (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); } & "${env:PYTHON}\pypy.exe" "${env:PYTMP}\get-pip.py"; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; } elseif ("${env:PYTHON_ID}" -eq "26" -or "${env:PYTHON_ID}" -eq "26-x64") { - # Skip flake8 for 2.6 since pip, flake8 and pycodestyle have all deprecated support for it + if (!(Test-Path "${env:PYTMP}\get-pip.py")) { + (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); + } + & "${env:PYTHON}\python.exe" "${env:PYTMP}\get-pip.py"; + # Skip flake8 for 2.6 since flake8 and pycodestyle have deprecated support for it + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install coverage; } else { - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8; + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; } & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install codecov; -- cgit v1.2.3 From 05745f3e133b2368fd3ed23940f7e962331b1ca8 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 13:26:54 -0500 Subject: Tweaks for running coverage on CI --- .travis.yml | 6 +++++- appveyor.yml | 4 ++-- dev/ci.py | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 83f8792..d77bb9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -93,7 +93,11 @@ install: fi fi else - pip install flake8 coverage; + if [ "$PYTHON_VERSION" == "3.2" ]; then + pip install flake8; + else + pip install flake8 coverage; + fi export PYTHON_BIN=python; fi script: diff --git a/appveyor.yml b/appveyor.yml index c292dca..aad688d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -59,9 +59,9 @@ install: if (!(Test-Path "${env:PYTMP}\get-pip.py")) { (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); } - & "${env:PYTHON}\python.exe" "${env:PYTMP}\get-pip.py"; + & "${env:PYTHON}\python.exe" -W ignore "${env:PYTMP}\get-pip.py"; # Skip flake8 for 2.6 since flake8 and pycodestyle have deprecated support for it - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install coverage; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; } else { & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; diff --git a/dev/ci.py b/dev/ci.py index 335d101..e68f4b6 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -27,7 +27,8 @@ def run(): sys.stdout.flush() tests_result = run_tests() - print('\nRunning coverage.py') - run_coverage(write_xml=True) + if sys.version_info < (3, 0) or sys.version_info >= (3, 3): + print('\nRunning coverage.py') + run_coverage(write_xml=True) return lint_result and tests_result -- cgit v1.2.3 From 0b9ee9b38666752045b0a03a8aba902d7da95dc1 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 14:13:24 -0500 Subject: Another Travis CI fix attempt --- .travis.yml | 10 +++++----- dev/ci.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index d77bb9d..1f4dbea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -89,17 +89,17 @@ install: curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; export PYTHON_BIN=/usr/bin/python2.6; - fi - fi - fi + fi; + fi; + fi; else if [ "$PYTHON_VERSION" == "3.2" ]; then pip install flake8; else pip install flake8 coverage; - fi + fi; export PYTHON_BIN=python; - fi + fi; script: - $PYTHON_BIN run.py ci after_success: diff --git a/dev/ci.py b/dev/ci.py index e68f4b6..4f2f9d7 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -6,7 +6,8 @@ import sys from .tests import run as run_tests if sys.version_info >= (2, 7): from .lint import run as run_lint -from .coverage import run as run_coverage +if sys.version_info < (3, 0) or sys.version_info >= (3, 3): + from .coverage import run as run_coverage def run(): -- cgit v1.2.3 From 09f4f3ddc46d505626da8f08213a0739cc65adce Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 14:58:55 -0500 Subject: Ensure pip is installed for Python 2.6 on Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1f4dbea..7037b08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -86,7 +86,7 @@ install: sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8', 'coverage'])"; export PYTHON_BIN=/usr/bin/python2.7; else - curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; + curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.6; sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; export PYTHON_BIN=/usr/bin/python2.6; fi; -- cgit v1.2.3 From 736159b41bd23bfed295c4f67b6897800b6bf117 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 14:59:46 -0500 Subject: Work around codecov-python issue with trying to pass multiple env vars via CLI --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index aad688d..256ae49 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,7 @@ version: "{build}" skip_tags: true environment: + CODECOV_ENV: PYTHON_VERSION,PLATFORM matrix: - PYTHON: "C:\\Python26" PYTHON_ID: "26" @@ -75,4 +76,4 @@ build: off test_script: - cmd: "%PYTHON_EXE% run.py ci" after_test: - - C:\Python27-x64\Scripts\codecov -X search -f coverage.xml -e PYTHON_VERSION,PLATFORM + - C:\Python27-x64\Scripts\codecov -X search -f coverage.xml -- cgit v1.2.3 From 6ddc1cab6d93631b94199e87d61a0a431fa8501e Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 15:03:58 -0500 Subject: Attempt to fix codecov-python env in appveyor.yml --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 256ae49..26b10a9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ version: "{build}" skip_tags: true environment: - CODECOV_ENV: PYTHON_VERSION,PLATFORM + CODECOV_ENV: "PYTHON_VERSION,PLATFORM" matrix: - PYTHON: "C:\\Python26" PYTHON_ID: "26" -- cgit v1.2.3 From d4811bf487aa537d21532ae4007adc4a9bf33540 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 15:17:25 -0500 Subject: More codecov fixes for Appveyor --- appveyor.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 26b10a9..60a2902 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,6 @@ version: "{build}" skip_tags: true environment: - CODECOV_ENV: "PYTHON_VERSION,PLATFORM" matrix: - PYTHON: "C:\\Python26" PYTHON_ID: "26" @@ -68,7 +67,7 @@ install: & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; } - & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install codecov; + & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/codecov/codecov-python/archive/v2.0.6.zip; - "SET PATH=%PYTHON%;%PATH%" cache: - '%TMP%\py\' @@ -76,4 +75,4 @@ build: off test_script: - cmd: "%PYTHON_EXE% run.py ci" after_test: - - C:\Python27-x64\Scripts\codecov -X search -f coverage.xml + - C:\Python27-x64\Scripts\codecov -X search -f coverage.xml -e PYTHON_VERSION PLATFORM -- cgit v1.2.3 From 2a7fc00b3510780b10cc5118eecd27572a6aa7b1 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 15:22:25 -0500 Subject: Ensure tox env gets coverage --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9e16787..39523b4 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,9 @@ envlist = py26,py27,py32,py33,py34,py35,pypy [testenv] -deps = flake8 +deps = + flake8 + coverage commands = {envpython} run.py ci [pep8] -- cgit v1.2.3 From b9186a426634a5e71c39fd5d7e22cc894f98a7ec Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 16:05:23 -0500 Subject: More tox.ini changes --- tox.ini | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 39523b4..dea143c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,25 @@ envlist = py26,py27,py32,py33,py34,py35,pypy [testenv] deps = - flake8 - coverage + py26: + coverage + py27: + flake8 + coverage + py32: + flake8 + py33: + flake8 + coverage + py34: + flake8 + coverage + py35: + flake8 + coverage + pypy: + flake8 + coverage commands = {envpython} run.py ci [pep8] -- cgit v1.2.3 From 8df5cccbd83cc6093018f8ee9fa757541a30a062 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 16:35:05 -0500 Subject: CI tweaks --- .travis.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7037b08..c69eb41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -103,4 +103,4 @@ install: script: - $PYTHON_BIN run.py ci after_success: - - bash <(curl -s https://codecov.io/bash) -X gcov -X xcode -X search -f coverage.xml -e PYTHON_VERSION,PLATFORM + - bash <(curl -s https://codecov.io/bash) -X gcov -f coverage.xml -e PYTHON_VERSION,PLATFORM diff --git a/appveyor.yml b/appveyor.yml index 60a2902..4d58f19 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -75,4 +75,4 @@ build: off test_script: - cmd: "%PYTHON_EXE% run.py ci" after_test: - - C:\Python27-x64\Scripts\codecov -X search -f coverage.xml -e PYTHON_VERSION PLATFORM + - C:\Python27-x64\Scripts\codecov -X gcov -f coverage.xml -e PYTHON_VERSION PLATFORM -- cgit v1.2.3 From d2da557ba12d7598599cf25aa8ba9f136cfbee43 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 16:35:48 -0500 Subject: Ensure CI coverage reports include initial import --- dev/ci.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dev/ci.py b/dev/ci.py index 4f2f9d7..e02b63d 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -3,11 +3,12 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys -from .tests import run as run_tests if sys.version_info >= (2, 7): from .lint import run as run_lint if sys.version_info < (3, 0) or sys.version_info >= (3, 3): from .coverage import run as run_coverage +else + from .tests import run as run_tests def run(): @@ -24,12 +25,14 @@ def run(): lint_result = run_lint() else: lint_result = True - print('\nRunning tests') - sys.stdout.flush() - tests_result = run_tests() if sys.version_info < (3, 0) or sys.version_info >= (3, 3): - print('\nRunning coverage.py') - run_coverage(write_xml=True) + print('\nRunning tests (via coverage.py)') + sys.stdout.flush() + tests_result = run_coverage(write_xml=True) + else + print('\nRunning tests') + sys.stdout.flush() + tests_result = run_tests() return lint_result and tests_result -- cgit v1.2.3 From ce4a96b2deba389dfdf91dc180994d58a40e2a55 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 16:36:47 -0500 Subject: Syntax fix --- dev/ci.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/ci.py b/dev/ci.py index e02b63d..5936023 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -7,7 +7,7 @@ if sys.version_info >= (2, 7): from .lint import run as run_lint if sys.version_info < (3, 0) or sys.version_info >= (3, 3): from .coverage import run as run_coverage -else +else: from .tests import run as run_tests @@ -30,7 +30,7 @@ def run(): print('\nRunning tests (via coverage.py)') sys.stdout.flush() tests_result = run_coverage(write_xml=True) - else + else: print('\nRunning tests') sys.stdout.flush() tests_result = run_tests() -- cgit v1.2.3 From ca2004a4d6037389120b1d6a25d18383dc7438e5 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 17:20:48 -0500 Subject: Readme updates with badges and link to Codecov [ci skip] --- readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.md b/readme.md index f63cd2a..7585478 100644 --- a/readme.md +++ b/readme.md @@ -14,6 +14,10 @@ A fast, pure Python library for parsing and serializing ASN.1 structures. - [Testing](#testing) - [Development](#development) +[![Travis CI](https://api.travis-ci.org/wbond/asn1crypto.svg?branch=master)](https://travis-ci.org/wbond/asn1crypto) +[![AppVeyor](https://ci.appveyor.com/api/projects/status/github/wbond/asn1crypto?branch=master&svg=true)](https://ci.appveyor.com/project/wbond/asn1crypto) +[![Codecov](https://codecov.io/gh/wbond/asn1crypto/branch/master/graph/badge.svg)](https://codecov.io/gh/wbond/asn1crypto) + ## Features In addition to an ASN.1 BER/DER decoder and DER serializer, the project includes @@ -150,6 +154,7 @@ links to the source for the various pre-defined type classes. - [Windows](https://ci.appveyor.com/project/wbond/asn1crypto/history) via AppVeyor - [OS X & Linux](https://travis-ci.org/wbond/asn1crypto/builds) via Travis CI + - [Test Coverage](https://codecov.io/gh/wbond/asn1crypto/commits) via Codecov ## Testing -- cgit v1.2.3 From 4fc83a030c73a6099a71a07dd15a8dfbb2610361 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 6 Dec 2016 17:25:13 -0500 Subject: Add readme badge for PyPI [ci skip] --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 7585478..f482e9e 100644 --- a/readme.md +++ b/readme.md @@ -17,6 +17,7 @@ A fast, pure Python library for parsing and serializing ASN.1 structures. [![Travis CI](https://api.travis-ci.org/wbond/asn1crypto.svg?branch=master)](https://travis-ci.org/wbond/asn1crypto) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/wbond/asn1crypto?branch=master&svg=true)](https://ci.appveyor.com/project/wbond/asn1crypto) [![Codecov](https://codecov.io/gh/wbond/asn1crypto/branch/master/graph/badge.svg)](https://codecov.io/gh/wbond/asn1crypto) +[![PyPI](https://img.shields.io/pypi/v/asn1crypto.svg)](https://pypi.python.org/pypi/asn1crypto) ## Features -- cgit v1.2.3 From 607dd56020a8dc567a4ac86a55bfab3819eff0a6 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 7 Dec 2016 10:04:12 -0500 Subject: Ignore Python 2.6 warning on Travis [ci skip] --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c69eb41..9300aae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -86,7 +86,7 @@ install: sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8', 'coverage'])"; export PYTHON_BIN=/usr/bin/python2.7; else - curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.6; + curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.6 -W ignore; sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; export PYTHON_BIN=/usr/bin/python2.6; fi; -- cgit v1.2.3 From 8133a88f014e3ac82b6a6ecc6517e83a427cde3e Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 10 Dec 2016 20:53:59 -0500 Subject: Restructure test loading for easier cross-package reuse --- dev/tests.py | 44 ++------------------------------------------ setup.py | 2 +- tests/__init__.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 43 deletions(-) diff --git a/dev/tests.py b/dev/tests.py index b9fdd4d..0fe0a1b 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -4,47 +4,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import unittest import re -from tests.test_algos import AlgoTests -from tests.test_cms import CMSTests -from tests.test_crl import CRLTests -from tests.test_csr import CSRTests -from tests.test_keys import KeysTests -from tests.test_ocsp import OCSPTests -from tests.test_pem import PEMTests -from tests.test_tsp import TSPTests -from tests.test_x509 import X509Tests -from tests.test_core import CoreTests - - -test_classes = [ - AlgoTests, - CMSTests, - CRLTests, - CSRTests, - KeysTests, - OCSPTests, - PEMTests, - TSPTests, - X509Tests, - CoreTests -] - - -def make_suite(): - """ - Constructs a unittest.TestSuite() of all tests for the package. For use - with setuptools. - - :return: - A unittest.TestSuite() object - """ - - loader = unittest.TestLoader() - suite = unittest.TestSuite() - for test_class in test_classes: - tests = loader.loadTestsFromTestCase(test_class) - suite.addTests(tests) - return suite +from tests import test_classes def run(matcher=None): @@ -61,7 +21,7 @@ def run(matcher=None): suite = unittest.TestSuite() loader = unittest.TestLoader() - for test_class in test_classes: + for test_class in test_classes(): if matcher: names = loader.getTestCaseNames(test_class) for name in names: diff --git a/setup.py b/setup.py index d8825d8..c6f8759 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ setup( packages=find_packages(exclude=['tests*', 'dev*']), - test_suite='dev.tests.make_suite', + test_suite='tests.make_suite', cmdclass={ 'clean': CleanCommand, diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..7a6a5b1 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,54 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import unittest + + +def make_suite(): + """ + Constructs a unittest.TestSuite() of all tests for the package. For use + with setuptools. + + :return: + A unittest.TestSuite() object + """ + + loader = unittest.TestLoader() + suite = unittest.TestSuite() + for test_class in test_classes(): + tests = loader.loadTestsFromTestCase(test_class) + suite.addTests(tests) + return suite + + +def test_classes(): + """ + Returns a list of unittest.TestCase classes for the package + + :return: + A list of unittest.TestCase classes + """ + + from .test_algos import AlgoTests + from .test_cms import CMSTests + from .test_crl import CRLTests + from .test_csr import CSRTests + from .test_keys import KeysTests + from .test_ocsp import OCSPTests + from .test_pem import PEMTests + from .test_tsp import TSPTests + from .test_x509 import X509Tests + from .test_core import CoreTests + + return [ + AlgoTests, + CMSTests, + CRLTests, + CSRTests, + KeysTests, + OCSPTests, + PEMTests, + TSPTests, + X509Tests, + CoreTests + ] -- cgit v1.2.3 From 37e20b4f97d286fd43e2f1d98bdb043019514aa8 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Jan 2017 06:44:47 -0500 Subject: Add support for year 0 --- asn1crypto/core.py | 46 ++- asn1crypto/util.py | 545 ++++++++++++++++++++++++++++++++- tests/__init__.py | 2 + tests/fixtures/9999-years-rsa-cert.pem | 23 ++ tests/test_util.py | 129 ++++++++ tests/test_x509.py | 7 + 6 files changed, 736 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/9999-years-rsa-cert.pem create mode 100644 tests/test_util.py diff --git a/asn1crypto/core.py b/asn1crypto/core.py index df1eb2e..1b0fde3 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -58,7 +58,7 @@ from . import _teletex_codec from ._errors import unwrap from ._ordereddict import OrderedDict from ._types import type_name, str_cls, byte_cls, int_types -from .util import int_to_bytes, int_from_bytes, timezone +from .util import int_to_bytes, int_from_bytes, timezone, extended_datetime # Python 2 if sys.version_info <= (3,): @@ -4277,7 +4277,7 @@ class UTCTime(AbstractTime): class GeneralizedTime(AbstractTime): """ Represents a generalized time from ASN.1 as a Python datetime.datetime - object in UTC + object or asn1crypto.util.extended_datetime object in UTC """ tag = 24 @@ -4287,13 +4287,14 @@ class GeneralizedTime(AbstractTime): Sets the value of the object :param value: - A unicode string or a datetime.datetime object + A unicode string, a datetime.datetime object or an + asn1crypto.util.extended_datetime object :raises: ValueError - when an invalid value is passed """ - if isinstance(value, datetime): + if isinstance(value, (datetime, extended_datetime)): value = value.strftime('%Y%m%d%H%M%SZ') if py2: value = value.decode('ascii') @@ -4311,22 +4312,37 @@ class GeneralizedTime(AbstractTime): A unicode string to parse :return: - A datetime.datetime object or a unicode string + A datetime.datetime object, asn1crypto.util.extended_datetime object or + a unicode string """ strlen = len(string) + date_format = None if strlen == 10: - return datetime.strptime(string, '%Y%m%d%H') - - if strlen == 12: - return datetime.strptime(string, '%Y%m%d%H%M') - - if strlen == 14: - return datetime.strptime(string, '%Y%m%d%H%M%S') - - if strlen == 18: - return datetime.strptime(string, '%Y%m%d%H%M%S.%f') + 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 28 shares a calendar with year 0, and is supported natively + t = datetime.strptime('0028' + 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) return string diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 13989bb..ecef69c 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -18,9 +18,13 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math import sys +import re +from datetime import datetime, date, time +from ._errors import unwrap from ._iri import iri_to_uri, uri_to_iri # noqa from ._ordereddict import OrderedDict # noqa +from ._types import type_name if sys.platform == 'win32': from ._inet import inet_ntop, inet_pton @@ -33,6 +37,8 @@ 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 @@ -110,7 +116,7 @@ if sys.version_info <= (3,): class utc(tzinfo): # noqa def tzname(self, _): - return 'UTC+00:00' + return b'UTC+00:00' def utcoffset(self, _): return timedelta(0) @@ -128,6 +134,8 @@ else: from datetime import timezone # noqa + py2 = False + def int_to_bytes(value, signed=False, width=None): """ Converts an integer to a byte string @@ -170,3 +178,538 @@ 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 +} + + +class extended_date(object): + """ + A datetime.date-like object that can represent the year 0. This is just + to handle 0000-01-01 found in some certificates. + """ + + year = None + month = None + day = None + + def __init__(self, year, month, day): + """ + :param year: + The integer 0 + + :param month: + An integer from 1 to 12 + + :param day: + An integer from 1 to 31 + """ + + 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 + + def _format(self, format): + """ + Performs strftime(), always returning a unicode string + + :param format: + A strftime() format string + + :return: + A unicode string of the formatted date + """ + + format = format.replace('%Y', '0000') + format = format.replace('%y', '00') + # Year 0 is 1BC and a leap year. Leap years repeat themselves + # every 28 years. Thus the simplest way to format is to use + # the calendar from year 28 + temp = date(28, self.month, self.day) + if '%c' in format: + c_out = temp.strftime('%c') + # Handle full years + c_out = c_out.replace('0028', '0000') + c_out = c_out.replace('%', '%%') + format = format.replace('%c', c_out) + if '%x' in format: + x_out = temp.strftime('%x') + # Handle format such as 08/16/28 + x_out = re.sub(r'\b28$', '00', x_out) + # Handle formats such as 08/16/0028 or 16.08.0028 + x_out = x_out.replace('0028', '0000') + x_out = x_out.replace('%', '%%') + format = format.replace('%x', x_out) + return temp.strftime(format) + + 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 strftime(self, format): + """ + Formats the date using strftime() + + :param format: + The strftime() format string + + :return: + 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 + + def replace(self, year=None, month=None, day=None): + """ + Returns a new datetime.date or asn1crypto.util.extended_date + object with the specified components replaced + + :return: + A datetime.date or asn1crypto.util.extended_date object + """ + + if year is None: + year = self.year + if month is None: + month = self.month + if day is None: + day = self.day + + if year > 0: + cls = date + else: + cls = extended_date + + return cls( + year, + month, + day + ) + + def __str__(self): + if py2: + return self.__bytes__() + else: + return self.__unicode__() + + def __bytes__(self): + return self.__unicode__().encode('utf-8') + + def __unicode__(self): + return self._format('%Y-%m-%d') + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return not self.__eq__(other) + + def _comparison_error(self, other): + raise TypeError(unwrap( + ''' + An asn1crypto.util.extended_date object can only be compared to + an asn1crypto.util.extended_date or datetime.date object, not %s + ''', + type_name(other) + )) + + def __cmp__(self, other): + 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: + return -1 + if st > ot: + return 1 + return 0 + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __le__(self, other): + return self.__cmp__(other) <= 0 + + def __gt__(self, other): + return self.__cmp__(other) > 0 + + def __ge__(self, other): + return self.__cmp__(other) >= 0 + + +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. + """ + + year = None + month = None + day = None + hour = None + minute = None + second = None + microsecond = None + tzinfo = None + + def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None): + """ + :param year: + The integer 0 + + :param month: + An integer from 1 to 12 + + :param day: + An integer from 1 to 31 + + :param hour: + An integer from 0 to 23 + + :param minute: + An integer from 0 to 59 + + :param second: + An integer from 0 to 59 + + :param microsecond: + An integer from 0 to 999999 + """ + + 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') + + if hour < 0 or hour > 23: + raise ValueError('hour is out of range') + + if minute < 0 or minute > 59: + raise ValueError('minute is out of range') + + if second < 0 or second > 59: + raise ValueError('second is out of range') + + if microsecond < 0 or microsecond > 999999: + raise ValueError('microsecond is out of range') + + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.microsecond = microsecond + self.tzinfo = tzinfo + + def date(self): + """ + :return: + An asn1crypto.util.extended_date of the date + """ + + return extended_date(self.year, self.month, self.day) + + def time(self): + """ + :return: + A datetime.time object of the time + """ + + return time(self.hour, self.minute, self.second, self.microsecond, self.tzinfo) + + def utcoffset(self): + """ + :return: + None or a datetime.timedelta() of the offset from UTC + """ + + if self.tzinfo is None: + return None + return self.tzinfo.utcoffset(self.replace(year=28)) + + def dst(self): + """ + :return: + None or a datetime.timedelta() of the daylight savings time offset + """ + + if self.tzinfo is None: + return None + return self.tzinfo.dst(self.replace(year=28)) + + def tzname(self): + """ + :return: + None or the name of the timezone as a unicode string in Python 3 + and a byte string in Python 2 + """ + + if self.tzinfo is None: + return None + return self.tzinfo.tzname(self.replace(year=28)) + + def _format(self, format): + """ + Performs strftime(), always returning a unicode string + + :param format: + A strftime() format string + + :return: + A unicode string of the formatted datetime + """ + + format = format.replace('%Y', '0000') + format = format.replace('%y', '00') + # Year 0 is 1BC and a leap year. Leap years repeat themselves + # every 28 years. Thus the simplest way to format is to use + # the calendar from year 28 + temp = datetime( + 28, + 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('0028', '0000') + c_out = c_out.replace('%', '%%') + format = format.replace('%c', c_out) + if '%x' in format: + x_out = temp.strftime('%x') + # Handle format such as 08/16/28 + x_out = re.sub(r'\b28$', '00', x_out) + # Handle formats such as 08/16/0028 or 16.08.0028 + x_out = x_out.replace('0028', '0000') + x_out = x_out.replace('%', '%%') + format = format.replace('%x', x_out) + return temp.strftime(format) + + def isoformat(self, sep='T'): + """ + Formats the date as "%Y-%m-%d %H:%M:%S" with the sep param between the + date and time portions + + :param set: + A single character of the separator to place between the date and + time + + :return: + The formatted datetime as a unicode string in Python 3 and a byte + 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) + + def strftime(self, format): + """ + Formats the date using strftime() + + :param format: + The strftime() format string + + :return: + 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 + + def replace(self, year=None, month=None, day=None, hour=None, minute=None, + second=None, microsecond=None, tzinfo=None): + """ + Returns a new datetime.datetime or asn1crypto.util.extended_datetime + object with the specified components replaced + + :return: + A datetime.datetime or asn1crypto.util.extended_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 + + if year > 0: + cls = datetime + else: + cls = extended_datetime + + return cls( + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo + ) + + def __str__(self): + if py2: + return self.__bytes__() + else: + return self.__unicode__() + + def __bytes__(self): + return self.__unicode__().encode('utf-8') + + def __unicode__(self): + format = '%Y-%m-%d %H:%M:%S' + if self.microsecond != 0: + format += '.%f' + return self._format(format) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + return self.__cmp__(other) == 0 + + def __ne__(self, other): + return not self.__eq__(other) + + def _comparison_error(self, other): + """ + Raises a TypeError about the other object not being suitable for + comparison + + :param other: + The object being compared to + """ + + raise TypeError(unwrap( + ''' + An asn1crypto.util.extended_datetime object can only be compared to + an asn1crypto.util.extended_datetime or datetime.datetime object, + not %s + ''', + type_name(other) + )) + + def __cmp__(self, other): + so = self.utcoffset() + oo = other.utcoffset() + + 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") + + if isinstance(other, datetime): + return -1 + + if not isinstance(other, self.__class__): + 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 st < ot: + return -1 + if st > ot: + return 1 + return 0 + + def __lt__(self, other): + return self.__cmp__(other) < 0 + + def __le__(self, other): + return self.__cmp__(other) <= 0 + + def __gt__(self, other): + return self.__cmp__(other) > 0 + + def __ge__(self, other): + return self.__cmp__(other) >= 0 diff --git a/tests/__init__.py b/tests/__init__.py index 7a6a5b1..5ea2832 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -38,6 +38,7 @@ def test_classes(): from .test_pem import PEMTests from .test_tsp import TSPTests from .test_x509 import X509Tests + from .test_util import UtilTests from .test_core import CoreTests return [ @@ -49,6 +50,7 @@ def test_classes(): OCSPTests, PEMTests, TSPTests, + UtilTests, X509Tests, CoreTests ] diff --git a/tests/fixtures/9999-years-rsa-cert.pem b/tests/fixtures/9999-years-rsa-cert.pem new file mode 100644 index 0000000..e251f0a --- /dev/null +++ b/tests/fixtures/9999-years-rsa-cert.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDwTCCAyqgAwIBAgICDh4wDQYJKoZIhvcNAQEFBQAwgZsxCzAJBgNVBAYTAkpQ +MQ4wDAYDVQQIEwVUb2t5bzEQMA4GA1UEBxMHQ2h1by1rdTERMA8GA1UEChMIRnJh +bms0REQxGDAWBgNVBAsTD1dlYkNlcnQgU3VwcG9ydDEYMBYGA1UEAxMPRnJhbms0 +REQgV2ViIENBMSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QGZyYW5rNGRkLmNvbTAi +GA8wMDAwMDEwMTAwMDAwMVoYDzk5OTkxMjMxMjM1OTU5WjCBgTELMAkGA1UEBhMC +SlAxDjAMBgNVBAgTBVRva3lvMREwDwYDVQQKEwhGcmFuazRERDEQMA4GA1UECxMH +U3VwcG9ydDEiMCAGCSqGSIb3DQEJARYTcHVibGljQGZyYW5rNGRkLmNvbTEZMBcG +A1UEAxMQd3d3LmZyYW5rNGRkLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC +gYEA4rkBL30FzR2ZHZ1vpF9kGBO0DMwhu2pcrkcLJ0SEuf52ggo+md0tPis8f1KN +Tchxj6DtxWT3c7ECW0c1ALpu6mNVE+GaM94KsckSDehoPfbLjT9Apcc/F0mqvDsC +N6fPdDixWrjx6xKT7xXi3lCy1yIKRMHA6Ha+T4qPyyCyMPECAwEAAaOCASYwggEi +MAwGA1UdEwEB/wQCMAAwCwYDVR0PBAQDAgWgMB0GA1UdDgQWBBRWKE5tXPIyS0pC +fE5taGO5Q84gyTCB0AYDVR0jBIHIMIHFgBRi83vtBtSx1Zx/SOXvxckVYf3ZEaGB +oaSBnjCBmzELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMRAwDgYDVQQHEwdD +aHVvLWt1MREwDwYDVQQKEwhGcmFuazRERDEYMBYGA1UECxMPV2ViQ2VydCBTdXBw +b3J0MRgwFgYDVQQDEw9GcmFuazRERCBXZWIgQ0ExIzAhBgkqhkiG9w0BCQEWFHN1 +cHBvcnRAZnJhbms0ZGQuY29tggkAxscECbwiW6AwEwYDVR0lBAwwCgYIKwYBBQUH +AwEwDQYJKoZIhvcNAQEFBQADgYEAfXCfXcePJwnMKc06qLa336cEPpXEsPed1bw4 +xiIXfgZ39duBnN+Nv4a49Yl2kbh4JO8tcr5h8WYAI/a/69w8qBFQBUAjTEY/+lcw +9/6wU7UA3kh7yexeqDiNTRflnPUv3sfiVdLDTjqLWWAxGS8L26PjVaCUFfJLNiYJ +jerREgM= +-----END CERTIFICATE----- diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..bbfb5d4 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,129 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import unittest +import sys +import os +from datetime import date, datetime, time + +from asn1crypto import util + +from .unittest_data import data_decorator, data +from ._unittest_compat import patch + +patch() + +if sys.version_info < (3,): + py2 = True + byte_cls = str + num_cls = long # noqa +else: + py2 = False + byte_cls = bytes + num_cls = int + + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') +utc = util.timezone.utc + + +@data_decorator +class UtilTests(unittest.TestCase): + + def test_extended_date_strftime(self): + def do_run(): + self.assertEqual('0000-01-01', util.extended_date(0, 1, 1).strftime('%Y-%m-%d')) + self.assertEqual('Sat Saturday Jan January', util.extended_date(0, 1, 1).strftime('%a %A %b %B')) + self.assertEqual('Tue Tuesday Feb February 29', util.extended_date(0, 2, 29).strftime('%a %A %b %B %d')) + self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_date(0, 1, 1).strftime('%c')) + self.assertEqual('01/01/00', util.extended_date(0, 1, 1).strftime('%x')) + # Python 2 doesn't allow strftime on years before 1900 + if py2: + with self.assertRaises(ValueError): + do_run() + else: + do_run() + + def test_extended_datetime_strftime(self): + def do_run(): + self.assertEqual('0000-01-01 00:00:00', util.extended_datetime(0, 1, 1).strftime('%Y-%m-%d %H:%M:%S')) + self.assertEqual('Sat Saturday Jan January', util.extended_datetime(0, 1, 1).strftime('%a %A %b %B')) + self.assertEqual('Tue Tuesday Feb February 29', util.extended_datetime(0, 2, 29).strftime('%a %A %b %B %d')) + self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_datetime(0, 1, 1).strftime('%c')) + self.assertEqual('01/01/00', util.extended_datetime(0, 1, 1).strftime('%x')) + # Python 2 doesn't allow strftime on years before 1900 + if py2: + with self.assertRaises(ValueError): + do_run() + else: + do_run() + + def test_extended_date_compare(self): + self.assertTrue(util.extended_date(0, 1, 1) < date(1, 1, 1)) + self.assertTrue(util.extended_date(0, 1, 1) <= date(1, 1, 1)) + self.assertTrue(util.extended_date(0, 1, 1) != date(1, 1, 1)) + self.assertFalse(util.extended_date(0, 1, 1) == date(1, 1, 1)) + self.assertFalse(util.extended_date(0, 1, 1) >= date(1, 1, 1)) + self.assertFalse(util.extended_date(0, 1, 1) > date(1, 1, 1)) + + self.assertFalse(util.extended_date(0, 1, 1) < util.extended_date(0, 1, 1)) + self.assertTrue(util.extended_date(0, 1, 1) <= util.extended_date(0, 1, 1)) + self.assertFalse(util.extended_date(0, 1, 1) != util.extended_date(0, 1, 1)) + self.assertTrue(util.extended_date(0, 1, 1) == util.extended_date(0, 1, 1)) + self.assertTrue(util.extended_date(0, 1, 1) >= util.extended_date(0, 1, 1)) + self.assertFalse(util.extended_date(0, 1, 1) > util.extended_date(0, 1, 1)) + + self.assertTrue(util.extended_date(0, 1, 1) < util.extended_date(0, 1, 2)) + self.assertTrue(util.extended_date(0, 1, 1) <= util.extended_date(0, 1, 2)) + self.assertTrue(util.extended_date(0, 1, 1) != util.extended_date(0, 1, 2)) + self.assertFalse(util.extended_date(0, 1, 1) == util.extended_date(0, 1, 2)) + self.assertFalse(util.extended_date(0, 1, 1) >= util.extended_date(0, 1, 2)) + self.assertFalse(util.extended_date(0, 1, 1) > util.extended_date(0, 1, 2)) + + self.assertFalse(util.extended_date(0, 1, 3) < util.extended_date(0, 1, 2)) + self.assertFalse(util.extended_date(0, 1, 3) <= util.extended_date(0, 1, 2)) + self.assertTrue(util.extended_date(0, 1, 3) != util.extended_date(0, 1, 2)) + self.assertFalse(util.extended_date(0, 1, 3) == util.extended_date(0, 1, 2)) + self.assertTrue(util.extended_date(0, 1, 3) >= util.extended_date(0, 1, 2)) + self.assertTrue(util.extended_date(0, 1, 3) > util.extended_date(0, 1, 2)) + + def test_extended_datetime_compare(self): + self.assertTrue(util.extended_datetime(0, 1, 1) < datetime(1, 1, 1)) + self.assertTrue(util.extended_datetime(0, 1, 1) <= datetime(1, 1, 1)) + self.assertTrue(util.extended_datetime(0, 1, 1) != datetime(1, 1, 1)) + self.assertFalse(util.extended_datetime(0, 1, 1) == datetime(1, 1, 1)) + self.assertFalse(util.extended_datetime(0, 1, 1) >= datetime(1, 1, 1)) + self.assertFalse(util.extended_datetime(0, 1, 1) > datetime(1, 1, 1)) + + self.assertFalse(util.extended_datetime(0, 1, 1) < util.extended_datetime(0, 1, 1)) + self.assertTrue(util.extended_datetime(0, 1, 1) <= util.extended_datetime(0, 1, 1)) + self.assertFalse(util.extended_datetime(0, 1, 1) != util.extended_datetime(0, 1, 1)) + self.assertTrue(util.extended_datetime(0, 1, 1) == util.extended_datetime(0, 1, 1)) + self.assertTrue(util.extended_datetime(0, 1, 1) >= util.extended_datetime(0, 1, 1)) + self.assertFalse(util.extended_datetime(0, 1, 1) > util.extended_datetime(0, 1, 1)) + + self.assertTrue(util.extended_datetime(0, 1, 1) < util.extended_datetime(0, 1, 2)) + self.assertTrue(util.extended_datetime(0, 1, 1) <= util.extended_datetime(0, 1, 2)) + self.assertTrue(util.extended_datetime(0, 1, 1) != util.extended_datetime(0, 1, 2)) + self.assertFalse(util.extended_datetime(0, 1, 1) == util.extended_datetime(0, 1, 2)) + self.assertFalse(util.extended_datetime(0, 1, 1) >= util.extended_datetime(0, 1, 2)) + self.assertFalse(util.extended_datetime(0, 1, 1) > util.extended_datetime(0, 1, 2)) + + self.assertFalse(util.extended_datetime(0, 1, 3) < util.extended_datetime(0, 1, 2)) + self.assertFalse(util.extended_datetime(0, 1, 3) <= util.extended_datetime(0, 1, 2)) + self.assertTrue(util.extended_datetime(0, 1, 3) != util.extended_datetime(0, 1, 2)) + self.assertFalse(util.extended_datetime(0, 1, 3) == util.extended_datetime(0, 1, 2)) + self.assertTrue(util.extended_datetime(0, 1, 3) >= util.extended_datetime(0, 1, 2)) + self.assertTrue(util.extended_datetime(0, 1, 3) > util.extended_datetime(0, 1, 2)) + + def test_extended_datetime_compare_tzinfo(self): + with self.assertRaises(TypeError): + self.assertTrue(util.extended_datetime(0, 1, 1, tzinfo=utc) < datetime(1, 1, 1)) + with self.assertRaises(TypeError): + self.assertTrue(util.extended_datetime(0, 1, 1) < datetime(1, 1, 1, tzinfo=utc)) + + def test_extended_datetime_date_time(self): + self.assertEqual(util.extended_date(0, 1, 1), util.extended_datetime(0, 1, 1).date()) + self.assertEqual(util.extended_date(0, 2, 29), util.extended_datetime(0, 2, 29).date()) + self.assertEqual(time(0, 0, 0), util.extended_datetime(0, 1, 1).time()) diff --git a/tests/test_x509.py b/tests/test_x509.py index 1cef8cf..0650f6b 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -3298,3 +3298,10 @@ class X509Tests(unittest.TestCase): ]) ] ) + + def test_extended_datetime(self): + cert = self._load_cert('9999-years-rsa-cert.pem') + self.assertEqual( + cert['tbs_certificate']['validity']['not_before'].native, + util.extended_datetime(0, 1, 1, 0, 0, 1, tzinfo=util.timezone.utc) + ) -- cgit v1.2.3 From 9712e07f67fab4b5d8afbec60eb972663ac54996 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Jan 2017 06:56:15 -0500 Subject: Add CI and docs for Python 3.6 --- .travis.yml | 12 +++++++++--- readme.md | 2 +- setup.py | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9300aae..d5e636d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,9 +16,9 @@ matrix: PYTHON_VERSION=2.7 PLATFORM=osx - os: osx - python: "3.5" + python: "3.6" env: - PYTHON_VERSION=3.5 + PYTHON_VERSION=3.6 PLATFORM=osx - os: osx python: "pypy" @@ -61,6 +61,12 @@ matrix: env: PYTHON_VERSION=3.5 PLATFORM=linux + - os: linux + language: python + python: "3.6" + env: + PYTHON_VERSION=3.6 + PLATFORM=linux - os: linux language: python python: "pypy" @@ -75,7 +81,7 @@ install: /usr/local/bin/pip_pypy install flake8 coverage; export PYTHON_BIN=/usr/local/bin/pypy; else - if [ "$PYTHON_VERSION" == "3.5" ]; then + if [ "$PYTHON_VERSION" == "3.6" ]; then brew update; brew install python3; /usr/local/bin/pip3 install flake8 coverage; diff --git a/readme.md b/readme.md index f482e9e..3be8d2b 100644 --- a/readme.md +++ b/readme.md @@ -113,7 +113,7 @@ faster to an order of magnitude of more. ## Dependencies -Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 or pypy. *No third-party packages +Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6 or pypy. *No third-party packages required.* ## Installation diff --git a/setup.py b/setup.py index c6f8759..66a779c 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Security :: Cryptography', -- cgit v1.2.3 From 862d4ebf9b3d7876e4c0b6945214ba9b65d5d4cd Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Jan 2017 08:53:03 -0500 Subject: Fix for formatting year 0 on Linux --- asn1crypto/core.py | 4 ++-- asn1crypto/util.py | 42 +++++++++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 1b0fde3..0a57e80 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -4330,8 +4330,8 @@ class GeneralizedTime(AbstractTime): if date_format: if len(string) >= 4 and string[0:4] == '0000': - # Year 28 shares a calendar with year 0, and is supported natively - t = datetime.strptime('0028' + string[4:], date_format) + # Year 980 shares a calendar with year 0, and is supported natively + t = datetime.strptime('0980' + string[4:], date_format) return extended_datetime( 0, t.month, diff --git a/asn1crypto/util.py b/asn1crypto/util.py index ecef69c..59492aa 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -246,20 +246,24 @@ class extended_date(object): format = format.replace('%y', '00') # Year 0 is 1BC and a leap year. Leap years repeat themselves # every 28 years. Thus the simplest way to format is to use - # the calendar from year 28 - temp = date(28, self.month, self.day) + # the calendar from year 980 + temp = date(980, self.month, self.day) if '%c' in format: c_out = temp.strftime('%c') # Handle full years - c_out = c_out.replace('0028', '0000') + c_out = c_out.replace('0980', '0000') + # libc on Linux seems to omit the leading zero + c_out = c_out.replace('980', '0000') c_out = c_out.replace('%', '%%') format = format.replace('%c', c_out) if '%x' in format: x_out = temp.strftime('%x') - # Handle format such as 08/16/28 - x_out = re.sub(r'\b28$', '00', x_out) - # Handle formats such as 08/16/0028 or 16.08.0028 - x_out = x_out.replace('0028', '0000') + # Handle format such as 08/16/80 + x_out = re.sub(r'\b80$', '00', x_out) + # Handle formats such as 08/16/0980 or 16.08.0980 + x_out = x_out.replace('0980', '0000') + # libc on Linux seems to omit the leading zero + x_out = x_out.replace('980', '0000') x_out = x_out.replace('%', '%%') format = format.replace('%x', x_out) return temp.strftime(format) @@ -478,7 +482,7 @@ class extended_datetime(object): if self.tzinfo is None: return None - return self.tzinfo.utcoffset(self.replace(year=28)) + return self.tzinfo.utcoffset(self.replace(year=980)) def dst(self): """ @@ -488,7 +492,7 @@ class extended_datetime(object): if self.tzinfo is None: return None - return self.tzinfo.dst(self.replace(year=28)) + return self.tzinfo.dst(self.replace(year=980)) def tzname(self): """ @@ -499,7 +503,7 @@ class extended_datetime(object): if self.tzinfo is None: return None - return self.tzinfo.tzname(self.replace(year=28)) + return self.tzinfo.tzname(self.replace(year=980)) def _format(self, format): """ @@ -516,9 +520,9 @@ class extended_datetime(object): format = format.replace('%y', '00') # Year 0 is 1BC and a leap year. Leap years repeat themselves # every 28 years. Thus the simplest way to format is to use - # the calendar from year 28 + # the calendar from year 980 temp = datetime( - 28, + 980, self.month, self.day, self.hour, @@ -530,15 +534,19 @@ class extended_datetime(object): if '%c' in format: c_out = temp.strftime('%c') # Handle full years - c_out = c_out.replace('0028', '0000') + c_out = c_out.replace('0980', '0000') + # libc on Linux seems to omit the leading zero + c_out = c_out.replace('980', '0000') c_out = c_out.replace('%', '%%') format = format.replace('%c', c_out) if '%x' in format: x_out = temp.strftime('%x') - # Handle format such as 08/16/28 - x_out = re.sub(r'\b28$', '00', x_out) - # Handle formats such as 08/16/0028 or 16.08.0028 - x_out = x_out.replace('0028', '0000') + # Handle format such as 08/16/80 + x_out = re.sub(r'\b80$', '00', x_out) + # Handle formats such as 08/16/0980 or 16.08.0980 + x_out = x_out.replace('0980', '0000') + # libc on Linux seems to omit the leading zero + x_out = x_out.replace('980', '0000') x_out = x_out.replace('%', '%%') format = format.replace('%x', x_out) return temp.strftime(format) -- cgit v1.2.3 From d2f0b41ae98f83b42fb9cfad4590a210ae979228 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Jan 2017 09:06:40 -0500 Subject: Use year 2000 for formatting year 0 because of Python 3 on Windows and Python 3.2 everywhere --- asn1crypto/core.py | 4 ++-- asn1crypto/util.py | 44 +++++++++++++++----------------------------- tests/test_util.py | 34 ++++++++++------------------------ 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 0a57e80..14c9f96 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -4330,8 +4330,8 @@ class GeneralizedTime(AbstractTime): if date_format: if len(string) >= 4 and string[0:4] == '0000': - # Year 980 shares a calendar with year 0, and is supported natively - t = datetime.strptime('0980' + string[4:], date_format) + # 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, diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 59492aa..d921c1d 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -243,27 +243,20 @@ class extended_date(object): """ format = format.replace('%Y', '0000') - format = format.replace('%y', '00') # Year 0 is 1BC and a leap year. Leap years repeat themselves - # every 28 years. Thus the simplest way to format is to use - # the calendar from year 980 - temp = date(980, self.month, self.day) + # 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('0980', '0000') - # libc on Linux seems to omit the leading zero - c_out = c_out.replace('980', '0000') + 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 format such as 08/16/80 - x_out = re.sub(r'\b80$', '00', x_out) - # Handle formats such as 08/16/0980 or 16.08.0980 - x_out = x_out.replace('0980', '0000') - # libc on Linux seems to omit the leading zero - x_out = x_out.replace('980', '0000') + # 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) @@ -482,7 +475,7 @@ class extended_datetime(object): if self.tzinfo is None: return None - return self.tzinfo.utcoffset(self.replace(year=980)) + return self.tzinfo.utcoffset(self.replace(year=2000)) def dst(self): """ @@ -492,7 +485,7 @@ class extended_datetime(object): if self.tzinfo is None: return None - return self.tzinfo.dst(self.replace(year=980)) + return self.tzinfo.dst(self.replace(year=2000)) def tzname(self): """ @@ -503,7 +496,7 @@ class extended_datetime(object): if self.tzinfo is None: return None - return self.tzinfo.tzname(self.replace(year=980)) + return self.tzinfo.tzname(self.replace(year=2000)) def _format(self, format): """ @@ -517,12 +510,11 @@ class extended_datetime(object): """ format = format.replace('%Y', '0000') - format = format.replace('%y', '00') # Year 0 is 1BC and a leap year. Leap years repeat themselves - # every 28 years. Thus the simplest way to format is to use - # the calendar from year 980 + # every 28 years. Because of adjustments and the proleptic gregorian + # calendar, the simplest way to format is to substitute year 2000. temp = datetime( - 980, + 2000, self.month, self.day, self.hour, @@ -534,19 +526,13 @@ class extended_datetime(object): if '%c' in format: c_out = temp.strftime('%c') # Handle full years - c_out = c_out.replace('0980', '0000') - # libc on Linux seems to omit the leading zero - c_out = c_out.replace('980', '0000') + 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 format such as 08/16/80 - x_out = re.sub(r'\b80$', '00', x_out) - # Handle formats such as 08/16/0980 or 16.08.0980 - x_out = x_out.replace('0980', '0000') - # libc on Linux seems to omit the leading zero - x_out = x_out.replace('980', '0000') + # 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) diff --git a/tests/test_util.py b/tests/test_util.py index bbfb5d4..b11c70c 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -32,32 +32,18 @@ utc = util.timezone.utc class UtilTests(unittest.TestCase): def test_extended_date_strftime(self): - def do_run(): - self.assertEqual('0000-01-01', util.extended_date(0, 1, 1).strftime('%Y-%m-%d')) - self.assertEqual('Sat Saturday Jan January', util.extended_date(0, 1, 1).strftime('%a %A %b %B')) - self.assertEqual('Tue Tuesday Feb February 29', util.extended_date(0, 2, 29).strftime('%a %A %b %B %d')) - self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_date(0, 1, 1).strftime('%c')) - self.assertEqual('01/01/00', util.extended_date(0, 1, 1).strftime('%x')) - # Python 2 doesn't allow strftime on years before 1900 - if py2: - with self.assertRaises(ValueError): - do_run() - else: - do_run() + self.assertEqual('0000-01-01', util.extended_date(0, 1, 1).strftime('%Y-%m-%d')) + self.assertEqual('Sat Saturday Jan January', util.extended_date(0, 1, 1).strftime('%a %A %b %B')) + self.assertEqual('Tue Tuesday Feb February 29', util.extended_date(0, 2, 29).strftime('%a %A %b %B %d')) + self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_date(0, 1, 1).strftime('%c')) + self.assertEqual('01/01/00', util.extended_date(0, 1, 1).strftime('%x')) def test_extended_datetime_strftime(self): - def do_run(): - self.assertEqual('0000-01-01 00:00:00', util.extended_datetime(0, 1, 1).strftime('%Y-%m-%d %H:%M:%S')) - self.assertEqual('Sat Saturday Jan January', util.extended_datetime(0, 1, 1).strftime('%a %A %b %B')) - self.assertEqual('Tue Tuesday Feb February 29', util.extended_datetime(0, 2, 29).strftime('%a %A %b %B %d')) - self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_datetime(0, 1, 1).strftime('%c')) - self.assertEqual('01/01/00', util.extended_datetime(0, 1, 1).strftime('%x')) - # Python 2 doesn't allow strftime on years before 1900 - if py2: - with self.assertRaises(ValueError): - do_run() - else: - do_run() + self.assertEqual('0000-01-01 00:00:00', util.extended_datetime(0, 1, 1).strftime('%Y-%m-%d %H:%M:%S')) + self.assertEqual('Sat Saturday Jan January', util.extended_datetime(0, 1, 1).strftime('%a %A %b %B')) + self.assertEqual('Tue Tuesday Feb February 29', util.extended_datetime(0, 2, 29).strftime('%a %A %b %B %d')) + self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_datetime(0, 1, 1).strftime('%c')) + self.assertEqual('01/01/00', util.extended_datetime(0, 1, 1).strftime('%x')) def test_extended_date_compare(self): self.assertTrue(util.extended_date(0, 1, 1) < date(1, 1, 1)) -- cgit v1.2.3 From d91530f709ba030aa5fa64315d1ef7b479a7f1e9 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Jan 2017 09:14:16 -0500 Subject: Fix tests to reflect %c date formatting on Windows --- asn1crypto/util.py | 1 - tests/test_util.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/asn1crypto/util.py b/asn1crypto/util.py index d921c1d..0d3bb94 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -18,7 +18,6 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import math import sys -import re from datetime import datetime, date, time from ._errors import unwrap diff --git a/tests/test_util.py b/tests/test_util.py index b11c70c..0d1f6d3 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -35,14 +35,20 @@ class UtilTests(unittest.TestCase): self.assertEqual('0000-01-01', util.extended_date(0, 1, 1).strftime('%Y-%m-%d')) self.assertEqual('Sat Saturday Jan January', util.extended_date(0, 1, 1).strftime('%a %A %b %B')) self.assertEqual('Tue Tuesday Feb February 29', util.extended_date(0, 2, 29).strftime('%a %A %b %B %d')) - self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_date(0, 1, 1).strftime('%c')) + if sys.platform == 'win32': + self.assertEqual('01/01/00 00:00:00', util.extended_date(0, 1, 1).strftime('%c')) + else: + self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_date(0, 1, 1).strftime('%c')) self.assertEqual('01/01/00', util.extended_date(0, 1, 1).strftime('%x')) def test_extended_datetime_strftime(self): self.assertEqual('0000-01-01 00:00:00', util.extended_datetime(0, 1, 1).strftime('%Y-%m-%d %H:%M:%S')) self.assertEqual('Sat Saturday Jan January', util.extended_datetime(0, 1, 1).strftime('%a %A %b %B')) self.assertEqual('Tue Tuesday Feb February 29', util.extended_datetime(0, 2, 29).strftime('%a %A %b %B %d')) - self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_datetime(0, 1, 1).strftime('%c')) + if sys.platform == 'win32': + self.assertEqual('01/01/00 00:00:00', util.extended_datetime(0, 1, 1).strftime('%c')) + else: + self.assertEqual('Sat Jan 1 00:00:00 0000', util.extended_datetime(0, 1, 1).strftime('%c')) self.assertEqual('01/01/00', util.extended_datetime(0, 1, 1).strftime('%x')) def test_extended_date_compare(self): -- cgit v1.2.3 From b006a2653cf7b4e83c2045e9ebfb469200dce39a Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 17 Jan 2017 09:39:19 -0500 Subject: Version 0.20.0 --- asn1crypto/__init__.py | 4 ++-- changelog.md | 10 ++++++++++ readme.md | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index d7bfc68..424894f 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -2,5 +2,5 @@ from __future__ import unicode_literals, division, absolute_import, print_function -__version__ = '0.19.0' -__version_info__ = (0, 19, 0) +__version__ = '0.20.0' +__version_info__ = (0, 20, 0) diff --git a/changelog.md b/changelog.md index 346d4da..e99bbbe 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # changelog +## 0.20.0 + + - Added support for year 0 + - Added the OID for unique identifier to `x509.NameType` + - Fixed a bug creating the native representation of a `core.BitString` with + leading null bytes + - Added a `.cast()` method to allow converting between different + representations of the same data, e.g. `core.BitString` and + `core.OctetBitString` + ## 0.19.0 - Force `algos.DigestAlgorithm` to encoding `parameters` as `Null` when the diff --git a/readme.md b/readme.md index 3be8d2b..1d89c0f 100644 --- a/readme.md +++ b/readme.md @@ -109,7 +109,7 @@ faster to an order of magnitude of more. ## Current Release -0.19.0 - [changelog](changelog.md) +0.20.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From fb1f98d67b7cb6ebc8bd33baa478997d5040f911 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 23 Jan 2017 12:18:18 -0500 Subject: Add algos.DSASignature --- asn1crypto/_int.py | 20 ++++++++++++++++++++ asn1crypto/algos.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py index f62a3a4..d0c2319 100644 --- a/asn1crypto/_int.py +++ b/asn1crypto/_int.py @@ -137,3 +137,23 @@ except (EnvironmentError, ImportError): return ud else: return ud + p + + +def fill_width(bytes_, width): + """ + Ensure a byte string representing a positive integer is a specific width + (in bytes) + + :param bytes_: + The integer byte string + + :param width: + The desired width as an integer + + :return: + A byte string of the width specified + """ + + while len(bytes_) < width: + bytes_ = b'\x00' + bytes_ + return bytes_ diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 1fe10ff..979207a 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -7,6 +7,7 @@ key cryptography. Exports the following items: - AlgorithmIdentifier() - DigestAlgorithm() - DigestInfo() + - DSASignature() - EncryptionAlgorithm() - HmacAlgorithm() - KdfAlgorithm() @@ -19,6 +20,8 @@ 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 ._int import fill_width +from .util import int_from_bytes, int_to_bytes from .core import ( Any, Choice, @@ -510,6 +513,57 @@ class RSAESOAEPParams(Sequence): ] +class DSASignature(Sequence): + """ + An ASN.1 class for translating between the OS crypto library's + representation of an (EC)DSA signature and the ASN.1 structure that is part + of various RFCs. + + Original Name: DSS-Sig-Value + Source: https://tools.ietf.org/html/rfc3279#section-2.2.2 + """ + + _fields = [ + ('r', Integer), + ('s', Integer), + ] + + @classmethod + def from_p1363(cls, data): + """ + Reads a signature from a byte string encoding accordint to IEEE P1363, + which is used by Microsoft's BCryptSignHash() function. + + :param data: + A byte string from BCryptSignHash() + + :return: + A DSASignature object + """ + + r = int_from_bytes(data[0:len(data) // 2]) + s = int_from_bytes(data[len(data) // 2:]) + return cls({'r': r, 's': s}) + + def to_p1363(self): + """ + Dumps a signature to a byte string compatible with Microsoft's + BCryptVerifySignature() function. + + :return: + A byte string compatible with BCryptVerifySignature() + """ + + r_bytes = int_to_bytes(self['r'].native) + s_bytes = int_to_bytes(self['s'].native) + + int_byte_length = max(len(r_bytes), len(s_bytes)) + r_bytes = fill_width(r_bytes, int_byte_length) + s_bytes = fill_width(s_bytes, int_byte_length) + + return r_bytes + s_bytes + + class EncryptionAlgorithmId(ObjectIdentifier): _map = { '1.3.14.3.2.7': 'des', -- cgit v1.2.3 From 9c134339ccccdade8a533008a8cd48633ce15f09 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 08:51:32 -0500 Subject: Add asn1crypto.version for setup.py and introspection --- asn1crypto/__init__.py | 7 +++++-- asn1crypto/version.py | 6 ++++++ setup.py | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 asn1crypto/version.py diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py index 424894f..afdeb43 100644 --- a/asn1crypto/__init__.py +++ b/asn1crypto/__init__.py @@ -1,6 +1,9 @@ # coding: utf-8 from __future__ import unicode_literals, division, absolute_import, print_function +from .version import __version__, __version_info__ -__version__ = '0.20.0' -__version_info__ = (0, 20, 0) +__all__ = [ + '__version__', + '__version_info__', +] diff --git a/asn1crypto/version.py b/asn1crypto/version.py new file mode 100644 index 0000000..424894f --- /dev/null +++ b/asn1crypto/version.py @@ -0,0 +1,6 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + + +__version__ = '0.20.0' +__version_info__ = (0, 20, 0) diff --git a/setup.py b/setup.py index 66a779c..7208e10 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import shutil from setuptools import setup, find_packages, Command -import asn1crypto +from asn1crypto import version class CleanCommand(Command): @@ -34,7 +34,7 @@ class CleanCommand(Command): setup( name='asn1crypto', - version=asn1crypto.__version__, + version=version.__version__, description=( 'Fast ASN.1 parser and serializer with definitions for private keys, ' -- cgit v1.2.3 From 7a650c127e5862124ae40f8450b972722f3c60a3 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 09:20:42 -0500 Subject: Add py36 to tox.ini --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index dea143c..cb5f680 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33,py34,py35,pypy +envlist = py26,py27,py32,py33,py34,py35,py36,pypy [testenv] deps = @@ -19,6 +19,9 @@ deps = py35: flake8 coverage + py36: + flake8 + coverage pypy: flake8 coverage -- cgit v1.2.3 From 695b206541875dc09c1af5d17fcf3f7b56297084 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 11:45:56 -0500 Subject: Run tests from other modularcrypto packages when measuring coverage on CI --- dev/coverage.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/dev/coverage.py b/dev/coverage.py index 1e781e7..30568c1 100644 --- a/dev/coverage.py +++ b/dev/coverage.py @@ -2,6 +2,9 @@ from __future__ import unicode_literals, division, absolute_import, print_function import coverage +import imp +import os +import unittest def run(write_xml=False): @@ -19,6 +22,19 @@ def run(write_xml=False): result = run_tests() print() + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for package_name in ['oscrypto', 'certbuilder', 'certvalidator', 'crlbuilder', 'csrbuild', 'ocspbuilder']: + for test_class in _load_package_tests(package_name): + suite.addTest(loader.loadTestsFromTestCase(test_class)) + + if suite.countTestCases() > 0: + print('Running tests from other modularcrypto packages') + other_result = unittest.TextTestRunner(verbosity=1).run(suite).wasSuccessful() + print() + else: + other_result = True + cov.stop() cov.save() @@ -26,4 +42,24 @@ def run(write_xml=False): if write_xml: cov.xml_report() - return result + return result and other_result + + +def _load_package_tests(name): + """ + Load the test classes from another modularcrypto package + + :param name: + A unicode string of the other package name + + :return: + A list of unittest.TestCase classes of the tests for the package + """ + + package_dir = os.path.join('..', name) + if not os.path.exists(package_dir): + return [] + + tests_module_info = imp.find_module('tests', [package_dir]) + tests_module = imp.load_module('%s.tests' % name, *tests_module_info) + return tests_module.test_classes() -- cgit v1.2.3 From 6c6fb47f530d5718956ce02c3b24bc7a6dcef87d Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 11:46:25 -0500 Subject: Ensure module is being loaded from source dir when tests are imported elsewhere --- tests/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index 5ea2832..f741eee 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals, division, absolute_import, print_function +import imp +import os import unittest @@ -29,6 +31,12 @@ def test_classes(): A list of unittest.TestCase classes """ + # Make sure the module is loaded from this source folder + module_name = 'asn1crypto' + src_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') + module_info = imp.find_module(module_name, [src_dir]) + imp.load_module(module_name, *module_info) + from .test_algos import AlgoTests from .test_cms import CMSTests from .test_crl import CRLTests -- cgit v1.2.3 From 9722b6de60bfde59eecf3a8d95a0684c5a51aa01 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 12:17:57 -0500 Subject: Check out other modularcrypto packages when testing via CI --- .travis.yml | 35 +++++++++++++++++++++++++++++++++++ appveyor.yml | 18 ++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/.travis.yml b/.travis.yml index d5e636d..9888694 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,21 +79,49 @@ install: brew update; brew install pypy; /usr/local/bin/pip_pypy install flake8 coverage; + echo "Installing other modularcrypto packages"; + /usr/local/bin/pip_pypy install https://github.com/wbond/oscrypto/archive/master.zip; + /usr/local/bin/pip_pypy install https://github.com/wbond/certbuilder/archive/master.zip; + /usr/local/bin/pip_pypy install https://github.com/wbond/certvalidator/archive/master.zip; + /usr/local/bin/pip_pypy install https://github.com/wbond/csrbuilder/archive/master.zip; + /usr/local/bin/pip_pypy install https://github.com/wbond/crlbuilder/archive/master.zip; + /usr/local/bin/pip_pypy install https://github.com/wbond/ocspbuilder/archive/master.zip; export PYTHON_BIN=/usr/local/bin/pypy; else if [ "$PYTHON_VERSION" == "3.6" ]; then brew update; brew install python3; /usr/local/bin/pip3 install flake8 coverage; + echo "Installing other modularcrypto packages"; + /usr/local/bin/pip3 install https://github.com/wbond/oscrypto/archive/master.zip; + /usr/local/bin/pip3 install https://github.com/wbond/certbuilder/archive/master.zip; + /usr/local/bin/pip3 install https://github.com/wbond/certvalidator/archive/master.zip; + /usr/local/bin/pip3 install https://github.com/wbond/csrbuilder/archive/master.zip; + /usr/local/bin/pip3 install https://github.com/wbond/crlbuilder/archive/master.zip; + /usr/local/bin/pip3 install https://github.com/wbond/ocspbuilder/archive/master.zip; export PYTHON_BIN=/usr/local/bin/python3; else if [ "$PYTHON_VERSION" == "2.7" ]; then curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8', 'coverage'])"; + echo "Installing other modularcrypto packages"; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/oscrypto/archive/master.zip'])"; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certbuilder/archive/master.zip'])"; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certvalidator/archive/master.zip'])"; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/csrbuilder/archive/master.zip'])"; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/crlbuilder/archive/master.zip'])"; + sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/ocspbuilder/archive/master.zip'])"; export PYTHON_BIN=/usr/bin/python2.7; else curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.6 -W ignore; sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; + echo "Installing other modularcrypto packages"; + sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/oscrypto/archive/master.zip'])"; + sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certbuilder/archive/master.zip'])"; + sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certvalidator/archive/master.zip'])"; + sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/csrbuilder/archive/master.zip'])"; + sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/crlbuilder/archive/master.zip'])"; + sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/ocspbuilder/archive/master.zip'])"; export PYTHON_BIN=/usr/bin/python2.6; fi; fi; @@ -104,6 +132,13 @@ install: else pip install flake8 coverage; fi; + echo "Installing other modularcrypto packages"; + pip install https://github.com/wbond/oscrypto/archive/master.zip; + pip install https://github.com/wbond/certbuilder/archive/master.zip; + pip install https://github.com/wbond/certvalidator/archive/master.zip; + pip install https://github.com/wbond/csrbuilder/archive/master.zip; + pip install https://github.com/wbond/crlbuilder/archive/master.zip; + pip install https://github.com/wbond/ocspbuilder/archive/master.zip; export PYTHON_BIN=python; fi; script: diff --git a/appveyor.yml b/appveyor.yml index 4d58f19..f59bea3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -54,6 +54,12 @@ install: } & "${env:PYTHON}\pypy.exe" "${env:PYTMP}\get-pip.py"; & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/oscrypto/archive/master.zip; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certbuilder/archive/master.zip; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certvalidator/archive/master.zip; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/csrbuilder/archive/master.zip; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/crlbuilder/archive/master.zip; + & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/ocspbuilder/archive/master.zip; } elseif ("${env:PYTHON_ID}" -eq "26" -or "${env:PYTHON_ID}" -eq "26-x64") { if (!(Test-Path "${env:PYTMP}\get-pip.py")) { @@ -62,9 +68,21 @@ install: & "${env:PYTHON}\python.exe" -W ignore "${env:PYTMP}\get-pip.py"; # Skip flake8 for 2.6 since flake8 and pycodestyle have deprecated support for it & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/oscrypto/archive/master.zip'])"; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certbuilder/archive/master.zip'])"; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certvalidator/archive/master.zip'])"; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/csrbuilder/archive/master.zip'])"; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/crlbuilder/archive/master.zip'])"; + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/ocspbuilder/archive/master.zip'])"; } else { & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/oscrypto/archive/master.zip; + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certbuilder/archive/master.zip; + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certvalidator/archive/master.zip; + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/csrbuilder/archive/master.zip; + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/crlbuilder/archive/master.zip; + & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/ocspbuilder/archive/master.zip; } & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/codecov/codecov-python/archive/v2.0.6.zip; -- cgit v1.2.3 From d0ea33eff59b9936fd7c3c18ff74a720dd04e43f Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 12:50:30 -0500 Subject: Clone, don't pip install modularcrypto packages --- .travis.yml | 44 +++++++++----------------------------------- appveyor.yml | 27 ++++++++------------------- 2 files changed, 17 insertions(+), 54 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9888694..a216222 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,49 +79,21 @@ install: brew update; brew install pypy; /usr/local/bin/pip_pypy install flake8 coverage; - echo "Installing other modularcrypto packages"; - /usr/local/bin/pip_pypy install https://github.com/wbond/oscrypto/archive/master.zip; - /usr/local/bin/pip_pypy install https://github.com/wbond/certbuilder/archive/master.zip; - /usr/local/bin/pip_pypy install https://github.com/wbond/certvalidator/archive/master.zip; - /usr/local/bin/pip_pypy install https://github.com/wbond/csrbuilder/archive/master.zip; - /usr/local/bin/pip_pypy install https://github.com/wbond/crlbuilder/archive/master.zip; - /usr/local/bin/pip_pypy install https://github.com/wbond/ocspbuilder/archive/master.zip; export PYTHON_BIN=/usr/local/bin/pypy; else if [ "$PYTHON_VERSION" == "3.6" ]; then brew update; brew install python3; /usr/local/bin/pip3 install flake8 coverage; - echo "Installing other modularcrypto packages"; - /usr/local/bin/pip3 install https://github.com/wbond/oscrypto/archive/master.zip; - /usr/local/bin/pip3 install https://github.com/wbond/certbuilder/archive/master.zip; - /usr/local/bin/pip3 install https://github.com/wbond/certvalidator/archive/master.zip; - /usr/local/bin/pip3 install https://github.com/wbond/csrbuilder/archive/master.zip; - /usr/local/bin/pip3 install https://github.com/wbond/crlbuilder/archive/master.zip; - /usr/local/bin/pip3 install https://github.com/wbond/ocspbuilder/archive/master.zip; export PYTHON_BIN=/usr/local/bin/python3; else if [ "$PYTHON_VERSION" == "2.7" ]; then curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8', 'coverage'])"; - echo "Installing other modularcrypto packages"; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/oscrypto/archive/master.zip'])"; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certbuilder/archive/master.zip'])"; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certvalidator/archive/master.zip'])"; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/csrbuilder/archive/master.zip'])"; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/crlbuilder/archive/master.zip'])"; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/ocspbuilder/archive/master.zip'])"; export PYTHON_BIN=/usr/bin/python2.7; else curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.6 -W ignore; sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; - echo "Installing other modularcrypto packages"; - sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/oscrypto/archive/master.zip'])"; - sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certbuilder/archive/master.zip'])"; - sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certvalidator/archive/master.zip'])"; - sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/csrbuilder/archive/master.zip'])"; - sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/crlbuilder/archive/master.zip'])"; - sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/ocspbuilder/archive/master.zip'])"; export PYTHON_BIN=/usr/bin/python2.6; fi; fi; @@ -132,15 +104,17 @@ install: else pip install flake8 coverage; fi; - echo "Installing other modularcrypto packages"; - pip install https://github.com/wbond/oscrypto/archive/master.zip; - pip install https://github.com/wbond/certbuilder/archive/master.zip; - pip install https://github.com/wbond/certvalidator/archive/master.zip; - pip install https://github.com/wbond/csrbuilder/archive/master.zip; - pip install https://github.com/wbond/crlbuilder/archive/master.zip; - pip install https://github.com/wbond/ocspbuilder/archive/master.zip; export PYTHON_BIN=python; fi; + + echo "Checking out modularcrypto packages for coverage"; + cd $TRAVIS_BUILD_DIR/..; + git clone https://github.com/wbond/oscrypto.git; + git clone https://github.com/wbond/certbuilder.git; + git clone https://github.com/wbond/certvalidator.git; + git clone https://github.com/wbond/crlbuilder.git; + git clone https://github.com/wbond/csrbuilder.git; + git clone https://github.com/wbond/ocspbuilder.git; script: - $PYTHON_BIN run.py ci after_success: diff --git a/appveyor.yml b/appveyor.yml index f59bea3..ac11fdc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -54,12 +54,6 @@ install: } & "${env:PYTHON}\pypy.exe" "${env:PYTMP}\get-pip.py"; & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/oscrypto/archive/master.zip; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certbuilder/archive/master.zip; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certvalidator/archive/master.zip; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/csrbuilder/archive/master.zip; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/crlbuilder/archive/master.zip; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/ocspbuilder/archive/master.zip; } elseif ("${env:PYTHON_ID}" -eq "26" -or "${env:PYTHON_ID}" -eq "26-x64") { if (!(Test-Path "${env:PYTMP}\get-pip.py")) { @@ -67,24 +61,19 @@ install: } & "${env:PYTHON}\python.exe" -W ignore "${env:PYTMP}\get-pip.py"; # Skip flake8 for 2.6 since flake8 and pycodestyle have deprecated support for it - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/oscrypto/archive/master.zip'])"; - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certbuilder/archive/master.zip'])"; - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/certvalidator/archive/master.zip'])"; - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/csrbuilder/archive/master.zip'])"; - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/crlbuilder/archive/master.zip'])"; - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'https://github.com/wbond/ocspbuilder/archive/master.zip'])"; } else { & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/oscrypto/archive/master.zip; - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certbuilder/archive/master.zip; - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/certvalidator/archive/master.zip; - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/csrbuilder/archive/master.zip; - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/crlbuilder/archive/master.zip; - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/wbond/ocspbuilder/archive/master.zip; } + cd "c:\projects\"; + git clone https://github.com/wbond/oscrypto.git; + git clone https://github.com/wbond/certbuilder.git; + git clone https://github.com/wbond/certvalidator.git; + git clone https://github.com/wbond/crlbuilder.git; + git clone https://github.com/wbond/csrbuilder.git; + git clone https://github.com/wbond/ocspbuilder.git; + & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/codecov/codecov-python/archive/v2.0.6.zip; - "SET PATH=%PYTHON%;%PATH%" cache: -- cgit v1.2.3 From a4f18b0d8c79a9edcab6cf633c47329072ea67a0 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 13:18:24 -0500 Subject: Fix AppVeyor issue with calling git clone --- appveyor.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ac11fdc..e4f754f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -67,12 +67,12 @@ install: } cd "c:\projects\"; - git clone https://github.com/wbond/oscrypto.git; - git clone https://github.com/wbond/certbuilder.git; - git clone https://github.com/wbond/certvalidator.git; - git clone https://github.com/wbond/crlbuilder.git; - git clone https://github.com/wbond/csrbuilder.git; - git clone https://github.com/wbond/ocspbuilder.git; + & git clone https://github.com/wbond/oscrypto.git 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/certbuilder.git 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/certvalidator.git 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/crlbuilder.git 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/csrbuilder.git 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/ocspbuilder.git 2>&1 | % { $_.ToString() }; & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/codecov/codecov-python/archive/v2.0.6.zip; - "SET PATH=%PYTHON%;%PATH%" -- cgit v1.2.3 From 50bf45a95479ec66844a0e4b3eca3d000f0c0706 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 13:22:15 -0500 Subject: Tweak git clones on AppVeyor --- appveyor.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index e4f754f..5ad46cd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -66,13 +66,12 @@ install: & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; } - cd "c:\projects\"; - & git clone https://github.com/wbond/oscrypto.git 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/certbuilder.git 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/certvalidator.git 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/crlbuilder.git 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/csrbuilder.git 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/ocspbuilder.git 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/oscrypto.git c:\projects\oscrypto 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/certbuilder.git c:\projects\certbuilder 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/certvalidator.git c:\projects\certvalidator 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/crlbuilder.git c:\projects\crlbuilder 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/csrbuilder.git c:\projects\csrbuilder 2>&1 | % { $_.ToString() }; + & git clone https://github.com/wbond/ocspbuilder.git c:\projects\ocspbuilder 2>&1 | % { $_.ToString() }; & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/codecov/codecov-python/archive/v2.0.6.zip; - "SET PATH=%PYTHON%;%PATH%" -- cgit v1.2.3 From df00010c46264c152464bc3459431eb5865b15a2 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 13:25:11 -0500 Subject: Add back AppVeyor command to install Coverage for Python 2.6 --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 5ad46cd..9108c9a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,6 +61,7 @@ install: } & "${env:PYTHON}\python.exe" -W ignore "${env:PYTMP}\get-pip.py"; # Skip flake8 for 2.6 since flake8 and pycodestyle have deprecated support for it + & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; } else { & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; -- cgit v1.2.3 From 9c8d3374cf84c1f565ab3f79868d7b8d0eb7784d Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 13:27:21 -0500 Subject: Install GeoTrust root for certvalidator tests on AppVeyor --- appveyor.yml | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 9108c9a..11f17d7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -44,6 +44,61 @@ install: New-Item -ItemType directory -Path "$env:PYTMP" | Out-Null; } + [Byte[]] $geotrustCaBytes = 0x30,0x82,0x03,0x7C,0x30,0x82,0x02,0x64,0xA0,0x03,0x02,0x01,0x02, + 0x02,0x10,0x18,0xAC,0xB5,0x6A,0xFD,0x69,0xB6,0x15,0x3A,0x63,0x6C,0xAF,0xDA,0xFA,0xC4,0xA1,0x30, + 0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30,0x58,0x31,0x0B,0x30, + 0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31,0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A, + 0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x49,0x6E,0x63,0x2E,0x31,0x31,0x30,0x2F, + 0x06,0x03,0x55,0x04,0x03,0x13,0x28,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20,0x50,0x72,0x69, + 0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20, + 0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x1E,0x17,0x0D,0x30,0x36,0x31,0x31,0x32,0x37, + 0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x33,0x36,0x30,0x37,0x31,0x36,0x32,0x33,0x35,0x39, + 0x35,0x39,0x5A,0x30,0x58,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x55,0x53,0x31, + 0x16,0x30,0x14,0x06,0x03,0x55,0x04,0x0A,0x13,0x0D,0x47,0x65,0x6F,0x54,0x72,0x75,0x73,0x74,0x20, + 0x49,0x6E,0x63,0x2E,0x31,0x31,0x30,0x2F,0x06,0x03,0x55,0x04,0x03,0x13,0x28,0x47,0x65,0x6F,0x54, + 0x72,0x75,0x73,0x74,0x20,0x50,0x72,0x69,0x6D,0x61,0x72,0x79,0x20,0x43,0x65,0x72,0x74,0x69,0x66, + 0x69,0x63,0x61,0x74,0x69,0x6F,0x6E,0x20,0x41,0x75,0x74,0x68,0x6F,0x72,0x69,0x74,0x79,0x30,0x82, + 0x01,0x22,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82, + 0x01,0x0F,0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xBE,0xB8,0x15,0x7B,0xFF,0xD4,0x7C, + 0x7D,0x67,0xAD,0x83,0x64,0x7B,0xC8,0x42,0x53,0x2D,0xDF,0xF6,0x84,0x08,0x20,0x61,0xD6,0x01,0x59, + 0x6A,0x9C,0x44,0x11,0xAF,0xEF,0x76,0xFD,0x95,0x7E,0xCE,0x61,0x30,0xBB,0x7A,0x83,0x5F,0x02,0xBD, + 0x01,0x66,0xCA,0xEE,0x15,0x8D,0x6F,0xA1,0x30,0x9C,0xBD,0xA1,0x85,0x9E,0x94,0x3A,0xF3,0x56,0x88, + 0x00,0x31,0xCF,0xD8,0xEE,0x6A,0x96,0x02,0xD9,0xED,0x03,0x8C,0xFB,0x75,0x6D,0xE7,0xEA,0xB8,0x55, + 0x16,0x05,0x16,0x9A,0xF4,0xE0,0x5E,0xB1,0x88,0xC0,0x64,0x85,0x5C,0x15,0x4D,0x88,0xC7,0xB7,0xBA, + 0xE0,0x75,0xE9,0xAD,0x05,0x3D,0x9D,0xC7,0x89,0x48,0xE0,0xBB,0x28,0xC8,0x03,0xE1,0x30,0x93,0x64, + 0x5E,0x52,0xC0,0x59,0x70,0x22,0x35,0x57,0x88,0x8A,0xF1,0x95,0x0A,0x83,0xD7,0xBC,0x31,0x73,0x01, + 0x34,0xED,0xEF,0x46,0x71,0xE0,0x6B,0x02,0xA8,0x35,0x72,0x6B,0x97,0x9B,0x66,0xE0,0xCB,0x1C,0x79, + 0x5F,0xD8,0x1A,0x04,0x68,0x1E,0x47,0x02,0xE6,0x9D,0x60,0xE2,0x36,0x97,0x01,0xDF,0xCE,0x35,0x92, + 0xDF,0xBE,0x67,0xC7,0x6D,0x77,0x59,0x3B,0x8F,0x9D,0xD6,0x90,0x15,0x94,0xBC,0x42,0x34,0x10,0xC1, + 0x39,0xF9,0xB1,0x27,0x3E,0x7E,0xD6,0x8A,0x75,0xC5,0xB2,0xAF,0x96,0xD3,0xA2,0xDE,0x9B,0xE4,0x98, + 0xBE,0x7D,0xE1,0xE9,0x81,0xAD,0xB6,0x6F,0xFC,0xD7,0x0E,0xDA,0xE0,0x34,0xB0,0x0D,0x1A,0x77,0xE7, + 0xE3,0x08,0x98,0xEF,0x58,0xFA,0x9C,0x84,0xB7,0x36,0xAF,0xC2,0xDF,0xAC,0xD2,0xF4,0x10,0x06,0x70, + 0x71,0x35,0x02,0x03,0x01,0x00,0x01,0xA3,0x42,0x30,0x40,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01, + 0x01,0xFF,0x04,0x05,0x30,0x03,0x01,0x01,0xFF,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF, + 0x04,0x04,0x03,0x02,0x01,0x06,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14,0x2C,0xD5, + 0x50,0x41,0x97,0x15,0x8B,0xF0,0x8F,0x36,0x61,0x5B,0x4A,0xFB,0x6B,0xD9,0x99,0xC9,0x33,0x92,0x30, + 0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x03,0x82,0x01,0x01,0x00, + 0x5A,0x70,0x7F,0x2C,0xDD,0xB7,0x34,0x4F,0xF5,0x86,0x51,0xA9,0x26,0xBE,0x4B,0xB8,0xAA,0xF1,0x71, + 0x0D,0xDC,0x61,0xC7,0xA0,0xEA,0x34,0x1E,0x7A,0x77,0x0F,0x04,0x35,0xE8,0x27,0x8F,0x6C,0x90,0xBF, + 0x91,0x16,0x24,0x46,0x3E,0x4A,0x4E,0xCE,0x2B,0x16,0xD5,0x0B,0x52,0x1D,0xFC,0x1F,0x67,0xA2,0x02, + 0x45,0x31,0x4F,0xCE,0xF3,0xFA,0x03,0xA7,0x79,0x9D,0x53,0x6A,0xD9,0xDA,0x63,0x3A,0xF8,0x80,0xD7, + 0xD3,0x99,0xE1,0xA5,0xE1,0xBE,0xD4,0x55,0x71,0x98,0x35,0x3A,0xBE,0x93,0xEA,0xAE,0xAD,0x42,0xB2, + 0x90,0x6F,0xE0,0xFC,0x21,0x4D,0x35,0x63,0x33,0x89,0x49,0xD6,0x9B,0x4E,0xCA,0xC7,0xE7,0x4E,0x09, + 0x00,0xF7,0xDA,0xC7,0xEF,0x99,0x62,0x99,0x77,0xB6,0x95,0x22,0x5E,0x8A,0xA0,0xAB,0xF4,0xB8,0x78, + 0x98,0xCA,0x38,0x19,0x99,0xC9,0x72,0x9E,0x78,0xCD,0x4B,0xAC,0xAF,0x19,0xA0,0x73,0x12,0x2D,0xFC, + 0xC2,0x41,0xBA,0x81,0x91,0xDA,0x16,0x5A,0x31,0xB7,0xF9,0xB4,0x71,0x80,0x12,0x48,0x99,0x72,0x73, + 0x5A,0x59,0x53,0xC1,0x63,0x52,0x33,0xED,0xA7,0xC9,0xD2,0x39,0x02,0x70,0xFA,0xE0,0xB1,0x42,0x66, + 0x29,0xAA,0x9B,0x51,0xED,0x30,0x54,0x22,0x14,0x5F,0xD9,0xAB,0x1D,0xC1,0xE4,0x94,0xF0,0xF8,0xF5, + 0x2B,0xF7,0xEA,0xCA,0x78,0x46,0xD6,0xB8,0x91,0xFD,0xA6,0x0D,0x2B,0x1A,0x14,0x01,0x3E,0x80,0xF0, + 0x42,0xA0,0x95,0x07,0x5E,0x6D,0xCD,0xCC,0x4B,0xA4,0x45,0x8D,0xAB,0x12,0xE8,0xB3,0xDE,0x5A,0xE5, + 0xA0,0x7C,0xE8,0x0F,0x22,0x1D,0x5A,0xE9,0x59; + $geotrustCa = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2; + $geotrustCa.Import($geotrustCaBytes); + $rootStore = Get-Item cert:\LocalMachine\Root; + $rootStore.Open("ReadWrite"); + $rootStore.Add($geotrustCa); + $rootStore.Close(); + if ("${env:PYTHON_ID}" -eq "pypy") { if (!(Test-Path "${env:PYTMP}\pypy2-v5.3.1-win32.zip")) { (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.3.1-win32.zip', "${env:PYTMP}\pypy2-v5.3.1-win32.zip"); -- cgit v1.2.3 From ba4b9773dd565fe7da5a490cf3bbc610d4d0152f Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 14:52:52 -0500 Subject: Tweak coverage output to be flushed when running ci task --- dev/coverage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev/coverage.py b/dev/coverage.py index 30568c1..c8c0e00 100644 --- a/dev/coverage.py +++ b/dev/coverage.py @@ -5,6 +5,7 @@ import coverage import imp import os import unittest +import sys def run(write_xml=False): @@ -30,8 +31,10 @@ def run(write_xml=False): if suite.countTestCases() > 0: print('Running tests from other modularcrypto packages') + sys.stdout.flush() other_result = unittest.TextTestRunner(verbosity=1).run(suite).wasSuccessful() print() + sys.stdout.flush() else: other_result = True -- cgit v1.2.3 From 197d673c2ec06156171c5a42e2d2c08b40398ed4 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 14:55:48 -0500 Subject: Expand AppVeyor build matrix to include winlegacy variant of oscrypto --- appveyor.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 11f17d7..e3c972a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,36 +7,64 @@ environment: PYTHON_EXE: python PYTHON_VERSION: "2.6" PLATFORM: win-x32 + BACKEND: win - PYTHON: "C:\\Python26-x64" PYTHON_ID: "26-x64" PYTHON_EXE: python PYTHON_VERSION: "2.6" PLATFORM: win-x64 + BACKEND: win - PYTHON: "C:\\Python27" PYTHON_ID: "27" PYTHON_EXE: python PYTHON_VERSION: "2.7" PLATFORM: win-x32 + BACKEND: win + - PYTHON: "C:\\Python27" + PYTHON_ID: "27" + PYTHON_EXE: python + PYTHON_VERSION: "2.7" + PLATFORM: win-x32 + BACKEND: winlegacy + OSCRYPTO_USE_WINLEGACY: 1 - PYTHON: "C:\\Python27-x64" PYTHON_ID: "27-x64" PYTHON_EXE: python PYTHON_VERSION: "2.7" PLATFORM: win-x64 + BACKEND: win - PYTHON: "C:\\Python33" PYTHON_ID: "33" PYTHON_EXE: python PYTHON_VERSION: "3.3" PLATFORM: win-x32 + BACKEND: win + - PYTHON: "C:\\Python33" + PYTHON_ID: "33" + PYTHON_EXE: python + PYTHON_VERSION: "3.3" + PLATFORM: win-x32 + BACKEND: winlegacy + OSCRYPTO_USE_WINLEGACY: 1 - PYTHON: "C:\\Python33-x64" PYTHON_ID: "33-x64" PYTHON_EXE: python PYTHON_VERSION: "3.3" PLATFORM: win-x64 + BACKEND: win + - PYTHON: "C:\\pypy2-v5.3.1-win32" + PYTHON_ID: "pypy" + PYTHON_EXE: pypy + PYTHON_VERSION: "pypy" + PLATFORM: win-x32 + BACKEND: win - PYTHON: "C:\\pypy2-v5.3.1-win32" PYTHON_ID: "pypy" PYTHON_EXE: pypy PYTHON_VERSION: "pypy" PLATFORM: win-x32 + BACKEND: winlegacy + OSCRYPTO_USE_WINLEGACY: 1 install: - ps: |- $env:PYTMP = "${env:TMP}\py"; @@ -137,4 +165,4 @@ build: off test_script: - cmd: "%PYTHON_EXE% run.py ci" after_test: - - C:\Python27-x64\Scripts\codecov -X gcov -f coverage.xml -e PYTHON_VERSION PLATFORM + - C:\Python27-x64\Scripts\codecov -X gcov -f coverage.xml -e PYTHON_VERSION PLATFORM BACKEND -- cgit v1.2.3 From a385badb70bf80711d8808d5d2fcc7e109f05395 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 24 Jan 2017 22:16:58 -0500 Subject: Change Travis CI clone of modularcrypto --- .travis.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index a216222..8610d53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,13 +108,12 @@ install: fi; echo "Checking out modularcrypto packages for coverage"; - cd $TRAVIS_BUILD_DIR/..; - git clone https://github.com/wbond/oscrypto.git; - git clone https://github.com/wbond/certbuilder.git; - git clone https://github.com/wbond/certvalidator.git; - git clone https://github.com/wbond/crlbuilder.git; - git clone https://github.com/wbond/csrbuilder.git; - git clone https://github.com/wbond/ocspbuilder.git; + git clone https://github.com/wbond/oscrypto.git $TRAVIS_BUILD_DIR/../oscrypto; + git clone https://github.com/wbond/certbuilder.git $TRAVIS_BUILD_DIR/../certbuilder; + git clone https://github.com/wbond/certvalidator.git $TRAVIS_BUILD_DIR/../certvalidator; + git clone https://github.com/wbond/crlbuilder.git $TRAVIS_BUILD_DIR/../crlbuilder; + git clone https://github.com/wbond/csrbuilder.git $TRAVIS_BUILD_DIR/../csrbuilder; + git clone https://github.com/wbond/ocspbuilder.git $TRAVIS_BUILD_DIR/../ocspbuilder; script: - $PYTHON_BIN run.py ci after_success: -- cgit v1.2.3 From 4e8f0776875d8cd063637554d2f5fea2ca6d5747 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 25 Jan 2017 05:32:43 -0500 Subject: Tweak Travis build matrix for better coverage and to fix issue with PyPy and OS X 10.11 --- .travis.yml | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8610d53..79c3e17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,69 +10,104 @@ matrix: env: PYTHON_VERSION=2.6 PLATFORM=osx + BACKEND=osx - os: osx python: "2.7" env: PYTHON_VERSION=2.7 - PLATFORM=osx + PLATFORM=osx-10.11 + BACKEND=osx + # This tests OpenSSL 0.9.8, which is what is bundled with OS X + - os: osx + python: "2.7" + env: + PYTHON_VERSION=2.7 + PLATFORM=osx-10.11 + BACKEND=openssl + OSCRYPTO_USE_OPENSSL=/usr/lib/libcrypto.dylib,/usr/lib/libssl.dylib + - os: osx + osx_image: xcode6.4 + python: "2.7" + env: + PYTHON_VERSION=2.7 + PLATFORM=osx-10.10 + BACKEND=osx + - os: osx + osx_image: xcode8.1 + python: "2.7" + env: + PYTHON_VERSION=2.7 + PLATFORM=osx-10.12 + BACKEND=osx - os: osx python: "3.6" env: PYTHON_VERSION=3.6 PLATFORM=osx + BACKEND=osx - os: osx + osx_image: xcode8.1 python: "pypy" env: PYTHON_VERSION=pypy - PLATFORM=osx + PLATFORM=osx-10.12 + BACKEND=osx - os: linux language: python python: "2.6" env: PYTHON_VERSION=2.6 PLATFORM=linux + BACKEND=openssl - os: linux language: python python: "2.7" env: PYTHON_VERSION=2.7 PLATFORM=linux + BACKEND=openssl - os: linux language: python python: "3.2" env: PYTHON_VERSION=3.2 PLATFORM=linux + BACKEND=openssl - os: linux language: python python: "3.3" env: PYTHON_VERSION=3.3 PLATFORM=linux + BACKEND=openssl - os: linux language: python python: "3.4" env: PYTHON_VERSION=3.4 PLATFORM=linux + BACKEND=openssl - os: linux language: python python: "3.5" env: PYTHON_VERSION=3.5 PLATFORM=linux + BACKEND=openssl - os: linux language: python python: "3.6" env: PYTHON_VERSION=3.6 PLATFORM=linux + BACKEND=openssl - os: linux language: python python: "pypy" env: PYTHON_VERSION=pypy PLATFORM=linux + BACKEND=openssl install: - if [ "$TRAVIS_OS_NAME" == "osx" ]; then if [ "$PYTHON_VERSION" == "pypy" ]; then @@ -117,4 +152,4 @@ install: script: - $PYTHON_BIN run.py ci after_success: - - bash <(curl -s https://codecov.io/bash) -X gcov -f coverage.xml -e PYTHON_VERSION,PLATFORM + - bash <(curl -s https://codecov.io/bash) -X gcov -f coverage.xml -e PYTHON_VERSION,PLATFORM,BACKEND -- cgit v1.2.3 From bf084e3cb10d1663126b1e23f16ca9e8ec614928 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 25 Jan 2017 12:12:07 -0500 Subject: Use a modern version of PyPy on Travis CI Linux --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 79c3e17..e20acee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -103,7 +103,7 @@ matrix: BACKEND=openssl - os: linux language: python - python: "pypy" + python: "pypy-5.3.1" env: PYTHON_VERSION=pypy PLATFORM=linux -- cgit v1.2.3 From 6d2b0ac8e783a67936c7c17f6995962402ad909b Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 30 Jan 2017 18:06:14 -0500 Subject: Added strict parameter to load methods, parser.emit() and parser.parse() --- asn1crypto/_types.py | 5 + asn1crypto/core.py | 227 ++++++++++++--------------------------------- asn1crypto/parser.py | 254 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+), 171 deletions(-) create mode 100644 asn1crypto/parser.py diff --git a/asn1crypto/_types.py b/asn1crypto/_types.py index 93d485e..b9ca8cc 100644 --- a/asn1crypto/_types.py +++ b/asn1crypto/_types.py @@ -13,6 +13,8 @@ if sys.version_info < (3,): def bytes_to_list(byte_string): return [ord(b) for b in byte_string] + chr_cls = chr + else: str_cls = str byte_cls = bytes @@ -20,6 +22,9 @@ else: bytes_to_list = list + def chr_cls(num): + return bytes([num]) + def type_name(value): """ diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 14c9f96..a9ccd2e 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -57,25 +57,20 @@ import sys from . import _teletex_codec from ._errors import unwrap from ._ordereddict import OrderedDict -from ._types import type_name, str_cls, byte_cls, int_types +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 -# Python 2 if sys.version_info <= (3,): from cStringIO import StringIO as BytesIO range = xrange # noqa - py2 = True - chr_cls = chr + _PY2 = True -# Python 3 else: from io import BytesIO - py2 = False - - def chr_cls(num): - return bytes([num]) + _PY2 = False _teletex_codec.register() @@ -154,22 +149,29 @@ class Asn1Value(object): _native = None @classmethod - def load(cls, encoded_data, **kwargs): + def load(cls, encoded_data, strict=False, **kwargs): """ Loads a BER/DER-encoded byte string using the current class as the spec :param encoded_data: - A byte string of BER or DER encoded data + A byte string of BER or DER-encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists :return: - A instance of the current class + An instance of the current class """ + if not isinstance(encoded_data, byte_cls): + raise TypeError('encoded_data must be a byte string, not %s' % type_name(encoded_data)) + spec = None if cls.tag is not None: spec = cls - value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs) + value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs, strict=strict) return value def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=None, contents=None): @@ -286,7 +288,7 @@ class Asn1Value(object): A unicode string """ - if py2: + if _PY2: return self.__bytes__() else: return self.__unicode__() @@ -297,7 +299,7 @@ class Asn1Value(object): A unicode string """ - if py2: + if _PY2: return '<%s %s b%s>' % (type_name(self), id(self), repr(self.dump())) else: return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump())) @@ -444,7 +446,7 @@ class Asn1Value(object): elif hasattr(self, 'chosen'): self.chosen.debug(nest_level + 2) else: - if py2 and isinstance(self.native, byte_cls): + if _PY2 and isinstance(self.native, byte_cls): print('%s Native: b%s' % (prefix, repr(self.native))) else: print('%s Native: %s' % (prefix, self.native)) @@ -764,18 +766,25 @@ class Choice(Asn1Value): _name_map = None @classmethod - def load(cls, encoded_data, **kwargs): + def load(cls, encoded_data, strict=False, **kwargs): """ Loads a BER/DER-encoded byte string using the current class as the spec :param encoded_data: A byte string of BER or DER encoded data + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + :return: A instance of the current class """ - value, _ = _parse_build(encoded_data, spec=cls, spec_params=kwargs) + if not isinstance(encoded_data, byte_cls): + raise TypeError('encoded_data must be a byte string, not %s' % type_name(encoded_data)) + + value, _ = _parse_build(encoded_data, spec=cls, spec_params=kwargs, strict=strict) return value def _setup(self): @@ -1063,20 +1072,24 @@ class Concat(object): _children = None @classmethod - def load(cls, encoded_data): + def load(cls, encoded_data, strict=False): """ Loads a BER/DER-encoded byte string using the current class as the spec :param encoded_data: A byte string of BER or DER encoded data + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + :return: A Concat object """ - return cls(contents=encoded_data) + return cls(contents=encoded_data, strict=strict) - def __init__(self, value=None, contents=None): + def __init__(self, value=None, contents=None, strict=False): """ :param value: A native Python datatype to initialize the object value with @@ -1084,6 +1097,10 @@ class Concat(object): :param contents: A byte string of the encoded contents of the value + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists in contents + :raises: ValueError - when an error occurs with one of the children TypeError - when an error occurs with one of the children @@ -1102,6 +1119,10 @@ class Concat(object): child_value = spec() self._children.append(child_value) + if strict and offset != contents_len: + extra_bytes = contents_len - offset + raise ValueError('Extra data - %d bytes of trailing data were provided' % extra_bytes) + except (ValueError, TypeError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args @@ -1122,7 +1143,7 @@ class Concat(object): A unicode string """ - if py2: + if _PY2: return self.__bytes__() else: return self.__unicode__() @@ -2496,7 +2517,7 @@ class ObjectIdentifier(Primitive, ValueMap): part = 0 for byte in self.contents: - if py2: + if _PY2: byte = ord(byte) part = part * 128 part += byte & 127 @@ -4238,7 +4259,7 @@ class UTCTime(AbstractTime): if isinstance(value, datetime): value = value.strftime('%y%m%d%H%M%SZ') - if py2: + if _PY2: value = value.decode('ascii') AbstractString.set(self, value) @@ -4296,7 +4317,7 @@ class GeneralizedTime(AbstractTime): if isinstance(value, (datetime, extended_datetime)): value = value.strftime('%Y%m%d%H%M%SZ') - if py2: + if _PY2: value = value.decode('ascii') AbstractString.set(self, value) @@ -4485,35 +4506,6 @@ def _fix_tagging(value, params): return value -def _dump_header(class_, method, tag, contents): - header = b'' - - id_num = 0 - id_num |= class_ << 6 - id_num |= method << 5 - - tag = tag - - if tag >= 31: - header += chr_cls(id_num | 31) - while tag > 0: - continuation_bit = 0x80 if tag > 0x7F else 0 - header += chr_cls(continuation_bit | (tag & 0x7F)) - tag = tag >> 7 - else: - header += chr_cls(id_num | tag) - - length = len(contents) - if length <= 127: - header += chr_cls(length) - else: - length_bytes = int_to_bytes(length) - header += chr_cls(0x80 | len(length_bytes)) - header += length_bytes - - return header - - def _build_id_tuple(params, spec): """ Builds a 2-element tuple used to identify fields by grabbing the class_ @@ -4765,122 +4757,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param return value -_MEMOIZED_PARSINGS = {} - - -def _parse(encoded_data, data_len, pointer=0, lengths_only=False): - """ - Parses a byte string into component parts - - :param encoded_data: - A byte string that contains BER-encoded data - - :param data_len: - The integer length of the encoded data - - :param pointer: - The index in the byte string to parse from - - :param lengths_only: - A boolean to cause the call to return a 2-element tuple of the integer - number of bytes in the header and the integer number of bytes in the - contents. Internal use only. - - :return: - A 2-element tuple: - - 0: A tuple of (class_, method, tag, header, content, trailer) - - 1: An integer indicating how many bytes were consumed - """ - - if data_len == 0: - return ((None, None, None, None, None, None), pointer) - - start = pointer - first_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] - pointer += 1 - - tag = first_octet & 31 - # Base 128 length using 8th bit as continuation indicator - if tag == 31: - tag = 0 - while True: - num = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] - pointer += 1 - tag *= 128 - tag += num & 127 - if num >> 7 == 0: - break - - length_octet = ord(encoded_data[pointer]) if py2 else encoded_data[pointer] - pointer += 1 - - if length_octet >> 7 == 0: - if lengths_only: - return (pointer, pointer + (length_octet & 127)) - contents_end = pointer + (length_octet & 127) - - else: - length_octets = length_octet & 127 - if length_octets: - pointer += length_octets - contents_end = pointer + int_from_bytes(encoded_data[pointer - length_octets:pointer], signed=False) - if lengths_only: - return (pointer, contents_end) - - else: - # To properly parse indefinite length values, we need to scan forward - # parsing headers until we find a value with a length of zero. If we - # just scanned looking for \x00\x00, nested indefinite length values - # would not work. - contents_end = pointer - 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: - break - if lengths_only: - return (pointer, contents_end) - if contents_end > data_len: - raise ValueError(unwrap( - ''' - Insufficient data - %s bytes requested but only %s available - ''', - contents_end, - data_len - )) - return ( - ( - first_octet >> 6, - (first_octet >> 5) & 1, - tag, - encoded_data[start:pointer], - encoded_data[pointer:contents_end - 2], - b'\x00\x00' - ), - contents_end - ) - - if contents_end > data_len: - raise ValueError(unwrap( - ''' - Insufficient data - %s bytes requested but only %s available - ''', - contents_end, - data_len - )) - return ( - ( - first_octet >> 6, - (first_octet >> 5) & 1, - tag, - encoded_data[start:pointer], - encoded_data[pointer:contents_end], - b'' - ), - contents_end - ) - - -def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None): +def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None, strict=False): """ Parses a byte string generically, or using a spec with optional params @@ -4900,11 +4777,19 @@ def _parse_build(encoded_data, pointer=0, spec=None, spec_params=None): :param spec_params: A dict of params to pass to the spec object + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + :return: A 2-element tuple: - 0: An object of the type spec, or if not specified, a child of Asn1Value - 1: An integer indicating how many bytes were consumed """ - info, new_pointer = _parse(encoded_data, len(encoded_data), pointer) + encoded_len = len(encoded_data) + info, new_pointer = _parse(encoded_data, encoded_len, pointer) + if strict and new_pointer != pointer + encoded_len: + extra_bytes = pointer + encoded_len - new_pointer + raise ValueError('Extra data - %d bytes of trailing data were provided' % extra_bytes) return (_build(*info, spec=spec, spec_params=spec_params), new_pointer) diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py new file mode 100644 index 0000000..df4063e --- /dev/null +++ b/asn1crypto/parser.py @@ -0,0 +1,254 @@ +# coding: utf-8 + +""" +Functions for parsing and dumping using the ASN.1 DER encoding. Exports the +following items: + + - emit() + - parse() + +Other type classes are defined that help compose the types listed above. +""" + +from __future__ import unicode_literals, division, absolute_import, print_function + +import sys + +from ._types import byte_cls, chr_cls, type_name +from .util import int_from_bytes, int_to_bytes + +_PY2 = sys.version_info <= (3,) +_INSUFFICIENT_DATA_MESSAGE = 'Insufficient data - %s bytes requested but only %s available' + + +def emit(class_, method, tag, contents): + """ + Constructs a byte string of an ASN.1 DER-encoded value + + This is typically not useful. Instead, use one of the standard classes from + asn1crypto.core, or construct a new class with specific fields, and call the + .dump() method. + + :param class_: + An integer ASN.1 class value: 0 (universal), 1 (application), + 2 (context), 3 (private) + + :param method: + An integer ASN.1 method value: 0 (primitive), 1 (constructed) + + :param tag: + An integer ASN.1 tag value + + :param contents: + A byte string of the encoded byte contents + + :return: + A byte string of the ASN.1 DER value (header and contents) + """ + + if not isinstance(class_, int): + raise TypeError('class_ must be an integer, not %s' % type_name(class_)) + + if class_ < 0 or class_ > 3: + raise ValueError('class_ must be one of 0, 1, 2 or 3, not %s' % class_) + + if not isinstance(method, int): + raise TypeError('method must be an integer, not %s' % type_name(method)) + + if method < 0 or method > 1: + raise ValueError('method must be 0 or 1, not %s' % method) + + if not isinstance(tag, int): + raise TypeError('tag must be an integer, not %s' % type_name(tag)) + + if tag < 0: + raise ValueError('tag must be greater than zero, not %s' % tag) + + if not isinstance(contents, byte_cls): + raise TypeError('contents must be a byte string, not %s' % type_name(contents)) + + return _dump_header(class_, method, tag, contents) + contents + + +def parse(contents, strict=False): + """ + Parses a byte string of ASN.1 BER/DER-encoded data. + + This is typically not useful. Instead, use one of the standard classes from + asn1crypto.core, or construct a new class with specific fields, and call the + .load() class method. + + :param contents: + A byte string of BER/DER-encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :return: + A 6-element tuple: + - 0: integer class (0 to 3) + - 1: integer method + - 2: integer tag + - 3: byte string header + - 4: byte string content + - 5: byte string trailer + """ + + if not isinstance(contents, byte_cls): + raise TypeError('contents must be a byte string, not %s' % type_name(contents)) + + contents_len = len(contents) + info, consumed = _parse(contents, contents_len) + if strict and consumed != contents_len: + raise ValueError('Extra data - %d bytes of trailing data were provided' % (contents_len - consumed)) + return info + + +def _parse(encoded_data, data_len, pointer=0, lengths_only=False): + """ + Parses a byte string into component parts + + :param encoded_data: + A byte string that contains BER-encoded data + + :param data_len: + The integer length of the encoded data + + :param pointer: + The index in the byte string to parse from + + :param lengths_only: + A boolean to cause the call to return a 2-element tuple of the integer + number of bytes in the header and the integer number of bytes in the + contents. Internal use only. + + :return: + A 2-element tuple: + - 0: A tuple of (class_, method, tag, header, content, trailer) + - 1: An integer indicating how many bytes were consumed + """ + + if data_len == 0: + return ((None, None, None, None, None, None), pointer) + + start = pointer + first_octet = ord(encoded_data[pointer]) if _PY2 else encoded_data[pointer] + pointer += 1 + + tag = first_octet & 31 + # Base 128 length using 8th bit as continuation indicator + if tag == 31: + tag = 0 + while True: + num = ord(encoded_data[pointer]) if _PY2 else encoded_data[pointer] + pointer += 1 + tag *= 128 + tag += num & 127 + if num >> 7 == 0: + break + + length_octet = ord(encoded_data[pointer]) if _PY2 else encoded_data[pointer] + pointer += 1 + + if length_octet >> 7 == 0: + if lengths_only: + return (pointer, pointer + (length_octet & 127)) + contents_end = pointer + (length_octet & 127) + + else: + length_octets = length_octet & 127 + if length_octets: + pointer += length_octets + contents_end = pointer + int_from_bytes(encoded_data[pointer - length_octets:pointer], signed=False) + if lengths_only: + return (pointer, contents_end) + + else: + # To properly parse indefinite length values, we need to scan forward + # parsing headers until we find a value with a length of zero. If we + # just scanned looking for \x00\x00, nested indefinite length values + # would not work. + contents_end = pointer + 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: + break + if lengths_only: + return (pointer, contents_end) + if contents_end > data_len: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (contents_end, data_len)) + return ( + ( + first_octet >> 6, + (first_octet >> 5) & 1, + tag, + encoded_data[start:pointer], + encoded_data[pointer:contents_end - 2], + b'\x00\x00' + ), + contents_end + ) + + if contents_end > data_len: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (contents_end, data_len)) + return ( + ( + first_octet >> 6, + (first_octet >> 5) & 1, + tag, + encoded_data[start:pointer], + encoded_data[pointer:contents_end], + b'' + ), + contents_end + ) + + +def _dump_header(class_, method, tag, contents): + """ + Constructs the header bytes for an ASN.1 object + + :param class_: + An integer ASN.1 class value: 0 (universal), 1 (application), + 2 (context), 3 (private) + + :param method: + An integer ASN.1 method value: 0 (primitive), 1 (constructed) + + :param tag: + An integer ASN.1 tag value + + :param contents: + A byte string of the encoded byte contents + + :return: + A byte string of the ASN.1 DER header + """ + + header = b'' + + id_num = 0 + id_num |= class_ << 6 + id_num |= method << 5 + + tag = tag + + if tag >= 31: + header += chr_cls(id_num | 31) + while tag > 0: + continuation_bit = 0x80 if tag > 0x7F else 0 + header += chr_cls(continuation_bit | (tag & 0x7F)) + tag = tag >> 7 + else: + header += chr_cls(id_num | tag) + + length = len(contents) + if length <= 127: + header += chr_cls(length) + else: + length_bytes = int_to_bytes(length) + header += chr_cls(0x80 | len(length_bytes)) + header += length_bytes + + return header -- cgit v1.2.3 From dd846997c28429adae0071e2c40240c4370473f2 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 30 Jan 2017 18:08:07 -0500 Subject: Added core.load() --- asn1crypto/core.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index a9ccd2e..b675a7c 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3,6 +3,7 @@ """ ASN.1 type classes for universal types. Exports the following items: + - load() - Any() - Asn1Value() - BitString() @@ -110,6 +111,60 @@ _OID_RE = re.compile('^\d+(\.\d+)*$') _SETUP_CLASSES = {} +def load(encoded_data, strict=False): + """ + Loads a BER/DER-encoded byte string and construct a universal object based + on the tag value: + + - 1: Boolean + - 2: Integer + - 3: BitString + - 4: OctetString + - 5: Null + - 6: ObjectIdentifier + - 7: ObjectDescriptor + - 8: InstanceOf + - 9: Real + - 10: Enumerated + - 11: EmbeddedPdv + - 12: UTF8String + - 13: RelativeOid + - 16: Sequence, + - 17: Set + - 18: NumericString + - 19: PrintableString + - 20: TeletexString + - 21: VideotexString + - 22: IA5String + - 23: UTCTime + - 24: GeneralizedTime + - 25: GraphicString + - 26: VisibleString + - 27: GeneralString + - 28: UniversalString + - 29: CharacterString + - 30: BMPString + + :param encoded_data: + A byte string of BER or DER-encoded data + + :param strict: + A boolean indicating if trailing data should be forbidden - if so, a + ValueError will be raised when trailing data exists + + :raises: + ValueError - when strict is True and trailing data is present + ValueError - when the encoded value tag a tag other than listed above + ValueError - when the ASN.1 header length is longer than the data + TypeError - when encoded_data is not a byte string + + :return: + An instance of the one of the universal classes + """ + + return Asn1Value.load(encoded_data, strict=strict) + + class Asn1Value(object): """ The basis of all ASN.1 values -- cgit v1.2.3 From 96144f4c8146061f1e6bf87aed1c266b7c96ab00 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 31 Jan 2017 05:58:44 -0500 Subject: Implementation of util.int_to_bytes() on Python 3 no longer requires expensive try/except --- asn1crypto/util.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/asn1crypto/util.py b/asn1crypto/util.py index 0d3bb94..2e55ef8 100644 --- a/asn1crypto/util.py +++ b/asn1crypto/util.py @@ -154,13 +154,17 @@ else: """ if width is None: - width_ = math.ceil(value.bit_length() / 8) or 1 - try: - return value.to_bytes(width_, byteorder='big', signed=signed) - except (OverflowError): - return value.to_bytes(width_ + 1, byteorder='big', signed=signed) - else: - return value.to_bytes(width, byteorder='big', signed=signed) + if signed: + if value < 0: + bits_required = abs(value + 1).bit_length() + else: + bits_required = value.bit_length() + if bits_required % 8 == 0: + bits_required += 1 + else: + bits_required = value.bit_length() + width = math.ceil(bits_required / 8) or 1 + return value.to_bytes(width, byteorder='big', signed=signed) def int_from_bytes(value, signed=False): """ -- cgit v1.2.3 From 9822531287beb1cfb8fecce58323aac42eae68d9 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 30 Jan 2017 18:14:51 -0500 Subject: Revamp CI integration - Add CircleCI for faster OS X builds - Use fewer workers on Travis for faster builds (3.4/3.5 dropped) - Move coverage uploads into custom Python for Python 2.6 support - Installtion of dependencies is now handled in new "deps" task - Improve AppVeyor and Travis CI configs for better granularity --- .gitignore | 10 +- .travis.yml | 131 +--------------- appveyor.yml | 131 ++++------------ circle.yml | 19 +++ codecov.json | 4 + dev-requirements.txt | 2 - dev/ci.py | 15 +- dev/coverage.py | 435 ++++++++++++++++++++++++++++++++++++++++++++++++++- dev/deps.py | 211 +++++++++++++++++++++++++ dev/tests.py | 3 +- readme.md | 53 ++++++- requires/ci | 2 + requires/coverage | 1 + requires/lint | 1 + requires/release | 1 + run.py | 15 +- tox.ini | 24 +-- 17 files changed, 778 insertions(+), 280 deletions(-) create mode 100644 circle.yml create mode 100644 codecov.json delete mode 100644 dev-requirements.txt create mode 100644 dev/deps.py create mode 100644 requires/ci create mode 100644 requires/coverage create mode 100644 requires/lint create mode 100644 requires/release diff --git a/.gitignore b/.gitignore index a5bf810..e7a7291 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ +.tox/ __pycache__/ -*.pyc -.python-version -*.egg-info/ build/ dist/ tests/output/ -.DS_Store tmp/ +*.egg-info/ +*.pyc +.python-version +.DS_Store .coverage -.tox coverage.xml diff --git a/.travis.yml b/.travis.yml index e20acee..ef69009 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,151 +5,24 @@ branches: - /^[0-9]+\.[0-9]+\.[0-9]$/ matrix: include: - - os: osx - python: "2.6" - env: - PYTHON_VERSION=2.6 - PLATFORM=osx - BACKEND=osx - - os: osx - python: "2.7" - env: - PYTHON_VERSION=2.7 - PLATFORM=osx-10.11 - BACKEND=osx - # This tests OpenSSL 0.9.8, which is what is bundled with OS X - - os: osx - python: "2.7" - env: - PYTHON_VERSION=2.7 - PLATFORM=osx-10.11 - BACKEND=openssl - OSCRYPTO_USE_OPENSSL=/usr/lib/libcrypto.dylib,/usr/lib/libssl.dylib - - os: osx - osx_image: xcode6.4 - python: "2.7" - env: - PYTHON_VERSION=2.7 - PLATFORM=osx-10.10 - BACKEND=osx - - os: osx - osx_image: xcode8.1 - python: "2.7" - env: - PYTHON_VERSION=2.7 - PLATFORM=osx-10.12 - BACKEND=osx - - os: osx - python: "3.6" - env: - PYTHON_VERSION=3.6 - PLATFORM=osx - BACKEND=osx - - os: osx - osx_image: xcode8.1 - python: "pypy" - env: - PYTHON_VERSION=pypy - PLATFORM=osx-10.12 - BACKEND=osx - os: linux language: python python: "2.6" - env: - PYTHON_VERSION=2.6 - PLATFORM=linux - BACKEND=openssl - os: linux language: python python: "2.7" - env: - PYTHON_VERSION=2.7 - PLATFORM=linux - BACKEND=openssl - os: linux language: python python: "3.2" - env: - PYTHON_VERSION=3.2 - PLATFORM=linux - BACKEND=openssl - os: linux language: python python: "3.3" - env: - PYTHON_VERSION=3.3 - PLATFORM=linux - BACKEND=openssl - - os: linux - language: python - python: "3.4" - env: - PYTHON_VERSION=3.4 - PLATFORM=linux - BACKEND=openssl - - os: linux - language: python - python: "3.5" - env: - PYTHON_VERSION=3.5 - PLATFORM=linux - BACKEND=openssl - os: linux language: python python: "3.6" - env: - PYTHON_VERSION=3.6 - PLATFORM=linux - BACKEND=openssl - os: linux language: python python: "pypy-5.3.1" - env: - PYTHON_VERSION=pypy - PLATFORM=linux - BACKEND=openssl -install: - - if [ "$TRAVIS_OS_NAME" == "osx" ]; then - if [ "$PYTHON_VERSION" == "pypy" ]; then - brew update; - brew install pypy; - /usr/local/bin/pip_pypy install flake8 coverage; - export PYTHON_BIN=/usr/local/bin/pypy; - else - if [ "$PYTHON_VERSION" == "3.6" ]; then - brew update; - brew install python3; - /usr/local/bin/pip3 install flake8 coverage; - export PYTHON_BIN=/usr/local/bin/python3; - else - if [ "$PYTHON_VERSION" == "2.7" ]; then - curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.7; - sudo /usr/bin/python2.7 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'flake8', 'coverage'])"; - export PYTHON_BIN=/usr/bin/python2.7; - else - curl --silent --show-error https://bootstrap.pypa.io/get-pip.py | sudo /usr/bin/python2.6 -W ignore; - sudo /usr/bin/python2.6 -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; - export PYTHON_BIN=/usr/bin/python2.6; - fi; - fi; - fi; - else - if [ "$PYTHON_VERSION" == "3.2" ]; then - pip install flake8; - else - pip install flake8 coverage; - fi; - export PYTHON_BIN=python; - fi; - - echo "Checking out modularcrypto packages for coverage"; - git clone https://github.com/wbond/oscrypto.git $TRAVIS_BUILD_DIR/../oscrypto; - git clone https://github.com/wbond/certbuilder.git $TRAVIS_BUILD_DIR/../certbuilder; - git clone https://github.com/wbond/certvalidator.git $TRAVIS_BUILD_DIR/../certvalidator; - git clone https://github.com/wbond/crlbuilder.git $TRAVIS_BUILD_DIR/../crlbuilder; - git clone https://github.com/wbond/csrbuilder.git $TRAVIS_BUILD_DIR/../csrbuilder; - git clone https://github.com/wbond/ocspbuilder.git $TRAVIS_BUILD_DIR/../ocspbuilder; script: - - $PYTHON_BIN run.py ci -after_success: - - bash <(curl -s https://codecov.io/bash) -X gcov -f coverage.xml -e PYTHON_VERSION,PLATFORM,BACKEND + - python run.py deps + - python run.py ci diff --git a/appveyor.yml b/appveyor.yml index e3c972a..7741087 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,76 +1,15 @@ version: "{build}" skip_tags: true -environment: - matrix: - - PYTHON: "C:\\Python26" - PYTHON_ID: "26" - PYTHON_EXE: python - PYTHON_VERSION: "2.6" - PLATFORM: win-x32 - BACKEND: win - - PYTHON: "C:\\Python26-x64" - PYTHON_ID: "26-x64" - PYTHON_EXE: python - PYTHON_VERSION: "2.6" - PLATFORM: win-x64 - BACKEND: win - - PYTHON: "C:\\Python27" - PYTHON_ID: "27" - PYTHON_EXE: python - PYTHON_VERSION: "2.7" - PLATFORM: win-x32 - BACKEND: win - - PYTHON: "C:\\Python27" - PYTHON_ID: "27" - PYTHON_EXE: python - PYTHON_VERSION: "2.7" - PLATFORM: win-x32 - BACKEND: winlegacy - OSCRYPTO_USE_WINLEGACY: 1 - - PYTHON: "C:\\Python27-x64" - PYTHON_ID: "27-x64" - PYTHON_EXE: python - PYTHON_VERSION: "2.7" - PLATFORM: win-x64 - BACKEND: win - - PYTHON: "C:\\Python33" - PYTHON_ID: "33" - PYTHON_EXE: python - PYTHON_VERSION: "3.3" - PLATFORM: win-x32 - BACKEND: win - - PYTHON: "C:\\Python33" - PYTHON_ID: "33" - PYTHON_EXE: python - PYTHON_VERSION: "3.3" - PLATFORM: win-x32 - BACKEND: winlegacy - OSCRYPTO_USE_WINLEGACY: 1 - - PYTHON: "C:\\Python33-x64" - PYTHON_ID: "33-x64" - PYTHON_EXE: python - PYTHON_VERSION: "3.3" - PLATFORM: win-x64 - BACKEND: win - - PYTHON: "C:\\pypy2-v5.3.1-win32" - PYTHON_ID: "pypy" - PYTHON_EXE: pypy - PYTHON_VERSION: "pypy" - PLATFORM: win-x32 - BACKEND: win - - PYTHON: "C:\\pypy2-v5.3.1-win32" - PYTHON_ID: "pypy" - PYTHON_EXE: pypy - PYTHON_VERSION: "pypy" - PLATFORM: win-x32 - BACKEND: winlegacy - OSCRYPTO_USE_WINLEGACY: 1 install: - ps: |- $env:PYTMP = "${env:TMP}\py"; if (!(Test-Path "$env:PYTMP")) { New-Item -ItemType directory -Path "$env:PYTMP" | Out-Null; } + if (!(Test-Path "${env:PYTMP}\pypy2-v5.3.1-win32.zip")) { + (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.3.1-win32.zip', "${env:PYTMP}\pypy2-v5.3.1-win32.zip"); + } + 7z x -y "${env:PYTMP}\pypy2-v5.3.1-win32.zip" -oC:\ | Out-Null; [Byte[]] $geotrustCaBytes = 0x30,0x82,0x03,0x7C,0x30,0x82,0x02,0x64,0xA0,0x03,0x02,0x01,0x02, 0x02,0x10,0x18,0xAC,0xB5,0x6A,0xFD,0x69,0xB6,0x15,0x3A,0x63,0x6C,0xAF,0xDA,0xFA,0xC4,0xA1,0x30, @@ -126,43 +65,33 @@ install: $rootStore.Open("ReadWrite"); $rootStore.Add($geotrustCa); $rootStore.Close(); - - if ("${env:PYTHON_ID}" -eq "pypy") { - if (!(Test-Path "${env:PYTMP}\pypy2-v5.3.1-win32.zip")) { - (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.3.1-win32.zip', "${env:PYTMP}\pypy2-v5.3.1-win32.zip"); - } - 7z x -y "${env:PYTMP}\pypy2-v5.3.1-win32.zip" -oC:\ | Out-Null; - if (!(Test-Path "${env:PYTMP}\get-pip.py")) { - (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); - } - & "${env:PYTHON}\pypy.exe" "${env:PYTMP}\get-pip.py"; - & "${env:PYTHON}\bin\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; - - } elseif ("${env:PYTHON_ID}" -eq "26" -or "${env:PYTHON_ID}" -eq "26-x64") { - if (!(Test-Path "${env:PYTMP}\get-pip.py")) { - (New-Object Net.WebClient).DownloadFile('https://bootstrap.pypa.io/get-pip.py', "${env:PYTMP}\get-pip.py"); - } - & "${env:PYTHON}\python.exe" -W ignore "${env:PYTMP}\get-pip.py"; - # Skip flake8 for 2.6 since flake8 and pycodestyle have deprecated support for it - & "${env:PYTHON}\python.exe" -W ignore -c "import pip; pip.main(['--disable-pip-version-check', '--quiet', 'install', 'coverage'])"; - - } else { - & "${env:PYTHON}\Scripts\pip.exe" --disable-pip-version-check --quiet install flake8 coverage; - } - - & git clone https://github.com/wbond/oscrypto.git c:\projects\oscrypto 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/certbuilder.git c:\projects\certbuilder 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/certvalidator.git c:\projects\certvalidator 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/crlbuilder.git c:\projects\crlbuilder 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/csrbuilder.git c:\projects\csrbuilder 2>&1 | % { $_.ToString() }; - & git clone https://github.com/wbond/ocspbuilder.git c:\projects\ocspbuilder 2>&1 | % { $_.ToString() }; - - & "C:\Python27-x64\Scripts\pip.exe" --disable-pip-version-check --quiet install https://github.com/codecov/codecov-python/archive/v2.0.6.zip; - - "SET PATH=%PYTHON%;%PATH%" cache: - '%TMP%\py\' build: off test_script: - - cmd: "%PYTHON_EXE% run.py ci" -after_test: - - C:\Python27-x64\Scripts\codecov -X gcov -f coverage.xml -e PYTHON_VERSION PLATFORM BACKEND + - ps: '& C:\Python26\python run.py deps' + - ps: '& C:\Python26\python run.py ci' + - ps: '& C:\Python26-x64\python run.py deps' + - ps: '& C:\Python26-x64\python run.py ci' + - ps: '& C:\Python27\python run.py deps' + - ps: '& C:\Python27\python run.py ci' + - ps: > + $env:OSCRYPTO_USE_WINLEGACY = "true"; + & C:\Python27\python run.py ci; + remove-item env:\OSCRYPTO_USE_WINLEGACY; + - ps: '& C:\Python27-x64\python run.py deps' + - ps: '& C:\Python27-x64\python run.py ci' + - ps: '& C:\Python33\python run.py deps' + - ps: '& C:\Python33\python run.py ci' + - ps: > + $env:OSCRYPTO_USE_WINLEGACY = "true"; + & C:\Python33\python run.py ci; + remove-item env:\OSCRYPTO_USE_WINLEGACY; + - ps: '& C:\Python33-x64\python run.py deps' + - ps: '& C:\Python33-x64\python run.py ci' + - ps: '& C:\pypy2-v5.3.1-win32\pypy run.py deps' + - ps: '& C:\pypy2-v5.3.1-win32\pypy run.py ci' + - ps: > + $env:OSCRYPTO_USE_WINLEGACY = "true"; + & C:\pypy2-v5.3.1-win32\pypy run.py ci; + remove-item env:\OSCRYPTO_USE_WINLEGACY; diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..7f1b331 --- /dev/null +++ b/circle.yml @@ -0,0 +1,19 @@ +machine: + pre: + - pip install --user --ignore-installed --upgrade virtualenv pip + - ln -s ~/Library/Python/2.7/bin/virtualenv /usr/local/bin/virtualenv +dependencies: + override: + - brew install python3 pypy +test: + override: + - /usr/bin/python2.6 run.py deps + - /usr/bin/python2.6 run.py ci + - /usr/bin/python2.7 run.py deps + - /usr/bin/python2.7 run.py ci + - OSCRYPTO_USE_OPENSSL=/usr/lib/libcrypto.dylib,/usr/lib/libssl.dylib /usr/bin/python2.7 run.py ci + - /usr/local/bin/python3 run.py deps + - /usr/local/bin/python3 run.py ci + - OSCRYPTO_USE_OPENSSL=/usr/lib/libcrypto.dylib,/usr/lib/libssl.dylib /usr/local/bin/python3 run.py ci + - /usr/local/bin/pypy run.py deps + - /usr/local/bin/pypy run.py ci diff --git a/codecov.json b/codecov.json new file mode 100644 index 0000000..2bec947 --- /dev/null +++ b/codecov.json @@ -0,0 +1,4 @@ +{ + "slug": "wbond/asn1crypto", + "token": "98876f5e-6517-4def-85ce-c6e508eee35a" +} diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index a178187..0000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -coverage>=4.0b1 -flake8 \ No newline at end of file diff --git a/dev/ci.py b/dev/ci.py index 5936023..5ebdb88 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -3,12 +3,18 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys -if sys.version_info >= (2, 7): +if sys.version_info[0:2] not in [(2, 6), (3, 2)]: from .lint import run as run_lint -if sys.version_info < (3, 0) or sys.version_info >= (3, 3): +else: + run_lint = None + +if sys.version_info[0:2] != (3, 2): from .coverage import run as run_coverage + run_tests = None + else: from .tests import run as run_tests + run_coverage = None def run(): @@ -20,13 +26,13 @@ def run(): """ print('Python ' + sys.version.replace('\n', '')) - if sys.version_info >= (2, 7): + if run_lint: print('') lint_result = run_lint() else: lint_result = True - if sys.version_info < (3, 0) or sys.version_info >= (3, 3): + if run_coverage: print('\nRunning tests (via coverage.py)') sys.stdout.flush() tests_result = run_coverage(write_xml=True) @@ -34,5 +40,6 @@ def run(): print('\nRunning tests') sys.stdout.flush() tests_result = run_tests() + sys.stdout.flush() return lint_result and tests_result diff --git a/dev/coverage.py b/dev/coverage.py index c8c0e00..7f7853f 100644 --- a/dev/coverage.py +++ b/dev/coverage.py @@ -3,9 +3,25 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import coverage import imp +import json import os import unittest import sys +import platform as _plat +import subprocess +from fnmatch import fnmatch + +if sys.version_info < (3,): + str_cls = unicode + from urllib2 import Request, urlopen, URLError, HTTPError + from urllib import urlencode + import cgi + from io import open +else: + str_cls = str + from urllib.request import Request, urlopen + from urllib.error import URLError, HTTPError + from urllib.parse import urlencode def run(write_xml=False): @@ -16,6 +32,10 @@ def run(write_xml=False): A bool - if the tests ran successfully """ + xml_report_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'coverage.xml')) + if os.path.exists(xml_report_path): + os.unlink(xml_report_path) + cov = coverage.Coverage(include='asn1crypto/*.py') cov.start() @@ -32,7 +52,7 @@ def run(write_xml=False): if suite.countTestCases() > 0: print('Running tests from other modularcrypto packages') sys.stdout.flush() - other_result = unittest.TextTestRunner(verbosity=1).run(suite).wasSuccessful() + other_result = unittest.TextTestRunner(stream=sys.stdout, verbosity=1).run(suite).wasSuccessful() print() sys.stdout.flush() else: @@ -42,9 +62,15 @@ def run(write_xml=False): cov.save() cov.report(show_missing=False) + print() + sys.stdout.flush() if write_xml: cov.xml_report() + if write_xml and result and other_result and os.path.exists(xml_report_path): + _codecov_submit() + print() + return result and other_result @@ -66,3 +92,410 @@ def _load_package_tests(name): tests_module_info = imp.find_module('tests', [package_dir]) tests_module = imp.load_module('%s.tests' % name, *tests_module_info) return tests_module.test_classes() + + +def _codecov_submit(): + if os.getenv('CI') == 'true' and os.getenv('TRAVIS') == 'true': + # http://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables + build_url = 'https://travis-ci.org/%s/jobs/%s' % (os.getenv('TRAVIS_REPO_SLUG'), os.getenv('TRAVIS_JOB_ID')) + query = { + 'service': 'travis', + 'branch': os.getenv('TRAVIS_BRANCH'), + 'build': os.getenv('TRAVIS_JOB_NUMBER'), + 'pr': os.getenv('TRAVIS_PULL_REQUEST'), + 'job': os.getenv('TRAVIS_JOB_ID'), + 'tag': os.getenv('TRAVIS_TAG'), + 'slug': os.getenv('TRAVIS_REPO_SLUG'), + 'commit': os.getenv('TRAVIS_COMMIT'), + 'build_url': build_url, + } + root = os.getenv('TRAVIS_BUILD_DIR') + + elif os.getenv('CI') == 'True' and os.getenv('APPVEYOR') == 'True': + # http://www.appveyor.com/docs/environment-variables + build_url = 'https://ci.appveyor.com/project/%s/build/%s' % (os.getenv('APPVEYOR_REPO_NAME'), os.getenv('APPVEYOR_BUILD_VERSION')) + query = { + 'service': "appveyor", + 'branch': os.getenv('APPVEYOR_REPO_BRANCH'), + 'build': os.getenv('APPVEYOR_JOB_ID'), + 'pr': os.getenv('APPVEYOR_PULL_REQUEST_NUMBER'), + 'job': '/'.join((os.getenv('APPVEYOR_ACCOUNT_NAME'), os.getenv('APPVEYOR_PROJECT_SLUG'), os.getenv('APPVEYOR_BUILD_VERSION'))), + 'tag': os.getenv('APPVEYOR_REPO_TAG_NAME'), + 'slug': os.getenv('APPVEYOR_REPO_NAME'), + 'commit': os.getenv('APPVEYOR_REPO_COMMIT'), + 'build_url': build_url, + } + root = os.getenv('APPVEYOR_BUILD_FOLDER') + + elif os.getenv('CI') == 'true' and os.getenv('CIRCLECI') == 'true': + # https://circleci.com/docs/environment-variables + query = { + 'service': 'circleci', + 'branch': os.getenv('CIRCLE_BRANCH'), + 'build': os.getenv('CIRCLE_BUILD_NUM'), + 'pr': os.getenv('CIRCLE_PR_NUMBER'), + 'job': os.getenv('CIRCLE_BUILD_NUM') + "." + os.getenv('CIRCLE_NODE_INDEX'), + 'tag': os.getenv('CIRCLE_TAG'), + 'slug': os.getenv('CIRCLE_PROJECT_USERNAME') + "/" + os.getenv('CIRCLE_PROJECT_REPONAME'), + 'commit': os.getenv('CIRCLE_SHA1'), + 'build_url': os.getenv('CIRCLE_BUILD_URL'), + } + if sys.version_info < (3,): + root = os.getcwdu() + else: + root = os.getcwd() + else: + root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + if not os.path.exists(os.path.join(root, '.git')): + print('git repository not found, not submitting coverage data') + return + git_status = _git_command(['status', '--porcelain'], root) + if git_status != '': + print('git repository has uncommitted changes, not submitting coverage data') + return + + slug = None + token = None + try: + with open(os.path.join(root, 'codecov.json'), 'rb') as f: + json_data = json.loads(f.read().decode('utf-8')) + slug = json_data['slug'] + token = json_data['token'] + except (OSError, ValueError, UnicodeDecodeError, KeyError): + print('error reading codecov.json') + return + + branch = _git_command(['rev-parse', '--abbrev-ref', 'HEAD'], root) + commit = _git_command(['rev-parse', '--verify', 'HEAD'], root) + tag = _git_command(['name-rev', '--tags', '--name-only', commit], root) + impl = _plat.python_implementation() + major, minor = _plat.python_version_tuple()[0:2] + build_name = '%s %s %s.%s' % (_platform_name(), impl, major, minor) + query = { + 'branch': branch, + 'commit': commit, + 'slug': slug, + 'token': token, + 'build': build_name, + } + if tag != 'undefined': + query['tag'] = tag + + payload = 'PLATFORM=%s\n' % _platform_name() + payload += 'PYTHON_VERSION=%s %s\n' % (_plat.python_version(), _plat.python_implementation()) + if 'oscrypto' in sys.modules: + payload += 'OSCRYPTO_BACKEND=%s\n' % sys.modules['oscrypto'].backend() + payload += '<<<<<< ENV\n' + + for path in _list_files(root): + payload += path + '\n' + payload += '<<<<<< network\n' + + payload += '# path=coverage.xml\n' + with open(os.path.join(root, 'coverage.xml'), 'r', encoding='utf-8') as f: + payload += f.read() + '\n' + payload +='<<<<<< EOF\n' + + url = 'https://codecov.io/upload/v4' + headers = { + 'Accept': 'text/plain' + } + filtered_query = {} + for key in query: + value = query[key] + if value == '' or value is None: + continue + filtered_query[key] = value + + print('Submitting coverage info to codecov.io') + info = _do_request( + 'POST', + url, + headers, + query_params=filtered_query + ) + + encoding = info[1] or 'utf-8' + text = info[2].decode(encoding).strip() + parts = text.split() + result, upload_url = parts[0], parts[1] + + headers = { + 'Content-Type': 'text/plain', + 'x-amz-acl': 'public-read', + 'x-amz-storage-class': 'REDUCED_REDUNDANCY' + } + + print('Uploading coverage data to codecov.io S3 bucket') + put_info = _do_request( + 'PUT', + upload_url, + headers, + data=payload.encode('utf-8') + ) + + +def _git_command(params, cwd): + """ + Executes a git command, returning the output + + :param params: + A list of the parameters to pass to git + + :param cwd: + The working directory to execute git in + + :return: + A 2-element tuple of (stdout, stderr) + """ + + proc = subprocess.Popen( + ['git'] + params, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=cwd + ) + stdout, stderr = proc.communicate() + code = proc.wait() + if code != 0: + e = OSError('git exit code was non-zero') + e.stdout = stdout + raise e + return stdout.decode('utf-8').strip() + + +def _parse_env_var_file(data): + """ + Parses a basic VAR="value data" file contents into a dict + + :param data: + A unicode string of the file data + + :return: + A dict of parsed name/value data + """ + + output = {} + for line in data.splitlines(): + line = line.strip() + if not line or '=' not in line: + continue + parts = line.split('=') + if len(parts) != 2: + continue + name = parts[0] + value = parts[1] + if len(value) > 1: + if value[0] == '"' and value[-1] == '"': + value = value[1:-1] + output[name] = value + return output + + +def _platform_name(): + """ + Returns information about the current operating system and version + + :return: + A unicode string containing the OS name and version + """ + + if sys.platform == 'darwin': + version = _plat.mac_ver()[0] + _plat_ver_info = tuple(map(int, version.split('.'))) + if _plat_ver_info < (10, 12): + name = 'OS X' + else: + name = 'macOS' + return '%s %s' % (name, version) + + elif sys.platform == 'win32': + _win_ver = sys.getwindowsversion() + _plat_ver_info = (_win_ver[0], _win_ver[1]) + return 'Windows %s' % _plat.win32_ver()[0] + + elif sys.platform in ['linux', 'linux2']: + if os.path.exists('/etc/os-release'): + with open('/etc/os-release', 'r', encoding='utf-8') as f: + pairs = _parse_env_var_file(f.read()) + if 'NAME' in pairs and 'VERSION_ID' in pairs: + return '%s %s' % (pairs['NAME'], pairs['VERSION_ID']) + version = pairs['VERSION_ID'] + elif 'PRETTY_NAME' in pairs: + return pairs['PRETTY_NAME'] + elif 'NAME' in pairs: + return pairs['NAME'] + else: + raise ValueError('No suitable version info found in /etc/os-release') + elif os.path.exists('/etc/lsb-release'): + with open('/etc/lsb-release', 'r', encoding='utf-8') as f: + pairs = _parse_env_var_file(f.read()) + if 'DISTRIB_DESCRIPTION' in pairs: + return pairs['DISTRIB_DESCRIPTION'] + else: + raise ValueError('No suitable version info found in /etc/lsb-release') + else: + return 'Linux' + + else: + return '%s %s' % (_plat.system(), _plat.release()) + + +def _list_files(root): + """ + Lists all of the files in a directory, taking into account any .gitignore + file that is present + + :param root: + A unicode filesystem path + + :return: + A list of unicode strings, containing paths of all files not ignored + by .gitignore with root, using relative paths + """ + + dir_patterns, file_patterns = _gitignore(root) + paths = [] + prefix = os.path.abspath(root) + os.sep + for base, dirs, files in os.walk(root): + for d in dirs: + for dir_pattern in dir_patterns: + if fnmatch(d, dir_pattern): + dirs.remove(d) + break + for f in files: + skip = False + for file_pattern in file_patterns: + if fnmatch(f, file_pattern): + skip = True + break + if skip: + continue + full_path = os.path.join(base, f) + if full_path[:len(prefix)] == prefix: + full_path = full_path[len(prefix):] + paths.append(full_path) + return sorted(paths) + + +def _gitignore(root): + """ + Parses a .gitignore file and returns patterns to match dirs and files. + Only basic gitignore patterns are supported. Pattern negation, ** wildcards + and anchored patterns are not currently implemented. + + :param root: + A unicode string of the path to the git repository + + :return: + A 2-element tuple: + - 0: a list of unicode strings to match against dirs + - 1: a list of unicode strings to match against dirs and files + """ + + gitignore_path = os.path.join(root, '.gitignore') + + dir_patterns = ['.git'] + file_patterns = [] + + if not os.path.exists(gitignore_path): + return (dir_patterns, file_patterns) + + with open(gitignore_path, 'r', encoding='utf-8') as f: + for line in f.readlines(): + line = line.strip() + if not line: + continue + if line.startswith('#'): + continue + if '**' in line: + raise NotImplementedError('gitignore ** wildcards are not implemented') + if line.startswith('!'): + raise NotImplementedError('gitignore pattern negation is not implemented') + if line.startswith('/'): + raise NotImplementedError('gitignore anchored patterns are not implemented') + if line.startswith('\\#'): + line = '#' + line[2:] + if line.startswith('\\!'): + line = '!' + line[2:] + if line.endswith('/'): + dir_patterns.append(line[:-1]) + else: + file_patterns.append(line) + + return (dir_patterns, file_patterns) + + +def _do_request(method, url, headers, data=None, query_params=None, timeout=20): + """ + Performs an HTTP request + + :param method: + A unicode string of 'GET', 'POST', 'PUT', or 'DELETE' + + :param url; + A unicode string of the URL to request + + :param headers: + A dict of unicode strings, where keys are header names and values are + the header values. + + :param data: + A dict of unicode strings (to be encoded as + application/x-www-form-urlencoded), or a byte string of data. + + :param query_params: + A dict of unicode keys and values to pass as query params + + :param timeout: + An integer number of seconds to use as the timeout + + :return: + A 3-element tuple: + - 0: A unicode string of the response content-type + - 1: A unicode string of the response encoding, or None + - 2: A byte string of the response body + """ + + if query_params: + url += '?' + urlencode(query_params).replace('+', '%20') + + request = Request(url) + request.get_method = lambda: method + + if isinstance(data, dict): + data_bytes = {} + for key in data: + data_bytes[key.encode('utf-8')] = data[key].encode('utf-8') + data = urlencode(data_bytes) + headers['Content-Type'] = 'application/x-www-form-urlencoded' + if isinstance(data, str_cls): + raise TypeError('data must be a byte string') + + for key in headers: + value = headers[key] + if sys.version_info < (3,): + key = key.encode('iso-8859-1') + value = value.encode('iso-8859-1') + request.add_header(key, value) + + response = urlopen(request, data, timeout) + if sys.version_info < (3,): + status = response.getcode() + try: + content_type, params = cgi.parse_header(response.headers['Content-Type'].strip()) + encoding = params.get('charset') + except (KeyError): + content_type = None + encoding = None + else: + status = response.status + content_type = response.info().get_content_type() + encoding = response.headers.get_content_charset() + if status != 200: + raise HTTPError('Unexpected HTTP %d response' % status) + return (content_type, encoding, response.read()) + + +if __name__ == '__main__': + _codecov_submit() diff --git a/dev/deps.py b/dev/deps.py new file mode 100644 index 0000000..72c6149 --- /dev/null +++ b/dev/deps.py @@ -0,0 +1,211 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import imp +import os +import subprocess +import sys +import warnings +import shutil +import tempfile +import platform +import site + + +OTHER_PACKAGES = [ + 'https://github.com/wbond/oscrypto.git', + 'https://github.com/wbond/certbuilder.git', + 'https://github.com/wbond/certvalidator.git', + 'https://github.com/wbond/crlbuilder.git', + 'https://github.com/wbond/csrbuilder.git', + 'https://github.com/wbond/ocspbuilder.git', +] + + +def run(): + """ + Ensures a recent version of pip is installed, then uses that to install + required development dependencies. Uses git to checkout other modularcrypto + repos for more accurate coverage data. + """ + + package_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + build_root = os.path.abspath(os.path.join(package_root, '..')) + try: + tmpdir = None + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + major_minor = '%s.%s' % sys.version_info[0:2] + tmpdir = tempfile.mkdtemp() + _pip = _bootstrap_pip(tmpdir) + + print("Using pip to install dependencies") + _pip(['install', '-q', '--upgrade', '-r', os.path.join(package_root, 'requires', 'ci')]) + + if OTHER_PACKAGES: + print("Checking out modularcrypto packages for coverage") + for pkg_url in OTHER_PACKAGES: + pkg_name = os.path.basename(pkg_url).replace('.git', '') + pkg_dir = os.path.join(build_root, pkg_name) + if os.path.exists(pkg_dir): + print("%s is already present" % pkg_name) + continue + print("Cloning %s" % pkg_url) + _execute(['git', 'clone', pkg_url], build_root) + print() + + finally: + if tmpdir: + shutil.rmtree(tmpdir, ignore_errors=True) + + return True + +def _download(url, dest): + """ + Downloads a URL to a directory + + :param url: + The URL to download + + :param dest: + The path to the directory to save the file in + + :return: + The filesystem path to the saved file + """ + + filename = os.path.basename(url) + dest_path = os.path.join(dest, filename) + + if sys.platform == 'win32': + system_root = os.environ.get('SystemRoot') + powershell_exe = os.path.join('system32\\WindowsPowerShell\\v1.0\\powershell.exe') + code = "(New-Object Net.WebClient).DownloadFile('%s', '%s');" % (url, dest_path) + _execute([powershell_exe, '-Command', code], dest) + + else: + _execute(['curl', '--silent', '--show-error', '-O', url], dest) + + return dest_path + + +def _execute(params, cwd): + """ + Executes a subprocess + + :param params: + A list of the executable and arguments to pass to it + + :param cwd: + The working directory to execute the command in + + :return: + A 2-element tuple of (stdout, stderr) + """ + + proc = subprocess.Popen( + params, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=cwd + ) + stdout, stderr = proc.communicate() + code = proc.wait() + if code != 0: + e = OSError('subprocess exit code was non-zero') + e.stdout = stdout + e.stderr = stderr + raise e + return (stdout, stderr) + + +def _get_pip_main(download_dir): + """ + Executes get-pip.py in the current Python interpreter + + :param download_dir: + The directory that contains get-pip.py + """ + + module_info = imp.find_module('get-pip', [download_dir]) + get_pip_module = imp.load_module('_cideps.get-pip', *module_info) + + orig_sys_exit = sys.exit + orig_sys_argv = sys.argv + sys.exit = lambda c: None + sys.argv = ['get-pip.py', '--user', '-q'] + + get_pip_module.main() + + sys.exit = orig_sys_exit + sys.argv = orig_sys_argv + + # Unload pip modules that came from the zip file + module_names = sorted(sys.modules.keys()) + end_token = os.sep + 'pip.zip' + mid_token = end_token + os.sep + 'pip' + for module_name in module_names: + try: + module_path = sys.modules[module_name].__file__ + if mid_token in module_path or module_path.endswith(end_token): + del sys.modules[module_name] + except AttributeError: + pass + + if sys.path[0].endswith('pip.zip'): + sys.path = sys.path[1:] + + if site.USER_SITE not in sys.path: + sys.path.append(site.USER_SITE) + + +def _bootstrap_pip(tmpdir): + """ + Bootstraps the current version of pip for use in the current Python + interpreter + + :param tmpdir: + A temporary directory to download get-pip.py and cacert.pem + + :return: + A function that invokes pip. Accepts one arguments, a list of parameters + to pass to pip. + """ + + try: + import pip + + print('Upgrading pip') + pip.main(['install', '-q', '--upgrade', 'pip']) + certs_path = None + + except ImportError: + print("Downloading cacert.pem from curl") + certs_path = _download('https://curl.haxx.se/ca/cacert.pem', tmpdir) + + print("Downloading get-pip.py") + if sys.version_info[0:2] == (3, 2): + path = _download('https://bootstrap.pypa.io/3.2/get-pip.py', tmpdir) + else: + path = _download('https://bootstrap.pypa.io/get-pip.py', tmpdir) + + print("Running get-pip.py") + _get_pip_main(tmpdir) + + import pip + + def _pip(args): + base_args = ['--disable-pip-version-check'] + if certs_path: + base_args += ['--cert', certs_path] + if sys.platform == 'darwin' and sys.version_info[0:2] in [(2, 6), (2, 7)]: + new_args = [] + for arg in args: + new_args.append(arg) + if arg == 'install': + new_args.append('--user') + args = new_args + pip.main(base_args + args) + + return _pip diff --git a/dev/tests.py b/dev/tests.py index 0fe0a1b..071ee23 100644 --- a/dev/tests.py +++ b/dev/tests.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import unittest import re +import sys from tests import test_classes @@ -30,5 +31,5 @@ def run(matcher=None): else: suite.addTest(loader.loadTestsFromTestCase(test_class)) verbosity = 2 if matcher else 1 - result = unittest.TextTestRunner(verbosity=verbosity).run(suite) + result = unittest.TextTestRunner(stream=sys.stdout, verbosity=verbosity).run(suite) return result.wasSuccessful() diff --git a/readme.md b/readme.md index 1d89c0f..c9ce69b 100644 --- a/readme.md +++ b/readme.md @@ -16,6 +16,7 @@ A fast, pure Python library for parsing and serializing ASN.1 structures. [![Travis CI](https://api.travis-ci.org/wbond/asn1crypto.svg?branch=master)](https://travis-ci.org/wbond/asn1crypto) [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/wbond/asn1crypto?branch=master&svg=true)](https://ci.appveyor.com/project/wbond/asn1crypto) +[![CircleCI](https://circleci.com/gh/wbond/asn1crypto.svg?style=shield)](https://circleci.com/gh/wbond/asn1crypto) [![Codecov](https://codecov.io/gh/wbond/asn1crypto/branch/master/graph/badge.svg)](https://codecov.io/gh/wbond/asn1crypto) [![PyPI](https://img.shields.io/pypi/v/asn1crypto.svg)](https://pypi.python.org/pypi/asn1crypto) @@ -154,7 +155,8 @@ links to the source for the various pre-defined type classes. ## Continuous Integration - [Windows](https://ci.appveyor.com/project/wbond/asn1crypto/history) via AppVeyor - - [OS X & Linux](https://travis-ci.org/wbond/asn1crypto/builds) via Travis CI + - [OS X](https://circleci.com/gh/wbond/asn1crypto) via CircleCI + - [Linux](https://travis-ci.org/wbond/asn1crypto/builds) via Travis CI - [Test Coverage](https://codecov.io/gh/wbond/asn1crypto/commits) via Codecov ## Testing @@ -173,23 +175,58 @@ python run.py tests ocsp ## Development -To install required development dependencies, execute: +To install the package used for linting, execute: ```bash -pip install -r dev-requirements.txt +pip install --user -r requires/lint ``` -The following commands will run the linter and test coverage: +The following command will run the linter: ```bash python run.py lint +``` + +Support for code coverage can be installed via: + +```bash +pip install --user -r requires/coverage +``` + +Coverage is measured by running: + +```bash python run.py coverage ``` -After creating a [semver](http://semver.org/) git tag, a `.tar.gz` and `.whl` -of the package can be created and uploaded to -[PyPi](https://pypi.python.org/pypi/asn1crypto) by executing: +To install the necessary packages for releasing a new version on PyPI, run: + +```bash +pip install --user -r requires/release +``` + +Releases are created by: + + - Making a git tag in [semver](http://semver.org/) format + - Running the command: + + ```bash + python run.py release + ``` + +Existing releases can be found at https://pypi.python.org/pypi/asn1crypto. + +## CI Tasks + +A task named `deps` exists to ensure a modern version of `pip` is installed, +along with all necessary testing dependencies. + +The `ci` task runs `lint` (if flake8 is avaiable for the version of Python) and +`coverage` (or `tests` if coverage is not available for the version of Python). +If the current directory is a clean git working copy, the coverage data is +submitted to codecov.io. ```bash -python run.py release +python run.py deps +python run.py ci ``` diff --git a/requires/ci b/requires/ci new file mode 100644 index 0000000..e7503fb --- /dev/null +++ b/requires/ci @@ -0,0 +1,2 @@ +-r ./coverage +-r ./lint diff --git a/requires/coverage b/requires/coverage new file mode 100644 index 0000000..8a80dcc --- /dev/null +++ b/requires/coverage @@ -0,0 +1 @@ +coverage >= 4.3.4 ; python_version != '3.2' \ No newline at end of file diff --git a/requires/lint b/requires/lint new file mode 100644 index 0000000..9c49d4e --- /dev/null +++ b/requires/lint @@ -0,0 +1 @@ +flake8 ; python_version == '2.7' or python_version >= '3.3' diff --git a/requires/release b/requires/release new file mode 100644 index 0000000..af996cf --- /dev/null +++ b/requires/release @@ -0,0 +1 @@ +twine diff --git a/run.py b/run.py index 1c35d09..aa86fe5 100644 --- a/run.py +++ b/run.py @@ -11,25 +11,25 @@ else: def show_usage(): - print('Usage: run.py (lint | tests [regex] | coverage | ci | release)', file=sys.stderr) + print('Usage: run.py (lint | tests [regex] | coverage | deps | ci | release)', file=sys.stderr) sys.exit(1) def get_arg(num): if len(sys.argv) < num + 1: - return None + return None, num arg = sys.argv[num] if isinstance(arg, byte_cls): arg = arg.decode('utf-8') - return arg + return arg, num + 1 if len(sys.argv) < 2 or len(sys.argv) > 3: show_usage() -task = get_arg(1) +task, next_arg = get_arg(1) -if task not in set(['lint', 'tests', 'coverage', 'ci', 'release']): +if task not in set(['lint', 'tests', 'coverage', 'deps', 'ci', 'release']): show_usage() if task != 'tests' and len(sys.argv) == 3: @@ -41,13 +41,16 @@ if task == 'lint': elif task == 'tests': from dev.tests import run - matcher = get_arg(2) + matcher, next_arg = get_arg(next_arg) if matcher: params.append(matcher) elif task == 'coverage': from dev.coverage import run +elif task == 'deps': + from dev.deps import run + elif task == 'ci': from dev.ci import run diff --git a/tox.ini b/tox.ini index cb5f680..5ef20e9 100644 --- a/tox.ini +++ b/tox.ini @@ -2,29 +2,7 @@ envlist = py26,py27,py32,py33,py34,py35,py36,pypy [testenv] -deps = - py26: - coverage - py27: - flake8 - coverage - py32: - flake8 - py33: - flake8 - coverage - py34: - flake8 - coverage - py35: - flake8 - coverage - py36: - flake8 - coverage - pypy: - flake8 - coverage +deps = -rrequires/ci commands = {envpython} run.py ci [pep8] -- cgit v1.2.3 From 043f1228ed90fad9898007cee15656dca071e108 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Feb 2017 09:27:26 -0500 Subject: Add tests for strict arg and core.load() --- tests/test_core.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index a91d45d..e9833d8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -93,6 +93,10 @@ class ConcatTest(core.Concat): _child_specs = [Seq, core.Integer] +class IntegerConcats(core.Concat): + _child_specs = [core.Integer, core.Integer] + + class MyOids(core.ObjectIdentifier): _map = { '1.2.3': 'abc', @@ -221,6 +225,27 @@ class CoreTests(unittest.TestCase): self.assertIsInstance(c, core.IntegerBitString) self.assertEqual(66051, c.native) + def test_load(self): + i = core.load(b'\x02\x01\x00') + self.assertIsInstance(i, core.Integer) + self.assertEqual(0, i.native) + + def test_strict(self): + with self.assertRaises(ValueError): + core.load(b'\x02\x01\x00\x00', strict=True) + + def test_strict_on_class(self): + with self.assertRaises(ValueError): + core.Integer.load(b'\x02\x01\x00\x00', strict=True) + + def test_strict_concat(self): + with self.assertRaises(ValueError): + IntegerConcats.load(b'\x02\x01\x00\x02\x01\x00\x00', strict=True) + + def test_strict_choice(self): + with self.assertRaises(ValueError): + NumChoice.load(b'\xA0\x03\x02\x01\x00\x00', strict=True) + def test_bit_string_item_access(self): named = core.BitString() named[0] = True -- cgit v1.2.3 From 916defc8514b71e8ac677ce5879745b36387cf09 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Feb 2017 09:27:55 -0500 Subject: Ensure explicitly tagged values have proper _header attribute --- asn1crypto/core.py | 9 +++++++-- tests/test_core.py | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index b675a7c..82d2832 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -4669,6 +4669,8 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param if header is None: return VOID + header_set = False + # If an explicit specification was passed in, make sure it matches if spec is not None: if spec_params: @@ -4718,6 +4720,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class value.explicit_tag = original_value.explicit_tag + header_set = True elif isinstance(value, Choice): value.validate(class_, tag, contents) @@ -4777,6 +4780,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class value.explicit_tag = original_value.explicit_tag + header_set = True # If no spec was specified, allow anything and just process what # is in the input data @@ -4795,8 +4799,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param value = spec(contents=contents, class_=class_) - value._header = header - value._trailer = trailer or b'' + if not header_set: + value._header = header + value._trailer = trailer or b'' # Destroy any default value that our contents have overwritten value._native = None diff --git a/tests/test_core.py b/tests/test_core.py index e9833d8..f1a83e4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -365,6 +365,11 @@ class CoreTests(unittest.TestCase): self.assertNotEqual(a.contents, b.contents) self.assertNotEqual(a.dump(), b.dump()) + def test_explicit_tag_header(self): + val = NumChoice.load(b'\xa0\x03\x02\x01\x00') + self.assertEqual(b'\xa0\x03\x02\x01', val.chosen._header) + self.assertEqual(b'\x00', val.chosen.contents) + def test_retag(self): a = core.Integer(200) b = a.retag('explicit', 0) -- cgit v1.2.3 From 9ec52de5d9e65ffd6f4dee3a8fca90145215c2d7 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Feb 2017 09:28:16 -0500 Subject: Add test for trying to load an invalid value into a core.Choice --- tests/test_core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index f1a83e4..14ba55c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -406,6 +406,10 @@ class CoreTests(unittest.TestCase): with self.assertRaises(ValueError): SeqChoice(('one', a, None)) + def test_load_invalid_choice(self): + with self.assertRaises(ValueError): + NumChoice.load(b'\x02\x01\x00') + def test_fix_tagging_choice(self): correct = core.Integer(200, tag_type='explicit', tag=2) choice = NumChoice( -- cgit v1.2.3 From 0c8a7e48c1c388b11d723f6762bbbaea59806de3 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Feb 2017 09:40:13 -0500 Subject: Test core.load() on non-bytes --- tests/test_core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 14ba55c..c85433e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -230,6 +230,10 @@ class CoreTests(unittest.TestCase): self.assertIsInstance(i, core.Integer) self.assertEqual(0, i.native) + def test_load_wrong_type(self): + with self.assertRaises(TypeError): + core.load('\x02\x01\x00') + def test_strict(self): with self.assertRaises(ValueError): core.load(b'\x02\x01\x00\x00', strict=True) -- cgit v1.2.3 From e8545da0c1ca0550535d3e305bac32e8a9681974 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Feb 2017 09:40:54 -0500 Subject: Change coverage task to not run modularcrypto tests when not on CI --- dev/ci.py | 2 +- dev/coverage.py | 40 ++++++++++++++++++++++------------------ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/dev/ci.py b/dev/ci.py index 5ebdb88..a5c3a37 100644 --- a/dev/ci.py +++ b/dev/ci.py @@ -35,7 +35,7 @@ def run(): if run_coverage: print('\nRunning tests (via coverage.py)') sys.stdout.flush() - tests_result = run_coverage(write_xml=True) + tests_result = run_coverage(ci=True) else: print('\nRunning tests') sys.stdout.flush() diff --git a/dev/coverage.py b/dev/coverage.py index 7f7853f..84a73d1 100644 --- a/dev/coverage.py +++ b/dev/coverage.py @@ -24,10 +24,14 @@ else: from urllib.parse import urlencode -def run(write_xml=False): +def run(ci=False): """ Runs the tests while measuring coverage + :param ci: + If coverage is being run in a CI environment - this triggers trying to + run the tests for the rest of modularcrypto and uploading coverage data + :return: A bool - if the tests ran successfully """ @@ -43,20 +47,20 @@ def run(write_xml=False): result = run_tests() print() - suite = unittest.TestSuite() - loader = unittest.TestLoader() - for package_name in ['oscrypto', 'certbuilder', 'certvalidator', 'crlbuilder', 'csrbuild', 'ocspbuilder']: - for test_class in _load_package_tests(package_name): - suite.addTest(loader.loadTestsFromTestCase(test_class)) - - if suite.countTestCases() > 0: - print('Running tests from other modularcrypto packages') - sys.stdout.flush() - other_result = unittest.TextTestRunner(stream=sys.stdout, verbosity=1).run(suite).wasSuccessful() - print() - sys.stdout.flush() - else: - other_result = True + if ci: + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for package_name in ['oscrypto', 'certbuilder', 'certvalidator', 'crlbuilder', 'csrbuild', 'ocspbuilder']: + for test_class in _load_package_tests(package_name): + suite.addTest(loader.loadTestsFromTestCase(test_class)) + + if suite.countTestCases() > 0: + print('Running tests from other modularcrypto packages') + sys.stdout.flush() + runner_result = unittest.TextTestRunner(stream=sys.stdout, verbosity=1).run(suite) + result = runner_result.wasSuccessful() and result + print() + sys.stdout.flush() cov.stop() cov.save() @@ -64,14 +68,14 @@ def run(write_xml=False): cov.report(show_missing=False) print() sys.stdout.flush() - if write_xml: + if ci: cov.xml_report() - if write_xml and result and other_result and os.path.exists(xml_report_path): + if ci and result and os.path.exists(xml_report_path): _codecov_submit() print() - return result and other_result + return result def _load_package_tests(name): -- cgit v1.2.3 From 6b358fc01ddabbce1fb33299801f77c4f06c81d2 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Feb 2017 10:15:11 -0500 Subject: Add tests for parser submodule --- tests/__init__.py | 2 ++ tests/test_parser.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/test_parser.py diff --git a/tests/__init__.py b/tests/__init__.py index f741eee..944a52d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -47,6 +47,7 @@ def test_classes(): from .test_tsp import TSPTests from .test_x509 import X509Tests from .test_util import UtilTests + from .test_parser import ParserTests from .test_core import CoreTests return [ @@ -59,6 +60,7 @@ def test_classes(): PEMTests, TSPTests, UtilTests, + ParserTests, X509Tests, CoreTests ] diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..ebf12b1 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,52 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import unittest + +from asn1crypto import parser + +from ._unittest_compat import patch + +patch() + + +class ParserTests(unittest.TestCase): + + def test_parser(self): + result = parser.parse(b'\x02\x01\x00') + self.assertIsInstance(result, tuple) + self.assertEqual(0, result[0]) + self.assertEqual(0, result[1]) + self.assertEqual(2, result[2]) + self.assertEqual(b'\x02\x01', result[3]) + self.assertEqual(b'\x00', result[4]) + self.assertEqual(b'', result[5]) + + def test_parser_strict(self): + with self.assertRaises(ValueError): + parser.parse(b'\x02\x01\x00\x00', strict=True) + + def test_emit(self): + self.assertEqual(b'\x02\x01\x00', parser.emit(0, 0, 2, b'\x00')) + + def test_emit_type_errors(self): + with self.assertRaises(TypeError): + parser.emit('0', 0, 2, b'\x00') + + with self.assertRaises(ValueError): + parser.emit(-1, 0, 2, b'\x00') + + with self.assertRaises(TypeError): + parser.emit(0, '0', 2, b'\x00') + + with self.assertRaises(ValueError): + parser.emit(0, 5, 2, b'\x00') + + with self.assertRaises(TypeError): + parser.emit(0, 0, '2', b'\x00') + + with self.assertRaises(ValueError): + parser.emit(0, 0, -1, b'\x00') + + with self.assertRaises(TypeError): + parser.emit(0, 0, 2, '\x00') -- cgit v1.2.3 From d2240365c670d679129df5589e429f0382d16445 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 1 Feb 2017 10:49:06 -0500 Subject: Version 0.21.0 --- asn1crypto/version.py | 4 ++-- changelog.md | 14 ++++++++++++++ readme.md | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/asn1crypto/version.py b/asn1crypto/version.py index 424894f..b87d55c 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.20.0' -__version_info__ = (0, 20, 0) +__version__ = '0.21.0' +__version_info__ = (0, 21, 0) diff --git a/changelog.md b/changelog.md index e99bbbe..59749fc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,19 @@ # changelog +## 0.21.0 + + - Added `core.load()` for loading standard, universal types without knowing + the spec beforehand + - Added a `strict` keyword arg to the various `load()` methods and functions in + `core` that checks for trailing data and raises a `ValueError` when found + - Added `asn1crypto.parser` submodule with `emit()` and `parse()` functions for + low-level integration + - Added `asn1crypto.version` for version introspection without side-effects + - Added `algos.DSASignature` + - Fixed a bug with the `_header` attribute of explicitly-tagged values only + containing the explicit tag header instead of both the explicit tag header + and the encapsulated value header + ## 0.20.0 - Added support for year 0 diff --git a/readme.md b/readme.md index c9ce69b..3d7418a 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,7 @@ faster to an order of magnitude of more. ## Current Release -0.20.0 - [changelog](changelog.md) +0.21.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 72e76c16fb4460a26d0f152c6579238b83654d31 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 7 Feb 2017 13:53:20 -0500 Subject: Ensure a ValueError is raised when DER-encoded value does not have necessary header bytes --- asn1crypto/parser.py | 4 ++-- tests/test_core.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py index df4063e..f7b8bc9 100644 --- a/asn1crypto/parser.py +++ b/asn1crypto/parser.py @@ -129,8 +129,8 @@ def _parse(encoded_data, data_len, pointer=0, lengths_only=False): - 1: An integer indicating how many bytes were consumed """ - if data_len == 0: - return ((None, None, None, None, None, None), pointer) + if data_len < pointer + 2: + raise ValueError(_INSUFFICIENT_DATA_MESSAGE % (2, data_len - pointer)) start = pointer first_octet = ord(encoded_data[pointer]) if _PY2 else encoded_data[pointer] diff --git a/tests/test_core.py b/tests/test_core.py index c85433e..de5b1d1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -234,6 +234,19 @@ class CoreTests(unittest.TestCase): with self.assertRaises(TypeError): core.load('\x02\x01\x00') + @staticmethod + def truncated_der_byte_strings(): + return ( + (b'',), + (b'\x30',), + (b'\x30\x03\x02\x00\x02',), + ) + + @data('truncated_der_byte_strings') + def truncated(self, der_bytes): + with self.assertRaises(ValueError): + core.load(der_bytes).native + def test_strict(self): with self.assertRaises(ValueError): core.load(b'\x02\x01\x00\x00', strict=True) -- cgit v1.2.3 From 7b1cd4bd5a416a66418b657ab4d2709e754a78e6 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 10 Feb 2017 05:11:14 -0500 Subject: Fix a bug with determining the spec of pkcs12.Attribute['values'] --- asn1crypto/cms.py | 5 +-- asn1crypto/pkcs12.py | 14 ++++++- tests/__init__.py | 2 + tests/fixtures/test-tripledes.p12 | Bin 0 -> 2910 bytes tests/test_pkcs12.py | 83 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/test-tripledes.p12 create mode 100644 tests/test_pkcs12.py diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index d66e931..8218797 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -406,10 +406,7 @@ class AttCertAttribute(Sequence): } def _values_spec(self): - spec = self._oid_specs.get(self['type'].native, SetOfAny) - if spec == SetOfAny: - print('UNKNOWN OID: %s' % self['type'].native) - return spec + return self._oid_specs.get(self['type'].native, SetOfAny) _spec_callbacks = { 'values': _values_spec diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index ac08246..281dddc 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -57,6 +57,10 @@ class AttributeType(ObjectIdentifier): } +class SetOfAny(SetOf): + _child_spec = Any + + class SetOfBMPString(SetOf): _child_spec = BMPString @@ -68,16 +72,22 @@ class SetOfOctetString(SetOf): class Attribute(Sequence): _fields = [ ('type', AttributeType), - ('values', SetOf, {'spec': Any}), + ('values', None), ] - _oid_pair = ('type', 'values') _oid_specs = { 'friendly_name': SetOfBMPString, 'local_key_id': SetOfOctetString, 'microsoft_csp_name': SetOfBMPString, } + def _values_spec(self): + return self._oid_specs.get(self['type'].native, SetOfAny) + + _spec_callbacks = { + 'values': _values_spec + } + class Attributes(SetOf): _child_spec = Attribute diff --git a/tests/__init__.py b/tests/__init__.py index 944a52d..783a20f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -44,6 +44,7 @@ def test_classes(): from .test_keys import KeysTests from .test_ocsp import OCSPTests from .test_pem import PEMTests + from .test_pkcs12 import PKCS12Tests from .test_tsp import TSPTests from .test_x509 import X509Tests from .test_util import UtilTests @@ -58,6 +59,7 @@ def test_classes(): KeysTests, OCSPTests, PEMTests, + PKCS12Tests, TSPTests, UtilTests, ParserTests, diff --git a/tests/fixtures/test-tripledes.p12 b/tests/fixtures/test-tripledes.p12 new file mode 100644 index 0000000..8fc5e18 Binary files /dev/null and b/tests/fixtures/test-tripledes.p12 differ diff --git a/tests/test_pkcs12.py b/tests/test_pkcs12.py new file mode 100644 index 0000000..519f9c7 --- /dev/null +++ b/tests/test_pkcs12.py @@ -0,0 +1,83 @@ +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import unittest +import os +import zlib +import sys +from datetime import datetime + +from asn1crypto import pkcs12, util, core +from ._unittest_compat import patch + +patch() + +if sys.version_info < (3,): + byte_cls = str +else: + byte_cls = bytes + +tests_root = os.path.dirname(__file__) +fixtures_dir = os.path.join(tests_root, 'fixtures') + + +class PKCS12Tests(unittest.TestCase): + + def test_parse_pfx(self): + with open(os.path.join(fixtures_dir, 'test-tripledes.p12'), 'rb') as f: + info = pkcs12.Pfx.load(f.read()) + + self.assertEqual( + 'v3', + info['version'].native + ) + + auth_safe = info['auth_safe'] + + self.assertEqual( + 'data', + auth_safe['content_type'].native + ) + + self.assertEqual( + 2, + len(info.authenticated_safe) + ) + + for i, content_info in enumerate(info.authenticated_safe): + if i == 0: + self.assertEqual( + 'encrypted_data', + content_info['content_type'].native + ) + else: + self.assertEqual( + 'data', + content_info['content_type'].native + ) + safe_contents = pkcs12.SafeContents.load(content_info['content'].native) + self.assertEqual( + 1, + len(safe_contents) + ) + bag_attributes = safe_contents[0]['bag_attributes'] + self.assertEqual( + 2, + len(bag_attributes) + ) + self.assertEqual( + 'local_key_id', + bag_attributes[0]['type'].native + ) + self.assertEqual( + [b'\x95\xd7\xcf\xd7&\x80\x02\x94Q\xc2}X\xee\xd7\x9eiQ\xc0\x10P'], + bag_attributes[0]['values'].native + ) + self.assertEqual( + 'friendly_name', + bag_attributes[1]['type'].native + ) + self.assertEqual( + ['PKCS#12 Test'], + bag_attributes[1]['values'].native + ) -- cgit v1.2.3 From 3fcf56ba417b12e81195a66422dbcaf925515291 Mon Sep 17 00:00:00 2001 From: Anthony Alba Date: Sat, 11 Feb 2017 09:02:15 +0800 Subject: Bag attribute for certificates. Used by Java to recognise a certificate as a trustedCertEntry in PKCS12 keystores. https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java --- asn1crypto/pkcs12.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 281dddc..b36a14c 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -54,6 +54,8 @@ class AttributeType(ObjectIdentifier): '1.2.840.113549.1.9.21': 'local_key_id', # https://support.microsoft.com/en-us/kb/287547 '1.3.6.1.4.1.311.17.1': 'microsoft_local_machine_keyset', + # https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java + '2.16.840.1.113894.746875.1.1': 'trusted_cert_entry', } -- cgit v1.2.3 From 850fd3e8fa1cc499a582c333e16a00e0e29a33f3 Mon Sep 17 00:00:00 2001 From: Anthony Alba Date: Sat, 11 Feb 2017 13:01:36 +0800 Subject: The correct name for the OID is trustedKeyUsage. Comment that its value is a set of Any, but usually only a single element of OID 2.5.29.37.0 (anyExtendedKeyUsage). --- asn1crypto/pkcs12.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index b36a14c..378eb99 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -55,7 +55,8 @@ class AttributeType(ObjectIdentifier): # https://support.microsoft.com/en-us/kb/287547 '1.3.6.1.4.1.311.17.1': 'microsoft_local_machine_keyset', # https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java - '2.16.840.1.113894.746875.1.1': 'trusted_cert_entry', + # this is a set of OIDs, representing key usage, the usual value is a SET of one element OID 2.5.29.37.0 + '2.16.840.1.113894.746875.1.1': 'trusted_key_usage', } -- cgit v1.2.3 From 83eab27ac464d7adf6592138b03ff4522c789a44 Mon Sep 17 00:00:00 2001 From: Will Bond Date: Wed, 15 Feb 2017 06:11:22 -0500 Subject: Utilize x509.KeyPurposeId for pkcs12.AttributeType of trusted_key_usage --- asn1crypto/pkcs12.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 378eb99..4035f30 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -28,7 +28,7 @@ from .core import ( SetOf, ) from .keys import PrivateKeyInfo, EncryptedPrivateKeyInfo -from .x509 import Certificate +from .x509 import Certificate, KeyPurposeId # The structures in this file are taken from https://tools.ietf.org/html/rfc7292 @@ -72,6 +72,10 @@ class SetOfOctetString(SetOf): _child_spec = OctetString +class SetOfKeyPurposeId(SetOf): + _child_spec = KeyPurposeId + + class Attribute(Sequence): _fields = [ ('type', AttributeType), @@ -82,6 +86,7 @@ class Attribute(Sequence): 'friendly_name': SetOfBMPString, 'local_key_id': SetOfOctetString, 'microsoft_csp_name': SetOfBMPString, + 'trusted_key_usage': SetOfKeyPurposeId, } def _values_spec(self): -- cgit v1.2.3 From 2047909e766f45902ffae056d8097de47233d413 Mon Sep 17 00:00:00 2001 From: Anthony Alba Date: Sun, 19 Feb 2017 11:52:34 +0800 Subject: Add test for CertBag attribute trusted_key_usage --- tests/fixtures/certbag.der | Bin 0 -> 2125 bytes tests/test_pkcs12.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/fixtures/certbag.der diff --git a/tests/fixtures/certbag.der b/tests/fixtures/certbag.der new file mode 100644 index 0000000..1bd235a Binary files /dev/null and b/tests/fixtures/certbag.der differ diff --git a/tests/test_pkcs12.py b/tests/test_pkcs12.py index 519f9c7..ffd6398 100644 --- a/tests/test_pkcs12.py +++ b/tests/test_pkcs12.py @@ -81,3 +81,38 @@ class PKCS12Tests(unittest.TestCase): ['PKCS#12 Test'], bag_attributes[1]['values'].native ) + + def test_parse_certbag(self): + '''test to parse the java oid "2.16.840.1.113894.746875.1.1"''' + with open(os.path.join(fixtures_dir, 'certbag.der'), 'rb') as f: + certbag = pkcs12.SafeBag.load(f.read()) + + self.assertEqual( + 2, + len(certbag['bag_attributes']) + ) + + attr_0 = certbag['bag_attributes'][0] + + self.assertEqual( + 'friendly_name', + attr_0['type'].native + ) + + self.assertEqual( + ['testcertificate'], + attr_0['values'].native + ) + + + attr_1 = certbag['bag_attributes'][1] + + self.assertEqual( + 'trusted_key_usage', + attr_1['type'].native + ) + + self.assertEqual( + ['any_extended_key_usage'], + attr_1['values'].native + ) -- cgit v1.2.3 From f65b386b1280a8c691969ab48b96c6395b99ad6e Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 19 Feb 2017 16:12:59 -0500 Subject: Fix DER encoding of explicit tagging in certain circumstances --- asn1crypto/core.py | 4 ++-- tests/test_core.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 82d2832..66a7dfe 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -4715,7 +4715,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param original_value = value info, _ = _parse(contents, len(contents)) value = _build(*info, spec=spec) - value._header = header + info[3] + value._header = header + value._header value._trailer += trailer or b'' value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class @@ -4775,7 +4775,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param original_value = Asn1Value(contents=contents, **spec_params) info, _ = _parse(contents, len(contents)) value = _build(*info, spec=spec) - value._header = header + info[3] + value._header = header + value._header value._trailer += trailer or b'' value.tag_type = 'explicit' value.explicit_class = original_value.explicit_class diff --git a/tests/test_core.py b/tests/test_core.py index de5b1d1..ab27421 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -78,6 +78,12 @@ class SeqChoice(core.Choice): ] +class ExplicitField(core.Sequence): + _fields = [ + ('field', NumChoice, {'tag_type': 'explicit', 'tag': 0}), + ] + + class SetTest(core.Set): _fields = [ ('two', core.Integer, {'tag_type': 'implicit', 'tag': 2}), @@ -387,6 +393,12 @@ class CoreTests(unittest.TestCase): self.assertEqual(b'\xa0\x03\x02\x01', val.chosen._header) self.assertEqual(b'\x00', val.chosen.contents) + def test_explicit_header_field_choice(self): + der = b'\x30\x07\xa0\x05\xa0\x03\x02\x01\x00' + val = ExplicitField.load(der) + self.assertEqual(0, val['field'].chosen.native) + self.assertEqual(der, val.dump(force=True)) + def test_retag(self): a = core.Integer(200) b = a.retag('explicit', 0) -- cgit v1.2.3 From e80ae1db5ad4341d84579fccf510f151e6c9754c Mon Sep 17 00:00:00 2001 From: wbond Date: Sun, 19 Feb 2017 16:14:24 -0500 Subject: Increase timeout for submitting coverage data --- dev/coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/coverage.py b/dev/coverage.py index 84a73d1..5a24a4d 100644 --- a/dev/coverage.py +++ b/dev/coverage.py @@ -430,7 +430,7 @@ def _gitignore(root): return (dir_patterns, file_patterns) -def _do_request(method, url, headers, data=None, query_params=None, timeout=20): +def _do_request(method, url, headers, data=None, query_params=None, timeout=30): """ Performs an HTTP request -- cgit v1.2.3 From c76c0c96cb441be2612e4b60782bd7f2e6e63b33 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Feb 2017 11:34:12 -0500 Subject: Version 0.21.1 --- asn1crypto/version.py | 4 ++-- changelog.md | 10 ++++++++++ readme.md | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/asn1crypto/version.py b/asn1crypto/version.py index b87d55c..7de299b 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.21.0' -__version_info__ = (0, 21, 0) +__version__ = '0.21.1' +__version_info__ = (0, 21, 1) diff --git a/changelog.md b/changelog.md index 59749fc..1e18060 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # changelog +## 0.21.1 + + - Fixed a regression where explicit tagging of a field containing a + `core.Choice` would result in an incorrect header + - Fixed a bug where an `IndexError` was being raised instead of a `ValueError` + when a value was truncated to not include enough bytes for the header + - Corrected the spec for the `value` field of `pkcs12.Attribute` + - Added support for `2.16.840.1.113894.746875.1.1` OID to + `pkcs12.AttributeType` + ## 0.21.0 - Added `core.load()` for loading standard, universal types without knowing diff --git a/readme.md b/readme.md index 3d7418a..d2570ec 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,7 @@ faster to an order of magnitude of more. ## Current Release -0.21.0 - [changelog](changelog.md) +0.21.1 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 8c9011fbb8cf8bf95146a0f580fba66f92d68ecf Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Feb 2017 06:35:30 -0500 Subject: Fix bugs with encoding LDAP URIs --- asn1crypto/_iri.py | 34 ++++++++++++++++++++++++++-------- tests/test_util.py | 14 ++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/asn1crypto/_iri.py b/asn1crypto/_iri.py index d6e3013..3df0d0c 100644 --- a/asn1crypto/_iri.py +++ b/asn1crypto/_iri.py @@ -53,14 +53,29 @@ def iri_to_uri(value): type_name(value) )) - parsed = urlsplit(value) - - scheme = _urlquote(parsed.scheme) + scheme = None + # Python 2.6 doesn't split properly is the URL doesn't start with http:// or https:// + if sys.version_info < (2, 7) and not value.startswith('http://') and not value.startswith('https://'): + real_prefix = None + prefix_match = re.match('^[^:]*://', value) + if prefix_match: + real_prefix = prefix_match.group(0) + value = 'http://' + value[len(real_prefix):] + parsed = urlsplit(value) + if real_prefix: + value = real_prefix + value[7:] + scheme = _urlquote(real_prefix[:-3]) + else: + parsed = urlsplit(value) + + if scheme is None: + scheme = _urlquote(parsed.scheme) hostname = parsed.hostname if hostname is not None: hostname = hostname.encode('idna') - username = _urlquote(parsed.username) - password = _urlquote(parsed.password) + # RFC 3986 allows userinfo to contain sub-delims + username = _urlquote(parsed.username, safe='!$&\'()*+,;=') + password = _urlquote(parsed.password, safe='!$&\'()*+,;=') port = parsed.port if port is not None: port = str_cls(port).encode('ascii') @@ -79,9 +94,12 @@ def iri_to_uri(value): if not default_http and not default_https: netloc += b':' + port - path = _urlquote(parsed.path, safe='/') - query = _urlquote(parsed.query, safe='&=') - fragment = _urlquote(parsed.fragment) + # RFC 3986 allows a path to contain sub-delims, plus "@" and ":" + path = _urlquote(parsed.path, safe='/!$&\'()*+,;=@:') + # RFC 3986 allows the query to contain sub-delims, plus "@", ":" , "/" and "?" + query = _urlquote(parsed.query, safe='/?!$&\'()*+,;=@:') + # 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'/': path = None diff --git a/tests/test_util.py b/tests/test_util.py index 0d1f6d3..a3f3e6e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -119,3 +119,17 @@ class UtilTests(unittest.TestCase): self.assertEqual(util.extended_date(0, 1, 1), util.extended_datetime(0, 1, 1).date()) self.assertEqual(util.extended_date(0, 2, 29), util.extended_datetime(0, 2, 29).date()) self.assertEqual(time(0, 0, 0), util.extended_datetime(0, 1, 1).time()) + + def test_iri_to_uri(self): + self.assertEqual( + b'ldap://ldap.e-szigno.hu/CN=Microsec%20e-Szigno%20Root%20CA,OU=e-Szigno%20CA,O=Microsec%20Ltd.,L=Budapest,C=HU?certificateRevocationList;binary', + util.iri_to_uri('ldap://ldap.e-szigno.hu/CN=Microsec e-Szigno Root CA,OU=e-Szigno CA,O=Microsec Ltd.,L=Budapest,C=HU?certificateRevocationList;binary') + ) + self.assertEqual( + b'ldap://directory.d-trust.net/CN=D-TRUST%20Root%20Class%203%20CA%202%202009,O=D-Trust%20GmbH,C=DE?certificaterevocationlist', + util.iri_to_uri('ldap://directory.d-trust.net/CN=D-TRUST Root Class 3 CA 2 2009,O=D-Trust GmbH,C=DE?certificaterevocationlist') + ) + self.assertEqual( + b'ldap://directory.d-trust.net/CN=D-TRUST%20Root%20Class%203%20CA%202%20EV%202009,O=D-Trust%20GmbH,C=DE?certificaterevocationlist', + util.iri_to_uri('ldap://directory.d-trust.net/CN=D-TRUST Root Class 3 CA 2 EV 2009,O=D-Trust GmbH,C=DE?certificaterevocationlist') + ) -- cgit v1.2.3 From ed82e3f593b9bc5c89a116b8f9116b226cecd614 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Feb 2017 06:52:50 -0500 Subject: Allow x509.DNSName to start with a . --- asn1crypto/x509.py | 28 ++++++++++++++++++++++++++++ tests/test_x509.py | 3 +++ 2 files changed, 31 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index efe6635..044eb10 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -90,6 +90,34 @@ class DNSName(IA5String): return self.contents.lower() == other.contents.lower() + def set(self, value): + """ + Sets the value of the DNS name + + :param value: + A unicode string + """ + + if not isinstance(value, str_cls): + raise TypeError(unwrap( + ''' + %s value must be a unicode string, not %s + ''', + type_name(self), + type_name(value) + )) + + if value.startswith('.'): + encoded_value = b'.' + value[1:].encode(self._encoding) + else: + encoded_value = value.encode(self._encoding) + + self._native = value + self.contents = encoded_value + self._header = None + if self._trailer != b'': + self._trailer = b'' + class URI(IA5String): diff --git a/tests/test_x509.py b/tests/test_x509.py index 0650f6b..ede88de 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -113,6 +113,9 @@ class X509Tests(unittest.TestCase): self.assertEqual(der_bytes, x509.IPAddress(unicode_string).dump()) self.assertEqual(unicode_string, x509.IPAddress.load(der_bytes).native) + def test_dnsname_begin_dot(self): + self.assertEqual(b'\x16\x03.gr', x509.DNSName(u'.gr').dump()) + @staticmethod def compare_dnsname_info(): return ( -- cgit v1.2.3 From 02c3d992f28335ff13fe8620d254a77e86def6c6 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Feb 2017 11:24:29 -0500 Subject: Ensure the explicit tag is moved to the parsed child from core.Any --- asn1crypto/core.py | 2 +- tests/test_x509.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 66a7dfe..f251dd3 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -758,7 +758,7 @@ class Any(Asn1Value): if self.tag_type == 'explicit': passed_params = {} if not spec_params else spec_params.copy() passed_params['tag_type'] = self.tag_type - passed_params['tag'] = self.tag + passed_params['tag'] = self.explicit_tag parsed_value, _ = _parse_build( self._header + self.contents + self._trailer, spec=spec, diff --git a/tests/test_x509.py b/tests/test_x509.py index ede88de..80b26f0 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -309,6 +309,11 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(one, two) + def test_dump_generalname(self): + data = b'0.\x82\x0fsuscerte.gob.ve\xa0\x1b\x06\x05`\x86^\x02\x02\xa0\x12\x0c\x10RIF-G-20004036-0' + alt = x509.GeneralNames.load(data) + self.assertEqual(data, alt.dump(force=True)) + @staticmethod def compare_name_info(): return ( -- cgit v1.2.3 From 7defdbf761928ecac2eaf40f261c383c104f1138 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Feb 2017 11:24:44 -0500 Subject: Remove useless re-assignment --- asn1crypto/parser.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py index f7b8bc9..c4c08c9 100644 --- a/asn1crypto/parser.py +++ b/asn1crypto/parser.py @@ -232,8 +232,6 @@ def _dump_header(class_, method, tag, contents): id_num |= class_ << 6 id_num |= method << 5 - tag = tag - if tag >= 31: header += chr_cls(id_num | 31) while tag > 0: -- cgit v1.2.3 From 8a21e7fed737a859a7c502d90e6a09b41af138b5 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Feb 2017 11:25:25 -0500 Subject: Clean up core.Any properties once .parsed has been accessed --- asn1crypto/core.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index f251dd3..f7487f4 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -495,8 +495,13 @@ class Asn1Value(object): """ prefix = ' ' * nest_level + + # This interacts with Any and moves the tag, tag_type, _header, contents, _footer + # to the parsed value so duplicate data isn't present + has_parsed = hasattr(self, 'parsed') + _basic_debug(prefix, self) - if hasattr(self, 'parsed'): + if has_parsed: self.parsed.debug(nest_level + 2) elif hasattr(self, 'chosen'): self.chosen.debug(nest_level + 2) @@ -759,12 +764,22 @@ class Any(Asn1Value): passed_params = {} if not spec_params else spec_params.copy() passed_params['tag_type'] = self.tag_type passed_params['tag'] = self.explicit_tag + contents = self._header + self.contents + self._trailer parsed_value, _ = _parse_build( - self._header + self.contents + self._trailer, + contents, spec=spec, spec_params=passed_params ) self._parsed = (parsed_value, spec, spec_params) + + # Once we've parsed the Any value, clear any attributes from this object + # since they are now duplicate + self.tag_type = None + self.tag = None + self._header = b'' + self.contents = contents + self._trailer = b'' + except (ValueError, TypeError) as e: args = e.args[1:] e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args -- cgit v1.2.3 From 98a92a430cbce778f02672a28e916a442b95f944 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 27 Feb 2017 11:34:27 -0500 Subject: Fix tests to be compatible with Python 3.2 --- tests/test_x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_x509.py b/tests/test_x509.py index 80b26f0..5df4c5d 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -114,7 +114,7 @@ class X509Tests(unittest.TestCase): self.assertEqual(unicode_string, x509.IPAddress.load(der_bytes).native) def test_dnsname_begin_dot(self): - self.assertEqual(b'\x16\x03.gr', x509.DNSName(u'.gr').dump()) + self.assertEqual(b'\x16\x03.gr', x509.DNSName('.gr').dump()) @staticmethod def compare_dnsname_info(): -- cgit v1.2.3 From f577cfff05e37b4baf4820973400730868155477 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 2 Mar 2017 09:45:40 -0500 Subject: Properly handle indefinite-length BER encoded values --- asn1crypto/core.py | 244 ++++++++++++++++++++++++++++++++++++++++++--------- asn1crypto/parser.py | 6 ++ tests/test_cms.py | 17 +--- tests/test_core.py | 54 ++++++++++++ 4 files changed, 264 insertions(+), 57 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index f7487f4..1c5e118 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -486,6 +486,9 @@ class Asn1Value(object): self.contents = other.contents self._native = copy_func(other._native) + if isinstance(other, Constructable): + self.method = copy_func(other.method) + self._indefinite = copy_func(other._indefinite) if hasattr(other, '_parsed'): self._parsed = copy_func(other._parsed) @@ -525,7 +528,10 @@ class Asn1Value(object): contents = self.contents - if self._header is None: + if self._header is None or force: + if isinstance(self, Constructable) and self._indefinite: + self.method = 0 + header = _dump_header(self.class_, self.method, self.tag, self.contents) trailer = b'' @@ -612,9 +618,56 @@ class Castable(): new_obj._header = self._header new_obj.contents = self.contents new_obj._trailer = self._trailer + if isinstance(self, Constructable): + new_obj.method = self.method + new_obj._indefinite = self._indefinite return new_obj +class Constructable(): + """ + A mixin to handle string types that may be constructed from chunks + contained within an indefinite length BER-encoded container + """ + + _indefinite = False + + _chunks_offset = 0 + + def _merge_chunks(self): + """ + :return: + A concatenation of the native values of the contained chunks + """ + + if not self._indefinite: + return self._as_chunk() + + pointer = self._chunks_offset + contents_len = len(self.contents) + output = None + while pointer < contents_len: + # We pass the current class as the spec so content semantics are preserved + sub_value, pointer = _parse_build(self.contents, pointer, spec=self.__class__) + if output is None: + output = sub_value._as_chunk() + else: + output += sub_value._as_chunk() + return output + + def _as_chunk(self): + """ + A method to return a chunk of data that can be combined for + constructed method values + + :return: + A native Python value that can be added together. Examples include + byte strings, unicode strings or tuples. + """ + + raise NotImplementedError() + + class Void(Asn1Value): """ A representation of an optional value that is not present. Has .native @@ -1120,7 +1173,7 @@ class Choice(Asn1Value): """ self.contents = self.chosen.dump(force=force) - if self._header is None: + if self._header is None or force: if self.tag_type == 'explicit': self._header = _dump_header(self.explicit_class, 1, self.explicit_tag, self.contents) else: @@ -1540,7 +1593,7 @@ class Primitive(Asn1Value): return self.dump() == other.dump() -class AbstractString(Primitive): +class AbstractString(Constructable, Primitive): """ A base class for all strings that have a known encoding. In general, we do not worry ourselves with confirming that the decoded values match a specific @@ -1579,7 +1632,17 @@ class AbstractString(Primitive): A unicode string """ - return self.contents.decode(self._encoding) + return self._merge_chunks().decode(self._encoding) + + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :return: + A byte string + """ + + return self.contents @property def native(self): @@ -1732,7 +1795,7 @@ class Integer(Primitive, ValueMap): return self._native -class BitString(Castable, Primitive, ValueMap, object): +class BitString(Constructable, Castable, Primitive, ValueMap, object): """ Represents a bit string from ASN.1 as a Python tuple of 1s and 0s """ @@ -1741,6 +1804,10 @@ class BitString(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 @@ -1804,6 +1871,8 @@ class BitString(Castable, Primitive, ValueMap, object): type_name(value) )) + self._chunk = None + if self._map is not None: if len(value) > self._size: raise ValueError(unwrap( @@ -1951,6 +2020,36 @@ class BitString(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): """ @@ -1969,27 +2068,7 @@ class BitString(Castable, Primitive, ValueMap, object): self.set(set()) if self._native is None: - 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] - - bits = tuple(map(int, tuple(bit_string))) + bits = self._merge_chunks() if self._map: self._native = set() for index, bit in enumerate(bits): @@ -2001,13 +2080,15 @@ class BitString(Castable, Primitive, ValueMap, object): return self._native -class OctetBitString(Castable, Primitive): +class OctetBitString(Constructable, Castable, Primitive): """ Represents a bit string in ASN.1 as a Python byte string """ tag = 3 + _chunks_offset = 1 + def set(self, value): """ Sets the value of the object @@ -2047,6 +2128,16 @@ class OctetBitString(Castable, Primitive): # unused_bits = struct.unpack('>B', self.contents[0:1])[0] return self.contents[1:] + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :return: + A byte string + """ + + return self.contents[1:] + @property def native(self): """ @@ -2060,17 +2151,19 @@ class OctetBitString(Castable, Primitive): return None if self._native is None: - self._native = self.__bytes__() + self._native = self._merge_chunks() return self._native -class IntegerBitString(Castable, Primitive): +class 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 @@ -2098,6 +2191,27 @@ class IntegerBitString(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): """ @@ -2112,16 +2226,17 @@ class IntegerBitString(Castable, Primitive): if self._native is None: extra_bits = int_from_bytes(self.contents[0:1]) - if extra_bits > 0: - bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:])) - bit_string = bit_string[0:0 - extra_bits] - self._native = int(bit_string, 2) - else: + # 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) return self._native -class OctetString(Castable, Primitive): +class OctetString(Constructable, Castable, Primitive): """ Represents a byte string in both ASN.1 and Python """ @@ -2136,6 +2251,16 @@ class OctetString(Castable, Primitive): return self.contents + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :return: + A byte string + """ + + return self.contents + @property def native(self): """ @@ -2149,11 +2274,11 @@ class OctetString(Castable, Primitive): return None if self._native is None: - self._native = self.__bytes__() + self._native = self._merge_chunks() return self._native -class IntegerOctetString(Castable, Primitive): +class IntegerOctetString(Constructable, Castable, Primitive): """ Represents a byte string in ASN.1 as a Python integer """ @@ -2186,6 +2311,16 @@ class IntegerOctetString(Castable, Primitive): if self._trailer != b'': self._trailer = b'' + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :return: + A byte string + """ + + return self.contents + @property def native(self): """ @@ -2199,11 +2334,11 @@ class IntegerOctetString(Castable, Primitive): return None if self._native is None: - self._native = int_from_bytes(self.contents) + self._native = int_from_bytes(self._merge_chunks()) return self._native -class ParsableOctetString(Castable, Primitive): +class ParsableOctetString(Constructable, Castable, Primitive): tag = 4 @@ -2253,7 +2388,7 @@ class ParsableOctetString(Castable, Primitive): """ if self._parsed is None or self._parsed[1:3] != (spec, spec_params): - parsed_value, _ = _parse_build(byte_cls(self), spec=spec, spec_params=spec_params) + parsed_value, _ = _parse_build(self._merge_chunks(), spec=spec, spec_params=spec_params) self._parsed = (parsed_value, spec, spec_params) return self._parsed[0] @@ -2265,6 +2400,16 @@ class ParsableOctetString(Castable, Primitive): return self.contents + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :return: + A byte string + """ + + return self.contents + @property def native(self): """ @@ -2281,7 +2426,7 @@ class ParsableOctetString(Castable, Primitive): if self._parsed is not None: self._native = self._parsed[0].native else: - self._native = self.__bytes__() + self._native = self._merge_chunks() return self._native @property @@ -2350,6 +2495,8 @@ class ParsableOctetBitString(ParsableOctetString): tag = 3 + _chunks_offset = 1 + def set(self, value): """ Sets the value of the object @@ -2383,6 +2530,16 @@ class ParsableOctetBitString(ParsableOctetString): A byte string """ + return self._merge_chunks() + + def _as_chunk(self): + """ + Allows reconstructing indefinite length values + + :return: + A byte string + """ + # 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 @@ -4764,7 +4921,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # Allow parsing a primitive method as constructed if the value # is indefinite length. This is to allow parsing BER. ber_indef = method == 1 and value.method == 0 and trailer == b'\x00\x00' - if not ber_indef: + if not ber_indef or not isinstance(value, Constructable): raise ValueError(unwrap( ''' Error parsing %s - method should have been %s, but %s was found @@ -4773,6 +4930,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param METHOD_NUM_TO_NAME_MAP.get(value.method), METHOD_NUM_TO_NAME_MAP.get(method, method) )) + else: + value.method = method + value._indefinite = True if tag != value.tag and tag != value._bad_tag: raise ValueError(unwrap( ''' diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py index c4c08c9..cbe1c44 100644 --- a/asn1crypto/parser.py +++ b/asn1crypto/parser.py @@ -170,6 +170,12 @@ 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: diff --git a/tests/test_cms.py b/tests/test_cms.py index 5d3c782..9e013e7 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -115,12 +115,7 @@ class CMSTests(unittest.TestCase): compressed_data.decompressed ) - def test_parse_content_info_jibberish(self): - # This DER data isn't really DER, but BER because it uses indefinite - # length encoding. Additionally, the CompressedData isn't actually - # real CompressedData, but a number of OctetString values concatenated - # together that can have their native values concatenated and then - # run through zlib. + def test_parse_content_info_indefinite(self): with open(os.path.join(fixtures_dir, 'meca2_compressed.der'), 'rb') as f: info = cms.ContentInfo.load(f.read()) @@ -146,15 +141,7 @@ class CMSTests(unittest.TestCase): 'data', compressed_data['encap_content_info']['content_type'].native ) - encap_data = compressed_data['encap_content_info']['content'].native - read = 0 - chunks = 0 - data = b'' - while read < len(encap_data): - value, read = core._parse_build(encap_data, read) - data += value.native - chunks += 1 - self.assertEqual(10, chunks) + data = compressed_data['encap_content_info']['content'].native self.assertIsInstance(zlib.decompress(data), byte_cls) def test_parse_content_info_digested_data(self): diff --git a/tests/test_core.py b/tests/test_core.py index ab27421..ed52227 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -492,3 +492,57 @@ class CoreTests(unittest.TestCase): def test_dump_set_of(self): st = SetOfTest([3, 2, 1]) self.assertEqual(b'1\x09\x02\x01\x01\x02\x01\x02\x02\x01\x03', st.dump()) + + def test_indefinite_length_octet_string(self): + data = b'$\x80\x04\r\x8d\xff\xf0\x98\x076\xaf\x93nB:\xcf\xcc\x04\x15\x92w\xf7\xf0\xe4y\xff\xc7\xdc3\xb2\xd0={\x1a\x18mDr\xaaI\x00\x00' + a = core.OctetString.load(data) + self.assertEqual( + b'\x8d\xff\xf0\x98\x076\xaf\x93nB:\xcf\xcc\x92w\xf7\xf0\xe4y\xff\xc7\xdc3\xb2\xd0={\x1a\x18mDr\xaaI', + a.native + ) + + def test_indefinite_length_integer_octet_string(self): + data = b'$\x80\x04\x02\x01\x01\x04\x01\x01\x00\x00' + a = core.IntegerOctetString.load(data) + self.assertEqual(65793, a.native) + self.assertEqual(1, a.method) + self.assertEqual(b'\x01\x01\x01', a.cast(core.OctetString).native) + + def test_indefinite_length_parsable_octet_string(self): + data = b'$\x80\x04\x02\x04\x01\x04\x01\x01\x00\x00' + a = core.ParsableOctetString.load(data) + self.assertEqual(b'\x04\x01\x01', a.parsed.dump()) + self.assertEqual(1, a.method) + self.assertEqual(b'\x01', a.parsed.native) + self.assertEqual(b'\x01', a.native) + self.assertEqual(b'\x04\x01\x01', a.cast(core.OctetString).native) + + def test_indefinite_length_utf8string(self): + data = b'\x2C\x80\x0C\x02\x61\x62\x0C\x01\x63\x00\x00' + a = core.UTF8String.load(data) + self.assertEqual('abc', a.native) + self.assertEqual(1, a.method) + # Ensure a forced re-encoding is proper DER + self.assertEqual(b'\x0C\x03\x61\x62\x63', a.dump(force=True)) + + def test_indefinite_length_bit_string(self): + data = b'#\x80\x00\x03\x02\x00\x01\x03\x02\x02\x04\x00\x00' + a = core.BitString.load(data) + self.assertEqual((0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1), a.native) + + def test_indefinite_length_integer_bit_string(self): + data = b'#\x80\x00\x03\x02\x00\x01\x03\x02\x00\x04\x00\x00' + a = core.IntegerBitString.load(data) + self.assertEqual(260, a.native) + + def test_indefinite_length_octet_bit_string(self): + data = b'#\x80\x00\x03\x02\x00\x01\x03\x02\x00\x04\x00\x00' + a = core.OctetBitString.load(data) + self.assertEqual(b'\x01\x04', a.native) + + def test_indefinite_length_parsable_octet_bit_string(self): + data = b'#\x80\x00\x03\x03\x00\x0C\x02\x03\x03\x00\x61\x62\x00\x00' + a = core.ParsableOctetBitString.load(data) + self.assertEqual(b'\x0C\x02\x61\x62', a.parsed.dump()) + self.assertEqual('ab', a.parsed.native) + self.assertEqual('ab', a.native) -- cgit v1.2.3 From f23008b723ea59cf88e8e6801be6bf2eda69e103 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 2 Mar 2017 09:47:00 -0500 Subject: Add parser.peek() --- asn1crypto/parser.py | 31 +++++++++++++++++++++++++++++++ tests/test_parser.py | 3 +++ 2 files changed, 34 insertions(+) diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py index cbe1c44..7ae3515 100644 --- a/asn1crypto/parser.py +++ b/asn1crypto/parser.py @@ -6,6 +6,7 @@ following items: - emit() - parse() + - peek() Other type classes are defined that help compose the types listed above. """ @@ -85,6 +86,10 @@ def parse(contents, strict=False): A boolean indicating if trailing data should be forbidden - if so, a ValueError will be raised when trailing data exists + :raises: + ValueError - when the contents do not contain an ASN.1 header or are truncated in some way + TypeError - when contents is not a byte string + :return: A 6-element tuple: - 0: integer class (0 to 3) @@ -105,6 +110,32 @@ def parse(contents, strict=False): return info +def peek(contents): + """ + Parses a byte string of ASN.1 BER/DER-encoded data to find the length + + This is typically used to look into an encoded value to see how long the + next chunk of ASN.1-encoded data is. Primarily it is useful when a + value is a concatenation of multiple values. + + :param contents: + A byte string of BER/DER-encoded data + + :raises: + ValueError - when the contents do not contain an ASN.1 header or are truncated in some way + TypeError - when contents is not a byte string + + :return: + An integer with the number of bytes occupied by the ASN.1 value + """ + + if not isinstance(contents, byte_cls): + raise TypeError('contents must be a byte string, not %s' % type_name(contents)) + + info, consumed = _parse(contents, len(contents)) + return consumed + + def _parse(encoded_data, data_len, pointer=0, lengths_only=False): """ Parses a byte string into component parts diff --git a/tests/test_parser.py b/tests/test_parser.py index ebf12b1..eb393a2 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -22,6 +22,9 @@ class ParserTests(unittest.TestCase): self.assertEqual(b'\x00', result[4]) self.assertEqual(b'', result[5]) + def test_peek(self): + self.assertEqual(3, parser.peek(b'\x02\x01\x00\x00')) + def test_parser_strict(self): with self.assertRaises(ValueError): parser.parse(b'\x02\x01\x00\x00', strict=True) -- cgit v1.2.3 From 04c7ea7657e57f54604b68e2ca497781819c7fc7 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 2 Mar 2017 10:44:56 -0500 Subject: Fix handling nested indefinite-length BER-encoded values --- asn1crypto/core.py | 13 +++++++++++-- asn1crypto/parser.py | 2 +- tests/test_core.py | 7 +++++++ tests/test_parser.py | 7 +++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 1c5e118..8f3ec6d 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -646,13 +646,18 @@ class Constructable(): pointer = self._chunks_offset contents_len = len(self.contents) output = None + while pointer < contents_len: # We pass the current class as the spec so content semantics are preserved sub_value, pointer = _parse_build(self.contents, pointer, spec=self.__class__) if output is None: - output = sub_value._as_chunk() + output = sub_value._merge_chunks() else: - output += sub_value._as_chunk() + output += sub_value._merge_chunks() + + if output is None: + return self._as_chunk() + return output def _as_chunk(self): @@ -4973,6 +4978,10 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param spec = _UNIVERSAL_SPECS[tag] value = spec(contents=contents, class_=class_) + ber_indef = method == 1 and value.method == 0 and trailer == b'\x00\x00' + if ber_indef and isinstance(value, Constructable): + value._indefinite = True + value.method = method if not header_set: value._header = header diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py index 7ae3515..07f53ab 100644 --- a/asn1crypto/parser.py +++ b/asn1crypto/parser.py @@ -209,7 +209,7 @@ def _parse(encoded_data, data_len, pointer=0, lengths_only=False): 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: + if contents_end == sub_header_end and encoded_data[contents_end - 2:contents_end] == b'\x00\x00': break if lengths_only: return (pointer, contents_end) diff --git a/tests/test_core.py b/tests/test_core.py index ed52227..2006c34 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -501,6 +501,13 @@ class CoreTests(unittest.TestCase): a.native ) + def test_nested_indefinite_length_octet_string(self): + data = b'\x24\x80\x24\x80\x24\x80\x04\x00\x00\x00\x00\x00\x00\x00' + a = core.load(data) + self.assertEqual(b'', a.native) + self.assertEqual(1, a.method) + self.assertEqual(b'\x04\x00', a.dump(force=True)) + def test_indefinite_length_integer_octet_string(self): data = b'$\x80\x04\x02\x01\x01\x04\x01\x01\x00\x00' a = core.IntegerOctetString.load(data) diff --git a/tests/test_parser.py b/tests/test_parser.py index eb393a2..b661c33 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -25,6 +25,13 @@ class ParserTests(unittest.TestCase): def test_peek(self): self.assertEqual(3, parser.peek(b'\x02\x01\x00\x00')) + def test_parse_indef_nested(self): + data = b'\x24\x80\x24\x80\x24\x80\x04\x00\x00\x00\x00\x00\x00\x00' + result = parser.parse(data) + self.assertEqual(b'\x24\x80', result[3]) + self.assertEqual(b'\x24\x80\x24\x80\x04\x00\x00\x00\x00\x00', result[4]) + self.assertEqual(b'\x00\x00', result[5]) + def test_parser_strict(self): with self.assertRaises(ValueError): parser.parse(b'\x02\x01\x00\x00', strict=True) -- cgit v1.2.3 From 2e7d2624d36b89a795c069b4ea1a008bb9b5f035 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 3 Mar 2017 06:21:12 -0500 Subject: Fix a bug with unquoting some URIs in util.uri_to_iri() --- asn1crypto/_iri.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/asn1crypto/_iri.py b/asn1crypto/_iri.py index 3df0d0c..57ddd40 100644 --- a/asn1crypto/_iri.py +++ b/asn1crypto/_iri.py @@ -262,7 +262,8 @@ def _urlunquote(byte_string, remap=None, preserve=None): if byte_string is None: return byte_string - byte_string = unquote_to_bytes(byte_string) + if byte_string == b'': + return '' if preserve: replacements = ['\x1A', '\x1C', '\x1D', '\x1E', '\x1F'] @@ -272,6 +273,8 @@ def _urlunquote(byte_string, remap=None, preserve=None): preserve_unmap[replacement] = char byte_string = byte_string.replace(char.encode('ascii'), replacement.encode('ascii')) + byte_string = unquote_to_bytes(byte_string) + if remap: for char in remap: byte_string = byte_string.replace(char.encode('ascii'), ('%%%02x' % ord(char)).encode('ascii')) -- cgit v1.2.3 From abf76d16c70f69c38cb3bd44d92096a2d8fa89ea Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 3 Mar 2017 07:18:21 -0500 Subject: Ensure .__bytes__(), .__unicode__() and .native are consistent for Constructable classes --- asn1crypto/core.py | 224 ++++++++++++++++++++++++++++++----------------------- asn1crypto/x509.py | 33 ++++---- tests/test_core.py | 12 +++ tests/test_x509.py | 36 +++++++++ 4 files changed, 194 insertions(+), 111 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 8f3ec6d..4adf5bc 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -200,7 +200,8 @@ class Asn1Value(object): # The BER/DER trailer bytes _trailer = b'' - # The native python representation of the value + # The native python representation of the value - this is not used by + # some classes since they utilize _bytes or _unicode _native = None @classmethod @@ -630,8 +631,12 @@ class Constructable(): contained within an indefinite length BER-encoded container """ + # Instance attribute indicating if an object was indefinite + # 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): @@ -670,7 +675,9 @@ class Constructable(): byte strings, unicode strings or tuples. """ - raise NotImplementedError() + if self._chunks_offset == 0: + return self.contents + return self.contents[self._chunks_offset:] class Void(Asn1Value): @@ -1608,6 +1615,9 @@ class AbstractString(Constructable, Primitive): # The Python encoding name to use when decoding or encoded the contents _encoding = 'latin1' + # Instance attribute of (possibly-merged) unicode string + _unicode = None + def set(self, value): """ Sets the value of the string @@ -1625,9 +1635,12 @@ class AbstractString(Constructable, Primitive): type_name(value) )) - self._native = value + self._unicode = value self.contents = value.encode(self._encoding) self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 if self._trailer != b'': self._trailer = b'' @@ -1637,17 +1650,11 @@ class AbstractString(Constructable, Primitive): A unicode string """ - return self._merge_chunks().decode(self._encoding) - - def _as_chunk(self): - """ - Allows reconstructing indefinite length values - - :return: - A byte string - """ - - return self.contents + if self.contents is None: + return '' + if self._unicode is None: + self._unicode = self._merge_chunks().decode(self._encoding) + return self._unicode @property def native(self): @@ -1661,9 +1668,7 @@ class AbstractString(Constructable, Primitive): if self.contents is None: return None - if self._native is None: - self._native = self.__unicode__() - return self._native + return self.__unicode__() class Boolean(Primitive): @@ -1918,6 +1923,9 @@ class BitString(Constructable, Castable, Primitive, ValueMap, object): self.contents = extra_bits_byte + value_bytes self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 if self._trailer != b'': self._trailer = b'' @@ -2092,8 +2100,15 @@ 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 + def set(self, value): """ Sets the value of the object @@ -2114,10 +2129,13 @@ class OctetBitString(Constructable, Castable, Primitive): type_name(value) )) - self._native = value + self._bytes = value # Set the unused bits to 0 self.contents = b'\x00' + value self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 if self._trailer != b'': self._trailer = b'' @@ -2127,21 +2145,11 @@ class OctetBitString(Constructable, Castable, Primitive): A byte string """ - # 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] - return self.contents[1:] - - def _as_chunk(self): - """ - Allows reconstructing indefinite length values - - :return: - A byte string - """ - - return self.contents[1:] + if self.contents is None: + return b'' + if self._bytes is None: + self._bytes = self._merge_chunks() + return self._bytes @property def native(self): @@ -2155,9 +2163,7 @@ class OctetBitString(Constructable, Castable, Primitive): if self.contents is None: return None - if self._native is None: - self._native = self._merge_chunks() - return self._native + return self.__bytes__() class IntegerBitString(Constructable, Castable, Primitive): @@ -2193,6 +2199,9 @@ class IntegerBitString(Constructable, Castable, Primitive): # Set the unused bits to 0 self.contents = b'\x00' + int_to_bytes(value, signed=True) self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 if self._trailer != b'': self._trailer = b'' @@ -2248,23 +2257,46 @@ class OctetString(Constructable, Castable, Primitive): tag = 4 - def __bytes__(self): + # Instance attribute of (possibly-merged) byte string + _bytes = None + + def set(self, value): """ - :return: + Sets the value of the object + + :param value: A byte string """ - return self.contents + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) - def _as_chunk(self): - """ - Allows reconstructing indefinite length values + self._bytes = value + self.contents = value + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' + def __bytes__(self): + """ :return: A byte string """ - return self.contents + if self.contents is None: + return b'' + if self._bytes is None: + self._bytes = self._merge_chunks() + return self._bytes @property def native(self): @@ -2278,9 +2310,7 @@ class OctetString(Constructable, Castable, Primitive): if self.contents is None: return None - if self._native is None: - self._native = self._merge_chunks() - return self._native + return self.__bytes__() class IntegerOctetString(Constructable, Castable, Primitive): @@ -2313,19 +2343,12 @@ class IntegerOctetString(Constructable, Castable, Primitive): self._native = value self.contents = int_to_bytes(value, signed=False) self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 if self._trailer != b'': self._trailer = b'' - def _as_chunk(self): - """ - Allows reconstructing indefinite length values - - :return: - A byte string - """ - - return self.contents - @property def native(self): """ @@ -2349,6 +2372,9 @@ class ParsableOctetString(Constructable, Castable, Primitive): _parsed = None + # Instance attribute of (possibly-merged) byte string + _bytes = None + def __init__(self, value=None, parsed=None, **kwargs): """ Allows providing a parsed object that will be serialized to get the @@ -2372,7 +2398,32 @@ class ParsableOctetString(Constructable, Castable, Primitive): if set_parsed: self._parsed = (parsed, parsed.__class__, None) - self._native = parsed.native + + def set(self, value): + """ + Sets the value of the object + + :param value: + A byte string + """ + + if not isinstance(value, byte_cls): + raise TypeError(unwrap( + ''' + %s value must be a byte string, not %s + ''', + type_name(self), + type_name(value) + )) + + self._bytes = value + self.contents = value + self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 + if self._trailer != b'': + self._trailer = b'' def parse(self, spec=None, spec_params=None): """ @@ -2393,7 +2444,7 @@ class ParsableOctetString(Constructable, Castable, Primitive): """ if self._parsed is None or self._parsed[1:3] != (spec, spec_params): - parsed_value, _ = _parse_build(self._merge_chunks(), spec=spec, spec_params=spec_params) + parsed_value, _ = _parse_build(self.__bytes__(), spec=spec, spec_params=spec_params) self._parsed = (parsed_value, spec, spec_params) return self._parsed[0] @@ -2403,17 +2454,11 @@ class ParsableOctetString(Constructable, Castable, Primitive): A byte string """ - return self.contents - - def _as_chunk(self): - """ - Allows reconstructing indefinite length values - - :return: - A byte string - """ - - return self.contents + if self.contents is None: + return b'' + if self._bytes is None: + self._bytes = self._merge_chunks() + return self._bytes @property def native(self): @@ -2427,12 +2472,10 @@ class ParsableOctetString(Constructable, Castable, Primitive): if self.contents is None: return None - if self._native is None: - if self._parsed is not None: - self._native = self._parsed[0].native - else: - self._native = self._merge_chunks() - return self._native + if self._parsed is not None: + return self._parsed[0].native + else: + return self.__bytes__() @property def parsed(self): @@ -2500,6 +2543,10 @@ 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): @@ -2522,35 +2569,16 @@ class ParsableOctetBitString(ParsableOctetString): type_name(value) )) - self._native = value + self._bytes = value # Set the unused bits to 0 self.contents = b'\x00' + value self._header = None + if self._indefinite: + self._indefinite = False + self.method = 0 if self._trailer != b'': self._trailer = b'' - def __bytes__(self): - """ - :return: - A byte string - """ - - return self._merge_chunks() - - def _as_chunk(self): - """ - Allows reconstructing indefinite length values - - :return: - A byte string - """ - - # 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] - return self.contents[1:] - class Null(Primitive): """ diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 044eb10..374f628 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -88,7 +88,7 @@ class DNSName(IA5String): if not isinstance(other, DNSName): return False - return self.contents.lower() == other.contents.lower() + return self.__unicode__().lower() == other.__unicode__().lower() def set(self, value): """ @@ -112,7 +112,7 @@ class DNSName(IA5String): else: encoded_value = value.encode(self._encoding) - self._native = value + self._unicode = value self.contents = encoded_value self._header = None if self._trailer != b'': @@ -138,8 +138,7 @@ class URI(IA5String): type_name(value) )) - self._normalized = True - self._native = value + self._unicode = value self.contents = iri_to_uri(value) self._header = None if self._trailer != b'': @@ -170,7 +169,11 @@ class URI(IA5String): A unicode string """ - return uri_to_iri(self.contents) + if self.contents is None: + return '' + if self._unicode is None: + self._unicode = uri_to_iri(self._merge_chunks()) + return self._unicode class EmailAddress(IA5String): @@ -222,7 +225,8 @@ class EmailAddress(IA5String): else: encoded_value = value.encode('ascii') - self._native = value + self._normalized = True + self._unicode = value self.contents = encoded_value self._header = None if self._trailer != b'': @@ -234,12 +238,14 @@ class EmailAddress(IA5String): A unicode string """ - if self.contents.find(b'@') == -1: - return self.contents.decode('ascii') - - mailbox, hostname = self.contents.rsplit(b'@', 1) - - return mailbox.decode('ascii') + '@' + hostname.decode('idna') + if self._unicode is None: + contents = self._merge_chunks() + if contents.find(b'@') == -1: + self._unicode = contents.decode('ascii') + else: + mailbox, hostname = contents.rsplit(b'@', 1) + self._unicode = mailbox.decode('ascii') + '@' + hostname.decode('idna') + return self._unicode def __ne__(self, other): return not self == other @@ -356,6 +362,7 @@ class IPAddress(OctetString): self._native = original_value self.contents = inet_pton(family, value) + cidr_bytes + self._bytes = self.contents self._header = None if self._trailer != b'': self._trailer = b'' @@ -406,7 +413,7 @@ class IPAddress(OctetString): if not isinstance(other, IPAddress): return False - return self.contents == other.contents + return self.__bytes__() == other.__bytes__() class Attribute(Sequence): diff --git a/tests/test_core.py b/tests/test_core.py index 2006c34..7ba5f3b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -494,6 +494,13 @@ class CoreTests(unittest.TestCase): self.assertEqual(b'1\x09\x02\x01\x01\x02\x01\x02\x02\x01\x03', st.dump()) def test_indefinite_length_octet_string(self): + data = b'$\x80\x04\x02\x01\x01\x04\x01\x01\x00\x00' + a = core.OctetString.load(data) + self.assertEqual(b'\x01\x01\x01', a.native) + self.assertEqual(b'\x01\x01\x01', a.__bytes__()) + self.assertEqual(1, a.method) + + def test_indefinite_length_octet_string_2(self): data = b'$\x80\x04\r\x8d\xff\xf0\x98\x076\xaf\x93nB:\xcf\xcc\x04\x15\x92w\xf7\xf0\xe4y\xff\xc7\xdc3\xb2\xd0={\x1a\x18mDr\xaaI\x00\x00' a = core.OctetString.load(data) self.assertEqual( @@ -505,6 +512,7 @@ class CoreTests(unittest.TestCase): data = b'\x24\x80\x24\x80\x24\x80\x04\x00\x00\x00\x00\x00\x00\x00' a = core.load(data) self.assertEqual(b'', a.native) + self.assertEqual(b'', a.__bytes__()) self.assertEqual(1, a.method) self.assertEqual(b'\x04\x00', a.dump(force=True)) @@ -519,6 +527,7 @@ class CoreTests(unittest.TestCase): data = b'$\x80\x04\x02\x04\x01\x04\x01\x01\x00\x00' a = core.ParsableOctetString.load(data) self.assertEqual(b'\x04\x01\x01', a.parsed.dump()) + self.assertEqual(b'\x04\x01\x01', a.__bytes__()) self.assertEqual(1, a.method) self.assertEqual(b'\x01', a.parsed.native) self.assertEqual(b'\x01', a.native) @@ -528,6 +537,7 @@ class CoreTests(unittest.TestCase): data = b'\x2C\x80\x0C\x02\x61\x62\x0C\x01\x63\x00\x00' a = core.UTF8String.load(data) self.assertEqual('abc', a.native) + self.assertEqual('abc', a.__unicode__()) self.assertEqual(1, a.method) # Ensure a forced re-encoding is proper DER self.assertEqual(b'\x0C\x03\x61\x62\x63', a.dump(force=True)) @@ -546,10 +556,12 @@ class CoreTests(unittest.TestCase): data = b'#\x80\x00\x03\x02\x00\x01\x03\x02\x00\x04\x00\x00' a = core.OctetBitString.load(data) self.assertEqual(b'\x01\x04', a.native) + self.assertEqual(b'\x01\x04', a.__bytes__()) def test_indefinite_length_parsable_octet_bit_string(self): data = b'#\x80\x00\x03\x03\x00\x0C\x02\x03\x03\x00\x61\x62\x00\x00' a = core.ParsableOctetBitString.load(data) self.assertEqual(b'\x0C\x02\x61\x62', a.parsed.dump()) + self.assertEqual(b'\x0C\x02\x61\x62', a.__bytes__()) self.assertEqual('ab', a.parsed.native) self.assertEqual('ab', a.native) diff --git a/tests/test_x509.py b/tests/test_x509.py index 5df4c5d..c9ed740 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -113,6 +113,18 @@ class X509Tests(unittest.TestCase): self.assertEqual(der_bytes, x509.IPAddress(unicode_string).dump()) self.assertEqual(unicode_string, x509.IPAddress.load(der_bytes).native) + def test_dnsname(self): + e = x509.DNSName('example.com') + self.assertEqual('example.com', e.native) + self.assertEqual('example.com', e.__unicode__()) + self.assertEqual(b'\x16\x0Bexample.com', e.dump()) + + def test_indef_dnsname(self): + e = x509.DNSName.load(b'\x36\x80\x16\x04exam\x16\x07ple.com\x00\x00') + self.assertEqual('example.com', e.native) + self.assertEqual('example.com', e.__unicode__()) + self.assertEqual(b'\x16\x0Bexample.com', e.dump(force=True)) + def test_dnsname_begin_dot(self): self.assertEqual(b'\x16\x03.gr', x509.DNSName('.gr').dump()) @@ -158,6 +170,18 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(one, two) + def test_uri(self): + u = x509.URI('https://example.com') + self.assertEqual('https://example.com', u.native) + self.assertEqual('https://example.com', u.__unicode__()) + self.assertEqual(b'\x16\x13https://example.com', u.dump()) + + def test_indef_uri(self): + u = x509.URI.load(b'\x36\x80\x16\x07https:/\x16\x07/exampl\x16\x05e.com\x00\x00') + self.assertEqual('https://example.com', u.native) + self.assertEqual('https://example.com', u.__unicode__()) + self.assertEqual(b'\x16\x13https://example.com', u.dump(force=True)) + @staticmethod def compare_uri_info(): return ( @@ -210,6 +234,18 @@ class X509Tests(unittest.TestCase): else: self.assertNotEqual(one, two) + def test_email_address(self): + e = x509.EmailAddress('john@example.com') + self.assertEqual('john@example.com', e.native) + self.assertEqual('john@example.com', e.__unicode__()) + self.assertEqual(b'\x16\x10john@example.com', e.dump()) + + def test_indef_email_address(self): + e = x509.EmailAddress.load(b'\x36\x80\x16\x07john@ex\x16\x09ample.com\x00\x00') + self.assertEqual('john@example.com', e.native) + self.assertEqual('john@example.com', e.__unicode__()) + self.assertEqual(b'\x16\x10john@example.com', e.dump(force=True)) + @staticmethod def compare_email_address_info(): return ( -- cgit v1.2.3 From ec51ff4ddd97f65a8ad8afe319138bdcf048171f Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 10 Mar 2017 14:23:54 -0500 Subject: Add support for setup.py clean -a --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7208e10..f4e9e18 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ from asn1crypto import version class CleanCommand(Command): user_options = [ - ('all', None, '(Compatibility with original clean command)') + ('all', 'a', '(Compatibility with original clean command)'), ] def initialize_options(self): -- cgit v1.2.3 From 7806ea6954a47d4e747e31e477cbce16e67c2266 Mon Sep 17 00:00:00 2001 From: wbond Date: Sat, 11 Mar 2017 06:29:23 -0500 Subject: Ensure .copy() preserves new ._bytes and ._unicode attributes --- asn1crypto/core.py | 168 +++++++++++++++++++++++++++++++---------------------- tests/test_core.py | 14 +++++ 2 files changed, 114 insertions(+), 68 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 4adf5bc..0fc16e6 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -487,11 +487,6 @@ class Asn1Value(object): self.contents = other.contents self._native = copy_func(other._native) - if isinstance(other, Constructable): - self.method = copy_func(other.method) - self._indefinite = copy_func(other._indefinite) - if hasattr(other, '_parsed'): - self._parsed = copy_func(other._parsed) def debug(self, nest_level=1): """ @@ -580,7 +575,7 @@ class ValueMap(): cls._reverse_map[value] = key -class Castable(): +class Castable(object): """ A mixin to handle converting an object between different classes that represent the same encoded value, but with different rules for converting @@ -625,7 +620,7 @@ class Castable(): return new_obj -class Constructable(): +class Constructable(object): """ A mixin to handle string types that may be constructed from chunks contained within an indefinite length BER-encoded container @@ -679,6 +674,22 @@ class Constructable(): return self.contents return self.contents[self._chunks_offset:] + def _copy(self, other, copy_func): + """ + Copies the contents of another Constructable 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(Constructable, self)._copy(other, copy_func) + self.method = other.method + self._indefinite = other._indefinite + class Void(Asn1Value): """ @@ -851,6 +862,21 @@ class Any(Asn1Value): raise e return self._parsed[0] + def _copy(self, other, copy_func): + """ + Copies the contents of another Any 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(Any, self)._copy(other, copy_func) + self._parsed = copy_func(other._parsed) + def dump(self, force=False): """ Encodes the value using DER @@ -1147,7 +1173,7 @@ class Choice(Asn1Value): def _copy(self, other, copy_func): """ - Copies the contents of another Asn1Value object to itself + Copies the contents of another Choice object to itself :param object: Another instance of the same class @@ -1157,17 +1183,7 @@ class Choice(Asn1Value): lists, dicts and objects """ - if self.__class__ != other.__class__: - raise TypeError(unwrap( - ''' - Can not copy values from %s object to %s object - ''', - type_name(other), - type_name(self) - )) - - self.contents = other.contents - self._native = copy_func(other._native) + super(Choice, self)._copy(other, copy_func) self._choice = other._choice self._name = other._name self._parsed = copy_func(other._parsed) @@ -1656,6 +1672,21 @@ class AbstractString(Constructable, Primitive): self._unicode = self._merge_chunks().decode(self._encoding) return self._unicode + def _copy(self, other, copy_func): + """ + Copies the contents of another AbstractString 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(AbstractString, self)._copy(other, copy_func) + self._unicode = other._unicode + @property def native(self): """ @@ -2151,6 +2182,21 @@ class OctetBitString(Constructable, Castable, Primitive): self._bytes = self._merge_chunks() return self._bytes + def _copy(self, other, copy_func): + """ + Copies the contents of another OctetBitString 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(OctetBitString, self)._copy(other, copy_func) + self._bytes = other._bytes + @property def native(self): """ @@ -2298,6 +2344,21 @@ class OctetString(Constructable, Castable, Primitive): self._bytes = self._merge_chunks() return self._bytes + def _copy(self, other, copy_func): + """ + Copies the contents of another OctetString 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(OctetString, self)._copy(other, copy_func) + self._bytes = other._bytes + @property def native(self): """ @@ -2460,6 +2521,22 @@ class ParsableOctetString(Constructable, Castable, Primitive): self._bytes = self._merge_chunks() return self._bytes + def _copy(self, other, copy_func): + """ + Copies the contents of another ParsableOctetString 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(ParsableOctetString, self)._copy(other, copy_func) + self._bytes = other._bytes + self._parsed = copy_func(other._parsed) + @property def native(self): """ @@ -2491,31 +2568,6 @@ class ParsableOctetString(Constructable, Castable, Primitive): return self._parsed[0] - def _copy(self, other, copy_func): - """ - Copies the contents of another ParsableOctetString 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 - """ - - if self.__class__ != other.__class__: - raise TypeError(unwrap( - ''' - Can not copy values from %s object to %s object - ''', - type_name(other), - type_name(self) - )) - - self.contents = other.contents - self._native = copy_func(other._native) - self._parsed = copy_func(other._parsed) - def dump(self, force=False): """ Encodes the value using DER @@ -3656,7 +3708,7 @@ class Sequence(Asn1Value): def _copy(self, other, copy_func): """ - Copies the contents of another Asn1Value object to itself + Copies the contents of another Sequence object to itself :param object: Another instance of the same class @@ -3666,17 +3718,7 @@ class Sequence(Asn1Value): lists, dicts and objects """ - if self.__class__ != other.__class__: - raise TypeError(unwrap( - ''' - Can not copy values from %s object to %s object - ''', - type_name(other), - type_name(self) - )) - - self.contents = other.contents - self._native = copy_func(other._native) + super(Sequence, self)._copy(other, copy_func) if self.children is not None: self.children = [] for child in other.children: @@ -4122,7 +4164,7 @@ class SequenceOf(Asn1Value): def _copy(self, other, copy_func): """ - Copies the contents of another Asn1Value object to itself + Copies the contents of another SequenceOf object to itself :param object: Another instance of the same class @@ -4132,17 +4174,7 @@ class SequenceOf(Asn1Value): lists, dicts and objects """ - if self.__class__ != other.__class__: - raise TypeError(unwrap( - ''' - Can not copy values from %s object to %s object - ''', - type_name(other), - type_name(self) - )) - - self.contents = other.contents - self._native = copy_func(other._native) + super(SequenceOf, self)._copy(other, copy_func) if self.children is not None: self.children = [] for child in other.children: diff --git a/tests/test_core.py b/tests/test_core.py index 7ba5f3b..b4c9e58 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -499,6 +499,8 @@ class CoreTests(unittest.TestCase): self.assertEqual(b'\x01\x01\x01', a.native) self.assertEqual(b'\x01\x01\x01', a.__bytes__()) self.assertEqual(1, a.method) + # Test copying moves internal state + self.assertEqual(a._bytes, a.copy()._bytes) def test_indefinite_length_octet_string_2(self): data = b'$\x80\x04\r\x8d\xff\xf0\x98\x076\xaf\x93nB:\xcf\xcc\x04\x15\x92w\xf7\xf0\xe4y\xff\xc7\xdc3\xb2\xd0={\x1a\x18mDr\xaaI\x00\x00' @@ -515,6 +517,8 @@ class CoreTests(unittest.TestCase): self.assertEqual(b'', a.__bytes__()) self.assertEqual(1, a.method) self.assertEqual(b'\x04\x00', a.dump(force=True)) + # Test copying moves internal state + self.assertEqual(a._bytes, a.copy()._bytes) def test_indefinite_length_integer_octet_string(self): data = b'$\x80\x04\x02\x01\x01\x04\x01\x01\x00\x00' @@ -532,6 +536,9 @@ class CoreTests(unittest.TestCase): self.assertEqual(b'\x01', a.parsed.native) self.assertEqual(b'\x01', a.native) self.assertEqual(b'\x04\x01\x01', a.cast(core.OctetString).native) + # Test copying moves internal state + self.assertEqual(a._bytes, a.copy()._bytes) + self.assertEqual(a._parsed, a.copy()._parsed) def test_indefinite_length_utf8string(self): data = b'\x2C\x80\x0C\x02\x61\x62\x0C\x01\x63\x00\x00' @@ -541,6 +548,8 @@ class CoreTests(unittest.TestCase): self.assertEqual(1, a.method) # Ensure a forced re-encoding is proper DER self.assertEqual(b'\x0C\x03\x61\x62\x63', a.dump(force=True)) + # Test copying moves internal state + self.assertEqual(a._unicode, a.copy()._unicode) def test_indefinite_length_bit_string(self): data = b'#\x80\x00\x03\x02\x00\x01\x03\x02\x02\x04\x00\x00' @@ -557,6 +566,8 @@ class CoreTests(unittest.TestCase): a = core.OctetBitString.load(data) self.assertEqual(b'\x01\x04', a.native) self.assertEqual(b'\x01\x04', a.__bytes__()) + # Test copying moves internal state + self.assertEqual(a._bytes, a.copy()._bytes) def test_indefinite_length_parsable_octet_bit_string(self): data = b'#\x80\x00\x03\x03\x00\x0C\x02\x03\x03\x00\x61\x62\x00\x00' @@ -565,3 +576,6 @@ class CoreTests(unittest.TestCase): self.assertEqual(b'\x0C\x02\x61\x62', a.__bytes__()) self.assertEqual('ab', a.parsed.native) self.assertEqual('ab', a.native) + # Test copying moves internal state + self.assertEqual(a._bytes, a.copy()._bytes) + self.assertEqual(a._parsed, a.copy()._parsed) -- cgit v1.2.3 From eff254fba6519fadb3cded956246ec510d3e72e2 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 15 Mar 2017 04:58:00 -0400 Subject: Version 0.22.0 --- asn1crypto/version.py | 4 ++-- changelog.md | 13 +++++++++++++ readme.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/asn1crypto/version.py b/asn1crypto/version.py index 7de299b..b9d2775 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.21.1' -__version_info__ = (0, 21, 1) +__version__ = '0.22.0' +__version_info__ = (0, 22, 0) diff --git a/changelog.md b/changelog.md index 1e18060..792740f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # changelog +## 0.22.0 + + - Added `parser.peek()` + - Implemented proper support for BER-encoded indefinite length strings of + all kinds - `core.BitString`, `core.OctetString` and all of the `core` + classes that are natively represented as Python unicode strings + - Fixed a bug with encoding LDAP URLs in `x509.URI` + - Correct `x509.DNSName` to allow a leading `.`, such as when used with + `x509.NameConstraints` + - Fixed an issue with dumping the parsed contents of `core.Any` when + explicitly tagged + - Custom `setup.py clean` now accepts the short `-a` flag for compatibility + ## 0.21.1 - Fixed a regression where explicit tagging of a field containing a diff --git a/readme.md b/readme.md index d2570ec..39f9285 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,7 @@ faster to an order of magnitude of more. ## Current Release -0.21.1 - [changelog](changelog.md) +0.22.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 6d1763ebf6db41e8b871d8a2acd2659f6033d1aa Mon Sep 17 00:00:00 2001 From: Thierry Bastian Date: Mon, 10 Apr 2017 14:46:38 +0200 Subject: Fixing deprecation warning from python 3.6 (#44) --- asn1crypto/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 0fc16e6..94940ec 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -101,7 +101,7 @@ METHOD_NUM_TO_NAME_MAP = { } -_OID_RE = re.compile('^\d+(\.\d+)*$') +_OID_RE = re.compile(r'^\d+(\.\d+)*$') # A global tracker to ensure that _setup() is called for every class, even -- cgit v1.2.3 From 040d4b56d78431bec3d2437f6c5b34fbc02e7f79 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 11 Apr 2017 12:18:21 -0400 Subject: Update copyright year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9172400..cd7ac9e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2016 Will Bond +Copyright (c) 2015-2017 Will Bond 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 -- cgit v1.2.3 From 64ab19707acb644772a9ac3d94479fbe79b49365 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 9 May 2017 09:41:15 -0400 Subject: Fix unicode error when looking for OpenSSL on Python 2 when a file path contains non-ASCII paths --- asn1crypto/_perf/_big_num_ctypes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/asn1crypto/_perf/_big_num_ctypes.py b/asn1crypto/_perf/_big_num_ctypes.py index dc4d853..8e37e9b 100644 --- a/asn1crypto/_perf/_big_num_ctypes.py +++ b/asn1crypto/_perf/_big_num_ctypes.py @@ -21,6 +21,8 @@ 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 @@ -28,7 +30,9 @@ from .._ffi import LibraryNotFoundError, FFIEngineError try: - libcrypto_path = find_library('crypto') + # 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') -- cgit v1.2.3 From 7c960bc748f40028e0850f737a0199314e0da8a0 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 9 May 2017 10:14:19 -0400 Subject: Upgrade AppVeyor to use pypy2 5.7.1 because of segfaults on 5.3.1 --- appveyor.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7741087..fdd2dd6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,10 +6,10 @@ install: if (!(Test-Path "$env:PYTMP")) { New-Item -ItemType directory -Path "$env:PYTMP" | Out-Null; } - if (!(Test-Path "${env:PYTMP}\pypy2-v5.3.1-win32.zip")) { - (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.3.1-win32.zip', "${env:PYTMP}\pypy2-v5.3.1-win32.zip"); + if (!(Test-Path "${env:PYTMP}\pypy2-v5.7.1-win32.zip")) { + (New-Object Net.WebClient).DownloadFile('https://bitbucket.org/pypy/pypy/downloads/pypy2-v5.7.1-win32.zip', "${env:PYTMP}\pypy2-v5.7.1-win32.zip"); } - 7z x -y "${env:PYTMP}\pypy2-v5.3.1-win32.zip" -oC:\ | Out-Null; + 7z x -y "${env:PYTMP}\pypy2-v5.7.1-win32.zip" -oC:\ | Out-Null; [Byte[]] $geotrustCaBytes = 0x30,0x82,0x03,0x7C,0x30,0x82,0x02,0x64,0xA0,0x03,0x02,0x01,0x02, 0x02,0x10,0x18,0xAC,0xB5,0x6A,0xFD,0x69,0xB6,0x15,0x3A,0x63,0x6C,0xAF,0xDA,0xFA,0xC4,0xA1,0x30, @@ -89,9 +89,9 @@ test_script: remove-item env:\OSCRYPTO_USE_WINLEGACY; - ps: '& C:\Python33-x64\python run.py deps' - ps: '& C:\Python33-x64\python run.py ci' - - ps: '& C:\pypy2-v5.3.1-win32\pypy run.py deps' - - ps: '& C:\pypy2-v5.3.1-win32\pypy run.py ci' + - ps: '& C:\pypy2-v5.7.1-win32\pypy run.py deps' + - ps: '& C:\pypy2-v5.7.1-win32\pypy run.py ci' - ps: > $env:OSCRYPTO_USE_WINLEGACY = "true"; - & C:\pypy2-v5.3.1-win32\pypy run.py ci; + & C:\pypy2-v5.7.1-win32\pypy run.py ci; remove-item env:\OSCRYPTO_USE_WINLEGACY; -- cgit v1.2.3 From e054cdeca56d6e5aaccc33dbeb87469281030b43 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 9 May 2017 10:34:32 -0400 Subject: Require WebClient uses TLS 1.2 to connect to curl.haxx.se --- dev/deps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/deps.py b/dev/deps.py index 72c6149..36370f0 100644 --- a/dev/deps.py +++ b/dev/deps.py @@ -81,7 +81,8 @@ def _download(url, dest): if sys.platform == 'win32': system_root = os.environ.get('SystemRoot') powershell_exe = os.path.join('system32\\WindowsPowerShell\\v1.0\\powershell.exe') - code = "(New-Object Net.WebClient).DownloadFile('%s', '%s');" % (url, dest_path) + code = "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;" + code += " (New-Object Net.WebClient).DownloadFile('%s', '%s');" % (url, dest_path) _execute([powershell_exe, '-Command', code], dest) else: -- cgit v1.2.3 From ad0e19eee39c9c7ae781645dc07afb67b6e5e5f8 Mon Sep 17 00:00:00 2001 From: Anthony Alba Date: Tue, 16 May 2017 22:21:18 +0800 Subject: Add support for CMS Attribute Protection RFC6211 --- asn1crypto/cms.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 8218797..7b62ab3 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -99,6 +99,8 @@ class CMSAttributeType(ObjectIdentifier): '1.2.840.113549.1.9.6': 'counter_signature', # https://tools.ietf.org/html/rfc3161#page-20 '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', } @@ -123,6 +125,14 @@ class ContentType(ObjectIdentifier): } +class CMSAlgorithmProtection(Sequence): + _fields = [ + ('digest_algorithm', DigestAlgorithm), + ('signature_algorithm', SignedDigestAlgorithm, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('mac_algorithm', HmacAlgorithm, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ] + + class SetOfContentType(SetOf): _child_spec = ContentType @@ -139,6 +149,10 @@ class SetOfAny(SetOf): _child_spec = Any +class SetOfCMSAlgorithmProtection(SetOf): + _child_spec = CMSAlgorithmProtection + + class CMSAttribute(Sequence): _fields = [ ('type', CMSAttributeType), @@ -912,4 +926,5 @@ CMSAttribute._oid_specs = { 'signing_time': SetOfTime, 'counter_signature': SignerInfos, 'signature_time_stamp_token': SetOfContentInfo, + 'cms_algorithm_protection': SetOfCMSAlgorithmProtection, } -- cgit v1.2.3 From baa00e7aa7fe205757db516fd7c5a2176f73c01b Mon Sep 17 00:00:00 2001 From: Troy Ross Date: Wed, 17 May 2017 16:24:38 -0600 Subject: make next update in crl optional --- asn1crypto/crl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 0baf5b4..0275847 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -278,7 +278,7 @@ class TbsCertList(Sequence): ('signature', SignedDigestAlgorithm), ('issuer', Name), ('this_update', Time), - ('next_update', Time), + ('next_update', Time, {'optional': True}), ('revoked_certificates', RevokedCertificates, {'optional': True}), ('crl_extensions', TBSCertListExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), ] -- cgit v1.2.3 From 42e7a83a1eea7216e14f36810eb1a7a33b825241 Mon Sep 17 00:00:00 2001 From: Pim Coster Date: Sat, 20 May 2017 19:56:51 +0200 Subject: Add sha256_fingerprint property --- asn1crypto/x509.py | 10 ++++++++++ tests/test_x509.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 374f628..8c6f1eb 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2518,6 +2518,16 @@ class Certificate(Sequence): self._sha256 = hashlib.sha256(self.dump()).digest() return self._sha256 + @property + def sha256_fingerprint(self): + """ + :return: + A unicode string of the SHA-256 hash, formatted using hex encoding + with a space between each pair of characters, all uppercase + """ + + return ' '.join('%02X' % c for c in bytes_to_list(self.sha256)) + def is_valid_domain_ip(self, domain_ip): """ Check if a domain name or IP address is valid according to the diff --git a/tests/test_x509.py b/tests/test_x509.py index c9ed740..6d9af6d 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -492,6 +492,12 @@ class X509Tests(unittest.TestCase): cert = self._load_cert('geotrust_certs/codex.crt') self.assertEqual('78 1C 9F 87 59 93 52 08 D2 21 FA 70 6C C5 F9 76 12 C9 6D 8B', cert.sha1_fingerprint) + def test_sha256_fingerprint(self): + cert = self._load_cert('geotrust_certs/codex.crt') + self.assertEqual( + 'E5 6D 97 3A 22 77 55 E4 85 6F 71 78 DA 4D 69 93 0C E2 87 F8 85 5E BE 1A 8C F7 FE 78 80 EB A5 F0', + cert.sha256_fingerprint) + def test_punycode_common_name(self): cert = self._load_cert('chromium/punycodetest.pem') self.assertEqual('xn--wgv71a119e.com', cert['tbs_certificate']['subject'].native['common_name']) -- cgit v1.2.3 From 9e74abf60819ada64cc333f7f47a84991107a343 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 13 Jun 2017 20:34:22 -0400 Subject: Initial support for RFC6962 --- asn1crypto/ocsp.py | 3 +++ asn1crypto/x509.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index bcdf1eb..52de481 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -351,6 +351,8 @@ class SingleResponseExtensionId(ObjectIdentifier): '2.5.29.21': 'crl_reason', '2.5.29.24': 'invalidity_date', '2.5.29.29': 'certificate_issuer', + # https://tools.ietf.org/html/rfc6962.html#page-13 + '1.3.6.1.4.1.11129.2.4.5': 'signed_certificate_timestamp_list', } @@ -368,6 +370,7 @@ class SingleResponseExtension(Sequence): 'crl_reason': CRLReason, 'invalidity_date': GeneralizedTime, 'certificate_issuer': GeneralNames, + 'signed_certificate_timestamp_list': OctetString, } diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 8c6f1eb..5fcf89c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1766,6 +1766,8 @@ class ExtensionId(ObjectIdentifier): '1.3.6.1.5.5.7.48.1.5': 'ocsp_no_check', '1.2.840.113533.7.65.0': 'entrust_version_extension', '2.16.840.1.113730.1.1': 'netscape_certificate_type', + # https://tools.ietf.org/html/rfc6962.html#page-14 + '1.3.6.1.4.1.11129.2.4.2': 'signed_certificate_timestamp_list', } @@ -1800,6 +1802,7 @@ class Extension(Sequence): 'ocsp_no_check': Null, 'entrust_version_extension': EntrustVersionInfo, 'netscape_certificate_type': NetscapeCertificateType, + 'signed_certificate_timestamp_list': OctetString, } -- cgit v1.2.3 From b5db1c30435ca7b311315f9309d93f6b4a44b529 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 13 Jun 2017 20:34:53 -0400 Subject: Fix Ans1Value -> Asn1Value --- asn1crypto/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 94940ec..9a29c1f 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -774,7 +774,7 @@ class Any(Asn1Value): if not isinstance(value, Asn1Value): raise TypeError(unwrap( ''' - value must be an instance of Ans1Value, not %s + value must be an instance of Asn1Value, not %s ''', type_name(value) )) @@ -3389,7 +3389,7 @@ class Sequence(Asn1Value): :return: A tuple containing the following elements: - unicode string of the field name - - Ans1Value class of the field spec + - Asn1Value class of the field spec - Asn1Value class of the value spec - None or dict of params to pass to the field spec - None or Asn1Value class indicating the value spec was derived fomr an OID or a spec callback -- cgit v1.2.3 From b1f50418f031c85e50c8488f2aa30c5f3cee2edc Mon Sep 17 00:00:00 2001 From: Ryan Guest Date: Wed, 26 Jul 2017 20:24:36 -0700 Subject: Fix a couple typos --- asn1crypto/_inet.py | 4 ++-- asn1crypto/core.py | 10 +++++----- asn1crypto/keys.py | 4 ++-- changelog.md | 2 +- docs/universal_types.md | 2 +- readme.md | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/asn1crypto/_inet.py b/asn1crypto/_inet.py index 0d78aa4..0322bb4 100644 --- a/asn1crypto/_inet.py +++ b/asn1crypto/_inet.py @@ -10,7 +10,7 @@ from ._types import byte_cls, bytes_to_list, str_cls, type_name def inet_ntop(address_family, packed_ip): """ - Windows compatiblity shim for socket.inet_ntop(). + Windows compatibility shim for socket.inet_ntop(). :param address_family: socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 @@ -83,7 +83,7 @@ def inet_ntop(address_family, packed_ip): def inet_pton(address_family, ip_string): """ - Windows compatiblity shim for socket.inet_ntop(). + Windows compatibility shim for socket.inet_ntop(). :param address_family: socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 9a29c1f..1665e38 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -337,7 +337,7 @@ class Asn1Value(object): def __str__(self): """ - Since str is differnt in Python 2 and 3, this calls the appropriate + Since str is different in Python 2 and 3, this calls the appropriate method, __unicode__() or __bytes__() :return: @@ -1287,7 +1287,7 @@ class Concat(object): def __str__(self): """ - Since str is differnt in Python 2 and 3, this calls the appropriate + Since str is different in Python 2 and 3, this calls the appropriate method, __unicode__() or __bytes__() :return: @@ -3392,7 +3392,7 @@ class Sequence(Asn1Value): - Asn1Value class of the field spec - Asn1Value class of the value spec - None or dict of params to pass to the field spec - - None or Asn1Value class indicating the value spec was derived fomr an OID or a spec callback + - None or Asn1Value class indicating the value spec was derived from an OID or a spec callback """ name, field_spec, field_params = self._fields[index] @@ -3638,8 +3638,8 @@ class Sequence(Asn1Value): """ Determines the spec to use for the field specified. Depending on how the spec is determined (_oid_pair or _spec_callbacks), it may be - necessary to set preceeding field values before calling this. Usually - specs, if dynamic, are controlled by a preceeding ObjectIdentifier + necessary to set preceding field values before calling this. Usually + specs, if dynamic, are controlled by a preceding ObjectIdentifier field. :param field_name: diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 903725f..35f4363 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -828,7 +828,7 @@ class PrivateKeyInfo(Sequence): Creates a fingerprint that can be compared with a public key to see if the two form a pair. - This fingerprint is not compatiable with fingerprints generated by any + This fingerprint is not compatible with fingerprints generated by any other software. :return: @@ -1191,7 +1191,7 @@ class PublicKeyInfo(Sequence): Creates a fingerprint that can be compared with a private key to see if the two form a pair. - This fingerprint is not compatiable with fingerprints generated by any + This fingerprint is not compatible with fingerprints generated by any other software. :return: diff --git a/changelog.md b/changelog.md index 792740f..65bc016 100644 --- a/changelog.md +++ b/changelog.md @@ -214,7 +214,7 @@ ## 0.12.0 - - Backwards Compatiblity Break: `core.NoValue` was renamed to `core.Void` and + - Backwards Compatibility Break: `core.NoValue` was renamed to `core.Void` and a singleton was added as `core.VOID` - 20-30% improvement in parsing performance - `core.Void` now implements `__nonzero__` diff --git a/docs/universal_types.md b/docs/universal_types.md index c58a323..cc61099 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -563,7 +563,7 @@ class MySequence(Sequence): Throughout the usage of ASN.1 in cryptography, a pattern is present where an `ObjectIdenfitier` is used to determine what specification should be used to interpret another field in a `Sequence`. Usually the other field is an instance -of `Any`, however ocassionally it is an `OctetString` or `OctetBitString`. +of `Any`, however occasionally it is an `OctetString` or `OctetBitString`. *asn1crypto* provides the `_oid_pair` and `_oid_specs` properties of the `Sequence` class to allow handling these situations. diff --git a/readme.md b/readme.md index 39f9285..33473b5 100644 --- a/readme.md +++ b/readme.md @@ -221,7 +221,7 @@ Existing releases can be found at https://pypi.python.org/pypi/asn1crypto. A task named `deps` exists to ensure a modern version of `pip` is installed, along with all necessary testing dependencies. -The `ci` task runs `lint` (if flake8 is avaiable for the version of Python) and +The `ci` task runs `lint` (if flake8 is available for the version of Python) and `coverage` (or `tests` if coverage is not available for the version of Python). If the current directory is a clean git working copy, the coverage data is submitted to codecov.io. -- cgit v1.2.3 From d7cf731ff4fd8c7abd859bb0598b11fd7ef7d0d5 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 4 Aug 2017 10:22:16 -0400 Subject: Allow explicit tagging to be defined on an Asn1Value class --- asn1crypto/core.py | 17 ++++++++++++++--- tests/test_core.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 1665e38..3d7f422 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -230,7 +230,8 @@ class Asn1Value(object): value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs, strict=strict) return value - def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=None, contents=None): + def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=None, contents=None, + no_explicit=False): """ 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 @@ -261,6 +262,11 @@ class Asn1Value(object): :param contents: A byte string of the encoded contents of the value + :param no_explicit: + If explicit tagging info should be removed from this instance. + Used internally to allow contructing the underlying value that + has been wrapped in an explicit tag. + :raises: ValueError - when tag_type, class_ or tag are invalid values """ @@ -324,6 +330,11 @@ class Asn1Value(object): if tag is not None: self.tag = tag + if no_explicit: + self.tag_type = None + self.explicit_tag = None + self.explicit_class = None + if contents is not None: self.contents = contents @@ -4951,7 +4962,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param )) original_value = value info, _ = _parse(contents, len(contents)) - value = _build(*info, spec=spec) + value = _build(*info, spec=spec, spec_params={'no_explicit': True}) value._header = header + value._header value._trailer += trailer or b'' value.tag_type = 'explicit' @@ -5014,7 +5025,7 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param elif spec_params and 'tag_type' in spec_params and spec_params['tag_type'] == 'explicit': original_value = Asn1Value(contents=contents, **spec_params) info, _ = _parse(contents, len(contents)) - value = _build(*info, spec=spec) + value = _build(*info, spec=spec, spec_params={'no_explicit': True}) value._header = header + value._header value._trailer += trailer or b'' value.tag_type = 'explicit' diff --git a/tests/test_core.py b/tests/test_core.py index b4c9e58..09954d6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -109,6 +109,11 @@ class MyOids(core.ObjectIdentifier): '4.5.6': 'def', } +class ApplicationTaggedInteger(core.Integer): + tag_type = 'explicit' + explicit_class = 1 + explicit_tag = 10 + @data_decorator class CoreTests(unittest.TestCase): @@ -579,3 +584,18 @@ class CoreTests(unittest.TestCase): # Test copying moves internal state self.assertEqual(a._bytes, a.copy()._bytes) self.assertEqual(a._parsed, a.copy()._parsed) + + def test_explicit_application_tag(self): + data = b'\x6a\x81\x03\x02\x01\x00' + ati = ApplicationTaggedInteger.load(data) + + self.assertEqual('explicit', ati.tag_type) + self.assertEqual(1, ati.explicit_class) + self.assertEqual(10, ati.explicit_tag) + self.assertEqual(0, ati.class_) + self.assertEqual(2, ati.tag) + self.assertEqual(0, ati.native) + + # The output encoding is DER, whereas the input was not, so + # the length encoding changes from long form to short form + self.assertEqual(b'\x6a\x03\x02\x01\x00', ati.dump(force=True)) -- cgit v1.2.3 From 5b458e503876068a6cbdaf93ea3eab3e304d7e4c Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 4 Aug 2017 19:44:11 +0200 Subject: Include LICENSE and documentation in sdist Closes: #58 Signed-off-by: Christian Heimes --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..40e672e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include readme.md changelog.md +recursive-include docs *.md -- cgit v1.2.3 From 96b6f85be813803b2ba85c44670aaa35b1f3ed6e Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sat, 5 Aug 2017 20:39:55 +0200 Subject: Testcase for nested seq with explicit app tag See issue #63 Signed-off-by: Christian Heimes --- tests/test_core.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 09954d6..acd7ade 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -115,6 +115,46 @@ class ApplicationTaggedInteger(core.Integer): explicit_tag = 10 +class ApplicationTaggedInner(core.Sequence): + """ + TESTCASE DEFINITIONS EXPLICIT TAGS ::= + BEGIN + + INNERSEQ ::= SEQUENCE { + innernumber [21] INTEGER + } + + INNER ::= [APPLICATION 20] INNERSEQ + """ + tag_type = 'explicit' + explicit_class = 1 + explicit_tag = 20 + + _fields = [ + ('innernumber', core.Integer, {'tag_type': 'explicit', 'tag': 21}), + ] + + +class ApplicationTaggedOuter(core.Sequence): + """ + OUTERSEQ ::= SEQUENCE { + outernumber [11] INTEGER, + inner [12] INNER + } + + OUTER ::= [APPLICATION 10] OUTERSEQ + END + """ + tag_type = 'explicit' + explicit_class = 1 + explicit_tag = 10 + + _fields = [ + ('outernumber', core.Integer, {'tag_type': 'explicit', 'tag': 11}), + ('inner', ApplicationTaggedInner, {'tag_type': 'explicit', 'tag': 12}), + ] + + @data_decorator class CoreTests(unittest.TestCase): @@ -599,3 +639,56 @@ class CoreTests(unittest.TestCase): # The output encoding is DER, whereas the input was not, so # the length encoding changes from long form to short form self.assertEqual(b'\x6a\x03\x02\x01\x00', ati.dump(force=True)) + + def test_explicit_application_tag_nested(self): + # tag = [APPLICATION 10] constructed; length = 18 + # OUTER SEQUENCE: tag = [UNIVERSAL 16] constructed; length = 16 + # outernumber : tag = [11] constructed; length = 3 + # INTEGER: tag = [UNIVERSAL 2] primitive; length = 1 + # 23 + # inner : tag = [12] constructed; length = 9 + # tag = [APPLICATION 20] constructed; length = 7 + # INNER SEQUENCE: tag = [UNIVERSAL 16] constructed; length = 5 + # innernumber : tag = [21] constructed; length = 3 + # INTEGER: tag = [UNIVERSAL 2] primitive; length = 1 + # 42 + der = ( + b'\x6A\x12\x30\x10\xAB\x03\x02\x01\x17\xAC\x09\x74' + b'\x07\x30\x05\xB5\x03\x02\x01\x2A' + ) + + ato = ApplicationTaggedOuter.load(der) + self.assertEqual('explicit', ato.tag_type) + self.assertEqual(1, ato.explicit_class) + self.assertEqual(10, ato.explicit_tag) + self.assertEqual(0, ato.class_) + self.assertEqual(16, ato.tag) + self.assertEqual(1, ato.method) + + onum = ato['outernumber'] + self.assertEqual('explicit', onum.tag_type) + self.assertEqual(2, onum.explicit_class) + self.assertEqual(11, onum.explicit_tag) + self.assertEqual(0, onum.class_) + self.assertEqual(2, onum.tag) + self.assertEqual(0, onum.method) + self.assertEqual(23, onum.native) + + ati = ato['inner'] + # HERE BE DRAGONS, ato['inner'] fails with an exception + self.assertEqual('explicit', ati.tag_type) + self.assertEqual(1, ati.explicit_class) + # XXX: 12 or 20? + self.assertEqual(20, ati.explicit_tag) + self.assertEqual(0, ati.class_) + self.assertEqual(2, ati.tag) + self.assertEqual(0, ati.native) + + inum = ati['innernumber'] + self.assertEqual('explicit', inum.tag_type) + self.assertEqual(2, inum.explicit_class) + self.assertEqual(21, inum.explicit_tag) + self.assertEqual(0, inum.class_) + self.assertEqual(2, inum.tag) + self.assertEqual(0, inum.method) + self.assertEqual(42, inum.native) -- cgit v1.2.3 From 42bed2522b9ad7646b122de9be8ccd4aea601899 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 30 Aug 2017 20:49:39 +0100 Subject: correct spelling mistake --- asn1crypto/x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5fcf89c..2cabd24 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -920,7 +920,7 @@ class Name(Choice): :param use_printable: A bool - if PrintableString should be used for encoding instead of - UTF8String. This is for backwards compatiblity with old software. + UTF8String. This is for backwards compatibility with old software. :return: An x509.Name object -- cgit v1.2.3 From 1ba880ef26e37151e71b9968ba1d29b2573cfd43 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Sep 2017 13:08:50 -0400 Subject: Teletex strings inside of x509.DirectoryString() now use Windows-1252 (aka ISO-8859-1) This is because OpenSSL likes to put ISO-8859-1 into TeletexString fields. There is also a new contextmanager x509.strict_teletex() that restores the previous behavior. --- asn1crypto/x509.py | 36 ++++++++++++++++++++- .../mozilla-generated-by-openssl.pkcs7.der | Bin 0 -> 4247 bytes tests/test_cms.py | 15 +++++++++ tests/test_x509.py | 21 ++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/mozilla-generated-by-openssl.pkcs7.der diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 5fcf89c..e8b095b 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -15,6 +15,7 @@ Other type classes are defined that help compose the types listed above. from __future__ import unicode_literals, division, absolute_import, print_function +from contextlib import contextmanager from encodings import idna # noqa import hashlib import re @@ -448,9 +449,42 @@ class PrivateKeyUsagePeriod(Sequence): ] +class NotReallyTeletexString(TeletexString): + """ + OpenSSL (and probably some other libraries) puts ISO-8859-1 + into TeletexString instead of ITU T.61. We use Windows-1252 when + decoding since it is a superset of ISO-8859-1, and less likely to + cause encoding issues, but we stay strict with encoding to prevent + us from creating bad data. + """ + + _decoding_encoding = 'cp1252' + + def __unicode__(self): + """ + :return: + A unicode string + """ + + if self.contents is None: + return '' + if self._unicode is None: + self._unicode = self._merge_chunks().decode(self._decoding_encoding) + return self._unicode + + +@contextmanager +def strict_teletex(): + try: + NotReallyTeletexString._decoding_encoding = 'teletex' + yield + finally: + NotReallyTeletexString._decoding_encoding = 'cp1252' + + class DirectoryString(Choice): _alternatives = [ - ('teletex_string', TeletexString), + ('teletex_string', NotReallyTeletexString), ('printable_string', PrintableString), ('universal_string', UniversalString), ('utf8_string', UTF8String), diff --git a/tests/fixtures/mozilla-generated-by-openssl.pkcs7.der b/tests/fixtures/mozilla-generated-by-openssl.pkcs7.der new file mode 100644 index 0000000..bb0fdc4 Binary files /dev/null and b/tests/fixtures/mozilla-generated-by-openssl.pkcs7.der differ diff --git a/tests/test_cms.py b/tests/test_cms.py index 9e013e7..a6746fa 100644 --- a/tests/test_cms.py +++ b/tests/test_cms.py @@ -886,3 +886,18 @@ class CMSTests(unittest.TestCase): b'\xBF\x6B\x77\x1A\x32\xC2\x0C\x93\xCC\x35\xBC\x66\xC6\x69', signer['signature'].native ) + + def test_bad_teletex_inside_pkcs7(self): + with open(os.path.join(fixtures_dir, 'mozilla-generated-by-openssl.pkcs7.der'), 'rb') as f: + content = cms.ContentInfo.load(f.read())['content'] + self.assertEqual( + util.OrderedDict([ + ('organizational_unit_name', 'Testing'), + ('country_name', 'US'), + ('locality_name', 'Mountain View'), + ('organization_name', 'Addons Testing'), + ('state_or_province_name', 'CA'), + ('common_name', '{02b860db-e71f-48d2-a5a0-82072a93d33c}') + ]), + content['certificates'][0].chosen['tbs_certificate']['subject'].native + ) diff --git a/tests/test_x509.py b/tests/test_x509.py index 6d9af6d..ecd1772 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -3355,3 +3355,24 @@ class X509Tests(unittest.TestCase): cert['tbs_certificate']['validity']['not_before'].native, util.extended_datetime(0, 1, 1, 0, 0, 1, tzinfo=util.timezone.utc) ) + + def test_teletex_that_is_really_latin1(self): + self.assertEqual( + '{}', + x509.DirectoryString.load(b'\x14\x02{}').native + ) + + def test_strict_teletex(self): + with x509.strict_teletex(): + with self.assertRaises(UnicodeDecodeError): + self.assertEqual( + '{}', + x509.DirectoryString.load(b'\x14\x02{}').native + ) + + # Make sure outside of the contextmanager we are back to + # liberal interpretation of TeletexString + self.assertEqual( + '{}', + x509.DirectoryString.load(b'\x14\x02{}').native + ) -- cgit v1.2.3 From 10f6d190d518224775bdb34b61afa1b95b2c33cc Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Sep 2017 14:04:30 -0400 Subject: x509.DistributionPoint().url and x509.Certificate().ocsp_urls now return https:// and ldap:// URLs also --- asn1crypto/x509.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 76e303c..c8399d7 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1527,7 +1527,8 @@ class DistributionPoint(Sequence): for general_name in name.chosen: if general_name.name == 'uniform_resource_identifier': url = general_name.native - if url[0:7] == 'http://': + lurl = url.lower() + if lurl.startswith('http://') or lurl.startswith('https://') or lurl.startswith('ldap://'): self._url = url break @@ -2411,7 +2412,8 @@ class Certificate(Sequence): if location.name != 'uniform_resource_identifier': continue url = location.native - if url.lower()[0:7] == 'http://': + lurl = url.lower() + if lurl.startswith('http://') or lurl.startswith('https://') or lurl.startswith('ldap://'): output.append(url) return output -- cgit v1.2.3 From c3dcc0ad37e06cdddf2da859bf5a5aaa4271a403 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Sep 2017 14:07:50 -0400 Subject: Remove en dashes from various comments and exceptions to help users with unicode issues in terminals --- asn1crypto/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 3d7f422..0e2f65b 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -638,7 +638,7 @@ class Constructable(object): """ # Instance attribute indicating if an object was indefinite - # length when parsed – affects parsing and dumping + # length when parsed - affects parsing and dumping _indefinite = False # Class attribute that indicates the offset into self.contents @@ -2267,7 +2267,7 @@ class IntegerBitString(Constructable, Castable, Primitive): Allows reconstructing indefinite length values :return: - A unicode string of bits – 1s and 0s + A unicode string of bits - 1s and 0s """ extra_bits = int_from_bytes(self.contents[0:1]) @@ -3471,7 +3471,7 @@ class Sequence(Asn1Value): raise ValueError(unwrap( ''' Can not set a native python value to %s, which has the - choice type of %s – value must be an instance of Asn1Value + choice type of %s - value must be an instance of Asn1Value ''', field_name, type_name(value_spec) @@ -3911,7 +3911,7 @@ class SequenceOf(Asn1Value): raise ValueError(unwrap( ''' Can not set a native python value to %s where the - _child_spec is Any – value must be an instance of Asn1Value + _child_spec is Any - value must be an instance of Asn1Value ''', type_name(self) )) @@ -3921,7 +3921,7 @@ class SequenceOf(Asn1Value): raise ValueError(unwrap( ''' Can not set a native python value to %s where the - _child_spec is the choice type %s – value must be an + _child_spec is the choice type %s - value must be an instance of Asn1Value ''', type_name(self), -- cgit v1.2.3 From 3af3664c3b860c20d863b42d21c818cca8a43865 Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Sep 2017 15:10:40 -0400 Subject: Make sure core.Sequence() has all fields before dumping --- asn1crypto/core.py | 13 +++++++++++++ tests/_unittest_compat.py | 16 ++++++++++++++-- tests/test_core.py | 4 ++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 0e2f65b..d9746c0 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -3769,6 +3769,19 @@ class Sequence(Asn1Value): if force: self._set_contents(force=force) + if self._fields and self.children is not None: + for index, (field_name, _, params) in enumerate(self._fields): + if self.children[index] is not VOID: + continue + if 'default' in params or 'optional' in params: + continue + raise ValueError(unwrap( + ''' + Field "%s" is missing from structure + ''', + field_name + )) + return Asn1Value.dump(self) diff --git a/tests/_unittest_compat.py b/tests/_unittest_compat.py index 7b867b4..2d4985d 100644 --- a/tests/_unittest_compat.py +++ b/tests/_unittest_compat.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals, division, absolute_import, print_functi import sys import unittest +import re _non_local = {'patched': False} @@ -17,6 +18,7 @@ def patch(): unittest.TestCase.assertIsInstance = _assert_is_instance unittest.TestCase.assertRaises = _assert_raises + unittest.TestCase.assertRaisesRegexp = _assert_raises_regexp _non_local['patched'] = True @@ -29,14 +31,24 @@ def _assert_is_instance(self, obj, cls, msg=None): self.fail(msg) -def _assert_raises(self, excClass, callableObj=None, *args, **kwargs): # noqa - context = _AssertRaisesContext(excClass, self) +def _assert_raises(self, expected_exception, callableObj=None, *args, **kwargs): # noqa + context = _AssertRaisesContext(expected_exception, self) if callableObj is None: return context with context: callableObj(*args, **kwargs) +def _assert_raises_regexp(self, expected_exception, expected_regexp, callable_obj=None, *args, **kwargs): + if expected_regexp is not None: + expected_regexp = re.compile(expected_regexp) + context = _AssertRaisesContext(expected_exception, self, expected_regexp) + if callable_obj is None: + return context + with context: + callable_obj(*args, **kwargs) + + class _AssertRaisesContext(object): """A context manager used to implement TestCase.assertRaises* methods.""" diff --git a/tests/test_core.py b/tests/test_core.py index 09954d6..31ef8ee 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -599,3 +599,7 @@ class CoreTests(unittest.TestCase): # The output encoding is DER, whereas the input was not, so # the length encoding changes from long form to short form self.assertEqual(b'\x6a\x03\x02\x01\x00', ati.dump(force=True)) + + def test_required_field(self): + with self.assertRaisesRegexp(ValueError, '"id" is missing from structure'): + Seq({'value': core.Integer(5)}).dump() -- cgit v1.2.3 From 627a625cb1b75f8d4cd54567cb3148466b96d49d Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 15 Sep 2017 07:11:36 -0400 Subject: Update explicit tagging implementation to handle nested explicit tags --- asn1crypto/core.py | 425 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 266 insertions(+), 159 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index d9746c0..be3d36b 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -183,13 +183,12 @@ class Asn1Value(object): # structures where a string value is encoded using an incorrect tag _bad_tag = None - # A unicode string or None - "explicit" or "implicit" for - # tagged values, None for normal - tag_type = None + # If the value has been implicitly tagged + implicit = False - # If "explicit"ly tagged, the class and tag for the wrapped header - explicit_class = None - explicit_tag = None + # If explicitly tagged, a tuple of 2-element tuples containing the + # class int and tag int, from innermost to outermost + explicit = None # The BER/DER header bytes _header = None @@ -230,16 +229,29 @@ class Asn1Value(object): value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs, strict=strict) return value - def __init__(self, tag_type=None, class_=None, tag=None, optional=None, default=None, contents=None, - no_explicit=False): + def __init__(self, explicit=None, implicit=None, no_explicit=False, tag_type=None, class_=None, tag=None, + optional=None, default=None, contents=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 args + :param explicit: + An int tag number for explicit tagging, or a 2-element tuple of + class and tag. + + :param implicit: + An int tag number for implicit tagging, or a 2-element tuple of + class and tag. + + :param no_explicit: + If explicit tagging info should be removed from this instance. + Used internally to allow contructing the underlying value that + has been wrapped in an explicit tag. + :param tag_type: None for normal values, or one of "implicit", "explicit" for tagged - values + values. Deprecated in favor of explicit and implicit params. :param class_: The class for the value - defaults to "universal" if tag_type is @@ -248,10 +260,11 @@ class Asn1Value(object): - "application" - "context" - "private" + Deprecated in favor of explicit and implicit params. :param tag: The integer tag to override - usually this is used with tag_type or - class_ + class_. Deprecated in favor of explicit and implicit params. :param optional: Dummy parameter that allows "optional" key in spec param dicts @@ -262,13 +275,8 @@ class Asn1Value(object): :param contents: A byte string of the encoded contents of the value - :param no_explicit: - If explicit tagging info should be removed from this instance. - Used internally to allow contructing the underlying value that - has been wrapped in an explicit tag. - :raises: - ValueError - when tag_type, class_ or tag are invalid values + ValueError - when implicit, explicit, tag_type, class_ or tag are invalid values """ try: @@ -278,43 +286,98 @@ class Asn1Value(object): self._setup() _SETUP_CLASSES[cls] = True + # Normalize tagging values + if explicit is not None: + if isinstance(explicit, int_types): + if class_ is None: + class_ = 'context' + explicit = (class_, explicit) + # Prevent both explicit and tag_type == 'explicit' + if tag_type == 'explicit': + tag_type = None + tag = None + + if implicit is not None: + if isinstance(implicit, int_types): + if class_ is None: + class_ = 'context' + implicit = (class_, implicit) + # Prevent both implicit and tag_type == 'implicit' + if tag_type == 'implicit': + tag_type = None + tag = None + + # Convert old tag_type API to explicit/implicit params if tag_type is not None: - if tag_type not in ('implicit', 'explicit'): + if class_ is None: + class_ = 'context' + if tag_type == 'explicit': + explicit = (class_, tag) + elif tag_type == 'implicit': + implicit = (class_, tag) + else: raise ValueError(unwrap( ''' tag_type must be one of "implicit", "explicit", not %s ''', repr(tag_type) )) - self.tag_type = tag_type - if class_ is None: - class_ = 'context' + if explicit is not None: + # Ensure we have a tuple of 2-element tuples + if len(explicit) == 2 and isinstance(explicit[1], int_types): + explicit = (explicit, ) + for class_, tag in explicit: + invalid_class = None + if isinstance(class_, int_types): + if class_ not in CLASS_NUM_TO_NAME_MAP: + invalid_class = class_ + else: + if class_ not in CLASS_NAME_TO_NUM_MAP: + invalid_class = class_ + class_ = CLASS_NAME_TO_NUM_MAP[class_] + if invalid_class is not None: + raise ValueError(unwrap( + ''' + explicit class must be one of "universal", "application", + "context", "private", not %s + ''', + repr(invalid_class) + )) + if tag is not None: + if not isinstance(tag, int_types): + raise TypeError(unwrap( + ''' + explicit tag must be an integer, not %s + ''', + type_name(tag) + )) + if self.explicit is None: + self.explicit = ((class_, tag), ) + else: + self.explicit = self.explicit + ((class_, tag), ) + + elif implicit is not None: + class_, tag = implicit if class_ not in CLASS_NAME_TO_NUM_MAP: raise ValueError(unwrap( ''' - class_ must be one of "universal", "application", + implicit class must be one of "universal", "application", "context", "private", not %s ''', repr(class_) )) - class_ = CLASS_NAME_TO_NUM_MAP[class_] - if tag is not None: if not isinstance(tag, int_types): raise TypeError(unwrap( ''' - tag must be an integer, not %s + implicit tag must be an integer, not %s ''', type_name(tag) )) - - if tag_type == 'implicit': - self.class_ = class_ - self.tag = tag - else: - self.explicit_class = class_ - self.explicit_tag = tag + self.class_ = CLASS_NAME_TO_NUM_MAP[class_] + self.tag = tag + self.implicit = True else: if class_ is not None: if class_ not in CLASS_NUM_TO_NAME_MAP: @@ -331,9 +394,7 @@ class Asn1Value(object): self.tag = tag if no_explicit: - self.tag_type = None - self.explicit_tag = None - self.explicit_class = None + self.explicit = None if contents is not None: self.contents = contents @@ -400,11 +461,10 @@ class Asn1Value(object): """ new_obj = self.__class__() - new_obj.tag_type = self.tag_type new_obj.class_ = self.class_ new_obj.tag = self.tag - new_obj.explicit_class = self.explicit_class - new_obj.explicit_tag = self.explicit_tag + new_obj.implicit = self.implicit + new_obj.explicit = self.explicit return new_obj def __copy__(self): @@ -445,21 +505,25 @@ class Asn1Value(object): return copy.deepcopy(self) - def retag(self, tag_type, tag): + def retag(self, tagging, tag=None): """ Copies the object, applying a new tagging to it - :param tag_type: - A unicode string of "implicit" or "explicit" + :param tagging: + A dict containing the keys "explicit" and "implicit". Legacy + API allows a unicode string of "implicit" or "explicit". :param tag: - A integer tag number + A integer tag number. Only used when tagging is a unicode string. :return: An Asn1Value object """ - new_obj = self.__class__(tag_type=tag_type, tag=tag) + # This is required to preserve the old API + if not isinstance(tagging, dict): + tagging = {tagging: tag} + new_obj = self.__class__(explicit=tagging.get('explicit'), implicit=tagging.get('implicit')) new_obj._copy(self, copy.deepcopy) return new_obj @@ -506,8 +570,8 @@ class Asn1Value(object): prefix = ' ' * nest_level - # This interacts with Any and moves the tag, tag_type, _header, contents, _footer - # to the parsed value so duplicate data isn't present + # This interacts with Any and moves the tag, implicit, explicit, _header, + # contents, _footer to the parsed value so duplicate data isn't present has_parsed = hasattr(self, 'parsed') _basic_debug(prefix, self) @@ -540,23 +604,15 @@ class Asn1Value(object): self.method = 0 header = _dump_header(self.class_, self.method, self.tag, self.contents) - trailer = b'' - - if self.tag_type == 'explicit': - container = Asn1Value() - container.method = 1 - container.class_ = self.explicit_class - container.tag = self.explicit_tag - container.contents = header + self.contents + trailer - # Force the container to generate the header and footer - container.dump() - header = container._header + header - trailer += container._trailer + + if self.explicit is not None: + for class_, tag in self.explicit: + header = _dump_header(class_, 1, tag, header + self.contents) + header self._header = header - self._trailer = trailer + self._trailer = b'' - return self._header + contents + self._trailer + return self._header + contents class ValueMap(): @@ -618,10 +674,9 @@ class Castable(object): )) new_obj = other_class() - new_obj.tag_type = self.tag_type new_obj.class_ = self.class_ - new_obj.explicit_class = self.explicit_class - new_obj.explicit_tag = self.explicit_tag + new_obj.implicit = self.implicit + new_obj.explicit = self.explicit new_obj._header = self._header new_obj.contents = self.contents new_obj._trailer = self._trailer @@ -846,11 +901,13 @@ class Any(Asn1Value): if self._parsed is None or self._parsed[1:3] != (spec, spec_params): try: - passed_params = spec_params - if self.tag_type == 'explicit': - passed_params = {} if not spec_params else spec_params.copy() - passed_params['tag_type'] = self.tag_type - passed_params['tag'] = self.explicit_tag + passed_params = spec_params or {} + _tag_type_to_explicit_implicit(passed_params) + if self.explicit is not None: + if 'explicit' in passed_params: + passed_params['explicit'] = self.explicit + passed_params['explicit'] + else: + passed_params['explicit'] = self.explicit contents = self._header + self.contents + self._trailer parsed_value, _ = _parse_build( contents, @@ -861,8 +918,9 @@ class Any(Asn1Value): # Once we've parsed the Any value, clear any attributes from this object # since they are now duplicate - self.tag_type = None self.tag = None + self.explicit = None + self.implicit = False self._header = b'' self.contents = contents self._trailer = b'' @@ -928,7 +986,7 @@ class Choice(Asn1Value): # # Option 2, same as Option 1, but with a dict of class params # - # ("name", Asn1ValueClass, {'tag_type': 'explicit', 'tag': 5}) + # ("name", Asn1ValueClass, {'explicit': 5}) _alternatives = None # A dict that maps tuples of (class_, tag) to an index in _alternatives @@ -975,7 +1033,7 @@ class Choice(Asn1Value): cls._id_map[id_] = index cls._name_map[info[0]] = index - def __init__(self, name=None, value=None, tag_type=None, **kwargs): + def __init__(self, name=None, value=None, **kwargs): """ Checks to ensure implicit tagging is not being used since it is incompatible with Choice, then forwards on to Asn1Value.__init__() @@ -989,18 +1047,16 @@ class Choice(Asn1Value): :param value: The alternative value to set - used with name - :param tag_type: - The tag_type of the value - None, "implicit" or "explicit" - :raises: - ValueError - when tag_type is "implicit" + ValueError - when implicit param is passed (or legacy tag_type param is "implicit") """ - kwargs['tag_type'] = tag_type + _tag_type_to_explicit_implicit(kwargs) + Asn1Value.__init__(self, **kwargs) try: - if tag_type == 'implicit': + if kwargs.get('implicit') is not None: raise ValueError(unwrap( ''' The Choice type can not be implicitly tagged even if in an @@ -1130,8 +1186,8 @@ class Choice(Asn1Value): id_ = (class_, tag) - if self.tag_type == 'explicit': - if (self.explicit_class, self.explicit_tag) != id_: + if self.explicit is not None: + if self.explicit[-1] != id_: raise ValueError(unwrap( ''' %s was explicitly tagged, but the value provided does not @@ -1213,10 +1269,10 @@ class Choice(Asn1Value): self.contents = self.chosen.dump(force=force) if self._header is None or force: - if self.tag_type == 'explicit': - self._header = _dump_header(self.explicit_class, 1, self.explicit_tag, self.contents) - else: - self._header = b'' + 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 @@ -1626,7 +1682,7 @@ class Primitive(Asn1Value): # When tagging is going on, do the extra work of constructing new # objects to see if the dumped representation are the same - if self.tag_type is not None or other.tag_type is not None: + if self.implicit or self.explicit or other.implicit or other.explicit: return self.untag().dump() == other.untag().dump() return self.dump() == other.dump() @@ -3016,7 +3072,7 @@ class Sequence(Asn1Value): # # Option 2, same as Option 1, but with a dict of class params # - # ("name", Asn1ValueClass, {'tag_type': 'explicit', 'tag': 5}) + # ("name", Asn1ValueClass, {'explicit': 5}) _fields = [] # A dict with keys being the name of a field and the value being a unicode @@ -3951,13 +4007,10 @@ class SequenceOf(Asn1Value): return self._child_spec(value=value) params = {} - if self._child_spec.tag_type is not None: - params['tag_type'] = self._child_spec.tag_type - if params['tag_type'] == 'explicit': - params['tag'] = self._child_spec.explicit_tag - else: - params['tag'] = self._child_spec.tag - + if self._child_spec.explicit: + params['explicit'] = self._child_spec.explicit + if self._child_spec.implicit: + params['implicit'] = (self._child_spec.class_, self._child_spec.tag) return _fix_tagging(new_value, params) def __len__(self): @@ -4762,19 +4815,20 @@ def _basic_debug(prefix, self): method_name = METHOD_NUM_TO_NAME_MAP.get(self.method) class_name = CLASS_NUM_TO_NAME_MAP.get(self.class_) - if self.tag_type == 'explicit': - print( - '%s %s tag %s (explicitly tagged)' % - ( - prefix, - CLASS_NUM_TO_NAME_MAP.get(self.explicit_class), - self.explicit_tag + if self.explicit is not None: + for class_, tag in self.explicit: + print( + '%s %s tag %s (explicitly tagged)' % + ( + prefix, + CLASS_NUM_TO_NAME_MAP.get(class_), + tag + ) ) - ) if has_header: print('%s %s %s %s' % (prefix, method_name, class_name, self.tag)) - elif self.tag_type == 'implicit': + elif self.implicit: if has_header: print('%s %s %s tag %s (implicitly tagged)' % (prefix, method_name, class_name, self.tag)) @@ -4784,6 +4838,25 @@ def _basic_debug(prefix, self): print('%s Data: 0x%s' % (prefix, binascii.hexlify(self.contents or b'').decode('utf-8'))) +def _tag_type_to_explicit_implicit(params): + """ + Converts old-style "tag_type" and "tag" params to "explicit" and "implicit" + + :param params: + A dict of parameters to convert from tag_type/tag to explicit/implicit + """ + + if 'tag_type' in params: + if params['tag_type'] == 'explicit': + params['explicit'] = (params.get('class', 2), params['tag']) + elif params['tag_type'] == 'implicit': + params['implicit'] = (params.get('class', 2), params['tag']) + del params['tag_type'] + del params['tag'] + if 'class' in params: + del params['class'] + + def _fix_tagging(value, params): """ Checks if a value is properly tagged based on the spec, and re/untags as @@ -4799,26 +4872,28 @@ def _fix_tagging(value, params): An Asn1Value that is properly tagged """ - if 'tag_type' in params: - required_tag_type = params['tag_type'] - retag = False + _tag_type_to_explicit_implicit(params) - if required_tag_type != value.tag_type: + retag = False + if 'implicit' not in params: + if value.implicit is not False: retag = True - - elif required_tag_type == 'explicit' and value.explicit_tag != params['tag']: + else: + if isinstance(params['implicit'], tuple): + class_, tag = params['implicit'] + else: + tag = params['implicit'] + class_ = 'context' + if value.implicit is False: retag = True - - elif required_tag_type == 'implicit' and value.tag != params['tag']: + elif value.class_ != CLASS_NAME_TO_NUM_MAP[class_] or value.tag != tag: retag = True - if retag: - return value.retag(params['tag_type'], params['tag']) - return value - - if value.tag_type: - return value.untag() + if params.get('explicit') != value.explicit: + retag = True + if retag: + return value.retag(params) return value @@ -4844,9 +4919,22 @@ def _build_id_tuple(params, spec): required_class = spec.class_ required_tag = spec.tag - tag_type = params.get('tag_type', spec.tag_type) - if tag_type is not None: - required_class = 2 + _tag_type_to_explicit_implicit(params) + + if 'explicit' in params: + if isinstance(params['explicit'], tuple): + required_class, required_tag = params['explicit'] + else: + required_class = 2 + required_tag = params['explicit'] + elif 'implicit' in params: + if isinstance(params['implicit'], tuple): + required_class, required_tag = params['implicit'] + else: + required_class = 2 + required_tag = params['implicit'] + if required_class is not None and not isinstance(required_class, int_types): + required_class = CLASS_NAME_TO_NUM_MAP[required_class] required_class = params.get('class_', required_class) required_tag = params.get('tag', required_tag) @@ -4927,6 +5015,9 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param An object of the type spec, or if not specified, a child of Asn1Value """ + if spec_params is not None: + _tag_type_to_explicit_implicit(spec_params) + if header is None: return VOID @@ -4942,45 +5033,54 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param if spec is Any: pass - elif value.tag_type == 'explicit': - if class_ != value.explicit_class: - raise ValueError(unwrap( - ''' - Error parsing %s - explicitly-tagged class should have been - %s, but %s was found - ''', - type_name(value), - CLASS_NUM_TO_NAME_MAP.get(value.explicit_class), - CLASS_NUM_TO_NAME_MAP.get(class_, class_) - )) - if method != 1: - raise ValueError(unwrap( - ''' - Error parsing %s - explicitly-tagged method should have - been %s, but %s was found - ''', - type_name(value), - METHOD_NUM_TO_NAME_MAP.get(1), - METHOD_NUM_TO_NAME_MAP.get(method, method) - )) - if tag != value.explicit_tag: - raise ValueError(unwrap( - ''' - Error parsing %s - explicitly-tagged tag should have been - %s, but %s was found - ''', - type_name(value), - value.explicit_tag, - tag - )) - original_value = value - info, _ = _parse(contents, len(contents)) + elif value.explicit: + original_explicit = value.explicit + explicit_info = reversed(original_explicit) + parsed_class = class_ + parsed_method = method + parsed_tag = tag + to_parse = contents + explicit_header = header + explicit_trailer = trailer or b'' + for expected_class, expected_tag in explicit_info: + if parsed_class != expected_class: + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged class should have been + %s, but %s was found + ''', + type_name(value), + CLASS_NUM_TO_NAME_MAP.get(expected_class), + CLASS_NUM_TO_NAME_MAP.get(parsed_class, parsed_class) + )) + if parsed_method != 1: + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged method should have + been %s, but %s was found + ''', + type_name(value), + METHOD_NUM_TO_NAME_MAP.get(1), + METHOD_NUM_TO_NAME_MAP.get(parsed_method, parsed_method) + )) + if parsed_tag != expected_tag: + raise ValueError(unwrap( + ''' + Error parsing %s - explicitly-tagged tag should have been + %s, but %s was found + ''', + type_name(value), + expected_tag, + parsed_tag + )) + 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 value = _build(*info, spec=spec, spec_params={'no_explicit': True}) - value._header = header + value._header - value._trailer += trailer or b'' - value.tag_type = 'explicit' - value.explicit_class = original_value.explicit_class - value.explicit_tag = original_value.explicit_tag + value._header = explicit_header + value._trailer = explicit_trailer + value.explicit = original_explicit header_set = True elif isinstance(value, Choice): @@ -5035,15 +5135,22 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # For explicitly tagged, un-speced parsings, we use a generic container # since we will be parsing the contents and discarding the outer object # anyway a little further on - elif spec_params and 'tag_type' in spec_params and spec_params['tag_type'] == 'explicit': + elif spec_params and 'explicit' in spec_params: original_value = Asn1Value(contents=contents, **spec_params) - info, _ = _parse(contents, len(contents)) + original_explicit = original_value.explicit + + to_parse = contents + explicit_header = header + explicit_trailer = trailer or b'' + for expected_class, expected_tag in reversed(original_explicit): + info, _ = _parse(to_parse, len(to_parse)) + _, _, _, parsed_header, to_parse, parsed_trailer = info + explicit_header += parsed_header + explicit_trailer = parsed_trailer + explicit_trailer value = _build(*info, spec=spec, spec_params={'no_explicit': True}) value._header = header + value._header value._trailer += trailer or b'' - value.tag_type = 'explicit' - value.explicit_class = original_value.explicit_class - value.explicit_tag = original_value.explicit_tag + value.explicit = original_explicit header_set = True # If no spec was specified, allow anything and just process what -- cgit v1.2.3 From d83c39856cc454cd5f0b2be7533b656652e6e194 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 15 Sep 2017 07:12:50 -0400 Subject: Test both new and old API for explicit/implicit tagging --- tests/test_core.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 31ef8ee..d806669 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -64,6 +64,14 @@ class Enum(core.Enumerated): class NumChoice(core.Choice): + _alternatives = [ + ('one', core.Integer, {'explicit': 0}), + ('two', core.Integer, {'implicit': 1}), + ('three', core.Integer, {'explicit': 2}), + ] + + +class NumChoiceOldApi(core.Choice): _alternatives = [ ('one', core.Integer, {'tag_type': 'explicit', 'tag': 0}), ('two', core.Integer, {'tag_type': 'implicit', 'tag': 1}), @@ -72,6 +80,13 @@ class NumChoice(core.Choice): class SeqChoice(core.Choice): + _alternatives = [ + ('one', CopySeq, {'explicit': 0}), + ('two', CopySeq, {'implicit': 1}), + ] + + +class SeqChoiceOldApi(core.Choice): _alternatives = [ ('one', CopySeq, {'tag_type': 'explicit', 'tag': 0}), ('two', CopySeq, {'tag_type': 'implicit', 'tag': 1}), @@ -84,6 +99,12 @@ class ExplicitField(core.Sequence): ] +class ExplicitFieldOldApi(core.Sequence): + _fields = [ + ('field', NumChoiceOldApi, {'explicit': 0}), + ] + + class SetTest(core.Set): _fields = [ ('two', core.Integer, {'tag_type': 'implicit', 'tag': 2}), @@ -91,6 +112,13 @@ class SetTest(core.Set): ] +class SetTestOldApi(core.Set): + _fields = [ + ('two', core.Integer, {'implicit': 2}), + ('one', core.Integer, {'implicit': 1}), + ] + + class SetOfTest(core.SetOf): _child_spec = core.Integer @@ -110,9 +138,7 @@ class MyOids(core.ObjectIdentifier): } class ApplicationTaggedInteger(core.Integer): - tag_type = 'explicit' - explicit_class = 1 - explicit_tag = 10 + explicit = ((1, 10), ) @data_decorator @@ -134,8 +160,8 @@ class CoreTests(unittest.TestCase): return ( (core.ObjectIdentifier('1.2.3'), core.ObjectIdentifier('1.2.3'), True), (core.Integer(1), Enum(1), False), - (core.Integer(1), core.Integer(1, tag_type='implicit', tag=5), True), - (core.Integer(1), core.Integer(1, tag_type='explicit', tag=5), True), + (core.Integer(1), core.Integer(1, implicit=5), True), + (core.Integer(1), core.Integer(1, explicit=5), True), (core.Integer(1), core.Integer(2), False), (core.OctetString(b''), core.OctetString(b''), True), (core.OctetString(b''), core.OctetString(b'1'), False), @@ -273,6 +299,8 @@ class CoreTests(unittest.TestCase): def test_strict_choice(self): with self.assertRaises(ValueError): NumChoice.load(b'\xA0\x03\x02\x01\x00\x00', strict=True) + with self.assertRaises(ValueError): + NumChoiceOldApi.load(b'\xA0\x03\x02\x01\x00\x00', strict=True) def test_bit_string_item_access(self): named = core.BitString() @@ -397,6 +425,9 @@ class CoreTests(unittest.TestCase): val = NumChoice.load(b'\xa0\x03\x02\x01\x00') self.assertEqual(b'\xa0\x03\x02\x01', val.chosen._header) self.assertEqual(b'\x00', val.chosen.contents) + val2 = NumChoiceOldApi.load(b'\xa0\x03\x02\x01\x00') + self.assertEqual(b'\xa0\x03\x02\x01', val2.chosen._header) + self.assertEqual(b'\x00', val2.chosen.contents) def test_explicit_header_field_choice(self): der = b'\x30\x07\xa0\x05\xa0\x03\x02\x01\x00' @@ -404,6 +435,10 @@ class CoreTests(unittest.TestCase): self.assertEqual(0, val['field'].chosen.native) self.assertEqual(der, val.dump(force=True)) + val2 = ExplicitFieldOldApi.load(der) + self.assertEqual(0, val2['field'].chosen.native) + self.assertEqual(der, val2.dump(force=True)) + def test_retag(self): a = core.Integer(200) b = a.retag('explicit', 0) @@ -412,7 +447,7 @@ class CoreTests(unittest.TestCase): self.assertNotEqual(a.dump(), b.dump()) def test_untag(self): - a = core.Integer(200, tag_type='explicit', tag=0) + a = core.Integer(200, explicit=0) b = a.untag() self.assertNotEqual(id(a), id(b)) self.assertEqual(a.contents, b.contents) @@ -429,6 +464,15 @@ class CoreTests(unittest.TestCase): with self.assertRaises(ValueError): SeqChoice({'one': a, 'two': a}) + choice2 = SeqChoiceOldApi({'one': a}) + self.assertEqual('one', choice2.name) + + with self.assertRaises(ValueError): + SeqChoiceOldApi({}) + + with self.assertRaises(ValueError): + SeqChoiceOldApi({'one': a, 'two': a}) + def test_choice_tuple_name(self): a = CopySeq({'name': 'foo', 'pair': {'id': '1.2.3', 'value': 5}}) choice = SeqChoice(('one', a)) @@ -440,19 +484,35 @@ class CoreTests(unittest.TestCase): with self.assertRaises(ValueError): SeqChoice(('one', a, None)) + choice2 = SeqChoiceOldApi(('one', a)) + self.assertEqual('one', choice2.name) + + with self.assertRaises(ValueError): + SeqChoiceOldApi(('one',)) + + with self.assertRaises(ValueError): + SeqChoiceOldApi(('one', a, None)) + def test_load_invalid_choice(self): with self.assertRaises(ValueError): NumChoice.load(b'\x02\x01\x00') + with self.assertRaises(ValueError): + NumChoiceOldApi.load(b'\x02\x01\x00') def test_fix_tagging_choice(self): - correct = core.Integer(200, tag_type='explicit', tag=2) + correct = core.Integer(200, explicit=2) choice = NumChoice( name='three', - value=core.Integer(200, tag_type='explicit', tag=1) + value=core.Integer(200, explicit=1) ) self.assertEqual(correct.dump(), choice.dump()) - self.assertEqual(correct.tag_type, choice.chosen.tag_type) - self.assertEqual(correct.explicit_tag, choice.chosen.explicit_tag) + self.assertEqual(correct.explicit, choice.chosen.explicit) + choice2 = NumChoiceOldApi( + name='three', + value=core.Integer(200, explicit=1) + ) + self.assertEqual(correct.dump(), choice2.dump()) + self.assertEqual(correct.explicit, choice2.chosen.explicit) def test_copy_choice_mutate(self): a = CopySeq({'name': 'foo', 'pair': {'id': '1.2.3', 'value': 5}}) @@ -465,6 +525,15 @@ class CoreTests(unittest.TestCase): choice.chosen['name'] = 'bar' self.assertNotEqual(choice.chosen['name'], choice_copy.chosen['name']) + choice2 = SeqChoiceOldApi( + name='one', + value=a + ) + choice2.dump() + choice2_copy = choice2.copy() + choice2.chosen['name'] = 'bar' + self.assertNotEqual(choice2.chosen['name'], choice2_copy.chosen['name']) + def test_concat(self): child1 = Seq({ 'id': '1.2.3', @@ -589,9 +658,7 @@ class CoreTests(unittest.TestCase): data = b'\x6a\x81\x03\x02\x01\x00' ati = ApplicationTaggedInteger.load(data) - self.assertEqual('explicit', ati.tag_type) - self.assertEqual(1, ati.explicit_class) - self.assertEqual(10, ati.explicit_tag) + self.assertEqual(((1, 10),), ati.explicit) self.assertEqual(0, ati.class_) self.assertEqual(2, ati.tag) self.assertEqual(0, ati.native) -- cgit v1.2.3 From d62ed9a17802c8b2136cfe119fcb56b1edc99d5c Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 15 Sep 2017 07:13:52 -0400 Subject: Update bundled definitions to use new tagging API --- asn1crypto/algos.py | 21 +++------ asn1crypto/cms.py | 118 +++++++++++++++++++++++++-------------------------- asn1crypto/crl.py | 14 +++--- asn1crypto/csr.py | 2 +- asn1crypto/keys.py | 6 +-- asn1crypto/ocsp.py | 42 +++++++++--------- asn1crypto/pdf.py | 6 +-- asn1crypto/pkcs12.py | 8 ++-- asn1crypto/tsp.py | 26 ++++++------ asn1crypto/x509.py | 116 +++++++++++++++++++++++++------------------------- 10 files changed, 176 insertions(+), 183 deletions(-) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index 979207a..dd4ae5b 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -182,8 +182,7 @@ class RSASSAPSSParams(Sequence): 'hash_algorithm', DigestAlgorithm, { - 'tag_type': 'explicit', - 'tag': 0, + 'explicit': 0, 'default': {'algorithm': 'sha1'}, } ), @@ -191,8 +190,7 @@ class RSASSAPSSParams(Sequence): 'mask_gen_algorithm', MaskGenAlgorithm, { - 'tag_type': 'explicit', - 'tag': 1, + 'explicit': 1, 'default': { 'algorithm': 'mgf1', 'parameters': {'algorithm': 'sha1'}, @@ -203,8 +201,7 @@ class RSASSAPSSParams(Sequence): 'salt_length', Integer, { - 'tag_type': 'explicit', - 'tag': 2, + 'explicit': 2, 'default': 20, } ), @@ -212,8 +209,7 @@ class RSASSAPSSParams(Sequence): 'trailer_field', TrailerField, { - 'tag_type': 'explicit', - 'tag': 3, + 'explicit': 3, 'default': 'trailer_field_bc', } ), @@ -481,8 +477,7 @@ class RSAESOAEPParams(Sequence): 'hash_algorithm', DigestAlgorithm, { - 'tag_type': 'explicit', - 'tag': 0, + 'explicit': 0, 'default': {'algorithm': 'sha1'} } ), @@ -490,8 +485,7 @@ class RSAESOAEPParams(Sequence): 'mask_gen_algorithm', MaskGenAlgorithm, { - 'tag_type': 'explicit', - 'tag': 1, + 'explicit': 1, 'default': { 'algorithm': 'mgf1', 'parameters': {'algorithm': 'sha1'} @@ -502,8 +496,7 @@ class RSAESOAEPParams(Sequence): 'p_source_algorithm', PSourceAlgorithm, { - 'tag_type': 'explicit', - 'tag': 2, + 'explicit': 2, 'default': { 'algorithm': 'p_specified', 'parameters': b'' diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 7b62ab3..2964f0a 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -128,8 +128,8 @@ class ContentType(ObjectIdentifier): class CMSAlgorithmProtection(Sequence): _fields = [ ('digest_algorithm', DigestAlgorithm), - ('signature_algorithm', SignedDigestAlgorithm, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('mac_algorithm', HmacAlgorithm, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('signature_algorithm', SignedDigestAlgorithm, {'implicit': 1, 'optional': True}), + ('mac_algorithm', HmacAlgorithm, {'implicit': 2, 'optional': True}), ] @@ -190,8 +190,8 @@ class AttCertVersion(Integer): class AttCertSubject(Choice): _alternatives = [ - ('base_certificate_id', IssuerSerial, {'tag_type': 'explicit', 'tag': 0}), - ('subject_name', GeneralNames, {'tag_type': 'explicit', 'tag': 1}), + ('base_certificate_id', IssuerSerial, {'explicit': 0}), + ('subject_name', GeneralNames, {'explicit': 1}), ] @@ -243,24 +243,24 @@ class ObjectDigestInfo(Sequence): class Holder(Sequence): _fields = [ - ('base_certificate_id', IssuerSerial, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('entity_name', GeneralNames, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('object_digest_info', ObjectDigestInfo, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('base_certificate_id', IssuerSerial, {'implicit': 0, 'optional': True}), + ('entity_name', GeneralNames, {'implicit': 1, 'optional': True}), + ('object_digest_info', ObjectDigestInfo, {'implicit': 2, 'optional': True}), ] class V2Form(Sequence): _fields = [ ('issuer_name', GeneralNames, {'optional': True}), - ('base_certificate_id', IssuerSerial, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('object_digest_info', ObjectDigestInfo, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('base_certificate_id', IssuerSerial, {'explicit': 0, 'optional': True}), + ('object_digest_info', ObjectDigestInfo, {'explicit': 1, 'optional': True}), ] class AttCertIssuer(Choice): _alternatives = [ ('v1_form', GeneralNames), - ('v2_form', V2Form, {'tag_type': 'explicit', 'tag': 0}), + ('v2_form', V2Form, {'explicit': 0}), ] @@ -278,7 +278,7 @@ class IetfAttrValues(SequenceOf): class IetfAttrSyntax(Sequence): _fields = [ - ('policy_authority', GeneralNames, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('policy_authority', GeneralNames, {'implicit': 0, 'optional': True}), ('values', IetfAttrValues), ] @@ -301,8 +301,8 @@ class SetOfSvceAuthInfo(SetOf): class RoleSyntax(Sequence): _fields = [ - ('role_authority', GeneralNames, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('role_name', GeneralName, {'tag_type': 'implicit', 'tag': 1}), + ('role_authority', GeneralNames, {'implicit': 0, 'optional': True}), + ('role_name', GeneralName, {'implicit': 1}), ] @@ -323,8 +323,8 @@ class ClassList(BitString): class SecurityCategory(Sequence): _fields = [ - ('type', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 0}), - ('value', Any, {'tag_type': 'implicit', 'tag': 1}), + ('type', ObjectIdentifier, {'implicit': 0}), + ('value', Any, {'implicit': 1}), ] @@ -334,9 +334,9 @@ class SetOfSecurityCategory(SetOf): class Clearance(Sequence): _fields = [ - ('policy_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 0}), - ('class_list', ClassList, {'tag_type': 'implicit', 'tag': 1, 'default': 'unclassified'}), - ('security_categories', SetOfSecurityCategory, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('policy_id', ObjectIdentifier, {'implicit': 0}), + ('class_list', ClassList, {'implicit': 1, 'default': 'unclassified'}), + ('security_categories', SetOfSecurityCategory, {'implicit': 2, 'optional': True}), ] @@ -380,8 +380,8 @@ class SetOfTimingMetrics(SetOf): class TimingPolicy(Sequence): _fields = [ ('policy_id', SequenceOf, {'spec': ObjectIdentifier}), - ('max_offset', BigTime, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('max_delay', BigTime, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('max_offset', BigTime, {'explicit': 0, 'optional': True}), + ('max_delay', BigTime, {'explicit': 1, 'optional': True}), ] @@ -466,10 +466,10 @@ class OtherCertificateFormat(Sequence): class CertificateChoices(Choice): _alternatives = [ ('certificate', Certificate), - ('extended_certificate', ExtendedCertificate, {'tag_type': 'implicit', 'tag': 0}), - ('v1_attr_cert', AttributeCertificateV1, {'tag_type': 'implicit', 'tag': 1}), - ('v2_attr_cert', AttributeCertificateV2, {'tag_type': 'implicit', 'tag': 2}), - ('other', OtherCertificateFormat, {'tag_type': 'implicit', 'tag': 3}), + ('extended_certificate', ExtendedCertificate, {'implicit': 0}), + ('v1_attr_cert', AttributeCertificateV1, {'implicit': 1}), + ('v2_attr_cert', AttributeCertificateV2, {'implicit': 2}), + ('other', OtherCertificateFormat, {'implicit': 3}), ] def validate(self, class_, tag, contents): @@ -505,7 +505,7 @@ class CertificateSet(SetOf): class ContentInfo(Sequence): _fields = [ ('content_type', ContentType), - ('content', Any, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('content', Any, {'explicit': 0, 'optional': True}), ] _oid_pair = ('content_type', 'content') @@ -519,7 +519,7 @@ class SetOfContentInfo(SetOf): class EncapsulatedContentInfo(Sequence): _fields = [ ('content_type', ContentType), - ('content', ParsableOctetString, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('content', ParsableOctetString, {'explicit': 0, 'optional': True}), ] _oid_pair = ('content_type', 'content') @@ -536,7 +536,7 @@ class IssuerAndSerialNumber(Sequence): class SignerIdentifier(Choice): _alternatives = [ ('issuer_and_serial_number', IssuerAndSerialNumber), - ('subject_key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ('subject_key_identifier', OctetString, {'implicit': 0}), ] @@ -550,7 +550,7 @@ class CertificateRevocationLists(SetOf): class SCVPReqRes(Sequence): _fields = [ - ('request', ContentInfo, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('request', ContentInfo, {'explicit': 0, 'optional': True}), ('response', ContentInfo), ] @@ -578,7 +578,7 @@ class OtherRevocationInfoFormat(Sequence): class RevocationInfoChoice(Choice): _alternatives = [ ('crl', CertificateList), - ('other', OtherRevocationInfoFormat, {'tag_type': 'implciit', 'tag': 1}), + ('other', OtherRevocationInfoFormat, {'implicit': 1}), ] @@ -591,10 +591,10 @@ class SignerInfo(Sequence): ('version', CMSVersion), ('sid', SignerIdentifier), ('digest_algorithm', DigestAlgorithm), - ('signed_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('signed_attrs', CMSAttributes, {'implicit': 0, 'optional': True}), ('signature_algorithm', SignedDigestAlgorithm), ('signature', OctetString), - ('unsigned_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('unsigned_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), ] @@ -607,8 +607,8 @@ class SignedData(Sequence): ('version', CMSVersion), ('digest_algorithms', DigestAlgorithms), ('encap_content_info', None), - ('certificates', CertificateSet, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('crls', RevocationInfoChoices, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('certificates', CertificateSet, {'implicit': 0, 'optional': True}), + ('crls', RevocationInfoChoices, {'implicit': 1, 'optional': True}), ('signer_infos', SignerInfos), ] @@ -633,15 +633,15 @@ class SignedData(Sequence): class OriginatorInfo(Sequence): _fields = [ - ('certs', CertificateSet, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('crls', RevocationInfoChoices, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('certs', CertificateSet, {'implicit': 0, 'optional': True}), + ('crls', RevocationInfoChoices, {'implicit': 1, 'optional': True}), ] class RecipientIdentifier(Choice): _alternatives = [ ('issuer_and_serial_number', IssuerAndSerialNumber), - ('subject_key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0}), + ('subject_key_identifier', OctetString, {'implicit': 0}), ] @@ -676,8 +676,8 @@ class KeyTransRecipientInfo(Sequence): class OriginatorIdentifierOrKey(Choice): _alternatives = [ ('issuer_and_serial_number', IssuerAndSerialNumber), - ('subject_key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0}), - ('originator_key', PublicKeyInfo, {'tag_type': 'implicit', 'tag': 1}), + ('subject_key_identifier', OctetString, {'implicit': 0}), + ('originator_key', PublicKeyInfo, {'implicit': 1}), ] @@ -699,7 +699,7 @@ class RecipientKeyIdentifier(Sequence): class KeyAgreementRecipientIdentifier(Choice): _alternatives = [ ('issuer_and_serial_number', IssuerAndSerialNumber), - ('r_key_id', RecipientKeyIdentifier, {'tag_type': 'implicit', 'tag': 0}), + ('r_key_id', RecipientKeyIdentifier, {'implicit': 0}), ] @@ -717,8 +717,8 @@ class RecipientEncryptedKeys(SequenceOf): class KeyAgreeRecipientInfo(Sequence): _fields = [ ('version', CMSVersion), - ('originator', OriginatorIdentifierOrKey, {'tag_type': 'explicit', 'tag': 0}), - ('ukm', OctetString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('originator', OriginatorIdentifierOrKey, {'explicit': 0}), + ('ukm', OctetString, {'explicit': 1, 'optional': True}), ('key_encryption_algorithm', KeyEncryptionAlgorithm), ('recipient_encrypted_keys', RecipientEncryptedKeys), ] @@ -744,7 +744,7 @@ class KEKRecipientInfo(Sequence): class PasswordRecipientInfo(Sequence): _fields = [ ('version', CMSVersion), - ('key_derivation_algorithm', KdfAlgorithm, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('key_derivation_algorithm', KdfAlgorithm, {'implicit': 0, 'optional': True}), ('key_encryption_algorithm', KeyEncryptionAlgorithm), ('encrypted_key', OctetString), ] @@ -760,10 +760,10 @@ class OtherRecipientInfo(Sequence): class RecipientInfo(Choice): _alternatives = [ ('ktri', KeyTransRecipientInfo), - ('kari', KeyAgreeRecipientInfo, {'tag_type': 'implicit', 'tag': 1}), - ('kekri', KEKRecipientInfo, {'tag_type': 'implicit', 'tag': 2}), - ('pwri', PasswordRecipientInfo, {'tag_type': 'implicit', 'tag': 3}), - ('ori', OtherRecipientInfo, {'tag_type': 'implicit', 'tag': 4}), + ('kari', KeyAgreeRecipientInfo, {'implicit': 1}), + ('kekri', KEKRecipientInfo, {'implicit': 2}), + ('pwri', PasswordRecipientInfo, {'implicit': 3}), + ('ori', OtherRecipientInfo, {'implicit': 4}), ] @@ -775,17 +775,17 @@ class EncryptedContentInfo(Sequence): _fields = [ ('content_type', ContentType), ('content_encryption_algorithm', EncryptionAlgorithm), - ('encrypted_content', OctetString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('encrypted_content', OctetString, {'implicit': 0, 'optional': True}), ] class EnvelopedData(Sequence): _fields = [ ('version', CMSVersion), - ('originator_info', OriginatorInfo, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}), ('recipient_infos', RecipientInfos), ('encrypted_content_info', EncryptedContentInfo), - ('unprotected_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('unprotected_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), ] @@ -795,8 +795,8 @@ class SignedAndEnvelopedData(Sequence): ('recipient_infos', RecipientInfos), ('digest_algorithms', DigestAlgorithms), ('encrypted_content_info', EncryptedContentInfo), - ('certificates', CertificateSet, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('crls', CertificateRevocationLists, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('certificates', CertificateSet, {'implicit': 0, 'optional': True}), + ('crls', CertificateRevocationLists, {'implicit': 1, 'optional': True}), ('signer_infos', SignerInfos), ] @@ -832,35 +832,35 @@ class EncryptedData(Sequence): _fields = [ ('version', CMSVersion), ('encrypted_content_info', EncryptedContentInfo), - ('unprotected_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('unprotected_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), ] class AuthenticatedData(Sequence): _fields = [ ('version', CMSVersion), - ('originator_info', OriginatorInfo, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}), ('recipient_infos', RecipientInfos), ('mac_algorithm', HmacAlgorithm), - ('digest_algorithm', DigestAlgorithm, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('digest_algorithm', DigestAlgorithm, {'implicit': 1, 'optional': True}), # This does not require the _spec_callbacks approach of SignedData and # DigestedData since AuthenticatedData was not part of PKCS#7 ('encap_content_info', EncapsulatedContentInfo), - ('auth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('auth_attrs', CMSAttributes, {'implicit': 2, 'optional': True}), ('mac', OctetString), - ('unauth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ('unauth_attrs', CMSAttributes, {'implicit': 3, 'optional': True}), ] class AuthEnvelopedData(Sequence): _fields = [ ('version', CMSVersion), - ('originator_info', OriginatorInfo, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}), ('recipient_infos', RecipientInfos), ('auth_encrypted_content_info', EncryptedContentInfo), - ('auth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('auth_attrs', CMSAttributes, {'implicit': 1, 'optional': True}), ('mac', OctetString), - ('unauth_attrs', CMSAttributes, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('unauth_attrs', CMSAttributes, {'implicit': 2, 'optional': True}), ] diff --git a/asn1crypto/crl.py b/asn1crypto/crl.py index 0275847..84cb168 100644 --- a/asn1crypto/crl.py +++ b/asn1crypto/crl.py @@ -50,12 +50,12 @@ class Version(Integer): class IssuingDistributionPoint(Sequence): _fields = [ - ('distribution_point', DistributionPointName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('only_contains_user_certs', Boolean, {'tag_type': 'implicit', 'tag': 1, 'default': False}), - ('only_contains_ca_certs', Boolean, {'tag_type': 'implicit', 'tag': 2, 'default': False}), - ('only_some_reasons', ReasonFlags, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), - ('indirect_crl', Boolean, {'tag_type': 'implicit', 'tag': 4, 'default': False}), - ('only_contains_attribute_certs', Boolean, {'tag_type': 'implicit', 'tag': 5, 'default': False}), + ('distribution_point', DistributionPointName, {'explicit': 0, 'optional': True}), + ('only_contains_user_certs', Boolean, {'implicit': 1, 'default': False}), + ('only_contains_ca_certs', Boolean, {'implicit': 2, 'default': False}), + ('only_some_reasons', ReasonFlags, {'implicit': 3, 'optional': True}), + ('indirect_crl', Boolean, {'implicit': 4, 'default': False}), + ('only_contains_attribute_certs', Boolean, {'implicit': 5, 'default': False}), ] @@ -280,7 +280,7 @@ class TbsCertList(Sequence): ('this_update', Time), ('next_update', Time, {'optional': True}), ('revoked_certificates', RevokedCertificates, {'optional': True}), - ('crl_extensions', TBSCertListExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('crl_extensions', TBSCertListExtensions, {'explicit': 0, 'optional': True}), ] diff --git a/asn1crypto/csr.py b/asn1crypto/csr.py index 051f510..7ea2848 100644 --- a/asn1crypto/csr.py +++ b/asn1crypto/csr.py @@ -84,7 +84,7 @@ class CertificationRequestInfo(Sequence): ('version', Version), ('subject', Name), ('subject_pk_info', PublicKeyInfo), - ('attributes', CRIAttributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('attributes', CRIAttributes, {'implicit': 0, 'optional': True}), ] diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 35f4363..3c4870d 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -412,8 +412,8 @@ class ECPrivateKey(Sequence): _fields = [ ('version', ECPrivateKeyVersion), ('private_key', IntegerOctetString), - ('parameters', ECDomainParameters, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('public_key', ECPointBitString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('parameters', ECDomainParameters, {'explicit': 0, 'optional': True}), + ('public_key', ECPointBitString, {'explicit': 1, 'optional': True}), ] @@ -497,7 +497,7 @@ class PrivateKeyInfo(Sequence): ('version', Integer), ('private_key_algorithm', PrivateKeyAlgorithm), ('private_key', ParsableOctetString), - ('attributes', Attributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('attributes', Attributes, {'implicit': 0, 'optional': True}), ] def _private_key_spec(self): diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py index 52de481..f18d8e8 100644 --- a/asn1crypto/ocsp.py +++ b/asn1crypto/ocsp.py @@ -84,7 +84,7 @@ class RequestExtensions(SequenceOf): class Request(Sequence): _fields = [ ('req_cert', CertId), - ('single_request_extensions', RequestExtensions, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('single_request_extensions', RequestExtensions, {'explicit': 0, 'optional': True}), ] _processed_extensions = False @@ -192,10 +192,10 @@ class TBSRequestExtensions(SequenceOf): class TBSRequest(Sequence): _fields = [ - ('version', Version, {'tag_type': 'explicit', 'tag': 0, 'default': 'v1'}), - ('requestor_name', GeneralName, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('version', Version, {'explicit': 0, 'default': 'v1'}), + ('requestor_name', GeneralName, {'explicit': 1, 'optional': True}), ('request_list', Requests), - ('request_extensions', TBSRequestExtensions, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ('request_extensions', TBSRequestExtensions, {'explicit': 2, 'optional': True}), ] @@ -207,14 +207,14 @@ class Signature(Sequence): _fields = [ ('signature_algorithm', SignedDigestAlgorithm), ('signature', OctetBitString), - ('certs', Certificates, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('certs', Certificates, {'explicit': 0, 'optional': True}), ] class OCSPRequest(Sequence): _fields = [ ('tbs_request', TBSRequest), - ('optional_signature', Signature, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('optional_signature', Signature, {'explicit': 0, 'optional': True}), ] _processed_extensions = False @@ -314,31 +314,31 @@ class OCSPResponseStatus(Enumerated): class ResponderId(Choice): _alternatives = [ - ('by_name', Name, {'tag_type': 'explicit', 'tag': 1}), - ('by_key', OctetString, {'tag_type': 'explicit', 'tag': 2}), + ('by_name', Name, {'explicit': 1}), + ('by_key', OctetString, {'explicit': 2}), ] class RevokedInfo(Sequence): _fields = [ ('revocation_time', GeneralizedTime), - ('revocation_reason', CRLReason, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('revocation_reason', CRLReason, {'explicit': 0, 'optional': True}), ] class CertStatus(Choice): _alternatives = [ - ('good', Null, {'tag_type': 'implicit', 'tag': 0}), - ('revoked', RevokedInfo, {'tag_type': 'implicit', 'tag': 1}), - ('unknown', Null, {'tag_type': 'implicit', 'tag': 2}), + ('good', Null, {'implicit': 0}), + ('revoked', RevokedInfo, {'implicit': 1}), + ('unknown', Null, {'implicit': 2}), ] class CrlId(Sequence): _fields = [ - ('crl_url', IA5String, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('crl_num', Integer, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), - ('crl_time', GeneralizedTime, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ('crl_url', IA5String, {'explicit': 0, 'optional': True}), + ('crl_num', Integer, {'explicit': 1, 'optional': True}), + ('crl_time', GeneralizedTime, {'explicit': 2, 'optional': True}), ] @@ -383,8 +383,8 @@ class SingleResponse(Sequence): ('cert_id', CertId), ('cert_status', CertStatus), ('this_update', GeneralizedTime), - ('next_update', GeneralizedTime, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('single_extensions', SingleResponseExtensions, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('next_update', GeneralizedTime, {'explicit': 0, 'optional': True}), + ('single_extensions', SingleResponseExtensions, {'explicit': 1, 'optional': True}), ] _processed_extensions = False @@ -529,11 +529,11 @@ class ResponseDataExtensions(SequenceOf): class ResponseData(Sequence): _fields = [ - ('version', Version, {'tag_type': 'explicit', 'tag': 0, 'default': 'v1'}), + ('version', Version, {'explicit': 0, 'default': 'v1'}), ('responder_id', ResponderId), ('produced_at', GeneralizedTime), ('responses', Responses), - ('response_extensions', ResponseDataExtensions, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), + ('response_extensions', ResponseDataExtensions, {'explicit': 1, 'optional': True}), ] @@ -542,7 +542,7 @@ class BasicOCSPResponse(Sequence): ('tbs_response_data', ResponseData), ('signature_algorithm', SignedDigestAlgorithm), ('signature', OctetBitString), - ('certs', Certificates, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('certs', Certificates, {'explicit': 0, 'optional': True}), ] @@ -561,7 +561,7 @@ class ResponseBytes(Sequence): class OCSPResponse(Sequence): _fields = [ ('response_status', OCSPResponseStatus), - ('response_bytes', ResponseBytes, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('response_bytes', ResponseBytes, {'explicit': 0, 'optional': True}), ] _processed_extensions = False diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py index 6bf325c..b72c886 100644 --- a/asn1crypto/pdf.py +++ b/asn1crypto/pdf.py @@ -63,9 +63,9 @@ class SequenceOfOtherRevInfo(SequenceOf): class RevocationInfoArchival(Sequence): _fields = [ - ('crl', SequenceOfCertificateList, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('ocsp', SequenceOfOCSPResponse, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), - ('other_rev_info', SequenceOfOtherRevInfo, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), + ('crl', SequenceOfCertificateList, {'explicit': 0, 'optional': True}), + ('ocsp', SequenceOfOCSPResponse, {'explicit': 1, 'optional': True}), + ('other_rev_info', SequenceOfOtherRevInfo, {'explicit': 2, 'optional': True}), ] diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py index 4035f30..7ebcefe 100644 --- a/asn1crypto/pkcs12.py +++ b/asn1crypto/pkcs12.py @@ -145,7 +145,7 @@ class CertId(ObjectIdentifier): class CertBag(Sequence): _fields = [ ('cert_id', CertId), - ('cert_value', ParsableOctetString, {'tag_type': 'explicit', 'tag': 0}), + ('cert_value', ParsableOctetString, {'explicit': 0}), ] _oid_pair = ('cert_id', 'cert_value') @@ -157,14 +157,14 @@ class CertBag(Sequence): class CrlBag(Sequence): _fields = [ ('crl_id', ObjectIdentifier), - ('crl_value', OctetString, {'tag_type': 'explicit', 'tag': 0}), + ('crl_value', OctetString, {'explicit': 0}), ] class SecretBag(Sequence): _fields = [ ('secret_type_id', ObjectIdentifier), - ('secret_value', OctetString, {'tag_type': 'explicit', 'tag': 0}), + ('secret_value', OctetString, {'explicit': 0}), ] @@ -175,7 +175,7 @@ class SafeContents(SequenceOf): class SafeBag(Sequence): _fields = [ ('bag_id', BagId), - ('bag_value', Any, {'tag_type': 'explicit', 'tag': 0}), + ('bag_value', Any, {'explicit': 0}), ('bag_attributes', Attributes, {'optional': True}), ] diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py index ac73821..bd40810 100644 --- a/asn1crypto/tsp.py +++ b/asn1crypto/tsp.py @@ -74,8 +74,8 @@ class MessageImprint(Sequence): class Accuracy(Sequence): _fields = [ ('seconds', Integer, {'optional': True}), - ('millis', Integer, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('micros', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('millis', Integer, {'implicit': 0, 'optional': True}), + ('micros', Integer, {'implicit': 1, 'optional': True}), ] @@ -101,8 +101,8 @@ class TSTInfo(Sequence): ('accuracy', Accuracy, {'optional': True}), ('ordering', Boolean, {'default': False}), ('nonce', Integer, {'optional': True}), - ('tsa', GeneralName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('tsa', GeneralName, {'explicit': 0, 'optional': True}), + ('extensions', Extensions, {'implicit': 1, 'optional': True}), ] @@ -113,7 +113,7 @@ class TimeStampReq(Sequence): ('req_policy', ObjectIdentifier, {'optional': True}), ('nonce', Integer, {'optional': True}), ('cert_req', Boolean, {'default': False}), - ('extensions', Extensions, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('extensions', Extensions, {'implicit': 0, 'optional': True}), ] @@ -201,9 +201,9 @@ class PartialHashtrees(SequenceOf): class ArchiveTimeStamp(Sequence): _fields = [ - ('digest_algorithm', DigestAlgorithm, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('attributes', Attributes, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('reduced_hashtree', PartialHashtrees, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('digest_algorithm', DigestAlgorithm, {'implicit': 0, 'optional': True}), + ('attributes', Attributes, {'implicit': 1, 'optional': True}), + ('reduced_hashtree', PartialHashtrees, {'implicit': 2, 'optional': True}), ('time_stamp', ContentInfo), ] @@ -216,8 +216,8 @@ class EvidenceRecord(Sequence): _fields = [ ('version', Version), ('digest_algorithms', DigestAlgorithms), - ('crypto_infos', Attributes, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('encryption_info', EncryptionInfo, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('crypto_infos', Attributes, {'implicit': 0, 'optional': True}), + ('encryption_info', EncryptionInfo, {'implicit': 1, 'optional': True}), ('archive_time_stamp_sequence', ArchiveTimeStampSequence), ] @@ -231,9 +231,9 @@ class OtherEvidence(Sequence): class Evidence(Choice): _alternatives = [ - ('tst_evidence', TimeStampTokenEvidence, {'tag_type': 'implicit', 'tag': 0}), - ('ers_evidence', EvidenceRecord, {'tag_type': 'implicit', 'tag': 1}), - ('other_evidence', OtherEvidence, {'tag_type': 'implicit', 'tag': 2}), + ('tst_evidence', TimeStampTokenEvidence, {'implicit': 0}), + ('ers_evidence', EvidenceRecord, {'implicit': 1}), + ('other_evidence', OtherEvidence, {'implicit': 2}), ] diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index c8399d7..577b66a 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -444,8 +444,8 @@ class KeyUsage(BitString): class PrivateKeyUsagePeriod(Sequence): _fields = [ - ('not_before', GeneralizedTime, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('not_after', GeneralizedTime, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('not_before', GeneralizedTime, {'implicit': 0, 'optional': True}), + ('not_after', GeneralizedTime, {'implicit': 1, 'optional': True}), ] @@ -1130,7 +1130,7 @@ class Name(Choice): class AnotherName(Sequence): _fields = [ ('type_id', ObjectIdentifier), - ('value', Any, {'tag_type': 'explicit', 'tag': 0}), + ('value', Any, {'explicit': 0}), ] @@ -1163,19 +1163,19 @@ class PrivateDomainName(Choice): class PersonalName(Set): _fields = [ - ('surname', PrintableString, {'tag_type': 'implicit', 'tag': 0}), - ('given_name', PrintableString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('initials', PrintableString, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), - ('generation_qualifier', PrintableString, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ('surname', PrintableString, {'implicit': 0}), + ('given_name', PrintableString, {'implicit': 1, 'optional': True}), + ('initials', PrintableString, {'implicit': 2, 'optional': True}), + ('generation_qualifier', PrintableString, {'implicit': 3, 'optional': True}), ] class TeletexPersonalName(Set): _fields = [ - ('surname', TeletexString, {'tag_type': 'implicit', 'tag': 0}), - ('given_name', TeletexString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('initials', TeletexString, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), - ('generation_qualifier', TeletexString, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), + ('surname', TeletexString, {'implicit': 0}), + ('given_name', TeletexString, {'implicit': 1, 'optional': True}), + ('initials', TeletexString, {'implicit': 2, 'optional': True}), + ('generation_qualifier', TeletexString, {'implicit': 3, 'optional': True}), ] @@ -1191,13 +1191,13 @@ class BuiltInStandardAttributes(Sequence): _fields = [ ('country_name', CountryName, {'optional': True}), ('administration_domain_name', AdministrationDomainName, {'optional': True}), - ('network_address', NumericString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('terminal_identifier', PrintableString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('private_domain_name', PrivateDomainName, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), - ('organization_name', PrintableString, {'tag_type': 'implicit', 'tag': 3, 'optional': True}), - ('numeric_user_identifier', NumericString, {'tag_type': 'implicit', 'tag': 4, 'optional': True}), - ('personal_name', PersonalName, {'tag_type': 'implicit', 'tag': 5, 'optional': True}), - ('organizational_unit_names', OrganizationalUnitNames, {'tag_type': 'implicit', 'tag': 6, 'optional': True}), + ('network_address', NumericString, {'implicit': 0, 'optional': True}), + ('terminal_identifier', PrintableString, {'implicit': 1, 'optional': True}), + ('private_domain_name', PrivateDomainName, {'explicit': 2, 'optional': True}), + ('organization_name', PrintableString, {'implicit': 3, 'optional': True}), + ('numeric_user_identifier', NumericString, {'implicit': 4, 'optional': True}), + ('personal_name', PersonalName, {'implicit': 5, 'optional': True}), + ('organizational_unit_names', OrganizationalUnitNames, {'implicit': 6, 'optional': True}), ] @@ -1257,8 +1257,8 @@ class UnformattedPostalAddress(Set): class E1634Address(Sequence): _fields = [ - ('number', NumericString, {'tag_type': 'implicit', 'tag': 0}), - ('sub_address', NumericString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('number', NumericString, {'implicit': 0}), + ('sub_address', NumericString, {'implicit': 1, 'optional': True}), ] @@ -1268,17 +1268,17 @@ class NAddresses(SetOf): class PresentationAddress(Sequence): _fields = [ - ('p_selector', OctetString, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('s_selector', OctetString, {'tag_type': 'explicit', 'tag': 1, 'optional': True}), - ('t_selector', OctetString, {'tag_type': 'explicit', 'tag': 2, 'optional': True}), - ('n_addresses', NAddresses, {'tag_type': 'explicit', 'tag': 3}), + ('p_selector', OctetString, {'explicit': 0, 'optional': True}), + ('s_selector', OctetString, {'explicit': 1, 'optional': True}), + ('t_selector', OctetString, {'explicit': 2, 'optional': True}), + ('n_addresses', NAddresses, {'explicit': 3}), ] class ExtendedNetworkAddress(Choice): _alternatives = [ ('e163_4_address', E1634Address), - ('psap_address', PresentationAddress, {'tag_type': 'implicit', 'tag': 0}) + ('psap_address', PresentationAddress, {'implicit': 0}) ] @@ -1323,8 +1323,8 @@ class ExtensionAttributeType(Integer): class ExtensionAttribute(Sequence): _fields = [ - ('extension_attribute_type', ExtensionAttributeType, {'tag_type': 'implicit', 'tag': 0}), - ('extension_attribute_value', Any, {'tag_type': 'explicit', 'tag': 1}), + ('extension_attribute_type', ExtensionAttributeType, {'implicit': 0}), + ('extension_attribute_value', Any, {'explicit': 1}), ] _oid_pair = ('extension_attribute_type', 'extension_attribute_value') @@ -1369,22 +1369,22 @@ class ORAddress(Sequence): class EDIPartyName(Sequence): _fields = [ - ('name_assigner', DirectoryString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('party_name', DirectoryString, {'tag_type': 'implicit', 'tag': 1}), + ('name_assigner', DirectoryString, {'implicit': 0, 'optional': True}), + ('party_name', DirectoryString, {'implicit': 1}), ] class GeneralName(Choice): _alternatives = [ - ('other_name', AnotherName, {'tag_type': 'implicit', 'tag': 0}), - ('rfc822_name', EmailAddress, {'tag_type': 'implicit', 'tag': 1}), - ('dns_name', DNSName, {'tag_type': 'implicit', 'tag': 2}), - ('x400_address', ORAddress, {'tag_type': 'implicit', 'tag': 3}), - ('directory_name', Name, {'tag_type': 'explicit', 'tag': 4}), - ('edi_party_name', EDIPartyName, {'tag_type': 'implicit', 'tag': 5}), - ('uniform_resource_identifier', URI, {'tag_type': 'implicit', 'tag': 6}), - ('ip_address', IPAddress, {'tag_type': 'implicit', 'tag': 7}), - ('registered_id', ObjectIdentifier, {'tag_type': 'implicit', 'tag': 8}), + ('other_name', AnotherName, {'implicit': 0}), + ('rfc822_name', EmailAddress, {'implicit': 1}), + ('dns_name', DNSName, {'implicit': 2}), + ('x400_address', ORAddress, {'implicit': 3}), + ('directory_name', Name, {'explicit': 4}), + ('edi_party_name', EDIPartyName, {'implicit': 5}), + ('uniform_resource_identifier', URI, {'implicit': 6}), + ('ip_address', IPAddress, {'implicit': 7}), + ('registered_id', ObjectIdentifier, {'implicit': 8}), ] def __ne__(self, other): @@ -1451,16 +1451,16 @@ class BasicConstraints(Sequence): class AuthorityKeyIdentifier(Sequence): _fields = [ - ('key_identifier', OctetString, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('authority_cert_issuer', GeneralNames, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('authority_cert_serial_number', Integer, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('key_identifier', OctetString, {'implicit': 0, 'optional': True}), + ('authority_cert_issuer', GeneralNames, {'implicit': 1, 'optional': True}), + ('authority_cert_serial_number', Integer, {'implicit': 2, 'optional': True}), ] class DistributionPointName(Choice): _alternatives = [ - ('full_name', GeneralNames, {'tag_type': 'implicit', 'tag': 0}), - ('name_relative_to_crl_issuer', RelativeDistinguishedName, {'tag_type': 'implicit', 'tag': 1}), + ('full_name', GeneralNames, {'implicit': 0}), + ('name_relative_to_crl_issuer', RelativeDistinguishedName, {'implicit': 1}), ] @@ -1481,8 +1481,8 @@ class ReasonFlags(BitString): class GeneralSubtree(Sequence): _fields = [ ('base', GeneralName), - ('minimum', Integer, {'tag_type': 'implicit', 'tag': 0, 'default': 0}), - ('maximum', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('minimum', Integer, {'implicit': 0, 'default': 0}), + ('maximum', Integer, {'implicit': 1, 'optional': True}), ] @@ -1492,16 +1492,16 @@ class GeneralSubtrees(SequenceOf): class NameConstraints(Sequence): _fields = [ - ('permitted_subtrees', GeneralSubtrees, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('excluded_subtrees', GeneralSubtrees, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('permitted_subtrees', GeneralSubtrees, {'implicit': 0, 'optional': True}), + ('excluded_subtrees', GeneralSubtrees, {'implicit': 1, 'optional': True}), ] class DistributionPoint(Sequence): _fields = [ - ('distribution_point', DistributionPointName, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), - ('reasons', ReasonFlags, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('crl_issuer', GeneralNames, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), + ('distribution_point', DistributionPointName, {'explicit': 0, 'optional': True}), + ('reasons', ReasonFlags, {'implicit': 1, 'optional': True}), + ('crl_issuer', GeneralNames, {'implicit': 2, 'optional': True}), ] _url = False @@ -1620,8 +1620,8 @@ class PolicyMappings(SequenceOf): class PolicyConstraints(Sequence): _fields = [ - ('require_explicit_policy', Integer, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), - ('inhibit_policy_mapping', Integer, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('require_explicit_policy', Integer, {'implicit': 0, 'optional': True}), + ('inhibit_policy_mapping', Integer, {'implicit': 1, 'optional': True}), ] @@ -1855,16 +1855,16 @@ class Version(Integer): class TbsCertificate(Sequence): _fields = [ - ('version', Version, {'tag_type': 'explicit', 'tag': 0, 'default': 'v1'}), + ('version', Version, {'explicit': 0, 'default': 'v1'}), ('serial_number', Integer), ('signature', SignedDigestAlgorithm), ('issuer', Name), ('validity', Validity), ('subject', Name), ('subject_public_key_info', PublicKeyInfo), - ('issuer_unique_id', OctetBitString, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), - ('subject_unique_id', OctetBitString, {'tag_type': 'implicit', 'tag': 2, 'optional': True}), - ('extensions', Extensions, {'tag_type': 'explicit', 'tag': 3, 'optional': True}), + ('issuer_unique_id', OctetBitString, {'implicit': 1, 'optional': True}), + ('subject_unique_id', OctetBitString, {'implicit': 2, 'optional': True}), + ('extensions', Extensions, {'explicit': 3, 'optional': True}), ] @@ -2719,10 +2719,10 @@ class SequenceOfAlgorithmIdentifiers(SequenceOf): class CertificateAux(Sequence): _fields = [ ('trust', KeyPurposeIdentifiers, {'optional': True}), - ('reject', KeyPurposeIdentifiers, {'tag_type': 'implicit', 'tag': 0, 'optional': True}), + ('reject', KeyPurposeIdentifiers, {'implicit': 0, 'optional': True}), ('alias', UTF8String, {'optional': True}), ('keyid', OctetString, {'optional': True}), - ('other', SequenceOfAlgorithmIdentifiers, {'tag_type': 'implicit', 'tag': 1, 'optional': True}), + ('other', SequenceOfAlgorithmIdentifiers, {'implicit': 1, 'optional': True}), ] -- cgit v1.2.3 From f007d3159761ef07e1f173a55991124026c65400 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 15 Sep 2017 07:14:04 -0400 Subject: Update docs for new tagging API --- docs/universal_types.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/universal_types.md b/docs/universal_types.md index cc61099..048a135 100644 --- a/docs/universal_types.md +++ b/docs/universal_types.md @@ -637,39 +637,39 @@ from asn1crypto.core import Sequence, Choice, IA5String, UTCTime, ObjectIdentifi class Person(Choice): _alternatives = [ ('name', IA5String), - ('email', IA5String, {'tag_type': 'implicit', 'tag': 0}), + ('email', IA5String, {'implicit': 0}), ] class Record(Sequence): _fields = [ ('id', ObjectIdentifier), ('created', UTCTime), - ('creator', Person, {'tag_type': 'explicit', 'tag': 0, 'optional': True}), + ('creator', Person, {'explicit': 0, 'optional': True}), ] ``` -As is shown above, the keys `tag_type` and `tag` are used for tagging, and are -passed to a type class constructor via the optional third element of a field -or alternative tuple. The `tag_type` may be the unicode strings `'implicit'` or -`'explicit'` and the `tag` may be any integer. +As is shown above, the keys `implicit` and `explicit` are used for tagging, +and are passed to a type class constructor via the optional third element of +a field or alternative tuple. Both parameters may be an integer tag number, or +a 2-element tuple of string class name and integer tag. If a tagging value needs its tagging changed, the `.untag()` method can be used to create a copy of the object without explicit/implicit tagging. The `.retag()` -method can be used to change the tagging. This method accepts two parameters: -a unicode string `tag_type` and an integer `tag`. +method can be used to change the tagging. This method accepts one parameter, a +dict with either or both of the keys `implicit` and `explicit`. ```python person = Person(name='email', value='will@wbond.net') -# Will display "implicit" -print(person.tag_type) +# Will display True +print(person.implicit) -# Will display nothing -print(person.untag().tag_type) +# Will display False +print(person.untag().implicit) # Will display 0 print(person.tag) # Will display 1 -print(person.retag('implicit', 1).tag) +print(person.retag({'implicit': 1}).tag) ``` -- cgit v1.2.3 From b8852cf94128e3f756ec057476be8200bfff6815 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 15 Sep 2017 07:29:39 -0400 Subject: Allow Ans1Value.explicit class attribute to be specified as a simple 2-element tuple instead of a nested tuple --- asn1crypto/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index be3d36b..97eeda3 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -282,6 +282,10 @@ class Asn1Value(object): try: if self.__class__ not in _SETUP_CLASSES: cls = self.__class__ + # Allow explicit to be specified as a simple 2-element tuple + # instead of requiring the user make a nested tuple + if cls.explicit is not None and isinstance(cls.explicit[0], int_types): + cls.explicit = (cls.explicit, ) if hasattr(cls, '_setup'): self._setup() _SETUP_CLASSES[cls] = True -- cgit v1.2.3 From 48bcd644286921e110e476c5baec5194792edfdf Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 19 Sep 2017 18:45:33 +0200 Subject: Support ldaps and make matching nicer It's a little known secret that startswith() and endwith() take a tuple of arguments, too. --- asn1crypto/x509.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 577b66a..25c1a4c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1527,8 +1527,7 @@ class DistributionPoint(Sequence): for general_name in name.chosen: if general_name.name == 'uniform_resource_identifier': url = general_name.native - lurl = url.lower() - if lurl.startswith('http://') or lurl.startswith('https://') or lurl.startswith('ldap://'): + if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')): self._url = url break @@ -2412,8 +2411,7 @@ class Certificate(Sequence): if location.name != 'uniform_resource_identifier': continue url = location.native - lurl = url.lower() - if lurl.startswith('http://') or lurl.startswith('https://') or lurl.startswith('ldap://'): + if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')): output.append(url) return output -- cgit v1.2.3 From fbd6c458798408760333e360ba6d10d89fb22f35 Mon Sep 17 00:00:00 2001 From: wbond Date: Fri, 22 Sep 2017 15:42:42 -0400 Subject: Version 0.23.0 --- asn1crypto/version.py | 4 ++-- changelog.md | 24 ++++++++++++++++++++++++ readme.md | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/asn1crypto/version.py b/asn1crypto/version.py index b9d2775..31da728 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.22.0' -__version_info__ = (0, 22, 0) +__version__ = '0.23.0' +__version_info__ = (0, 23, 0) diff --git a/changelog.md b/changelog.md index 65bc016..7fbc594 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,29 @@ # changelog +## 0.23.0 + + - Backwards compatibility break: the `tag_type`, `explicit_tag` and + `explicit_class` attributes on `core.Asn1Value` no longer exist and were + replaced by the `implicit` and `explicit` attributes. Field param dicts + may use the new `explicit` and `implicit` keys, or the old `tag_type` and + `tag` keys. The attribute changes will likely to have little to no impact + since they were primarily an implementation detail. + - Teletex strings used inside of X.509 certificates are now interpreted + using Windows-1252 (a superset of ISO-8859-1). This enables compatibility + with certificates generated by OpenSSL. Strict parsing of Teletex strings + can be retained by using the `x509.strict_teletex()` context manager. + - Added support for nested explicit tagging, supporting values that are + defined with explicit tagging and then added as a field of another + structure using explicit tagging. + - Fixed a `UnicodeDecodeError` when trying to find the (optional) dependency + OpenSSL on Python 2 + - Fixed `next_update` field of `crl.TbsCertList` to be optional + - Added the `x509.Certificate.sha256_fingerprint` property + - `x509.Certificate.ocsp_urls` and `x509.DistributionPoint.url` will now + return `https://`, `ldap://` and `ldaps://` URLs in addition to `http://`. + - Added CMS Attribute Protection definitions from RFC 6211 + - Added OIDs from RFC 6962 + ## 0.22.0 - Added `parser.peek()` diff --git a/readme.md b/readme.md index 33473b5..574ba23 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,7 @@ faster to an order of magnitude of more. ## Current Release -0.22.0 - [changelog](changelog.md) +0.23.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3 From 4a074945b635549513e87a623eb14b86d849410e Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 7 Nov 2017 16:10:18 -0500 Subject: Change Windows to Unix line endings --- asn1crypto/_inet.py | 340 ++++++++++++++++++++++++++-------------------------- 1 file changed, 170 insertions(+), 170 deletions(-) diff --git a/asn1crypto/_inet.py b/asn1crypto/_inet.py index 0322bb4..045ba56 100644 --- a/asn1crypto/_inet.py +++ b/asn1crypto/_inet.py @@ -1,170 +1,170 @@ -# coding: utf-8 -from __future__ import unicode_literals, division, absolute_import, print_function - -import socket -import struct - -from ._errors import unwrap -from ._types import byte_cls, bytes_to_list, str_cls, type_name - - -def inet_ntop(address_family, packed_ip): - """ - Windows compatibility shim for socket.inet_ntop(). - - :param address_family: - socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 - - :param packed_ip: - A byte string of the network form of an IP address - - :return: - A unicode string of the IP address - """ - - if address_family not in set([socket.AF_INET, socket.AF_INET6]): - raise ValueError(unwrap( - ''' - address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), - not %s - ''', - repr(socket.AF_INET), - repr(socket.AF_INET6), - repr(address_family) - )) - - if not isinstance(packed_ip, byte_cls): - raise TypeError(unwrap( - ''' - packed_ip must be a byte string, not %s - ''', - type_name(packed_ip) - )) - - required_len = 4 if address_family == socket.AF_INET else 16 - if len(packed_ip) != required_len: - raise ValueError(unwrap( - ''' - packed_ip must be %d bytes long - is %d - ''', - required_len, - len(packed_ip) - )) - - if address_family == socket.AF_INET: - return '%d.%d.%d.%d' % tuple(bytes_to_list(packed_ip)) - - octets = struct.unpack(b'!HHHHHHHH', packed_ip) - - runs_of_zero = {} - longest_run = 0 - zero_index = None - for i, octet in enumerate(octets + (-1,)): - if octet != 0: - if zero_index is not None: - length = i - zero_index - if length not in runs_of_zero: - runs_of_zero[length] = zero_index - longest_run = max(longest_run, length) - zero_index = None - elif zero_index is None: - zero_index = i - - hexed = [hex(o)[2:] for o in octets] - - if longest_run < 2: - return ':'.join(hexed) - - zero_start = runs_of_zero[longest_run] - zero_end = zero_start + longest_run - - return ':'.join(hexed[:zero_start]) + '::' + ':'.join(hexed[zero_end:]) - - -def inet_pton(address_family, ip_string): - """ - Windows compatibility shim for socket.inet_ntop(). - - :param address_family: - socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 - - :param ip_string: - A unicode string of an IP address - - :return: - A byte string of the network form of the IP address - """ - - if address_family not in set([socket.AF_INET, socket.AF_INET6]): - raise ValueError(unwrap( - ''' - address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), - not %s - ''', - repr(socket.AF_INET), - repr(socket.AF_INET6), - repr(address_family) - )) - - if not isinstance(ip_string, str_cls): - raise TypeError(unwrap( - ''' - ip_string must be a unicode string, not %s - ''', - type_name(ip_string) - )) - - if address_family == socket.AF_INET: - octets = ip_string.split('.') - error = len(octets) != 4 - if not error: - ints = [] - for o in octets: - o = int(o) - if o > 255 or o < 0: - error = True - break - ints.append(o) - - if error: - raise ValueError(unwrap( - ''' - ip_string must be a dotted string with four integers in the - range of 0 to 255, got %s - ''', - repr(ip_string) - )) - - return struct.pack(b'!BBBB', *ints) - - error = False - omitted = ip_string.count('::') - if omitted > 1: - error = True - elif omitted == 0: - octets = ip_string.split(':') - error = len(octets) != 8 - else: - begin, end = ip_string.split('::') - begin_octets = begin.split(':') - end_octets = end.split(':') - missing = 8 - len(begin_octets) - len(end_octets) - octets = begin_octets + (['0'] * missing) + end_octets - - if not error: - ints = [] - for o in octets: - o = int(o, 16) - if o > 65535 or o < 0: - error = True - break - ints.append(o) - - return struct.pack(b'!HHHHHHHH', *ints) - - raise ValueError(unwrap( - ''' - ip_string must be a valid ipv6 string, got %s - ''', - repr(ip_string) - )) +# coding: utf-8 +from __future__ import unicode_literals, division, absolute_import, print_function + +import socket +import struct + +from ._errors import unwrap +from ._types import byte_cls, bytes_to_list, str_cls, type_name + + +def inet_ntop(address_family, packed_ip): + """ + Windows compatibility shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param packed_ip: + A byte string of the network form of an IP address + + :return: + A unicode string of the IP address + """ + + if address_family not in set([socket.AF_INET, socket.AF_INET6]): + raise ValueError(unwrap( + ''' + address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), + not %s + ''', + repr(socket.AF_INET), + repr(socket.AF_INET6), + repr(address_family) + )) + + if not isinstance(packed_ip, byte_cls): + raise TypeError(unwrap( + ''' + packed_ip must be a byte string, not %s + ''', + type_name(packed_ip) + )) + + required_len = 4 if address_family == socket.AF_INET else 16 + if len(packed_ip) != required_len: + raise ValueError(unwrap( + ''' + packed_ip must be %d bytes long - is %d + ''', + required_len, + len(packed_ip) + )) + + if address_family == socket.AF_INET: + return '%d.%d.%d.%d' % tuple(bytes_to_list(packed_ip)) + + octets = struct.unpack(b'!HHHHHHHH', packed_ip) + + runs_of_zero = {} + longest_run = 0 + zero_index = None + for i, octet in enumerate(octets + (-1,)): + if octet != 0: + if zero_index is not None: + length = i - zero_index + if length not in runs_of_zero: + runs_of_zero[length] = zero_index + longest_run = max(longest_run, length) + zero_index = None + elif zero_index is None: + zero_index = i + + hexed = [hex(o)[2:] for o in octets] + + if longest_run < 2: + return ':'.join(hexed) + + zero_start = runs_of_zero[longest_run] + zero_end = zero_start + longest_run + + return ':'.join(hexed[:zero_start]) + '::' + ':'.join(hexed[zero_end:]) + + +def inet_pton(address_family, ip_string): + """ + Windows compatibility shim for socket.inet_ntop(). + + :param address_family: + socket.AF_INET for IPv4 or socket.AF_INET6 for IPv6 + + :param ip_string: + A unicode string of an IP address + + :return: + A byte string of the network form of the IP address + """ + + if address_family not in set([socket.AF_INET, socket.AF_INET6]): + raise ValueError(unwrap( + ''' + address_family must be socket.AF_INET (%s) or socket.AF_INET6 (%s), + not %s + ''', + repr(socket.AF_INET), + repr(socket.AF_INET6), + repr(address_family) + )) + + if not isinstance(ip_string, str_cls): + raise TypeError(unwrap( + ''' + ip_string must be a unicode string, not %s + ''', + type_name(ip_string) + )) + + if address_family == socket.AF_INET: + octets = ip_string.split('.') + error = len(octets) != 4 + if not error: + ints = [] + for o in octets: + o = int(o) + if o > 255 or o < 0: + error = True + break + ints.append(o) + + if error: + raise ValueError(unwrap( + ''' + ip_string must be a dotted string with four integers in the + range of 0 to 255, got %s + ''', + repr(ip_string) + )) + + return struct.pack(b'!BBBB', *ints) + + error = False + omitted = ip_string.count('::') + if omitted > 1: + error = True + elif omitted == 0: + octets = ip_string.split(':') + error = len(octets) != 8 + else: + begin, end = ip_string.split('::') + begin_octets = begin.split(':') + end_octets = end.split(':') + missing = 8 - len(begin_octets) - len(end_octets) + octets = begin_octets + (['0'] * missing) + end_octets + + if not error: + ints = [] + for o in octets: + o = int(o, 16) + if o > 65535 or o < 0: + error = True + break + ints.append(o) + + return struct.pack(b'!HHHHHHHH', *ints) + + raise ValueError(unwrap( + ''' + ip_string must be a valid ipv6 string, got %s + ''', + repr(ip_string) + )) -- cgit v1.2.3 From 99b15fbfd13803ad245c6ff00418c801bbaa6bfc Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 7 Nov 2017 16:33:30 -0500 Subject: flake8 doesn't like the var name l any longer --- asn1crypto/_elliptic_curve.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py index 0ecab2d..8c0f12d 100644 --- a/asn1crypto/_elliptic_curve.py +++ b/asn1crypto/_elliptic_curve.py @@ -160,10 +160,10 @@ class PrimePoint(): p = self.curve.p - l = ((other.y - self.y) * inverse_mod(other.x - self.x, p)) % 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 + x3 = (l_ * l_ - self.x - other.x) % p + y3 = (l_ * (self.x - x3) - self.y) % p return PrimePoint(self.curve, x3, y3) @@ -232,10 +232,10 @@ class PrimePoint(): p = self.curve.p a = self.curve.a - l = ((3 * self.x * self.x + a) * inverse_mod(2 * self.y, p)) % p + 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 + x3 = (l_ * l_ - 2 * self.x) % p + y3 = (l_ * (self.x - x3) - self.y) % p return PrimePoint(self.curve, x3, y3) -- cgit v1.2.3 From 2d9186d2b7b76c89273aa3e7a38efc4c7028af30 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 7 Nov 2017 16:42:16 -0500 Subject: Fix CircleCI --- circle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/circle.yml b/circle.yml index 7f1b331..19a1b75 100644 --- a/circle.yml +++ b/circle.yml @@ -2,6 +2,7 @@ machine: pre: - pip install --user --ignore-installed --upgrade virtualenv pip - ln -s ~/Library/Python/2.7/bin/virtualenv /usr/local/bin/virtualenv + - brew update dependencies: override: - brew install python3 pypy -- cgit v1.2.3 From 63fa2f5d3bc9e7cf708d1da28575a1872fd6e9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 14 Nov 2017 20:11:12 +0100 Subject: Improve CMS documentation: It's always ContentInfo! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should avoid tickets like #74. Signed-off-by: Jörn Heissler --- asn1crypto/cms.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py index 2964f0a..9cad949 100644 --- a/asn1crypto/cms.py +++ b/asn1crypto/cms.py @@ -15,6 +15,8 @@ compatible with PKCS#7. Exports the following items: - SignedData() Other type classes are defined that help compose the types listed above. + +Most CMS structures in the wild are formatted as ContentInfo encapsulating one of the other types. """ from __future__ import unicode_literals, division, absolute_import, print_function -- cgit v1.2.3 From dc9d09ca0f6499c1f644a90561f88ff3213c2726 Mon Sep 17 00:00:00 2001 From: Matt Cooper Date: Fri, 17 Nov 2017 19:29:45 -0500 Subject: One bug fix and a few missing EKU oids changed subject_directory_attributes_value(self) to return self._subject_directory_attributes added some missing EKUs to the map --- asn1crypto/x509.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 25c1a4c..166448c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1684,6 +1684,8 @@ class KeyPurposeId(ObjectIdentifier): '1.3.6.1.4.1.311.10.3.12': 'microsoft_document_signing', '1.3.6.1.4.1.311.10.3.13': 'microsoft_lifetime_signing', '1.3.6.1.4.1.311.10.3.14': 'microsoft_mobile_device_software', + # https://support.microsoft.com/en-us/help/287547/object-ids-associated-with-microsoft-cryptography + '1.3.6.1.4.1.311.20.2.2': 'microsoft_smart_card_logon', # https://opensource.apple.com/source # - /Security/Security-57031.40.6/Security/libsecurity_keychain/lib/SecPolicy.cpp # - /libsecurity_cssm/libsecurity_cssm-36064/lib/oidsalg.c @@ -1719,6 +1721,14 @@ class KeyPurposeId(ObjectIdentifier): '1.2.840.113625.100.1.32': 'apple_test_smp_encryption', '1.2.840.113635.100.1.33': 'apple_server_authentication', '1.2.840.113635.100.1.34': 'apple_pcs_escrow_service', + # http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.201-2.pdf + '2.16.840.1.101.3.6.8': 'piv_card_authentication', + '2.16.840.1.101.3.6.7': 'piv_content_signing', + # https://tools.ietf.org/html/rfc4556.html + '1.3.6.1.5.2.3.4': 'pkinit_kpclientauth', + '1.3.6.1.5.2.3.4': 'pkinit_kpkdc', + # https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html + '1.2.840.113583.1.1.5': 'adobe_authentic_documents_trust', } @@ -1950,7 +1960,7 @@ class Certificate(Sequence): if not self._processed_extensions: self._set_extensions() - return self._key_identifier_value + return self._subject_directory_attributes @property def key_identifier_value(self): -- cgit v1.2.3 From 34f87fa70e2211c2ae356952d52b8fca334a122b Mon Sep 17 00:00:00 2001 From: Matt Cooper Date: Fri, 17 Nov 2017 19:46:32 -0500 Subject: oops, two .4s won't work there! --- asn1crypto/x509.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 166448c..f051e40 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1726,9 +1726,9 @@ class KeyPurposeId(ObjectIdentifier): '2.16.840.1.101.3.6.7': 'piv_content_signing', # https://tools.ietf.org/html/rfc4556.html '1.3.6.1.5.2.3.4': 'pkinit_kpclientauth', - '1.3.6.1.5.2.3.4': 'pkinit_kpkdc', + '1.3.6.1.5.2.3.5': 'pkinit_kpkdc', # https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html - '1.2.840.113583.1.1.5': 'adobe_authentic_documents_trust', + '1.2.840.113583.1.1.5': 'adobe_authentic_documents_trust', } -- cgit v1.2.3 From 764716321c4cca85115906bcf82b6a6d74d6f764 Mon Sep 17 00:00:00 2001 From: wbond Date: Mon, 20 Nov 2017 07:31:11 -0500 Subject: Don't ever return "yes" from x509.Certificate.self_signed --- asn1crypto/x509.py | 17 ++++++++++------- tests/test_x509.py | 10 +++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index f051e40..df37205 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2513,11 +2513,14 @@ class Certificate(Sequence): def self_signed(self): """ :return: - A unicode string of "yes", "no" or "maybe". The "maybe" result will - be returned if the certificate does not contain a key identifier - extension, but is issued by the subject. In this case the - certificate signature will need to be verified using the subject - public key to determine a "yes" or "no" answer. + A unicode string of "no" or "maybe". The "maybe" result will + be returned if the certificate issuer and subject are the same. + If a key identifier and authority key identifier are present, + they will need to match otherwise "no" will be returned. + + To verify is a certificate is truly self-signed, the signature + will need to be verified. See the certvalidator package for + one possible solution. """ if self._self_signed is None: @@ -2525,9 +2528,9 @@ class Certificate(Sequence): if self.self_issued: if self.key_identifier: if not self.authority_key_identifier: - self._self_signed = 'yes' + self._self_signed = 'maybe' elif self.authority_key_identifier == self.key_identifier: - self._self_signed = 'yes' + self._self_signed = 'maybe' else: self._self_signed = 'maybe' return self._self_signed diff --git a/tests/test_x509.py b/tests/test_x509.py index ecd1772..5394836 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -2606,7 +2606,7 @@ class X509Tests(unittest.TestCase): return ( ( 'keys/test-der.crt', - 'yes' + 'maybe' ), ( 'keys/test-inter-der.crt', @@ -2618,11 +2618,11 @@ class X509Tests(unittest.TestCase): ), ( 'geotrust_certs/GeoTrust_Universal_CA.crt', - 'yes' + 'maybe' ), ( 'geotrust_certs/GeoTrust_Primary_CA.crt', - 'yes' + 'maybe' ), ( 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt', @@ -2634,7 +2634,7 @@ class X509Tests(unittest.TestCase): ), ( 'lets_encrypt/isrgrootx1.pem', - 'yes' + 'maybe' ), ( 'lets_encrypt/letsencryptauthorityx1.pem', @@ -2650,7 +2650,7 @@ class X509Tests(unittest.TestCase): ), ( 'globalsign_example_keys/rootCA.cer', - 'yes' + 'maybe' ), ( 'globalsign_example_keys/SSL1.cer', -- cgit v1.2.3 From ad3b61e1b6b23991bff5a50072b8db30ff28ffcd Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 21 Nov 2017 12:06:25 -0500 Subject: Fix handling of explicitly tagged fields with defaults --- asn1crypto/core.py | 112 ++++++++++++++++++++++++++++------------------------- tests/test_core.py | 18 +++++++++ 2 files changed, 78 insertions(+), 52 deletions(-) diff --git a/asn1crypto/core.py b/asn1crypto/core.py index 97eeda3..14a8203 100644 --- a/asn1crypto/core.py +++ b/asn1crypto/core.py @@ -5029,15 +5029,14 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param # If an explicit specification was passed in, make sure it matches if spec is not None: - if spec_params: - value = spec(contents=contents, **spec_params) - else: - value = spec(contents=contents) - - if spec is Any: - pass - - elif value.explicit: + # If there is explicit tagging and contents, we have to split + # the header and trailer off before we do the parsing + no_explicit = spec_params and 'no_explicit' in spec_params + if not no_explicit and (spec.explicit or (spec_params and 'explicit' in spec_params)): + if spec_params: + value = spec(**spec_params) + else: + value = spec() original_explicit = value.explicit explicit_info = reversed(original_explicit) parsed_class = class_ @@ -5081,60 +5080,69 @@ def _build(class_, method, tag, header, contents, trailer, spec=None, spec_param parsed_class, parsed_method, parsed_tag, parsed_header, to_parse, parsed_trailer = info explicit_header += parsed_header explicit_trailer = parsed_trailer + explicit_trailer + value = _build(*info, spec=spec, spec_params={'no_explicit': True}) value._header = explicit_header value._trailer = explicit_trailer value.explicit = original_explicit header_set = True - - elif isinstance(value, Choice): - value.validate(class_, tag, contents) - try: - # Force parsing the Choice now - value.contents = header + value.contents - header = b'' - value.parse() - except (ValueError, TypeError) as e: - args = e.args[1:] - e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args - raise e - else: - if class_ != value.class_: - raise ValueError(unwrap( - ''' - Error parsing %s - class should have been %s, but %s was - found - ''', - type_name(value), - CLASS_NUM_TO_NAME_MAP.get(value.class_), - CLASS_NUM_TO_NAME_MAP.get(class_, class_) - )) - if method != value.method: - # Allow parsing a primitive method as constructed if the value - # is indefinite length. This is to allow parsing BER. - ber_indef = method == 1 and value.method == 0 and trailer == b'\x00\x00' - if not ber_indef or not isinstance(value, Constructable): + if spec_params: + value = spec(contents=contents, **spec_params) + else: + value = spec(contents=contents) + + if spec is Any: + pass + + elif isinstance(value, Choice): + value.validate(class_, tag, contents) + try: + # Force parsing the Choice now + value.contents = header + value.contents + header = b'' + value.parse() + except (ValueError, TypeError) as e: + args = e.args[1:] + e.args = (e.args[0] + '\n while parsing %s' % type_name(value),) + args + raise e + + else: + if class_ != value.class_: raise ValueError(unwrap( ''' - Error parsing %s - method should have been %s, but %s was found + Error parsing %s - class should have been %s, but %s was + found ''', type_name(value), - METHOD_NUM_TO_NAME_MAP.get(value.method), - METHOD_NUM_TO_NAME_MAP.get(method, method) + CLASS_NUM_TO_NAME_MAP.get(value.class_), + CLASS_NUM_TO_NAME_MAP.get(class_, class_) + )) + if method != value.method: + # Allow parsing a primitive method as constructed if the value + # is indefinite length. This is to allow parsing BER. + ber_indef = method == 1 and value.method == 0 and trailer == b'\x00\x00' + if not ber_indef or not isinstance(value, Constructable): + raise ValueError(unwrap( + ''' + Error parsing %s - method should have been %s, but %s was found + ''', + type_name(value), + METHOD_NUM_TO_NAME_MAP.get(value.method), + METHOD_NUM_TO_NAME_MAP.get(method, method) + )) + 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 )) - 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 - )) # 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/tests/test_core.py b/tests/test_core.py index 1d4f0ce..94fd8aa 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -63,6 +63,13 @@ class Enum(core.Enumerated): } +class ExplicitFieldDefault(core.Sequence): + _fields = [ + ('bits', NamedBits), + ('seq', Seq, {'explicit': 2, 'default': {'id': '1.2.3', 'value': 10}}), + ] + + class NumChoice(core.Choice): _alternatives = [ ('one', core.Integer, {'explicit': 0}), @@ -471,6 +478,17 @@ class CoreTests(unittest.TestCase): self.assertEqual(b'\xa0\x03\x02\x01', val2.chosen._header) self.assertEqual(b'\x00', val2.chosen.contents) + def test_explicit_field_default(self): + val = ExplicitFieldDefault.load(b'\x30\x0f\x03\x02\x06@\xa2\x090\x07\x06\x02*\x03\x02\x01\x01') + self.assertEqual(set(['one']), val['bits'].native) + self.assertEqual( + util.OrderedDict([ + ('id', '1.2.3'), + ('value', 1) + ]), + val['seq'].native + ) + def test_explicit_header_field_choice(self): der = b'\x30\x07\xa0\x05\xa0\x03\x02\x01\x00' val = ExplicitField.load(der) -- cgit v1.2.3 From c3d8505acb5e0eba1dd0d568a844f26fb7039600 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 22 Nov 2017 11:10:48 -0500 Subject: Add algos.AnyAlgorithmIdentifier --- asn1crypto/algos.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/asn1crypto/algos.py b/asn1crypto/algos.py index dd4ae5b..c805433 100644 --- a/asn1crypto/algos.py +++ b/asn1crypto/algos.py @@ -5,6 +5,7 @@ ASN.1 type classes for various algorithms using in various aspects of public key cryptography. Exports the following items: - AlgorithmIdentifier() + - AnyAlgorithmIdentifier() - DigestAlgorithm() - DigestInfo() - DSASignature() @@ -1113,3 +1114,30 @@ class Pkcs5MacAlgorithm(Sequence): EncryptionAlgorithm._oid_specs['pbes2'] = Pbes2Params + + +class AnyAlgorithmId(ObjectIdentifier): + _map = {} + + def _setup(self): + _map = self.__class__._map + for other_cls in (EncryptionAlgorithmId, SignedDigestAlgorithmId, DigestAlgorithmId): + for oid, name in other_cls._map.items(): + _map[oid] = name + + +class AnyAlgorithmIdentifier(_ForceNullParameters, Sequence): + _fields = [ + ('algorithm', AnyAlgorithmId), + ('parameters', Any, {'optional': True}), + ] + + _oid_pair = ('algorithm', 'parameters') + _oid_specs = {} + + def _setup(self): + Sequence._setup(self) + specs = self.__class__._oid_specs + for other_cls in (EncryptionAlgorithm, SignedDigestAlgorithm): + for oid, spec in other_cls._oid_specs.items(): + specs[oid] = spec -- cgit v1.2.3 From a11c450e2acd5d3d3bef2ac22da6cc47bd93ac74 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 22 Nov 2017 11:13:00 -0500 Subject: Add better handling of Subject Directory Attributes, especially related to TPM --- asn1crypto/keys.py | 6 +- asn1crypto/x509.py | 264 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 259 insertions(+), 11 deletions(-) diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py index 3c4870d..9a09a31 100644 --- a/asn1crypto/keys.py +++ b/asn1crypto/keys.py @@ -30,7 +30,7 @@ from ._elliptic_curve import ( ) from ._errors import unwrap from ._types import type_name, str_cls, byte_cls -from .algos import _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm +from .algos import _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm, RSAESOAEPParams from .core import ( Any, Asn1Value, @@ -930,6 +930,8 @@ class PublicKeyAlgorithmId(ObjectIdentifier): _map = { # https://tools.ietf.org/html/rfc3279#page-19 '1.2.840.113549.1.1.1': 'rsa', + # https://tools.ietf.org/html/rfc3447#page-47 + '1.2.840.113549.1.1.7': 'rsaes_oaep', # https://tools.ietf.org/html/rfc3279#page-18 '1.2.840.10040.4.1': 'dsa', # https://tools.ietf.org/html/rfc3279#page-13 @@ -955,6 +957,7 @@ class PublicKeyAlgorithm(_ForceNullParameters, Sequence): 'dsa': DSAParams, 'ec': ECDomainParameters, 'dh': DomainParameters, + 'rsaes_oaep': RSAESOAEPParams, } @@ -973,6 +976,7 @@ class PublicKeyInfo(Sequence): algorithm = self['algorithm']['algorithm'].native return { 'rsa': RSAPublicKey, + 'rsaes_oaep': RSAPublicKey, 'dsa': Integer, # We override the field spec with ECPoint so that users can easily # decompose the byte string into the constituent X and Y coords diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index df37205..f749e7c 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -28,7 +28,7 @@ from ._errors import unwrap from ._iri import iri_to_uri, uri_to_iri from ._ordereddict import OrderedDict from ._types import type_name, str_cls, bytes_to_list -from .algos import AlgorithmIdentifier, SignedDigestAlgorithm +from .algos import AlgorithmIdentifier, AnyAlgorithmIdentifier, DigestAlgorithm, SignedDigestAlgorithm from .core import ( Any, BitString, @@ -36,6 +36,7 @@ from .core import ( Boolean, Choice, Concat, + Enumerated, GeneralizedTime, GeneralString, IA5String, @@ -517,6 +518,13 @@ class NameType(ObjectIdentifier): '2.5.4.46': 'dn_qualifier', '2.5.4.65': 'pseudonym', '2.5.4.97': 'organization_identifier', + # https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf + '2.23.133.2.1': 'tpm_manufacturer', + '2.23.133.2.2': 'tpm_model', + '2.23.133.2.3': 'tpm_version', + '2.23.133.2.4': 'platform_manufacturer', + '2.23.133.2.5': 'platform_model', + '2.23.133.2.6': 'platform_version', # https://tools.ietf.org/html/rfc2985#page-26 '1.2.840.113549.1.9.1': 'email_address', # Page 10 of https://cabforum.org/wp-content/uploads/EV-V1_5_5.pdf @@ -559,6 +567,12 @@ class NameType(ObjectIdentifier): 'domain_component', 'name_distinguisher', 'organization_identifier', + 'tpm_manufacturer', + 'tpm_model', + 'tpm_version', + 'platform_manufacturer', + 'platform_model', + 'platform_version', ] @classmethod @@ -616,6 +630,12 @@ class NameType(ObjectIdentifier): 'domain_component': 'Domain Component', 'name_distinguisher': 'Name Distinguisher', 'organization_identifier': 'Organization Identifier', + 'tpm_manufacturer': 'TPM Manufacturer', + 'tpm_model': 'TPM Model', + 'tpm_version': 'TPM Version', + 'platform_manufacturer': 'Platform Manufacturer', + 'platform_model': 'Platform Model', + 'platform_version': 'Platform Version', }.get(self.native, self.native) @@ -656,6 +676,12 @@ class NameTypeAndValue(Sequence): 'domain_component': DNSName, 'name_distinguisher': DirectoryString, 'organization_identifier': DirectoryString, + 'tpm_manufacturer': UTF8String, + 'tpm_model': UTF8String, + 'tpm_version': UTF8String, + 'platform_manufacturer': UTF8String, + 'platform_model': UTF8String, + 'platform_version': UTF8String, } _prepped = None @@ -1785,6 +1811,232 @@ class NetscapeCertificateType(BitString): } +class Version(Integer): + _map = { + 0: 'v1', + 1: 'v2', + 2: 'v3', + } + + +class TPMSpecification(Sequence): + _fields = [ + ('family', UTF8String), + ('level', Integer), + ('revision', Integer), + ] + + +class SetOfTPMSpecification(SetOf): + _child_spec = TPMSpecification + + +class TCGSpecificationVersion(Sequence): + _fields = [ + ('major_version', Integer), + ('minor_version', Integer), + ('revision', Integer), + ] + + +class TCGPlatformSpecification(Sequence): + _fields = [ + ('version', TCGSpecificationVersion), + ('platform_class', OctetString), + ] + + +class SetOfTCGPlatformSpecification(SetOf): + _child_spec = TCGPlatformSpecification + + +class EKGenerationType(Enumerated): + _map = { + 0: 'internal', + 1: 'injected', + 2: 'internal_revocable', + 3: 'injected_revocable', + } + + +class EKGenerationLocation(Enumerated): + _map = { + 0: 'tpm_manufacturer', + 1: 'platform_manufacturer', + 2: 'ek_cert_signer', + } + + +class EKCertificateGenerationLocation(Enumerated): + _map = { + 0: 'tpm_manufacturer', + 1: 'platform_manufacturer', + 2: 'ek_cert_signer', + } + + +class EvaluationAssuranceLevel(Enumerated): + _map = { + 1: 'level1', + 2: 'level2', + 3: 'level3', + 4: 'level4', + 5: 'level5', + 6: 'level6', + 7: 'level7', + } + + +class EvaluationStatus(Enumerated): + _map = { + 0: 'designed_to_meet', + 1: 'evaluation_in_progress', + 2: 'evaluation_completed', + } + + +class StrengthOfFunction(Enumerated): + _map = { + 0: 'basic', + 1: 'medium', + 2: 'high', + } + + +class URIReference(Sequence): + _fields = [ + ('uniform_resource_identifier', IA5String), + ('hash_algorithm', DigestAlgorithm, {'optional': True}), + ('hash_value', BitString, {'optional': True}), + ] + + +class CommonCriteriaMeasures(Sequence): + _fields = [ + ('version', IA5String), + ('assurance_level', EvaluationAssuranceLevel), + ('evaluation_status', EvaluationStatus), + ('plus', Boolean, {'default': False}), + ('strengh_of_function', StrengthOfFunction, {'implicit': 0, 'optional': True}), + ('profile_oid', ObjectIdentifier, {'implicit': 1, 'optional': True}), + ('profile_url', URIReference, {'implicit': 2, 'optional': True}), + ('target_oid', ObjectIdentifier, {'implicit': 3, 'optional': True}), + ('target_uri', URIReference, {'implicit': 4, 'optional': True}), + ] + + +class SecurityLevel(Enumerated): + _map = { + 1: 'level1', + 2: 'level2', + 3: 'level3', + 4: 'level4', + } + + +class FIPSLevel(Sequence): + _fields = [ + ('version', IA5String), + ('level', SecurityLevel), + ('plus', Boolean, {'default': False}), + ] + + +class TPMSecurityAssertions(Sequence): + _fields = [ + ('version', Version, {'default': 'v1'}), + ('field_upgradable', Boolean, {'default': False}), + ('ek_generation_type', EKGenerationType, {'implicit': 0, 'optional': True}), + ('ek_generation_location', EKGenerationLocation, {'implicit': 1, 'optional': True}), + ('ek_certificate_generation_location', EKCertificateGenerationLocation, {'implicit': 2, 'optional': True}), + ('cc_info', CommonCriteriaMeasures, {'implicit': 3, 'optional': True}), + ('fips_level', FIPSLevel, {'implicit': 4, 'optional': True}), + ('iso_9000_certified', Boolean, {'implicit': 5, 'default': False}), + ('iso_9000_uri', IA5String, {'optional': True}), + ] + + +class SetOfTPMSecurityAssertions(SetOf): + _child_spec = TPMSecurityAssertions + + +class SubjectDirectoryAttributeId(ObjectIdentifier): + _map = { + # https://tools.ietf.org/html/rfc2256#page-11 + '2.5.4.52': 'supported_algorithms', + # https://www.trustedcomputinggroup.org/wp-content/uploads/Credential_Profile_EK_V2.0_R14_published.pdf + '2.23.133.2.16': 'tpm_specification', + '2.23.133.2.17': 'tcg_platform_specification', + '2.23.133.2.18': 'tpm_security_assertions', + # https://tools.ietf.org/html/rfc3739#page-18 + '1.3.6.1.5.5.7.9.1': 'pda_date_of_birth', + '1.3.6.1.5.5.7.9.2': 'pda_place_of_birth', + '1.3.6.1.5.5.7.9.3': 'pda_gender', + '1.3.6.1.5.5.7.9.4': 'pda_country_of_citizenship', + '1.3.6.1.5.5.7.9.5': 'pda_country_of_residence', + # https://holtstrom.com/michael/tools/asn1decoder.php + '1.2.840.113533.7.68.29': 'entrust_user_role', + } + + +class SetOfGeneralizedTime(SetOf): + _child_spec = GeneralizedTime + + +class SetOfDirectoryString(SetOf): + _child_spec = DirectoryString + + +class SetOfPrintableString(SetOf): + _child_spec = PrintableString + + +class SupportedAlgorithm(Sequence): + _fields = [ + ('algorithm_identifier', AnyAlgorithmIdentifier), + ('intended_usage', KeyUsage, {'explicit': 0, 'optional': True}), + ('intended_certificate_policies', CertificatePolicies, {'explicit': 1, 'optional': True}), + ] + + +class SetOfSupportedAlgorithm(SetOf): + _child_spec = SupportedAlgorithm + + +class SubjectDirectoryAttribute(Sequence): + _fields = [ + ('type', SubjectDirectoryAttributeId), + ('values', Any), + ] + + _oid_pair = ('type', 'values') + _oid_specs = { + 'supported_algorithms': SetOfSupportedAlgorithm, + 'tpm_specification': SetOfTPMSpecification, + 'tcg_platform_specification': SetOfTCGPlatformSpecification, + 'tpm_security_assertions': SetOfTPMSecurityAssertions, + 'pda_date_of_birth': SetOfGeneralizedTime, + 'pda_place_of_birth': SetOfDirectoryString, + 'pda_gender': SetOfPrintableString, + 'pda_country_of_citizenship': SetOfPrintableString, + 'pda_country_of_residence': SetOfPrintableString, + } + + def _values_spec(self): + type_ = self['type'].native + if type_ in self._oid_specs: + return self._oid_specs[type_] + return SetOf + + _spec_callbacks = { + 'values': _values_spec + } + + +class SubjectDirectoryAttributes(SequenceOf): + _child_spec = SubjectDirectoryAttribute + + class ExtensionId(ObjectIdentifier): _map = { '2.5.29.9': 'subject_directory_attributes', @@ -1824,7 +2076,7 @@ class Extension(Sequence): _oid_pair = ('extn_id', 'extn_value') _oid_specs = { - 'subject_directory_attributes': Attributes, + 'subject_directory_attributes': SubjectDirectoryAttributes, 'key_identifier': OctetString, 'key_usage': KeyUsage, 'private_key_usage_period': PrivateKeyUsagePeriod, @@ -1854,14 +2106,6 @@ class Extensions(SequenceOf): _child_spec = Extension -class Version(Integer): - _map = { - 0: 'v1', - 1: 'v2', - 2: 'v3', - } - - class TbsCertificate(Sequence): _fields = [ ('version', Version, {'explicit': 0, 'default': 'v1'}), -- cgit v1.2.3 From 7f587d56f2b5c4815bfed835cfc10abd9c49cdb2 Mon Sep 17 00:00:00 2001 From: wbond Date: Wed, 22 Nov 2017 11:13:45 -0500 Subject: Add x509.KeyPurposeId OID for Federal PKI --- asn1crypto/x509.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index f749e7c..7519cc6 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -1755,6 +1755,8 @@ class KeyPurposeId(ObjectIdentifier): '1.3.6.1.5.2.3.5': 'pkinit_kpkdc', # https://www.adobe.com/devnet-docs/acrobatetk/tools/DigSig/changes.html '1.2.840.113583.1.1.5': 'adobe_authentic_documents_trust', + # https://www.idmanagement.gov/wp-content/uploads/sites/1171/uploads/fpki-pivi-cert-profiles.pdf + '2.16.840.1.101.3.8.7': 'fpki_pivi_content_signing' } -- cgit v1.2.3 From b7256ed36f786a79ce93c72a5c6bcbe2826c8832 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 28 Nov 2017 11:12:32 -0500 Subject: Fix type checking in pem.armor() --- asn1crypto/pem.py | 8 ++++---- tests/test_pem.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py index 8cb6024..245dc13 100644 --- a/asn1crypto/pem.py +++ b/asn1crypto/pem.py @@ -16,7 +16,7 @@ import re import sys from ._errors import unwrap -from ._types import type_name, str_cls, byte_cls +from ._types import type_name as _type_name, str_cls, byte_cls if sys.version_info < (3,): from cStringIO import StringIO as BytesIO @@ -41,7 +41,7 @@ def detect(byte_string): ''' byte_string must be a byte string, not %s ''', - type_name(byte_string) + _type_name(byte_string) )) return byte_string.find(b'-----BEGIN') != -1 or byte_string.find(b'---- BEGIN') != -1 @@ -71,7 +71,7 @@ def armor(type_name, der_bytes, headers=None): raise TypeError(unwrap( ''' der_bytes must be a byte string, not %s - ''' % type_name(der_bytes) + ''' % _type_name(der_bytes) )) if not isinstance(type_name, str_cls): @@ -79,7 +79,7 @@ def armor(type_name, der_bytes, headers=None): ''' type_name must be a unicode string, not %s ''', - type_name(type_name) + _type_name(type_name) )) type_name = type_name.upper().encode('ascii') diff --git a/tests/test_pem.py b/tests/test_pem.py index 05dcd3d..ce4cc9a 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -127,3 +127,15 @@ class PEMTests(unittest.TestCase): with open(os.path.join(fixtures_dir, expected_bytes_filename), 'rb') as f: expected_bytes = f.read() self.assertEqual(expected_bytes, encoded_bytes) + + def test_armor_wrong_type(self): + with self.assertRaisesRegexp(TypeError, 'type_name must be a unicode string'): + pem.armor(b'CERTIFICATE', b'') + + def test_armor_wrong_type2(self): + with self.assertRaisesRegexp(TypeError, 'der_bytes must be a byte string'): + pem.armor('CERTIFICATE', '') + + def test_detect_wrong_type(self): + with self.assertRaisesRegexp(TypeError, 'byte_string must be a byte string'): + pem.detect('CERTIFICATE') -- cgit v1.2.3 From 0fb49f7ab440471ebaffffbda7c64ac1aaa39645 Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 28 Nov 2017 11:12:46 -0500 Subject: Fix docstring for pem.armor() --- asn1crypto/pem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py index 245dc13..01e717e 100644 --- a/asn1crypto/pem.py +++ b/asn1crypto/pem.py @@ -51,15 +51,15 @@ def armor(type_name, der_bytes, headers=None): """ Armors a DER-encoded byte string in PEM - :param der_bytes: - A byte string to be armored - :param type_name: A unicode string that will be capitalized and placed in the header and footer of the block. E.g. "CERTIFICATE", "PRIVATE KEY", etc. This will appear as "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----". + :param der_bytes: + A byte string to be armored + :param headers: An OrderedDict of the header lines to write after the BEGIN line -- cgit v1.2.3 From e8d1b1f6b20e459cc588fc45a6a1be299274636a Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 28 Nov 2017 11:13:05 -0500 Subject: Add test for pem.unarmor(data, multiple=True) --- tests/test_pem.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_pem.py b/tests/test_pem.py index ce4cc9a..34a1498 100644 --- a/tests/test_pem.py +++ b/tests/test_pem.py @@ -118,6 +118,23 @@ class PEMTests(unittest.TestCase): expected_bytes = f.read() self.assertEqual(expected_bytes, decoded_bytes) + def test_unarmor_multiple(self): + data = self.unarmor_armor_files() + input_data = b'' + der_data = [] + for pem_file, der_file in ((data[0][0], data[0][1]), (data[1][0], data[1][1])): + with open(os.path.join(fixtures_dir, pem_file), 'rb') as f: + input_data += f.read() + b'\n' + with open(os.path.join(fixtures_dir, der_file), 'rb') as f: + der_data.append(f.read()) + i = 0 + for name, headers, der_bytes in pem.unarmor(input_data, True): + self.assertEqual('CERTIFICATE', name) + self.assertEqual({}, headers) + self.assertEqual(der_data[i], der_bytes) + i += 1 + self.assertEqual(2, i) + @data('unarmor_armor_files') def armor(self, expected_bytes_filename, relative_path, type_name, headers): with open(os.path.join(fixtures_dir, relative_path), 'rb') as f: -- cgit v1.2.3 From 5aeea999427f74b21636226b901d2114b36611ff Mon Sep 17 00:00:00 2001 From: wbond Date: Tue, 28 Nov 2017 11:18:36 -0500 Subject: Fix _type_name() reference in pem --- asn1crypto/pem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asn1crypto/pem.py b/asn1crypto/pem.py index 01e717e..511ea4b 100644 --- a/asn1crypto/pem.py +++ b/asn1crypto/pem.py @@ -132,7 +132,7 @@ def _unarmor(pem_bytes): ''' pem_bytes must be a byte string, not %s ''', - type_name(pem_bytes) + _type_name(pem_bytes) )) # Valid states include: "trash", "headers", "body" -- cgit v1.2.3 From 8300e4dcf20a943be1f11830562c6849e27f4200 Mon Sep 17 00:00:00 2001 From: Matt Cooper Date: Tue, 5 Dec 2017 15:19:46 -0500 Subject: private_key_usage_period_value property (#79) Adding private_key_usage_period_value property to x509.Certificate --- asn1crypto/x509.py | 17 ++++++++++++++++- tests/fixtures/ocsp-with-pkup.pem | 34 ++++++++++++++++++++++++++++++++++ tests/test_x509.py | 14 ++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/ocsp-with-pkup.pem diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py index 7519cc6..5a572a3 100644 --- a/asn1crypto/x509.py +++ b/asn1crypto/x509.py @@ -2149,6 +2149,7 @@ class Certificate(Sequence): _extended_key_usage_value = None _authority_information_access_value = None _subject_information_access_value = None + _private_key_usage_period_value = None _tls_feature_value = None _ocsp_no_check_value = None _issuer_serial = None @@ -2194,6 +2195,20 @@ class Certificate(Sequence): self._set_extensions() return self._critical_extensions + @property + def private_key_usage_period_value(self): + """ + This extension is used to constrain the period over which the subject + private key may be used + + :return: + None or a PrivateKeyUsagePeriod object + """ + + if not self._processed_extensions: + self._set_extensions() + return self._private_key_usage_period_value + @property def subject_directory_attributes_value(self): """ @@ -2201,7 +2216,7 @@ class Certificate(Sequence): about the subject. :return: - None or an Attributes object + None or a SubjectDirectoryAttributes object """ if not self._processed_extensions: diff --git a/tests/fixtures/ocsp-with-pkup.pem b/tests/fixtures/ocsp-with-pkup.pem new file mode 100644 index 0000000..6e502a1 --- /dev/null +++ b/tests/fixtures/ocsp-with-pkup.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF9TCCBN2gAwIBAgIEWOfTMzANBgkqhkiG9w0BAQsFADCBoDELMAkGA1UEBhMC +VVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEnMCUGA1UECxMeRGVwYXJ0bWVu +dCBvZiBWZXRlcmFucyBBZmZhaXJzMSIwIAYDVQQLExlDZXJ0aWZpY2F0aW9uIEF1 +dGhvcml0aWVzMSowKAYDVQQLEyFEZXBhcnRtZW50IG9mIFZldGVyYW5zIEFmZmFp +cnMgQ0EwHhcNMTcwOTE4MTUxNzM2WhcNMTgwMTAxMDQxNDIxWjCBvzELMAkGA1UE +BhMCVVMxGDAWBgNVBAoTD1UuUy4gR292ZXJubWVudDEnMCUGA1UECxMeRGVwYXJ0 +bWVudCBvZiBWZXRlcmFucyBBZmZhaXJzMSIwIAYDVQQLExlDZXJ0aWZpY2F0aW9u +IEF1dGhvcml0aWVzMSowKAYDVQQLEyFEZXBhcnRtZW50IG9mIFZldGVyYW5zIEFm +ZmFpcnMgQ0ExHTAbBgNVBAMTFE9DU1AgU2lnbmVyIDRlMzk3ZjIyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvx21ftIpP7KLKtjogcSQ+QfU0hKTL6e6 +UXPkudhIjQTEQFmP5bpvMDm6jypS5ws+3ND9prICuqN688hzPedOrQkBMn3+gd93 +3TnF0NIV1klR9HLfnvJd6wWv9otSplMKufCBWS6KRvcimqe2+4/2ksctgaAltkXI +imrw/5QmudC+wIa+nBj7+DWCMqjqp4mAHdlcUzrWn5XrbhvFwSM2smnunjYePKVm +OZrVU9CCOLsF1IWXaZxiceeM7ijD8eAD1jadkXBPEd1e1tlbii0+/zzaK9Q6i6ZO +IYXTqatrJgjVADperqJbqPtdc7CupjhdulyhXZPJj4t+QLgmUIf/rwIDAQABo4IC +FDCCAhAwDgYDVR0PAQH/BAQDAgeAMIHBBgNVHSAEgbkwgbYwDAYKYIZIAWUDAgED +BjAMBgpghkgBZQMCAQMHMAwGCmCGSAFlAwIBAwgwDAYKYIZIAWUDAgEDDTAMBgpg +hkgBZQMCAQMQMAwGCmCGSAFlAwIBAxEwDAYKYIZIAWUDAgEDJDAMBgpghkgBZQMC +AQMnMAwGCmCGSAFlAwIBAygwDAYKYIZIAWUDAgEDKTAMBgpghkgBZQMCAQULMAwG +CmCGSAFlAwIBBQowDAYKYIZIAWUDAgEFDDBDBggrBgEFBQcBAQQ3MDUwMwYIKwYB +BQUHMAKGJ2h0dHA6Ly9wa2kudHJlYXN1cnkuZ292L3ZhY2FfZWVfYWlhLnA3YzAT +BgNVHSUEDDAKBggrBgEFBQcDCTAPBgkrBgEFBQcwAQUEAgUAMDwGA1UdEQQ1MDOC +FE9DU1AgU2lnbmVyIDRlMzk3ZjIygRtwa2lfb3BzQGZpc2NhbC50cmVhc3VyeS5n +b3YwKwYDVR0QBCQwIoAPMjAxNzA5MTgxNTE3MzZagQ8yMDE4MDEwMTA0MTQyMVow +HwYDVR0jBBgwFoAUz3k87U28GSXyRWlOEi+cKVPJp0YwHQYDVR0OBBYEFMIlFB4K +dCvftTaEuessnxmWVjheMAkGA1UdEwQCMAAwGQYJKoZIhvZ9B0EABAwwChsEVjgu +MQMCBDAwDQYJKoZIhvcNAQELBQADggEBAJl4L4BxTEUH1RSBnqluzQYvNNV352QJ +W6kn0XUDPfCM46gLjB+3UxkcYi3wulPnUniPjkDSIODzMVdYTjMccdjeazl2Wlbg +hCe64L8XhwWVBm8pfYxPpBLIlreDco4yEW1GHf46oRtyHvcSSnFnU4hN4WISifDc +d7TuxFe2Hff9mGRRxpuZt9HLwkQHA88YV9GKJqWXM439+KVC6Tl+OVeQv49jhQ6Y +zp/Da19nWb79/TN4avdL9vkGlpCXTKoh6ZlHo9w9VME/aFXUchXC5TShOhR4Lxhn +/VrflrXQobt9qzCao5y1ZoO2QbCDLC21UcOO9dvbAkcN1GCLL0YZBQ4= +-----END CERTIFICATE----- diff --git a/tests/test_x509.py b/tests/test_x509.py index 5394836..f06ab9b 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -1849,6 +1849,20 @@ class X509Tests(unittest.TestCase): value = cert.ocsp_no_check_value self.assertEqual(ocsp_no_check_value, value.native if value else None) + @staticmethod + def private_key_usage_period_value_info(): + return ( + ( + 'ocsp-with-pkup.pem', + b'\x80\x0f20170918151736Z\x81\x0f20180101041421Z' + ), + ) + + @data('private_key_usage_period_value_info') + def private_key_usage_period_value(self, relative_path, private_key_usage_period_value): + cert = self._load_cert(relative_path) + self.assertEqual(private_key_usage_period_value, cert.private_key_usage_period_value.contents) + @staticmethod def serial_number_info(): return ( -- cgit v1.2.3 From 6060d29ac5ef67abc9f4a8347813c0ca87f328bd Mon Sep 17 00:00:00 2001 From: wbond Date: Thu, 14 Dec 2017 16:01:07 -0500 Subject: Version 0.24.0 --- asn1crypto/version.py | 4 ++-- changelog.md | 19 +++++++++++++++++++ readme.md | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/asn1crypto/version.py b/asn1crypto/version.py index 31da728..2ce2408 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.23.0' -__version_info__ = (0, 23, 0) +__version__ = '0.24.0' +__version_info__ = (0, 24, 0) diff --git a/changelog.md b/changelog.md index 7fbc594..9bbf933 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,24 @@ # changelog +## 0.24.0 + + - `x509.Certificate().self_signed` will no longer return `"yes"` under any + circumstances. This helps prevent confusion since the library does not + verify the signature. Instead a library like oscrypto should be used + to confirm if a certificate is self-signed. + - Added various OIDs to `x509.KeyPurposeId()` + - Added `x509.Certificate().private_key_usage_period_value` + - Added structures for parsing common subject directory attributes for + X.509 certificates, including `x509.SubjectDirectoryAttribute()` + - Added `algos.AnyAlgorithmIdentifier()` for situations where an + algorithm identifier may contain a digest, signed digest or encryption + algorithm OID + - Fixed a bug with `x509.Certificate().subject_directory_attributes_value` + not returning the correct value + - Fixed a bug where explicitly-tagged fields in a `core.Sequence()` would + not function properly when the field had a default value + - Fixed a bug with type checking in `pem.armor()` + ## 0.23.0 - Backwards compatibility break: the `tag_type`, `explicit_tag` and diff --git a/readme.md b/readme.md index 574ba23..8dc45a5 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,7 @@ faster to an order of magnitude of more. ## Current Release -0.23.0 - [changelog](changelog.md) +0.24.0 - [changelog](changelog.md) ## Dependencies -- cgit v1.2.3