aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKevin Cheng <kevcheng@google.com>2019-05-02 20:20:26 -0700
committerandroid-build-merger <android-build-merger@google.com>2019-05-02 20:20:26 -0700
commit74a20bea2375ea47f8dda6117132f195c13389f7 (patch)
tree2814c17ac11e4a95a1c0d1f85f06ddcd88e9909d
parent1208f0f8b78755298ebb98a9f4ce5ef76814c708 (diff)
parentd314b9f425739746e7637ade9e3a8b8b9ed90940 (diff)
downloadplatform_external_python_asn1crypto-74a20bea2375ea47f8dda6117132f195c13389f7.tar.gz
platform_external_python_asn1crypto-74a20bea2375ea47f8dda6117132f195c13389f7.tar.bz2
platform_external_python_asn1crypto-74a20bea2375ea47f8dda6117132f195c13389f7.zip
Merge commit '6060d29' into import am: b80da1501c am: 0d2b31c24f
am: d314b9f425 Change-Id: I13790b448aede2ee03336d09c8a5f652e3abea1a
-rw-r--r--.gitignore12
-rw-r--r--.travis.yml28
-rw-r--r--LICENSE19
-rw-r--r--MANIFEST.in3
-rw-r--r--METADATA17
-rw-r--r--MODULE_LICENSE_MIT0
l---------NOTICE1
-rw-r--r--appveyor.yml97
-rw-r--r--asn1crypto/Android.bp30
-rw-r--r--asn1crypto/__init__.py9
-rw-r--r--asn1crypto/_elliptic_curve.py314
-rw-r--r--asn1crypto/_errors.py45
-rw-r--r--asn1crypto/_ffi.py45
-rw-r--r--asn1crypto/_inet.py170
-rw-r--r--asn1crypto/_int.py159
-rw-r--r--asn1crypto/_iri.py288
-rw-r--r--asn1crypto/_ordereddict.py135
-rw-r--r--asn1crypto/_perf/__init__.py0
-rw-r--r--asn1crypto/_perf/_big_num_ctypes.py69
-rw-r--r--asn1crypto/_teletex_codec.py331
-rw-r--r--asn1crypto/_types.py46
-rw-r--r--asn1crypto/algos.py1143
-rw-r--r--asn1crypto/cms.py932
-rw-r--r--asn1crypto/core.py5242
-rw-r--r--asn1crypto/crl.py536
-rw-r--r--asn1crypto/csr.py96
-rw-r--r--asn1crypto/keys.py1249
-rw-r--r--asn1crypto/ocsp.py652
-rw-r--r--asn1crypto/parser.py289
-rw-r--r--asn1crypto/pdf.py84
-rw-r--r--asn1crypto/pem.py222
-rw-r--r--asn1crypto/pkcs12.py193
-rw-r--r--asn1crypto/tsp.py310
-rw-r--r--asn1crypto/util.py712
-rw-r--r--asn1crypto/version.py6
-rw-r--r--asn1crypto/x509.py3002
-rw-r--r--changelog.md339
-rw-r--r--circle.yml20
-rw-r--r--codecov.json4
-rw-r--r--dev/__init__.py0
-rw-r--r--dev/ci.py45
-rw-r--r--dev/coverage.py505
-rw-r--r--dev/deps.py212
-rw-r--r--dev/lint.py39
-rw-r--r--dev/release.py67
-rw-r--r--dev/tests.py35
-rw-r--r--docs/pem.md79
-rw-r--r--docs/readme.md23
-rw-r--r--docs/universal_types.md675
-rw-r--r--readme.md232
-rw-r--r--requires/ci2
-rw-r--r--requires/coverage1
-rw-r--r--requires/lint1
-rw-r--r--requires/release1
-rw-r--r--run.py61
-rw-r--r--setup.py81
-rw-r--r--tests/__init__.py68
-rw-r--r--tests/_unittest_compat.py84
-rw-r--r--tests/fixtures/9999-years-rsa-cert.pem23
-rw-r--r--tests/fixtures/DSAParametersInheritedCACert.crtbin0 -> 546 bytes
-rw-r--r--tests/fixtures/admin.ch.crtbin0 -> 1800 bytes
-rw-r--r--tests/fixtures/certbag.derbin0 -> 2125 bytes
-rw-r--r--tests/fixtures/chromium/ndn.ca.crt35
-rw-r--r--tests/fixtures/chromium/punycodetest.pem19
-rw-r--r--tests/fixtures/chromium/readme.md2
-rw-r--r--tests/fixtures/chromium/subjectAltName_sanity_check.pem23
-rw-r--r--tests/fixtures/cms-compressed.derbin0 -> 109 bytes
-rw-r--r--tests/fixtures/cms-compressed.pem5
-rw-r--r--tests/fixtures/cms-digested.derbin0 -> 117 bytes
-rw-r--r--tests/fixtures/cms-digested.pem5
-rw-r--r--tests/fixtures/cms-encrypted.derbin0 -> 131 bytes
-rw-r--r--tests/fixtures/cms-encrypted.pem5
-rw-r--r--tests/fixtures/cms-enveloped.derbin0 -> 579 bytes
-rw-r--r--tests/fixtures/cms-enveloped.pem15
-rw-r--r--tests/fixtures/cms-signed-digested.derbin0 -> 1864 bytes
-rw-r--r--tests/fixtures/cms-signed-digested.pem41
-rw-r--r--tests/fixtures/cms-signed-indefinite-length.der1414
-rw-r--r--tests/fixtures/cms-signed.derbin0 -> 1919 bytes
-rw-r--r--tests/fixtures/cms-signed.pem42
-rw-r--r--tests/fixtures/eid2011.crlbin0 -> 1181874 bytes
-rw-r--r--tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.crtbin0 -> 804 bytes
-rw-r--r--tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.pem19
-rw-r--r--tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.cer26
-rw-r--r--tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crtbin0 -> 1138 bytes
-rw-r--r--tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.crtbin0 -> 896 bytes
-rw-r--r--tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.pem21
-rw-r--r--tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.crtbin0 -> 1388 bytes
-rw-r--r--tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.pem31
-rw-r--r--tests/fixtures/geotrust_certs/codex.crt37
-rw-r--r--tests/fixtures/geotrust_certs/readme.md1
-rw-r--r--tests/fixtures/globalsign_example_keys/IssuingCA-der.cerbin0 -> 1262 bytes
-rwxr-xr-xtests/fixtures/globalsign_example_keys/IssuingCA.cer29
-rwxr-xr-xtests/fixtures/globalsign_example_keys/IssuingCA.key27
-rw-r--r--tests/fixtures/globalsign_example_keys/SSL1-der.cerbin0 -> 1126 bytes
-rwxr-xr-xtests/fixtures/globalsign_example_keys/SSL1.cer26
-rwxr-xr-xtests/fixtures/globalsign_example_keys/SSL1.key27
-rw-r--r--tests/fixtures/globalsign_example_keys/SSL2-der.cerbin0 -> 1214 bytes
-rwxr-xr-xtests/fixtures/globalsign_example_keys/SSL2.cer28
-rwxr-xr-xtests/fixtures/globalsign_example_keys/SSL2.key27
-rw-r--r--tests/fixtures/globalsign_example_keys/SSL3-der.cerbin0 -> 1198 bytes
-rwxr-xr-xtests/fixtures/globalsign_example_keys/SSL3.cer27
-rwxr-xr-xtests/fixtures/globalsign_example_keys/SSL3.key27
-rw-r--r--tests/fixtures/globalsign_example_keys/readme.md2
-rw-r--r--tests/fixtures/globalsign_example_keys/rootCA-der.cerbin0 -> 991 bytes
-rwxr-xr-xtests/fixtures/globalsign_example_keys/rootCA.cer23
-rwxr-xr-xtests/fixtures/globalsign_example_keys/rootCA.key27
-rw-r--r--tests/fixtures/keys/readme.md2
-rw-r--r--tests/fixtures/keys/test-aes128-der.keybin0 -> 1200 bytes
-rw-r--r--tests/fixtures/keys/test-aes128.key30
-rw-r--r--tests/fixtures/keys/test-der.crtbin0 -> 1227 bytes
-rw-r--r--tests/fixtures/keys/test-der.keybin0 -> 1192 bytes
-rw-r--r--tests/fixtures/keys/test-dsa-der.crtbin0 -> 1783 bytes
-rw-r--r--tests/fixtures/keys/test-dsa-der.keybin0 -> 1242 bytes
-rw-r--r--tests/fixtures/keys/test-dsa.crt40
-rw-r--r--tests/fixtures/keys/test-dsa.key28
-rw-r--r--tests/fixtures/keys/test-dsa.param19
-rw-r--r--tests/fixtures/keys/test-ec-der.crtbin0 -> 892 bytes
-rw-r--r--tests/fixtures/keys/test-ec-der.keybin0 -> 364 bytes
-rw-r--r--tests/fixtures/keys/test-ec-named-der.crtbin0 -> 647 bytes
-rw-r--r--tests/fixtures/keys/test-ec-named-der.keybin0 -> 121 bytes
-rw-r--r--tests/fixtures/keys/test-ec-named.crt16
-rw-r--r--tests/fixtures/keys/test-ec-named.key5
-rw-r--r--tests/fixtures/keys/test-ec.crt21
-rw-r--r--tests/fixtures/keys/test-ec.key18
-rw-r--r--tests/fixtures/keys/test-inter-der.crtbin0 -> 1032 bytes
-rw-r--r--tests/fixtures/keys/test-inter.crt24
-rw-r--r--tests/fixtures/keys/test-pkcs8-der.keybin0 -> 1218 bytes
-rw-r--r--tests/fixtures/keys/test-pkcs8-dsa-der.keybin0 -> 873 bytes
-rw-r--r--tests/fixtures/keys/test-pkcs8-dsa.key21
-rw-r--r--tests/fixtures/keys/test-pkcs8-ec-der.keybin0 -> 381 bytes
-rw-r--r--tests/fixtures/keys/test-pkcs8-ec-named-der.keybin0 -> 138 bytes
-rw-r--r--tests/fixtures/keys/test-pkcs8-ec-named.key5
-rw-r--r--tests/fixtures/keys/test-pkcs8-ec.key10
-rw-r--r--tests/fixtures/keys/test-pkcs8.key28
-rw-r--r--tests/fixtures/keys/test-public-der.keybin0 -> 294 bytes
-rw-r--r--tests/fixtures/keys/test-public-dsa-der.keybin0 -> 1226 bytes
-rw-r--r--tests/fixtures/keys/test-public-dsa.key28
-rw-r--r--tests/fixtures/keys/test-public-ec-der.keybin0 -> 335 bytes
-rw-r--r--tests/fixtures/keys/test-public-ec-named-der.keybin0 -> 91 bytes
-rw-r--r--tests/fixtures/keys/test-public-ec-named.key4
-rw-r--r--tests/fixtures/keys/test-public-ec.key9
-rw-r--r--tests/fixtures/keys/test-public-rsa-der.keybin0 -> 270 bytes
-rw-r--r--tests/fixtures/keys/test-public-rsa.key8
-rw-r--r--tests/fixtures/keys/test-public.key9
-rw-r--r--tests/fixtures/keys/test-rc2.p12bin0 -> 2910 bytes
-rw-r--r--tests/fixtures/keys/test-third-der.crtbin0 -> 1023 bytes
-rw-r--r--tests/fixtures/keys/test-third.crt24
-rw-r--r--tests/fixtures/keys/test.crt28
-rw-r--r--tests/fixtures/keys/test.key27
-rw-r--r--tests/fixtures/lets_encrypt/isrgrootx1.pem31
-rw-r--r--tests/fixtures/lets_encrypt/letsencryptauthorityx1.pem32
-rw-r--r--tests/fixtures/lets_encrypt/letsencryptauthorityx2.pem32
-rw-r--r--tests/fixtures/lets_encrypt/readme.md1
-rw-r--r--tests/fixtures/meca2_compressed.derbin0 -> 9639 bytes
-rw-r--r--tests/fixtures/message.der1
-rw-r--r--tests/fixtures/message.pem4
-rw-r--r--tests/fixtures/message.txt1
-rw-r--r--tests/fixtures/mozilla-generated-by-openssl.pkcs7.derbin0 -> 4247 bytes
-rw-r--r--tests/fixtures/ocsp-with-pkup.pem34
-rw-r--r--tests/fixtures/ocsp_requestbin0 -> 120 bytes
-rw-r--r--tests/fixtures/ocsp_responsebin0 -> 1425 bytes
-rw-r--r--tests/fixtures/pkcs7-signed-digested.derbin0 -> 1866 bytes
-rw-r--r--tests/fixtures/pkcs7-signed-digested.pem41
-rw-r--r--tests/fixtures/pkcs7-signed.derbin0 -> 2046 bytes
-rw-r--r--tests/fixtures/pkcs7-signed.pem45
-rw-r--r--tests/fixtures/readme.md6
-rw-r--r--tests/fixtures/self-signed-repeated-subject-fields.derbin0 -> 985 bytes
-rw-r--r--tests/fixtures/sender_dummycorp.com.crt33
-rw-r--r--tests/fixtures/test-inter-der.csrbin0 -> 756 bytes
-rw-r--r--tests/fixtures/test-inter.csr18
-rw-r--r--tests/fixtures/test-third-der.csrbin0 -> 805 bytes
-rw-r--r--tests/fixtures/test-third.csr19
-rw-r--r--tests/fixtures/test-tripledes.p12bin0 -> 2910 bytes
-rw-r--r--tests/fixtures/tsp_requestbin0 -> 51 bytes
-rw-r--r--tests/fixtures/tsp_responsebin0 -> 711 bytes
-rw-r--r--tests/fixtures/universal/object_identifier.der1
-rw-r--r--tests/test_algos.py33
-rw-r--r--tests/test_cms.py903
-rw-r--r--tests/test_core.py777
-rw-r--r--tests/test_crl.py42
-rw-r--r--tests/test_csr.py141
-rw-r--r--tests/test_keys.py550
-rw-r--r--tests/test_ocsp.py155
-rw-r--r--tests/test_parser.py62
-rw-r--r--tests/test_pem.py158
-rw-r--r--tests/test_pkcs12.py118
-rw-r--r--tests/test_tsp.py245
-rw-r--r--tests/test_util.py135
-rw-r--r--tests/test_x509.py3392
-rw-r--r--tests/unittest_data.py60
-rw-r--r--tox.ini13
191 files changed, 28581 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e7a7291
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.tox/
+__pycache__/
+build/
+dist/
+tests/output/
+tmp/
+*.egg-info/
+*.pyc
+.python-version
+.DS_Store
+.coverage
+coverage.xml
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ef69009
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,28 @@
+sudo: false
+language: c
+branches:
+ except:
+ - /^[0-9]+\.[0-9]+\.[0-9]$/
+matrix:
+ include:
+ - 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.6"
+ - os: linux
+ language: python
+ python: "pypy-5.3.1"
+script:
+ - python run.py deps
+ - python run.py ci
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..cd7ac9e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2015-2017 Will Bond <will@wbond.net>
+
+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/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
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..b732ef7
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,17 @@
+name: "asn1crypto"
+description:
+ "A fast, pure Python library for parsing and serializing ASN.1 structures."
+
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://github.com/wbond/asn1crypto"
+ }
+ url {
+ type: GIT
+ value: "https://github.com/wbond/asn1crypto"
+ }
+ version: "0.24.0"
+ last_upgrade_date { year: 2019 month: 2 day: 26 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_MIT b/MODULE_LICENSE_MIT
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_MIT
diff --git a/NOTICE b/NOTICE
new file mode 120000
index 0000000..7a694c9
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1 @@
+LICENSE \ No newline at end of file
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..fdd2dd6
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,97 @@
+version: "{build}"
+skip_tags: true
+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.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.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,
+ 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();
+cache:
+ - '%TMP%\py\'
+build: off
+test_script:
+ - 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.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.7.1-win32\pypy run.py ci;
+ remove-item env:\OSCRYPTO_USE_WINLEGACY;
diff --git a/asn1crypto/Android.bp b/asn1crypto/Android.bp
new file mode 100644
index 0000000..43e55a3
--- /dev/null
+++ b/asn1crypto/Android.bp
@@ -0,0 +1,30 @@
+// Copyright 2019 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+python_library {
+ name: "py-asn1crypto",
+ host_supported: true,
+ srcs: [
+ "*.py",
+ "_perf/*.py",
+ ],
+ version: {
+ py2: {
+ enabled: true,
+ },
+ py3: {
+ enabled: true,
+ },
+ },
+ pkg_path: "asn1crypto",
+}
diff --git a/asn1crypto/__init__.py b/asn1crypto/__init__.py
new file mode 100644
index 0000000..afdeb43
--- /dev/null
+++ b/asn1crypto/__init__.py
@@ -0,0 +1,9 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+from .version import __version__, __version_info__
+
+__all__ = [
+ '__version__',
+ '__version_info__',
+]
diff --git a/asn1crypto/_elliptic_curve.py b/asn1crypto/_elliptic_curve.py
new file mode 100644
index 0000000..8c0f12d
--- /dev/null
+++ b/asn1crypto/_elliptic_curve.py
@@ -0,0 +1,314 @@
+# coding: utf-8
+
+"""
+Classes and objects to represent prime-field elliptic curves and points on them.
+Exports the following items:
+
+ - PrimeCurve()
+ - PrimePoint()
+ - SECP192R1_CURVE
+ - SECP192R1_BASE_POINT
+ - SECP224R1_CURVE
+ - SECP224R1_BASE_POINT
+ - SECP256R1_CURVE
+ - SECP256R1_BASE_POINT
+ - SECP384R1_CURVE
+ - SECP384R1_BASE_POINT
+ - SECP521R1_CURVE
+ - SECP521R1_BASE_POINT
+
+The curve constants are all PrimeCurve() objects and the base point constants
+are all PrimePoint() objects.
+
+Some of the following source code is derived from
+http://webpages.charter.net/curryfans/peter/downloads.html, but has been heavily
+modified to fit into this projects lint settings. The original project license
+is listed below:
+
+Copyright (c) 2014 Peter Pearson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+from ._int import inverse_mod
+
+
+class PrimeCurve():
+ """
+ Elliptic curve over a prime field. Characteristic two field curves are not
+ supported.
+ """
+
+ def __init__(self, p, a, b):
+ """
+ The curve of points satisfying y^2 = x^3 + a*x + b (mod p)
+
+ :param p:
+ The prime number as an integer
+
+ :param a:
+ The component a as an integer
+
+ :param b:
+ The component b as an integer
+ """
+
+ self.p = p
+ self.a = a
+ self.b = b
+
+ def contains(self, point):
+ """
+ :param point:
+ A Point object
+
+ :return:
+ Boolean if the point is on this curve
+ """
+
+ y2 = point.y * point.y
+ x3 = point.x * point.x * point.x
+ return (y2 - (x3 + self.a * point.x + self.b)) % self.p == 0
+
+
+class PrimePoint():
+ """
+ A point on a prime-field elliptic curve
+ """
+
+ def __init__(self, curve, x, y, order=None):
+ """
+ :param curve:
+ A PrimeCurve object
+
+ :param x:
+ The x coordinate of the point as an integer
+
+ :param y:
+ The y coordinate of the point as an integer
+
+ :param order:
+ The order of the point, as an integer - optional
+ """
+
+ self.curve = curve
+ self.x = x
+ self.y = y
+ self.order = order
+
+ # self.curve is allowed to be None only for INFINITY:
+ if self.curve:
+ if not self.curve.contains(self):
+ raise ValueError('Invalid EC point')
+
+ if self.order:
+ if self * self.order != INFINITY:
+ raise ValueError('Invalid EC point')
+
+ def __cmp__(self, other):
+ """
+ :param other:
+ A PrimePoint object
+
+ :return:
+ 0 if identical, 1 otherwise
+ """
+ if self.curve == other.curve and self.x == other.x and self.y == other.y:
+ return 0
+ else:
+ return 1
+
+ def __add__(self, other):
+ """
+ :param other:
+ A PrimePoint object
+
+ :return:
+ A PrimePoint object
+ """
+
+ # X9.62 B.3:
+
+ if other == INFINITY:
+ return self
+ if self == INFINITY:
+ return other
+ assert self.curve == other.curve
+ if self.x == other.x:
+ if (self.y + other.y) % self.curve.p == 0:
+ return INFINITY
+ else:
+ return self.double()
+
+ p = self.curve.p
+
+ l_ = ((other.y - self.y) * inverse_mod(other.x - self.x, p)) % p
+
+ x3 = (l_ * l_ - self.x - other.x) % p
+ y3 = (l_ * (self.x - x3) - self.y) % p
+
+ return PrimePoint(self.curve, x3, y3)
+
+ def __mul__(self, other):
+ """
+ :param other:
+ An integer to multiple the Point by
+
+ :return:
+ A PrimePoint object
+ """
+
+ def leftmost_bit(x):
+ assert x > 0
+ result = 1
+ while result <= x:
+ result = 2 * result
+ return result // 2
+
+ e = other
+ if self.order:
+ e = e % self.order
+ if e == 0:
+ return INFINITY
+ if self == INFINITY:
+ return INFINITY
+ assert e > 0
+
+ # From X9.62 D.3.2:
+
+ e3 = 3 * e
+ negative_self = PrimePoint(self.curve, self.x, -self.y, self.order)
+ i = leftmost_bit(e3) // 2
+ result = self
+ # print "Multiplying %s by %d (e3 = %d):" % ( self, other, e3 )
+ while i > 1:
+ result = result.double()
+ if (e3 & i) != 0 and (e & i) == 0:
+ result = result + self
+ if (e3 & i) == 0 and (e & i) != 0:
+ result = result + negative_self
+ # print ". . . i = %d, result = %s" % ( i, result )
+ i = i // 2
+
+ return result
+
+ def __rmul__(self, other):
+ """
+ :param other:
+ An integer to multiple the Point by
+
+ :return:
+ A PrimePoint object
+ """
+
+ return self * other
+
+ def double(self):
+ """
+ :return:
+ A PrimePoint object that is twice this point
+ """
+
+ # X9.62 B.3:
+
+ p = self.curve.p
+ a = self.curve.a
+
+ l_ = ((3 * self.x * self.x + a) * inverse_mod(2 * self.y, p)) % p
+
+ x3 = (l_ * l_ - 2 * self.x) % p
+ y3 = (l_ * (self.x - x3) - self.y) % p
+
+ return PrimePoint(self.curve, x3, y3)
+
+
+# This one point is the Point At Infinity for all purposes:
+INFINITY = PrimePoint(None, None, None)
+
+
+# NIST Curve P-192:
+SECP192R1_CURVE = PrimeCurve(
+ 6277101735386680763835789423207666416083908700390324961279,
+ -3,
+ 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
+)
+SECP192R1_BASE_POINT = PrimePoint(
+ SECP192R1_CURVE,
+ 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012,
+ 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811,
+ 6277101735386680763835789423176059013767194773182842284081
+)
+
+
+# NIST Curve P-224:
+SECP224R1_CURVE = PrimeCurve(
+ 26959946667150639794667015087019630673557916260026308143510066298881,
+ -3,
+ 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4
+)
+SECP224R1_BASE_POINT = PrimePoint(
+ SECP224R1_CURVE,
+ 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21,
+ 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34,
+ 26959946667150639794667015087019625940457807714424391721682722368061
+)
+
+
+# NIST Curve P-256:
+SECP256R1_CURVE = PrimeCurve(
+ 115792089210356248762697446949407573530086143415290314195533631308867097853951,
+ -3,
+ 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
+)
+SECP256R1_BASE_POINT = PrimePoint(
+ SECP256R1_CURVE,
+ 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
+ 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5,
+ 115792089210356248762697446949407573529996955224135760342422259061068512044369
+)
+
+
+# NIST Curve P-384:
+SECP384R1_CURVE = PrimeCurve(
+ 39402006196394479212279040100143613805079739270465446667948293404245721771496870329047266088258938001861606973112319, # noqa
+ -3,
+ 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef
+)
+SECP384R1_BASE_POINT = PrimePoint(
+ SECP384R1_CURVE,
+ 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7,
+ 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f,
+ 39402006196394479212279040100143613805079739270465446667946905279627659399113263569398956308152294913554433653942643
+)
+
+
+# NIST Curve P-521:
+SECP521R1_CURVE = PrimeCurve(
+ 6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151, # noqa
+ -3,
+ 0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00 # noqa
+)
+SECP521R1_BASE_POINT = PrimePoint(
+ SECP521R1_CURVE,
+ 0xc6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66, # noqa
+ 0x11839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650, # noqa
+ 6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449 # noqa
+)
diff --git a/asn1crypto/_errors.py b/asn1crypto/_errors.py
new file mode 100644
index 0000000..cc785a5
--- /dev/null
+++ b/asn1crypto/_errors.py
@@ -0,0 +1,45 @@
+# coding: utf-8
+
+"""
+Helper for formatting exception messages. Exports the following items:
+
+ - unwrap()
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import re
+import textwrap
+
+
+def unwrap(string, *params):
+ """
+ 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:
+ The formatted string
+ """
+
+ 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
new file mode 100644
index 0000000..2a4f5bf
--- /dev/null
+++ b/asn1crypto/_ffi.py
@@ -0,0 +1,45 @@
+# coding: utf-8
+
+"""
+FFI helper compatibility functions. Exports the following items:
+
+ - LibraryNotFoundError
+ - FFIEngineError
+ - bytes_from_buffer()
+ - buffer_from_bytes()
+ - null()
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+from ctypes import create_string_buffer
+
+
+def buffer_from_bytes(initializer):
+ return create_string_buffer(initializer)
+
+
+def bytes_from_buffer(buffer, maxlen=None):
+ return buffer.raw
+
+
+def null():
+ return None
+
+
+class LibraryNotFoundError(Exception):
+
+ """
+ An exception when trying to find a shared library
+ """
+
+ pass
+
+
+class FFIEngineError(Exception):
+
+ """
+ An exception when trying to instantiate ctypes or cffi
+ """
+
+ pass
diff --git a/asn1crypto/_inet.py b/asn1crypto/_inet.py
new file mode 100644
index 0000000..045ba56
--- /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 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)
+ ))
diff --git a/asn1crypto/_int.py b/asn1crypto/_int.py
new file mode 100644
index 0000000..d0c2319
--- /dev/null
+++ b/asn1crypto/_int.py
@@ -0,0 +1,159 @@
+# coding: utf-8
+
+"""
+Function for calculating the modular inverse. Exports the following items:
+
+ - inverse_mod()
+
+Source code is derived from
+http://webpages.charter.net/curryfans/peter/downloads.html, but has been heavily
+modified to fit into this projects lint settings. The original project license
+is listed below:
+
+Copyright (c) 2014 Peter Pearson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import math
+import platform
+
+from .util import int_to_bytes, int_from_bytes
+
+# First try to use ctypes with OpenSSL for better performance
+try:
+ from ._ffi import (
+ buffer_from_bytes,
+ bytes_from_buffer,
+ FFIEngineError,
+ LibraryNotFoundError,
+ null,
+ )
+
+ # Some versions of PyPy have segfault issues, so we just punt on PyPy
+ if platform.python_implementation() == 'PyPy':
+ raise EnvironmentError()
+
+ try:
+ from ._perf._big_num_ctypes import libcrypto
+
+ def inverse_mod(a, p):
+ """
+ Compute the modular inverse of a (mod p)
+
+ :param a:
+ An integer
+
+ :param p:
+ An integer
+
+ :return:
+ An integer
+ """
+
+ ctx = libcrypto.BN_CTX_new()
+
+ a_bytes = int_to_bytes(abs(a))
+ p_bytes = int_to_bytes(abs(p))
+
+ a_buf = buffer_from_bytes(a_bytes)
+ a_bn = libcrypto.BN_bin2bn(a_buf, len(a_bytes), null())
+ if a < 0:
+ libcrypto.BN_set_negative(a_bn, 1)
+
+ p_buf = buffer_from_bytes(p_bytes)
+ p_bn = libcrypto.BN_bin2bn(p_buf, len(p_bytes), null())
+ if p < 0:
+ libcrypto.BN_set_negative(p_bn, 1)
+
+ r_bn = libcrypto.BN_mod_inverse(null(), a_bn, p_bn, ctx)
+ r_len_bits = libcrypto.BN_num_bits(r_bn)
+ r_len = int(math.ceil(r_len_bits / 8))
+ r_buf = buffer_from_bytes(r_len)
+ libcrypto.BN_bn2bin(r_bn, r_buf)
+ r_bytes = bytes_from_buffer(r_buf, r_len)
+ result = int_from_bytes(r_bytes)
+
+ libcrypto.BN_free(a_bn)
+ libcrypto.BN_free(p_bn)
+ libcrypto.BN_free(r_bn)
+ libcrypto.BN_CTX_free(ctx)
+
+ return result
+ except (LibraryNotFoundError, FFIEngineError):
+ raise EnvironmentError()
+
+# If there was an issue using ctypes or OpenSSL, we fall back to pure python
+except (EnvironmentError, ImportError):
+
+ def inverse_mod(a, p):
+ """
+ Compute the modular inverse of a (mod p)
+
+ :param a:
+ An integer
+
+ :param p:
+ An integer
+
+ :return:
+ An integer
+ """
+
+ if a < 0 or p <= a:
+ a = a % p
+
+ # From Ferguson and Schneier, roughly:
+
+ c, d = a, p
+ uc, vc, ud, vd = 1, 0, 0, 1
+ while c != 0:
+ q, c, d = divmod(d, c) + (c,)
+ uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc
+
+ # At this point, d is the GCD, and ud*a+vd*p = d.
+ # If d == 1, this means that ud is a inverse.
+
+ assert d == 1
+ if ud > 0:
+ return ud
+ else:
+ return ud + p
+
+
+def fill_width(bytes_, width):
+ """
+ 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/_iri.py b/asn1crypto/_iri.py
new file mode 100644
index 0000000..57ddd40
--- /dev/null
+++ b/asn1crypto/_iri.py
@@ -0,0 +1,288 @@
+# 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
+
+from ._errors import unwrap
+from ._types import byte_cls, str_cls, type_name, bytes_to_list, int_types
+
+if sys.version_info < (3,):
+ from urlparse import urlsplit, urlunsplit
+ from urllib import (
+ quote as urlquote,
+ unquote as unquote_to_bytes,
+ )
+
+else:
+ from urllib.parse import (
+ quote as urlquote,
+ unquote_to_bytes,
+ urlsplit,
+ urlunsplit,
+ )
+
+
+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)
+ ))
+
+ 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')
+ # 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')
+
+ 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
+
+ # 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
+
+ # 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 and not isinstance(port, int_types):
+ 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 += ':' + str_cls(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
+
+ if byte_string == b'':
+ return ''
+
+ 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'))
+
+ 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'))
+
+ 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/_ordereddict.py b/asn1crypto/_ordereddict.py
new file mode 100644
index 0000000..2f18ab5
--- /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
+
+ class OrderedDict(dict, DictMixin):
+
+ def __init__(self, *args, **kwds):
+ 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()
+ 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/_perf/__init__.py b/asn1crypto/_perf/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/asn1crypto/_perf/__init__.py
diff --git a/asn1crypto/_perf/_big_num_ctypes.py b/asn1crypto/_perf/_big_num_ctypes.py
new file mode 100644
index 0000000..8e37e9b
--- /dev/null
+++ b/asn1crypto/_perf/_big_num_ctypes.py
@@ -0,0 +1,69 @@
+# coding: utf-8
+
+"""
+ctypes interface for BN_mod_inverse() function from OpenSSL. Exports the
+following items:
+
+ - libcrypto
+ - BN_bn2bin()
+ - BN_CTX_free()
+ - BN_CTX_new()
+ - BN_free()
+ - BN_mod_inverse()
+ - BN_new()
+ - BN_num_bits()
+ - BN_set_negative()
+
+Will raise asn1crypto._ffi.LibraryNotFoundError() if libcrypto can not be
+found. Will raise asn1crypto._ffi.FFIEngineError() if there is an error
+interfacing with libcrypto.
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import sys
+
+from ctypes import CDLL, c_int, c_char_p, c_void_p
+from ctypes.util import find_library
+
+from .._ffi import LibraryNotFoundError, FFIEngineError
+
+
+try:
+ # On Python 2, the unicode string here may raise a UnicodeDecodeError as it
+ # tries to join a bytestring path to the unicode name "crypto"
+ libcrypto_path = find_library(b'crypto' if sys.version_info < (3,) else 'crypto')
+ if not libcrypto_path:
+ raise LibraryNotFoundError('The library libcrypto could not be found')
+
+ libcrypto = CDLL(libcrypto_path)
+
+ libcrypto.BN_new.argtypes = []
+ libcrypto.BN_new.restype = c_void_p
+
+ libcrypto.BN_bin2bn.argtypes = [c_char_p, c_int, c_void_p]
+ libcrypto.BN_bin2bn.restype = c_void_p
+
+ libcrypto.BN_bn2bin.argtypes = [c_void_p, c_char_p]
+ libcrypto.BN_bn2bin.restype = c_int
+
+ libcrypto.BN_set_negative.argtypes = [c_void_p, c_int]
+ libcrypto.BN_set_negative.restype = None
+
+ libcrypto.BN_num_bits.argtypes = [c_void_p]
+ libcrypto.BN_num_bits.restype = c_int
+
+ libcrypto.BN_free.argtypes = [c_void_p]
+ libcrypto.BN_free.restype = None
+
+ libcrypto.BN_CTX_new.argtypes = []
+ libcrypto.BN_CTX_new.restype = c_void_p
+
+ libcrypto.BN_CTX_free.argtypes = [c_void_p]
+ libcrypto.BN_CTX_free.restype = None
+
+ libcrypto.BN_mod_inverse.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p]
+ libcrypto.BN_mod_inverse.restype = c_void_p
+
+except (AttributeError):
+ raise FFIEngineError('Error initializing ctypes')
diff --git a/asn1crypto/_teletex_codec.py b/asn1crypto/_teletex_codec.py
new file mode 100644
index 0000000..b5991aa
--- /dev/null
+++ b/asn1crypto/_teletex_codec.py
@@ -0,0 +1,331 @@
+# coding: utf-8
+
+"""
+Implementation of the teletex T.61 codec. Exports the following items:
+
+ - register()
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+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):
+ """
+ Search function for teletex codec that is passed to codecs.register()
+ """
+
+ 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():
+ """
+ Registers the teletex codec
+ """
+
+ 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/_types.py b/asn1crypto/_types.py
new file mode 100644
index 0000000..b9ca8cc
--- /dev/null
+++ b/asn1crypto/_types.py
@@ -0,0 +1,46 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import inspect
+import sys
+
+
+if sys.version_info < (3,):
+ str_cls = unicode # noqa
+ byte_cls = str
+ int_types = (int, long) # noqa
+
+ def bytes_to_list(byte_string):
+ return [ord(b) for b in byte_string]
+
+ chr_cls = chr
+
+else:
+ str_cls = str
+ byte_cls = bytes
+ int_types = int
+
+ bytes_to_list = list
+
+ def chr_cls(num):
+ return bytes([num])
+
+
+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/algos.py b/asn1crypto/algos.py
new file mode 100644
index 0000000..c805433
--- /dev/null
+++ b/asn1crypto/algos.py
@@ -0,0 +1,1143 @@
+# coding: utf-8
+
+"""
+ASN.1 type classes for various algorithms using in various aspects of public
+key cryptography. Exports the following items:
+
+ - AlgorithmIdentifier()
+ - AnyAlgorithmIdentifier()
+ - DigestAlgorithm()
+ - DigestInfo()
+ - DSASignature()
+ - EncryptionAlgorithm()
+ - HmacAlgorithm()
+ - KdfAlgorithm()
+ - Pkcs5MacAlgorithm()
+ - SignedDigestAlgorithm()
+
+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,
+ Integer,
+ Null,
+ ObjectIdentifier,
+ OctetString,
+ Sequence,
+ Void,
+)
+
+
+# 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 = [
+ ('algorithm', ObjectIdentifier),
+ ('parameters', Any, {'optional': True}),
+ ]
+
+
+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 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):
+ 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',
+ '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),
+ ('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',
+ '2.16.840.1.101.3.4.2.5': 'sha512_224',
+ '2.16.840.1.101.3.4.2.6': 'sha512_256',
+ }
+
+
+class DigestAlgorithm(_ForceNullParameters, 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 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,
+ {
+ 'explicit': 0,
+ 'default': {'algorithm': 'sha1'},
+ }
+ ),
+ (
+ 'mask_gen_algorithm',
+ MaskGenAlgorithm,
+ {
+ 'explicit': 1,
+ 'default': {
+ 'algorithm': 'mgf1',
+ 'parameters': {'algorithm': 'sha1'},
+ },
+ }
+ ),
+ (
+ 'salt_length',
+ Integer,
+ {
+ 'explicit': 2,
+ 'default': 20,
+ }
+ ),
+ (
+ 'trailer_field',
+ TrailerField,
+ {
+ 'explicit': 3,
+ 'default': 'trailer_field_bc',
+ }
+ ),
+ ]
+
+
+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.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',
+ '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': 'rsassa_pkcs1v15',
+ '1.2.840.10040.4.1': 'dsa',
+ '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(_ForceNullParameters, Sequence):
+ _fields = [
+ ('algorithm', SignedDigestAlgorithmId),
+ ('parameters', Any, {'optional': True}),
+ ]
+
+ _oid_pair = ('algorithm', 'parameters')
+ _oid_specs = {
+ 'rsassa_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(unwrap(
+ '''
+ 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(unwrap(
+ '''
+ Hash algorithm not known for %s
+ ''',
+ algorithm
+ ))
+
+
+class Pbkdf2Salt(Choice):
+ _alternatives = [
+ ('specified', OctetString),
+ ('other_source', AlgorithmIdentifier),
+ ]
+
+
+class Pbkdf2Params(Sequence):
+ _fields = [
+ ('salt', Pbkdf2Salt),
+ ('iteration_count', Integer),
+ ('key_length', Integer, {'optional': True}),
+ ('prf', HmacAlgorithm, {'default': {'algorithm': 'sha1'}}),
+ ]
+
+
+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 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}),
+ ('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 Pbes1Params(Sequence):
+ _fields = [
+ ('salt', OctetString),
+ ('iterations', Integer),
+ ]
+
+
+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,
+ {
+ 'explicit': 0,
+ 'default': {'algorithm': 'sha1'}
+ }
+ ),
+ (
+ 'mask_gen_algorithm',
+ MaskGenAlgorithm,
+ {
+ 'explicit': 1,
+ 'default': {
+ 'algorithm': 'mgf1',
+ 'parameters': {'algorithm': 'sha1'}
+ }
+ }
+ ),
+ (
+ 'p_source_algorithm',
+ PSourceAlgorithm,
+ {
+ 'explicit': 2,
+ 'default': {
+ 'algorithm': 'p_specified',
+ 'parameters': b''
+ }
+ }
+ ),
+ ]
+
+
+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',
+ '1.2.840.113549.3.7': 'tripledes_3key',
+ '1.2.840.113549.3.2': 'rc2',
+ '1.2.840.113549.3.9': 'rc5',
+ # From http://csrc.nist.gov/groups/ST/crypto_apps_infra/csor/algorithms.html#AES
+ '2.16.840.1.101.3.4.1.1': 'aes128_ecb',
+ '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',
+ '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',
+ # PKCS#1 v2.2
+ '1.2.840.113549.1.1.1': 'rsaes_pkcs1v15',
+ '1.2.840.113549.1.1.7': 'rsaes_oaep',
+ }
+
+
+class EncryptionAlgorithm(_ForceNullParameters, Sequence):
+ _fields = [
+ ('algorithm', EncryptionAlgorithmId),
+ ('parameters', Any, {'optional': True}),
+ ]
+
+ _oid_pair = ('algorithm', 'parameters')
+ _oid_specs = {
+ 'des': OctetString,
+ 'tripledes_3key': OctetString,
+ 'rc2': Rc2Params,
+ 'rc5': Rc5Params,
+ '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,
+ '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,
+ # PKCS#1 v2.2
+ 'rsaes_oaep': RSAESOAEPParams,
+ }
+
+ @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']['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(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):
+ """
+ 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']['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(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):
+ """
+ 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']['key_derivation_func']['parameters']['salt']
+
+ if salt.name == 'other_source':
+ 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
+
+ if encryption_algo.find('.') == -1:
+ if encryption_algo.find('_') != -1:
+ return self['parameters']['salt'].native
+
+ 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):
+ """
+ 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']['parameters']['iteration_count'].native
+
+ if encryption_algo.find('.') == -1:
+ if encryption_algo.find('_') != -1:
+ return self['parameters']['iterations'].native
+
+ 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):
+ """
+ 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
+ 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[0:3] == 'aes':
+ return {
+ 'aes128_': 16,
+ 'aes192_': 24,
+ 'aes256_': 32,
+ }[encryption_algo[0:7]]
+
+ cipher_lengths = {
+ 'des': 8,
+ 'tripledes_3key': 24,
+ }
+
+ if encryption_algo in cipher_lengths:
+ return cipher_lengths[encryption_algo]
+
+ if encryption_algo == '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(unwrap(
+ '''
+ Invalid RC2 parameter version found in EncryptionAlgorithm
+ parameters
+ '''
+ ))
+
+ if encryption_algo == 'pbes2':
+ key_length = self['parameters']['key_derivation_func']['parameters']['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(unwrap(
+ '''
+ Unrecognized encryption algorithm "%s"
+ ''',
+ 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):
+ """
+ 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"
+ """
+
+ encryption_algo = self['algorithm'].native
+
+ 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
+
+ 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(unwrap(
+ '''
+ Unrecognized encryption algorithm "%s"
+ ''',
+ encryption_algo
+ ))
+
+ @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
+ """
+
+ encryption_algo = self['algorithm'].native
+
+ if encryption_algo[0:7] in set(['aes128_', 'aes192_', 'aes256_']):
+ return 16
+
+ cipher_map = {
+ 'des': 8,
+ 'tripledes_3key': 8,
+ 'rc2': 8,
+ }
+ if encryption_algo in cipher_map:
+ return cipher_map[encryption_algo]
+
+ if encryption_algo == 'rc5':
+ return self['parameters'].parsed['block_size_in_bits'].native / 8
+
+ 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(unwrap(
+ '''
+ Unrecognized encryption algorithm "%s"
+ ''',
+ 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 in set(['rc2', 'rc5']):
+ return self['parameters'].parsed['iv'].native
+
+ # For DES/Triple DES and AES the IV is the entirety of the parameters
+ 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':
+ 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(unwrap(
+ '''
+ 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
+
+
+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
diff --git a/asn1crypto/cms.py b/asn1crypto/cms.py
new file mode 100644
index 0000000..9cad949
--- /dev/null
+++ b/asn1crypto/cms.py
@@ -0,0 +1,932 @@
+# 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()
+ - DigestedData()
+ - EncryptedData()
+ - EnvelopedData()
+ - SignedAndEnvelopedData()
+ - 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
+
+try:
+ import zlib
+except (ImportError):
+ zlib = None
+
+from .algos import (
+ _ForceNullParameters,
+ DigestAlgorithm,
+ EncryptionAlgorithm,
+ HmacAlgorithm,
+ KdfAlgorithm,
+ SignedDigestAlgorithm,
+)
+from .core import (
+ Any,
+ BitString,
+ Choice,
+ Enumerated,
+ GeneralizedTime,
+ Integer,
+ ObjectIdentifier,
+ OctetBitString,
+ OctetString,
+ ParsableOctetString,
+ Sequence,
+ SequenceOf,
+ SetOf,
+ UTCTime,
+ UTF8String,
+)
+from .crl import CertificateList
+from .keys import PublicKeyInfo
+from .ocsp import OCSPResponse
+from .x509 import Attributes, Certificate, Extensions, GeneralName, 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',
+ # 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',
+ }
+
+
+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 CMSAlgorithmProtection(Sequence):
+ _fields = [
+ ('digest_algorithm', DigestAlgorithm),
+ ('signature_algorithm', SignedDigestAlgorithm, {'implicit': 1, 'optional': True}),
+ ('mac_algorithm', HmacAlgorithm, {'implicit': 2, 'optional': True}),
+ ]
+
+
+class SetOfContentType(SetOf):
+ _child_spec = ContentType
+
+
+class SetOfOctetString(SetOf):
+ _child_spec = OctetString
+
+
+class SetOfTime(SetOf):
+ _child_spec = Time
+
+
+class SetOfAny(SetOf):
+ _child_spec = Any
+
+
+class SetOfCMSAlgorithmProtection(SetOf):
+ _child_spec = CMSAlgorithmProtection
+
+
+class CMSAttribute(Sequence):
+ _fields = [
+ ('type', CMSAttributeType),
+ ('values', None),
+ ]
+
+ _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
+
+
+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, {'explicit': 0}),
+ ('subject_name', GeneralNames, {'explicit': 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, {'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, {'explicit': 0, 'optional': True}),
+ ('object_digest_info', ObjectDigestInfo, {'explicit': 1, 'optional': True}),
+ ]
+
+
+class AttCertIssuer(Choice):
+ _alternatives = [
+ ('v1_form', GeneralNames),
+ ('v2_form', V2Form, {'explicit': 0}),
+ ]
+
+
+class IetfAttrValue(Choice):
+ _alternatives = [
+ ('octets', OctetString),
+ ('oid', ObjectIdentifier),
+ ('string', UTF8String),
+ ]
+
+
+class IetfAttrValues(SequenceOf):
+ _child_spec = IetfAttrValue
+
+
+class IetfAttrSyntax(Sequence):
+ _fields = [
+ ('policy_authority', GeneralNames, {'implicit': 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, {'implicit': 0, 'optional': True}),
+ ('role_name', GeneralName, {'implicit': 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, {'implicit': 0}),
+ ('value', Any, {'implicit': 1}),
+ ]
+
+
+class SetOfSecurityCategory(SetOf):
+ _child_spec = SecurityCategory
+
+
+class Clearance(Sequence):
+ _fields = [
+ ('policy_id', ObjectIdentifier, {'implicit': 0}),
+ ('class_list', ClassList, {'implicit': 1, 'default': 'unclassified'}),
+ ('security_categories', SetOfSecurityCategory, {'implicit': 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, {'explicit': 0, 'optional': True}),
+ ('max_delay', BigTime, {'explicit': 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):
+ return self._oid_specs.get(self['type'].native, SetOfAny)
+
+ _spec_callbacks = {
+ 'values': _values_spec
+ }
+
+
+class AttCertAttributes(SequenceOf):
+ _child_spec = AttCertAttribute
+
+
+class AttributeCertificateInfoV2(Sequence):
+ _fields = [
+ ('version', AttCertVersion),
+ ('holder', Holder),
+ ('issuer', AttCertIssuer),
+ ('signature', SignedDigestAlgorithm),
+ ('serial_number', Integer),
+ ('att_cert_validity_period', AttCertValidityPeriod),
+ ('attributes', AttCertAttributes),
+ ('issuer_unique_id', OctetBitString, {'optional': True}),
+ ('extensions', Extensions, {'optional': True}),
+ ]
+
+
+class AttributeCertificateV2(Sequence):
+ # Handle the situation where a V2 cert is encoded as V1
+ _bad_tag = 1
+
+ _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, {'implicit': 0}),
+ ('v1_attr_cert', AttributeCertificateV1, {'implicit': 1}),
+ ('v2_attr_cert', AttributeCertificateV2, {'implicit': 2}),
+ ('other', OtherCertificateFormat, {'implicit': 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
+
+
+class ContentInfo(Sequence):
+ _fields = [
+ ('content_type', ContentType),
+ ('content', Any, {'explicit': 0, 'optional': True}),
+ ]
+
+ _oid_pair = ('content_type', 'content')
+ _oid_specs = {}
+
+
+class SetOfContentInfo(SetOf):
+ _child_spec = ContentInfo
+
+
+class EncapsulatedContentInfo(Sequence):
+ _fields = [
+ ('content_type', ContentType),
+ ('content', ParsableOctetString, {'explicit': 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, {'implicit': 0}),
+ ]
+
+
+class DigestAlgorithms(SetOf):
+ _child_spec = DigestAlgorithm
+
+
+class CertificateRevocationLists(SetOf):
+ _child_spec = CertificateList
+
+
+class SCVPReqRes(Sequence):
+ _fields = [
+ ('request', ContentInfo, {'explicit': 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, {'implicit': 1}),
+ ]
+
+
+class RevocationInfoChoices(SetOf):
+ _child_spec = RevocationInfoChoice
+
+
+class SignerInfo(Sequence):
+ _fields = [
+ ('version', CMSVersion),
+ ('sid', SignerIdentifier),
+ ('digest_algorithm', DigestAlgorithm),
+ ('signed_attrs', CMSAttributes, {'implicit': 0, 'optional': True}),
+ ('signature_algorithm', SignedDigestAlgorithm),
+ ('signature', OctetString),
+ ('unsigned_attrs', CMSAttributes, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class SignerInfos(SetOf):
+ _child_spec = SignerInfo
+
+
+class SignedData(Sequence):
+ _fields = [
+ ('version', CMSVersion),
+ ('digest_algorithms', DigestAlgorithms),
+ ('encap_content_info', None),
+ ('certificates', CertificateSet, {'implicit': 0, 'optional': True}),
+ ('crls', RevocationInfoChoices, {'implicit': 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, {'implicit': 0, 'optional': True}),
+ ('crls', RevocationInfoChoices, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class RecipientIdentifier(Choice):
+ _alternatives = [
+ ('issuer_and_serial_number', IssuerAndSerialNumber),
+ ('subject_key_identifier', OctetString, {'implicit': 0}),
+ ]
+
+
+class KeyEncryptionAlgorithmId(ObjectIdentifier):
+ _map = {
+ '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',
+ }
+
+
+class KeyEncryptionAlgorithm(_ForceNullParameters, 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, {'implicit': 0}),
+ ('originator_key', PublicKeyInfo, {'implicit': 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, {'implicit': 0}),
+ ]
+
+
+class RecipientEncryptedKey(Sequence):
+ _fields = [
+ ('rid', KeyAgreementRecipientIdentifier),
+ ('encrypted_key', OctetString),
+ ]
+
+
+class RecipientEncryptedKeys(SequenceOf):
+ _child_spec = RecipientEncryptedKey
+
+
+class KeyAgreeRecipientInfo(Sequence):
+ _fields = [
+ ('version', CMSVersion),
+ ('originator', OriginatorIdentifierOrKey, {'explicit': 0}),
+ ('ukm', OctetString, {'explicit': 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, {'implicit': 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, {'implicit': 1}),
+ ('kekri', KEKRecipientInfo, {'implicit': 2}),
+ ('pwri', PasswordRecipientInfo, {'implicit': 3}),
+ ('ori', OtherRecipientInfo, {'implicit': 4}),
+ ]
+
+
+class RecipientInfos(SetOf):
+ _child_spec = RecipientInfo
+
+
+class EncryptedContentInfo(Sequence):
+ _fields = [
+ ('content_type', ContentType),
+ ('content_encryption_algorithm', EncryptionAlgorithm),
+ ('encrypted_content', OctetString, {'implicit': 0, 'optional': True}),
+ ]
+
+
+class EnvelopedData(Sequence):
+ _fields = [
+ ('version', CMSVersion),
+ ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}),
+ ('recipient_infos', RecipientInfos),
+ ('encrypted_content_info', EncryptedContentInfo),
+ ('unprotected_attrs', CMSAttributes, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class SignedAndEnvelopedData(Sequence):
+ _fields = [
+ ('version', CMSVersion),
+ ('recipient_infos', RecipientInfos),
+ ('digest_algorithms', DigestAlgorithms),
+ ('encrypted_content_info', EncryptedContentInfo),
+ ('certificates', CertificateSet, {'implicit': 0, 'optional': True}),
+ ('crls', CertificateRevocationLists, {'implicit': 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, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class AuthenticatedData(Sequence):
+ _fields = [
+ ('version', CMSVersion),
+ ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}),
+ ('recipient_infos', RecipientInfos),
+ ('mac_algorithm', HmacAlgorithm),
+ ('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, {'implicit': 2, 'optional': True}),
+ ('mac', OctetString),
+ ('unauth_attrs', CMSAttributes, {'implicit': 3, 'optional': True}),
+ ]
+
+
+class AuthEnvelopedData(Sequence):
+ _fields = [
+ ('version', CMSVersion),
+ ('originator_info', OriginatorInfo, {'implicit': 0, 'optional': True}),
+ ('recipient_infos', RecipientInfos),
+ ('auth_encrypted_content_info', EncryptedContentInfo),
+ ('auth_attrs', CMSAttributes, {'implicit': 1, 'optional': True}),
+ ('mac', OctetString),
+ ('unauth_attrs', CMSAttributes, {'implicit': 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, {'optional': True}),
+ ]
+
+
+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 = {
+ '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 = {
+ '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 = {
+ 'content_type': SetOfContentType,
+ 'message_digest': SetOfOctetString,
+ 'signing_time': SetOfTime,
+ 'counter_signature': SignerInfos,
+ 'signature_time_stamp_token': SetOfContentInfo,
+ 'cms_algorithm_protection': SetOfCMSAlgorithmProtection,
+}
diff --git a/asn1crypto/core.py b/asn1crypto/core.py
new file mode 100644
index 0000000..14a8203
--- /dev/null
+++ b/asn1crypto/core.py
@@ -0,0 +1,5242 @@
+# coding: utf-8
+
+"""
+ASN.1 type classes for universal types. Exports the following items:
+
+ - load()
+ - Any()
+ - Asn1Value()
+ - BitString()
+ - BMPString()
+ - Boolean()
+ - CharacterString()
+ - Choice()
+ - EmbeddedPdv()
+ - Enumerated()
+ - GeneralizedTime()
+ - GeneralString()
+ - GraphicString()
+ - IA5String()
+ - InstanceOf()
+ - Integer()
+ - IntegerBitString()
+ - IntegerOctetString()
+ - Null()
+ - NumericString()
+ - ObjectDescriptor()
+ - ObjectIdentifier()
+ - OctetBitString()
+ - OctetString()
+ - PrintableString()
+ - Real()
+ - RelativeOid()
+ - Sequence()
+ - SequenceOf()
+ - Set()
+ - SetOf()
+ - TeletexString()
+ - UniversalString()
+ - UTCTime()
+ - UTF8String()
+ - VideotexString()
+ - VisibleString()
+ - VOID
+ - Void()
+
+Other type classes are defined that help compose the types listed above.
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+from datetime import datetime, timedelta
+import binascii
+import copy
+import math
+import re
+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, chr_cls
+from .parser import _parse, _dump_header
+from .util import int_to_bytes, int_from_bytes, timezone, extended_datetime
+
+if sys.version_info <= (3,):
+ from cStringIO import StringIO as BytesIO
+
+ range = xrange # noqa
+ _PY2 = True
+
+else:
+ from io import BytesIO
+
+ _PY2 = False
+
+
+_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',
+}
+
+
+_OID_RE = re.compile(r'^\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
+# would just see the parent class attributes and would use them.
+_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
+ """
+
+ # 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
+
+ # 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
+
+ # If the value has been implicitly tagged
+ implicit = False
+
+ # 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
+
+ # 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 - this is not used by
+ # some classes since they utilize _bytes or _unicode
+ _native = None
+
+ @classmethod
+ 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:
+ 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, strict=strict)
+ return value
+
+ def __init__(self, explicit=None, implicit=None, no_explicit=False, tag_type=None, class_=None, tag=None,
+ optional=None, default=None, contents=None):
+ """
+ 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. Deprecated in favor of explicit and implicit params.
+
+ :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"
+ Deprecated in favor of explicit and implicit params.
+
+ :param tag:
+ The integer tag to override - usually this is used with tag_type or
+ class_. Deprecated in favor of explicit and implicit params.
+
+ :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
+
+ :param contents:
+ A byte string of the encoded contents of the value
+
+ :raises:
+ ValueError - when implicit, explicit, tag_type, class_ or tag are invalid values
+ """
+
+ 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
+
+ # 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 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)
+ ))
+
+ 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(
+ '''
+ implicit class must be one of "universal", "application",
+ "context", "private", not %s
+ ''',
+ repr(class_)
+ ))
+ if tag is not None:
+ if not isinstance(tag, int_types):
+ raise TypeError(unwrap(
+ '''
+ implicit tag must be an integer, not %s
+ ''',
+ type_name(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:
+ 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:
+ self.tag = tag
+
+ if no_explicit:
+ self.explicit = None
+
+ if contents is not None:
+ self.contents = contents
+
+ elif default is not None:
+ self.set(default)
+
+ 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 different 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
+ """
+
+ 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):
+ """
+ Constructs a new copy of the current object, preserving any tagging
+
+ :return:
+ An Asn1Value object
+ """
+
+ new_obj = self.__class__()
+ new_obj.class_ = self.class_
+ new_obj.tag = self.tag
+ new_obj.implicit = self.implicit
+ new_obj.explicit = self.explicit
+ 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, tagging, tag=None):
+ """
+ Copies the object, applying a new tagging to it
+
+ :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. Only used when tagging is a unicode string.
+
+ :return:
+ An Asn1Value object
+ """
+
+ # 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
+
+ def untag(self):
+ """
+ Copies the object, removing any special tagging from it
+
+ :return:
+ An Asn1Value object
+ """
+
+ new_obj = self.__class__()
+ new_obj._copy(self, copy.deepcopy)
+ return new_obj
+
+ 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__:
+ 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)
+
+ def debug(self, nest_level=1):
+ """
+ Show the binary data and parsed data in a tree structure
+ """
+
+ prefix = ' ' * nest_level
+
+ # 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)
+ if has_parsed:
+ self.parsed.debug(nest_level + 2)
+ elif hasattr(self, 'chosen'):
+ self.chosen.debug(nest_level + 2)
+ else:
+ 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):
+ """
+ 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 = self.contents
+
+ 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)
+
+ 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 = b''
+
+ return self._header + contents
+
+
+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
+
+ def _setup(self):
+ """
+ Generates _reverse_map from _map
+ """
+
+ cls = self.__class__
+ if cls._map is None or cls._reverse_map is not None:
+ return
+ cls._reverse_map = {}
+ for key, value in cls._map.items():
+ cls._reverse_map[value] = key
+
+
+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
+ 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.class_ = self.class_
+ 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
+ if isinstance(self, Constructable):
+ new_obj.method = self.method
+ new_obj._indefinite = self._indefinite
+ return new_obj
+
+
+class Constructable(object):
+ """
+ A mixin to handle string types that may be constructed from chunks
+ 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):
+ """
+ :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._merge_chunks()
+ else:
+ output += sub_value._merge_chunks()
+
+ if output is None:
+ return self._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.
+ """
+
+ if self._chunks_offset == 0:
+ 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):
+ """
+ A representation of an optional value that is not present. Has .native
+ property and .dump() method to be compatible with other value classes.
+ """
+
+ contents = b''
+
+ def __eq__(self, other):
+ """
+ :param other:
+ The other Primitive to compare to
+
+ :return:
+ A boolean
+ """
+
+ return other.__class__ == self.__class__
+
+ def __nonzero__(self):
+ return False
+
+ def __len__(self):
+ return 0
+
+ def __iter__(self):
+ return iter(())
+
+ @property
+ def native(self):
+ """
+ The a native Python datatype representation of this value
+
+ :return:
+ None
+ """
+
+ return None
+
+ 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 b''
+
+
+VOID = Void()
+
+
+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
+
+ 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 TypeError(unwrap(
+ '''
+ value must be an instance of Asn1Value, not %s
+ ''',
+ type_name(value)
+ ))
+
+ self._parsed = (value, value.__class__, None)
+ self.contents = value.dump()
+
+ except (ValueError, TypeError) as e:
+ args = e.args[1:]
+ e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args
+ raise e
+
+ @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):
+ try:
+ 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,
+ 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 = None
+ self.explicit = None
+ self.implicit = False
+ 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
+ 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
+
+ :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 self._parsed is None:
+ self.parse()
+
+ return self._parsed[0].dump(force=force)
+
+
+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, {'explicit': 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
+
+ @classmethod
+ 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
+ """
+
+ 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):
+ """
+ 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):
+ 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
+
+ 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__()
+
+ :param name:
+ The name of the alternative to be set - used with value.
+ Alternatively this may be a dict with a single key being the name
+ and the value being the value, or a two-element tuple of the the
+ name and the value.
+
+ :param value:
+ The alternative value to set - used with name
+
+ :raises:
+ ValueError - when implicit param is passed (or legacy tag_type param is "implicit")
+ """
+
+ _tag_type_to_explicit_implicit(kwargs)
+
+ Asn1Value.__init__(self, **kwargs)
+
+ try:
+ if kwargs.get('implicit') is not None:
+ 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 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(
+ '''
+ The name specified, "%s", is not a valid alternative
+ for %s
+ ''',
+ name,
+ type_name(self)
+ ))
+
+ self._choice = self._name_map[name]
+ _, spec, params = self._alternatives[self._choice]
+
+ if not isinstance(value, spec):
+ value = spec(value, **params)
+ else:
+ value = _fix_tagging(value, params)
+ self._parsed = 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
+
+ @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:
+ _, spec, params = self._alternatives[self._choice]
+ self._parsed, _ = _parse_build(self.contents, spec=spec, spec_params=params)
+ except (ValueError, TypeError) as e:
+ args = e.args[1:]
+ e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args
+ raise e
+
+ @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, contents):
+ """
+ Ensures that the class and tag specified exist as an alternative
+
+ :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
+ """
+
+ id_ = (class_, tag)
+
+ if self.explicit is not None:
+ if self.explicit[-1] != 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, len(contents))
+ 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(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(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):
+ """
+ :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 _copy(self, other, copy_func):
+ """
+ Copies the contents of another Choice 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(Choice, self)._copy(other, copy_func)
+ self._choice = other._choice
+ self._name = other._name
+ self._parsed = copy_func(other._parsed)
+
+ 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
+ """
+
+ self.contents = self.chosen.dump(force=force)
+ if self._header is None or force:
+ self._header = b''
+ if self.explicit is not None:
+ for class_, tag in self.explicit:
+ self._header = _dump_header(class_, 1, tag, self._header + self.contents) + self._header
+ return self._header + self.contents
+
+
+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, 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, strict=strict)
+
+ def __init__(self, value=None, contents=None, strict=False):
+ """
+ :param value:
+ A native Python datatype to initialize the object value with
+
+ :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
+ """
+
+ 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, offset = _parse_build(contents, pointer=offset, spec=spec)
+ else:
+ 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
+ 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):
+ """
+ Since str is different 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 defined 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
+ """
+
+ class_ = 0
+
+ method = 0
+
+ def __init__(self, value=None, default=None, contents=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
+
+ :param contents:
+ A byte string of the encoded contents of the value
+ """
+
+ Asn1Value.__init__(self, **kwargs)
+
+ try:
+ 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, TypeError) as e:
+ args = e.args[1:]
+ e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args
+ raise e
+
+ 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._native = value
+ self.contents = value
+ self._header = None
+ 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)
+
+ 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__) | 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
+
+ # When tagging is going on, do the extra work of constructing new
+ # objects to see if the dumped representation are the same
+ if self.implicit or self.explicit or other.implicit or other.explicit:
+ return self.untag().dump() == other.untag().dump()
+
+ return self.dump() == other.dump()
+
+
+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
+ 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'
+
+ # Instance attribute of (possibly-merged) unicode string
+ _unicode = None
+
+ def set(self, value):
+ """
+ Sets the value of the string
+
+ :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)
+ ))
+
+ 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''
+
+ 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._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):
+ """
+ The a native Python datatype representation of this value
+
+ :return:
+ A unicode string or None
+ """
+
+ if self.contents is None:
+ return None
+
+ return self.__unicode__()
+
+
+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(unwrap(
+ '''
+ %s value is a unicode string, but no _map provided
+ ''',
+ type_name(self)
+ ))
+
+ if value not in self._reverse_map:
+ 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 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
+
+ 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(Constructable, Castable, Primitive, ValueMap, object):
+ """
+ Represents a bit string from ASN.1 as a Python tuple of 1s and 0s
+ """
+
+ tag = 3
+
+ _size = None
+
+ # Used with _as_chunk() from Constructable
+ _chunk = None
+ _chunks_offset = 1
+
+ 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
+
+ :param value:
+ An integer or a tuple of integers 0 and 1
+
+ :raises:
+ ValueError - when an invalid value is passed
+ """
+
+ if isinstance(value, set):
+ if self._map is None:
+ raise ValueError(unwrap(
+ '''
+ %s._map has not been defined
+ ''',
+ type_name(self)
+ ))
+
+ 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 value.__class__ == 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))
+
+ else:
+ 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)
+ ))
+
+ self._chunk = None
+
+ if self._map is not None:
+ if len(value) > self._size:
+ raise ValueError(unwrap(
+ '''
+ %s value must be at most %s bits long, specified was %s long
+ ''',
+ type_name(self),
+ self._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)
+
+ size_mod = size % 8
+ extra_bits = 0
+ if size_mod != 0:
+ extra_bits = 8 - size_mod
+ value += '0' * extra_bits
+
+ 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._indefinite:
+ self._indefinite = False
+ self.method = 0
+ if self._trailer != b'':
+ self._trailer = b''
+
+ def __getitem__(self, key):
+ """
+ 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
+
+ :raises:
+ ValueError - when _map is not set or the key name is invalid
+
+ :return:
+ A boolean if the bit is set
+ """
+
+ is_int = isinstance(key, int_types)
+ if not is_int:
+ if not isinstance(self._map, dict):
+ raise ValueError(unwrap(
+ '''
+ %s._map has not been defined
+ ''',
+ type_name(self)
+ ))
+
+ if key not in self._reverse_map:
+ raise ValueError(unwrap(
+ '''
+ %s._map does not contain an entry for "%s"
+ ''',
+ type_name(self),
+ key
+ ))
+
+ if self._native is None:
+ self.native
+
+ 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):
+ """
+ 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 boolean value
+
+ :raises:
+ ValueError - when _map is not set or the key name is invalid
+ """
+
+ is_int = isinstance(key, int_types)
+ if not is_int:
+ if self._map is None:
+ raise ValueError(unwrap(
+ '''
+ %s._map has not been defined
+ ''',
+ type_name(self)
+ ))
+
+ if key not in self._reverse_map:
+ raise ValueError(unwrap(
+ '''
+ %s._map does not contain an entry for "%s"
+ ''',
+ type_name(self),
+ key
+ ))
+
+ if self._native is None:
+ self.native
+
+ 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)
+
+ else:
+ if is_int:
+ key = self._map.get(key, key)
+
+ if value:
+ if key not in self._native:
+ self._native.add(key)
+ else:
+ if key in self._native:
+ self._native.remove(key)
+
+ self.set(self._native)
+
+ def _as_chunk(self):
+ """
+ Allows reconstructing indefinite length values
+
+ :return:
+ A tuple of integers
+ """
+
+ extra_bits = int_from_bytes(self.contents[0:1])
+ bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:]))
+ byte_len = len(self.contents[1:])
+ bit_len = len(bit_string)
+
+ # Left-pad the bit string to a byte multiple to ensure we didn't
+ # lose any zero bits on the left
+ mod_bit_len = bit_len % 8
+ if mod_bit_len != 0:
+ bit_string = ('0' * (8 - mod_bit_len)) + bit_string
+ bit_len = len(bit_string)
+
+ if bit_len // 8 < byte_len:
+ missing_bytes = byte_len - (bit_len // 8)
+ bit_string = ('0' * (8 * missing_bytes)) + bit_string
+
+ # Trim off the extra bits on the right used to fill the last byte
+ if extra_bits > 0:
+ bit_string = bit_string[0:0 - extra_bits]
+
+ return tuple(map(int, tuple(bit_string)))
+
+ @property
+ def native(self):
+ """
+ The a native Python datatype representation of this value
+
+ :return:
+ 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:
+ if self._map is None:
+ self.set(())
+ else:
+ self.set(set())
+
+ if self._native is None:
+ bits = self._merge_chunks()
+ if self._map:
+ 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
+
+
+class OctetBitString(Constructable, Castable, Primitive):
+ """
+ Represents a bit string in ASN.1 as a Python byte string
+ """
+
+ 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
+
+ :param value:
+ A byte string
+
+ :raises:
+ ValueError - when an invalid value is passed
+ """
+
+ 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
+ # 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
+ """
+
+ if self.contents is None:
+ return b''
+ if self._bytes is None:
+ 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):
+ """
+ The a native Python datatype representation of this value
+
+ :return:
+ A byte string or None
+ """
+
+ if self.contents is None:
+ return None
+
+ return self.__bytes__()
+
+
+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
+
+ :param value:
+ An integer
+
+ :raises:
+ ValueError - when an invalid value is passed
+ """
+
+ if not isinstance(value, int_types):
+ 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
+ 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''
+
+ def _as_chunk(self):
+ """
+ Allows reconstructing indefinite length values
+
+ :return:
+ A unicode string of bits - 1s and 0s
+ """
+
+ extra_bits = int_from_bytes(self.contents[0:1])
+ bit_string = '{0:b}'.format(int_from_bytes(self.contents[1:]))
+
+ # Ensure we have leading zeros since these chunks may be concatenated together
+ mod_bit_len = len(bit_string) % 8
+ if mod_bit_len != 0:
+ bit_string = ('0' * (8 - mod_bit_len)) + bit_string
+
+ if extra_bits > 0:
+ return bit_string[0:0 - extra_bits]
+
+ return bit_string
+
+ @property
+ def native(self):
+ """
+ The a native Python datatype representation of this value
+
+ :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])
+ # 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(Constructable, Castable, Primitive):
+ """
+ Represents a byte string in both ASN.1 and Python
+ """
+
+ tag = 4
+
+ # Instance attribute of (possibly-merged) byte string
+ _bytes = None
+
+ 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 __bytes__(self):
+ """
+ :return:
+ A byte string
+ """
+
+ if self.contents is None:
+ return b''
+ if self._bytes is None:
+ 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):
+ """
+ The a native Python datatype representation of this value
+
+ :return:
+ A byte string or None
+ """
+
+ if self.contents is None:
+ return None
+
+ return self.__bytes__()
+
+
+class IntegerOctetString(Constructable, Castable, Primitive):
+ """
+ Represents a byte string in ASN.1 as a Python integer
+ """
+
+ tag = 4
+
+ 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 TypeError(unwrap(
+ '''
+ %s value must be an integer, not %s
+ ''',
+ type_name(self),
+ type_name(value)
+ ))
+
+ 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''
+
+ @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._merge_chunks())
+ return self._native
+
+
+class ParsableOctetString(Constructable, Castable, Primitive):
+
+ tag = 4
+
+ _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
+ byte string value
+
+ :param value:
+ A native Python datatype to initialize the object value with
+
+ :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, **kwargs)
+
+ if set_parsed:
+ self._parsed = (parsed, parsed.__class__, None)
+
+ 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):
+ """
+ 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
+ """
+
+ if self.contents is None:
+ return b''
+ if self._bytes is None:
+ 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):
+ """
+ The a native Python datatype representation of this value
+
+ :return:
+ A byte string or None
+ """
+
+ if self.contents is None:
+ return None
+
+ if self._parsed is not None:
+ return self._parsed[0].native
+ else:
+ return self.__bytes__()
+
+ @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 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 ParsableOctetBitString(ParsableOctetString):
+
+ tag = 3
+
+ # Whenever dealing with octet-based bit strings, we really want the
+ # bytes, so we just ignore the unused bits portion since it isn't
+ # applicable to the current use case
+ # unused_bits = struct.unpack('>B', self.contents[0:1])[0]
+ _chunks_offset = 1
+
+ def set(self, value):
+ """
+ Sets the value of the object
+
+ :param value:
+ A byte string
+
+ :raises:
+ ValueError - when an invalid value is passed
+ """
+
+ 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
+ # 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''
+
+
+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
+ """
+
+ self.contents = b''
+
+ @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
+
+ # 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
+
+ :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 TypeError(unwrap(
+ '''
+ %s value must be a unicode string, not %s
+ ''',
+ type_name(self),
+ type_name(value)
+ ))
+
+ self._native = value
+
+ if self._map is not None:
+ if value in self._reverse_map:
+ value = self._reverse_map[value]
+
+ self.contents = b''
+ 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 = chr_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
+ """
+
+ return self.dotted
+
+ @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):
+ """
+ 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.dotted
+ 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_types) and not isinstance(value, str_cls):
+ 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(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(unwrap(
+ '''
+ %s value %s is not a valid value
+ ''',
+ type_name(self),
+ 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
+
+ # 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
+ #
+ # ("name", Asn1ValueClass)
+ #
+ # Option 2, same as Option 1, but with a dict of class params
+ #
+ # ("name", Asn1ValueClass, {'explicit': 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
+
+ # 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
+ 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)
+
+ 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:
+ 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]
+ 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
+ # been set for the field, then skip it
+ 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:]
+ e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + 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()
+
+ return mutated
+
+ 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 child.__class__ == tuple:
+ child = self.children[index] = _build(*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:
+ KeyError - 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_types):
+ if key not in self._field_map:
+ 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(unwrap(
+ '''
+ No field numbered %s is present in this %s
+ ''',
+ key,
+ type_name(self)
+ ))
+
+ 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):
+ """
+ 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_types):
+ if key not in self._field_map:
+ 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)
+
+ new_value = self._make_value(field_name, field_spec, value_spec, field_params, value)
+
+ 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(unwrap(
+ '''
+ Value for field "%s" of %s is not set
+ ''',
+ field_name,
+ type_name(self)
+ ))
+
+ self.children[key] = new_value
+
+ if self._native is not None:
+ self._native[self._fields[key][0]] = self.children[key].native
+ self._mutated = True
+
+ 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_types):
+ if key not in self._field_map:
+ raise KeyError(unwrap(
+ '''
+ No field named "%s" defined for %s
+ ''',
+ key,
+ type_name(self)
+ ))
+ key = self._field_map[key]
+
+ 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
+ ''',
+ name,
+ type_name(self)
+ ))
+
+ if 'optional' in params:
+ self.children[key] = VOID
+ if self._native is not None:
+ self._native[name] = None
+ else:
+ self.__setitem__(key, None)
+ self._mutated = True
+
+ def __iter__(self):
+ """
+ :return:
+ An iterator of field key names
+ """
+
+ for info in self._fields:
+ yield info[0]
+
+ 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()
+
+ contents = BytesIO()
+ for index, info in enumerate(self._fields):
+ child = self.children[index]
+ if child is None:
+ child_dump = b''
+ elif child.__class__ == 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 info[2] and 'default' in info[2]:
+ default_value = info[1](**info[2])
+ if default_value.dump() == child_dump:
+ continue
+ contents.write(child_dump)
+ self._contents = contents.getvalue()
+
+ self._header = None
+ if self._trailer != b'':
+ self._trailer = b''
+
+ def _setup(self):
+ """
+ Generates _field_map, _field_ids and _oid_nums for use in parsing
+ """
+
+ 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 + ({},)
+ cls._fields[index] = field
+ cls._field_map[field[0]] = index
+ 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]])
+
+ 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
+
+ :param index:
+ The field number
+
+ :return:
+ A tuple containing the following elements:
+ - unicode string of the field name
+ - 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 from an OID or a spec callback
+ """
+
+ name, field_spec, field_params = self._fields[index]
+ value_spec = field_spec
+ spec_override = None
+
+ 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
+ # the override, for situations such as OctetString and parse_as
+ 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
+ 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
+
+ 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 (name, 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
+ """
+
+ if value is None and 'optional' in field_params:
+ return VOID
+
+ specs_different = field_spec != value_spec
+ is_any = issubclass(field_spec, Any)
+
+ if issubclass(value_spec, Choice):
+ if not isinstance(value, 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
+ ''',
+ field_name,
+ type_name(value_spec)
+ ))
+ if not isinstance(value, value_spec):
+ wrapper = value_spec()
+ wrapper.validate(value.class_, value.tag, value.contents)
+ wrapper._parsed = value
+ 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, new_value.__class__, None)
+ new_value = wrapper
+
+ new_value = _fix_tagging(new_value, field_params)
+
+ return new_value
+
+ 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
+ """
+
+ 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:
+ 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:
+ self.children = []
+ contents_length = len(self._contents)
+ child_pointer = 0
+ field = 0
+ field_len = len(self._fields)
+ 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 = (
+ 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):
+ 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
+ choice_match = False
+ if issubclass(field_spec, Choice):
+ try:
+ tester = field_spec(**field_params)
+ tester.validate(parts[0], parts[2], parts[4])
+ choice_match = True
+ except (ValueError):
+ pass
+
+ if not choice_match:
+ if 'optional' in field_params:
+ self.children.append(VOID)
+ 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)):
+ 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)
+
+ # Handle situations where an optional or defaulted field definition is incorrect
+ elif field_len > 0 and field + 1 <= field_len:
+ 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(unwrap(
+ '''
+ Data for field %s (%s class, %s method, tag %s) does
+ not match the field definition%s of %s
+ ''',
+ field + 1,
+ 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
+
+ if recurse:
+ child = _build(*child)
+ if isinstance(child, (Sequence, SequenceOf)):
+ child._parse_children(recurse=True)
+
+ self.children.append(child)
+ field += 1
+ parts = None
+
+ index = len(self.children)
+ 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:
+ self.children.append(VOID)
+ else:
+ raise ValueError(unwrap(
+ '''
+ Field "%s" is missing from structure
+ ''',
+ name
+ ))
+ index += 1
+
+ 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 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 preceding field values before calling this. Usually
+ specs, if dynamic, are controlled by a preceding 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 TypeError(unwrap(
+ '''
+ field_name must be a unicode string, not %s
+ ''',
+ type_name(field_name)
+ ))
+
+ if self._fields is None:
+ 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)
+
+ return info[2]
+
+ @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)
+ try:
+ 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):
+ """
+ Copies the contents of another Sequence 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(Sequence, self)._copy(other, copy_func)
+ if self.children is not None:
+ self.children = []
+ for child in other.children:
+ if child.__class__ == 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:
+ child = self._lazy_child(self._field_map[field_name])
+ if child is not VOID:
+ print('%s Field "%s"' % (prefix, field_name))
+ child.debug(nest_level + 3)
+
+ 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)
+
+ 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)
+
+
+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
+
+ # 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
+
+ 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__()
+
+ :param value:
+ A native Python datatype to initialize the object value with
+
+ :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
+ """
+
+ if spec:
+ self._child_spec = spec
+
+ Asn1Value.__init__(self, **kwargs)
+
+ try:
+ 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)
+
+ # Make sure a blank list is serialized
+ if self.contents is None:
+ self._set_contents()
+
+ except (ValueError, TypeError) as e:
+ args = e.args[1:]
+ e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + 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()
+
+ return mutated
+
+ 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 child.__class__ == tuple:
+ child = _build(*child)
+ 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):
+ new_value = value
+
+ elif issubclass(self._child_spec, Any):
+ if isinstance(value, Asn1Value):
+ new_value = value
+ else:
+ 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(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, value.contents)
+ wrapper._parsed = value
+ value = wrapper
+ new_value = value
+
+ else:
+ return self._child_spec(value=value)
+
+ params = {}
+ 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):
+ """
+ :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()
+
+ 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)
+
+ self.children[key] = new_value
+
+ if self._native is not None:
+ self._native[key] = self.children[key].native
+
+ self._mutated = True
+
+ 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.pop(key)
+ if self._native is not None:
+ self._native.pop(key)
+
+ self._mutated = True
+
+ def __iter__(self):
+ """
+ :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 __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
+
+ :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._mutated = True
+
+ 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()
+
+ contents = BytesIO()
+ for child in self:
+ contents.write(child.dump(force=force))
+ self._contents = contents.getvalue()
+ 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 = []
+ if self._contents is None:
+ return
+ contents_length = len(self._contents)
+ child_pointer = 0
+ while child_pointer < contents_length:
+ parts, child_pointer = _parse(self._contents, contents_length, 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)
+ self.children.append(child)
+ 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 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):
+ """
+ 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)
+ try:
+ 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):
+ """
+ Copies the contents of another SequenceOf 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(SequenceOf, self)._copy(other, copy_func)
+ if self.children is not None:
+ self.children = []
+ for child in other.children:
+ if child.__class__ == 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:
+ child.debug(nest_level + 1)
+
+ 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):
+ """
+ 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
+
+ def _setup(self):
+ """
+ Generates _field_map, _field_ids and _oid_nums for use in parsing
+ """
+
+ 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 + ({},)
+ cls._fields[index] = field
+ cls._field_map[field[0]] = 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]])
+
+ 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
+ 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
+ """
+
+ 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:
+ 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)
+ child_pointer = 0
+ seen_field = 0
+ while child_pointer < contents_length:
+ parts, child_pointer = _parse(self.contents, contents_length, pointer=child_pointer)
+
+ id_ = (parts[0], parts[2])
+
+ 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],
+ ))
+
+ _, 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 spec_override:
+ child = parts + (field_spec, field_params, value_spec)
+ else:
+ child = parts + (field_spec, field_params)
+
+ if recurse:
+ child = _build(*child)
+ if isinstance(child, (Sequence, SequenceOf)):
+ child._parse_children(recurse=True)
+
+ child_map[field] = child
+ seen_field += 1
+
+ total_fields = len(self._fields)
+
+ for index in range(0, total_fields):
+ if index in child_map:
+ continue
+
+ 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
+
+ if not field_params:
+ missing = True
+ elif 'optional' not in field_params and 'default' not in field_params:
+ missing = True
+ elif 'optional' in field_params:
+ child_map[index] = VOID
+ elif 'default' in field_params:
+ child_map[index] = field_spec(**field_params)
+
+ if missing:
+ raise ValueError(unwrap(
+ '''
+ Missing required field "%s" from %s
+ ''',
+ name,
+ type_name(self)
+ ))
+
+ self.children = []
+ for index in range(0, total_fields):
+ self.children.append(child_map[index])
+
+ 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 _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):
+ """
+ Represents a set (unordered) of a single type of values from ASN.1 as a
+ Python object with a list-like interface
+ """
+
+ 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):
+ """
+ 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 = 'ascii'
+
+
+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')
+ if _PY2:
+ value = value.decode('ascii')
+
+ 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)
+
+ year_num = int(string[0:2])
+ if year_num < 50:
+ prefix = '20'
+ else:
+ prefix = '19'
+
+ if strlen == 10:
+ return datetime.strptime(prefix + string, '%Y%m%d%H%M')
+
+ if strlen == 12:
+ return datetime.strptime(prefix + string, '%Y%m%d%H%M%S')
+
+ return string
+
+
+class GeneralizedTime(AbstractTime):
+ """
+ Represents a generalized time from ASN.1 as a Python datetime.datetime
+ object or asn1crypto.util.extended_datetime object in UTC
+ """
+
+ tag = 24
+
+ def set(self, value):
+ """
+ Sets the value of the object
+
+ :param value:
+ 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, extended_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
+ # 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, asn1crypto.util.extended_datetime object or
+ a unicode string
+ """
+
+ strlen = len(string)
+
+ date_format = None
+ if strlen == 10:
+ date_format = '%Y%m%d%H'
+ elif strlen == 12:
+ date_format = '%Y%m%d%H%M'
+ elif strlen == 14:
+ date_format = '%Y%m%d%H%M%S'
+ elif strlen == 18:
+ date_format = '%Y%m%d%H%M%S.%f'
+
+ if date_format:
+ if len(string) >= 4 and string[0:4] == '0000':
+ # Year 2000 shares a calendar with year 0, and is supported natively
+ t = datetime.strptime('2000' + string[4:], date_format)
+ return extended_datetime(
+ 0,
+ t.month,
+ t.day,
+ t.hour,
+ t.minute,
+ t.second,
+ t.microsecond,
+ t.tzinfo
+ )
+ return datetime.strptime(string, date_format)
+
+ 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 _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, 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:
+ method_name = METHOD_NUM_TO_NAME_MAP.get(self.method)
+ class_name = CLASS_NUM_TO_NAME_MAP.get(self.class_)
+
+ 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.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 _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
+ necessary
+
+ :param value:
+ An Asn1Value object
+
+ :param params:
+ A dict of spec params
+
+ :return:
+ An Asn1Value that is properly tagged
+ """
+
+ _tag_type_to_explicit_implicit(params)
+
+ retag = False
+ if 'implicit' not in params:
+ if value.implicit is not False:
+ retag = True
+ else:
+ if isinstance(params['implicit'], tuple):
+ class_, tag = params['implicit']
+ else:
+ tag = params['implicit']
+ class_ = 'context'
+ if value.implicit is False:
+ retag = True
+ elif value.class_ != CLASS_NAME_TO_NUM_MAP[class_] or value.tag != tag:
+ retag = True
+
+ if params.get('explicit') != value.explicit:
+ retag = True
+
+ if retag:
+ return value.retag(params)
+ return value
+
+
+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_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)
+
+ 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
+
+ :param class_:
+ An integer representing the ASN.1 class
+
+ :param method:
+ An integer representing the ASN.1 method
+
+ :param tag:
+ An integer representing the ASN.1 tag
+
+ :param header:
+ A byte string of the ASN.1 header (class, method, tag, length)
+
+ :param contents:
+ A byte string of the ASN.1 value
+
+ :param trailer:
+ 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
+ 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 spec_params is not None:
+ _tag_type_to_explicit_implicit(spec_params)
+
+ 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 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_
+ 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 = explicit_header
+ value._trailer = explicit_trailer
+ value.explicit = original_explicit
+ header_set = True
+ else:
+ 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 - 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):
+ 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
+ ))
+
+ # 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 'explicit' in spec_params:
+ original_value = Asn1Value(contents=contents, **spec_params)
+ 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.explicit = original_explicit
+ header_set = True
+
+ # If no spec was specified, allow anything and just process what
+ # is in the input data
+ else:
+ if tag not in _UNIVERSAL_SPECS:
+ 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_)
+ 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
+ value._trailer = trailer or b''
+
+ # Destroy any default value that our contents have overwritten
+ value._native = None
+
+ 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
+
+ return value
+
+
+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
+
+ :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
+
+ :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
+ """
+
+ 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/crl.py b/asn1crypto/crl.py
new file mode 100644
index 0000000..84cb168
--- /dev/null
+++ b/asn1crypto/crl.py
@@ -0,0 +1,536 @@
+# 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, division, absolute_import, print_function
+
+import hashlib
+
+from .algos import SignedDigestAlgorithm
+from .core import (
+ Boolean,
+ Enumerated,
+ GeneralizedTime,
+ Integer,
+ ObjectIdentifier,
+ OctetBitString,
+ ParsableOctetString,
+ Sequence,
+ SequenceOf,
+)
+from .x509 import (
+ AuthorityInfoAccessSyntax,
+ AuthorityKeyIdentifier,
+ CRLDistributionPoints,
+ DistributionPointName,
+ 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, {'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}),
+ ]
+
+
+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', ParsableOctetString),
+ ]
+
+ _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',
+ }
+
+ @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 = {
+ '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',
+ }
+
+
+class CRLEntryExtension(Sequence):
+ _fields = [
+ ('extn_id', CRLEntryExtensionId),
+ ('critical', Boolean, {'default': False}),
+ ('extn_value', ParsableOctetString),
+ ]
+
+ _oid_pair = ('extn_id', 'extn_value')
+ _oid_specs = {
+ 'crl_reason': CRLReason,
+ 'hold_instruction_code': ObjectIdentifier,
+ '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}),
+ ]
+
+ _processed_extensions = False
+ _critical_extensions = None
+ _crl_reason_value = None
+ _invalidity_date_value = None
+ _certificate_issuer_value = None
+ _issuer_name = False
+
+ def _set_extensions(self):
+ """
+ Sets common named extensions to private attributes and creates a list
+ of critical extensions
+ """
+
+ self._critical_extensions = set()
+
+ 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.add(name)
+
+ self._processed_extensions = True
+
+ @property
+ def critical_extensions(self):
+ """
+ Returns a set of the names (or OID if not a known extension) of the
+ extensions marked as critical
+
+ :return:
+ A set 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._set_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._set_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._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
+
+
+class TbsCertList(Sequence):
+ _fields = [
+ ('version', Version, {'optional': True}),
+ ('signature', SignedDigestAlgorithm),
+ ('issuer', Name),
+ ('this_update', Time),
+ ('next_update', Time, {'optional': True}),
+ ('revoked_certificates', RevokedCertificates, {'optional': True}),
+ ('crl_extensions', TBSCertListExtensions, {'explicit': 0, 'optional': True}),
+ ]
+
+
+class CertificateList(Sequence):
+ _fields = [
+ ('tbs_cert_list', TbsCertList),
+ ('signature_algorithm', SignedDigestAlgorithm),
+ ('signature', OctetBitString),
+ ]
+
+ _processed_extensions = False
+ _critical_extensions = 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
+ _issuer_cert_urls = None
+ _delta_crl_distribution_points = None
+ _sha1 = None
+ _sha256 = None
+
+ def _set_extensions(self):
+ """
+ Sets common named extensions to private attributes and creates a list
+ of critical extensions
+ """
+
+ self._critical_extensions = set()
+
+ 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.add(name)
+
+ self._processed_extensions = True
+
+ @property
+ def critical_extensions(self):
+ """
+ Returns a set of the names (or OID if not a known extension) of the
+ extensions marked as critical
+
+ :return:
+ A set 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._set_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._set_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._set_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._set_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._set_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._set_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._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):
+ """
+ :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 X.509 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']
+ # 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
+ 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
+
+ @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
diff --git a/asn1crypto/csr.py b/asn1crypto/csr.py
new file mode 100644
index 0000000..7ea2848
--- /dev/null
+++ b/asn1crypto/csr.py
@@ -0,0 +1,96 @@
+# 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
+# and https://tools.ietf.org/html/rfc2985
+
+
+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, {'implicit': 0, 'optional': True}),
+ ]
+
+
+class CertificationRequest(Sequence):
+ _fields = [
+ ('certification_request_info', CertificationRequestInfo),
+ ('signature_algorithm', SignedDigestAlgorithm),
+ ('signature', OctetBitString),
+ ]
diff --git a/asn1crypto/keys.py b/asn1crypto/keys.py
new file mode 100644
index 0000000..9a09a31
--- /dev/null
+++ b/asn1crypto/keys.py
@@ -0,0 +1,1249 @@
+# coding: utf-8
+
+"""
+ASN.1 type classes for public and private keys. Exports the following items:
+
+ - DSAPrivateKey()
+ - ECPrivateKey()
+ - EncryptedPrivateKeyInfo()
+ - PrivateKeyInfo()
+ - PublicKeyInfo()
+ - RSAPrivateKey()
+ - RSAPublicKey()
+
+Other type classes are defined that help compose the types listed above.
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import hashlib
+import math
+
+from ._elliptic_curve import (
+ SECP192R1_BASE_POINT,
+ SECP224R1_BASE_POINT,
+ SECP256R1_BASE_POINT,
+ SECP384R1_BASE_POINT,
+ SECP521R1_BASE_POINT,
+ PrimeCurve,
+ PrimePoint,
+)
+from ._errors import unwrap
+from ._types import type_name, str_cls, byte_cls
+from .algos import _ForceNullParameters, DigestAlgorithm, EncryptionAlgorithm, RSAESOAEPParams
+from .core import (
+ Any,
+ Asn1Value,
+ BitString,
+ Choice,
+ Integer,
+ IntegerOctetString,
+ Null,
+ ObjectIdentifier,
+ OctetBitString,
+ OctetString,
+ ParsableOctetString,
+ ParsableOctetBitString,
+ Sequence,
+ SequenceOf,
+ SetOf,
+)
+from .util import int_from_bytes, int_to_bytes
+
+
+class OtherPrimeInfo(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-46
+ """
+
+ _fields = [
+ ('prime', Integer),
+ ('exponent', Integer),
+ ('coefficient', Integer),
+ ]
+
+
+class OtherPrimeInfos(SequenceOf):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-46
+ """
+
+ _child_spec = OtherPrimeInfo
+
+
+class RSAPrivateKeyVersion(Integer):
+ """
+ Original Name: Version
+ Source: https://tools.ietf.org/html/rfc3447#page-45
+ """
+
+ _map = {
+ 0: 'two-prime',
+ 1: 'multi',
+ }
+
+
+class RSAPrivateKey(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-45
+ """
+
+ _fields = [
+ ('version', RSAPrivateKeyVersion),
+ ('modulus', Integer),
+ ('public_exponent', Integer),
+ ('private_exponent', Integer),
+ ('prime1', Integer),
+ ('prime2', Integer),
+ ('exponent1', Integer),
+ ('exponent2', Integer),
+ ('coefficient', Integer),
+ ('other_prime_infos', OtherPrimeInfos, {'optional': True})
+ ]
+
+
+class RSAPublicKey(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3447#page-44
+ """
+
+ _fields = [
+ ('modulus', Integer),
+ ('public_exponent', Integer)
+ ]
+
+
+class DSAPrivateKey(Sequence):
+ """
+ The ASN.1 structure that OpenSSL uses to store a DSA private key that is
+ not part of a PKCS#8 structure. Reversed engineered from english-language
+ description on linked OpenSSL documentation page.
+
+ Original Name: None
+ Source: https://www.openssl.org/docs/apps/dsa.html
+ """
+
+ _fields = [
+ ('version', Integer),
+ ('p', Integer),
+ ('q', Integer),
+ ('g', Integer),
+ ('public_key', Integer),
+ ('private_key', Integer),
+ ]
+
+
+class _ECPoint():
+ """
+ In both PublicKeyInfo and PrivateKeyInfo, the EC public key is a byte
+ string that is encoded as a bit string. This class adds convenience
+ methods for converting to and from the byte string to a pair of integers
+ that are the X and Y coordinates.
+ """
+
+ @classmethod
+ def from_coords(cls, x, y):
+ """
+ Creates an ECPoint object from the X and Y integer coordinates of the
+ point
+
+ :param x:
+ The X coordinate, as an integer
+
+ :param y:
+ The Y coordinate, as an integer
+
+ :return:
+ An ECPoint object
+ """
+
+ x_bytes = int(math.ceil(math.log(x, 2) / 8.0))
+ y_bytes = int(math.ceil(math.log(y, 2) / 8.0))
+
+ num_bytes = max(x_bytes, y_bytes)
+
+ byte_string = b'\x04'
+ byte_string += int_to_bytes(x, width=num_bytes)
+ byte_string += int_to_bytes(y, width=num_bytes)
+
+ return cls(byte_string)
+
+ def to_coords(self):
+ """
+ Returns the X and Y coordinates for this EC point, as native Python
+ integers
+
+ :return:
+ A 2-element tuple containing integers (X, Y)
+ """
+
+ data = self.native
+ first_byte = data[0:1]
+
+ # Uncompressed
+ if first_byte == b'\x04':
+ remaining = data[1:]
+ field_len = len(remaining) // 2
+ x = int_from_bytes(remaining[0:field_len])
+ y = int_from_bytes(remaining[field_len:])
+ return (x, y)
+
+ if first_byte not in set([b'\x02', b'\x03']):
+ raise ValueError(unwrap(
+ '''
+ Invalid EC public key - first byte is incorrect
+ '''
+ ))
+
+ raise ValueError(unwrap(
+ '''
+ Compressed representations of EC public keys are not supported due
+ to patent US6252960
+ '''
+ ))
+
+
+class ECPoint(OctetString, _ECPoint):
+
+ pass
+
+
+class ECPointBitString(OctetBitString, _ECPoint):
+
+ pass
+
+
+class SpecifiedECDomainVersion(Integer):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 104
+ """
+ _map = {
+ 1: 'ecdpVer1',
+ 2: 'ecdpVer2',
+ 3: 'ecdpVer3',
+ }
+
+
+class FieldType(ObjectIdentifier):
+ """
+ Original Name: None
+ Source: http://www.secg.org/sec1-v2.pdf page 101
+ """
+
+ _map = {
+ '1.2.840.10045.1.1': 'prime_field',
+ '1.2.840.10045.1.2': 'characteristic_two_field',
+ }
+
+
+class CharacteristicTwoBasis(ObjectIdentifier):
+ """
+ Original Name: None
+ Source: http://www.secg.org/sec1-v2.pdf page 102
+ """
+
+ _map = {
+ '1.2.840.10045.1.2.1.1': 'gn_basis',
+ '1.2.840.10045.1.2.1.2': 'tp_basis',
+ '1.2.840.10045.1.2.1.3': 'pp_basis',
+ }
+
+
+class Pentanomial(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 102
+ """
+
+ _fields = [
+ ('k1', Integer),
+ ('k2', Integer),
+ ('k3', Integer),
+ ]
+
+
+class CharacteristicTwo(Sequence):
+ """
+ Original Name: Characteristic-two
+ Source: http://www.secg.org/sec1-v2.pdf page 101
+ """
+
+ _fields = [
+ ('m', Integer),
+ ('basis', CharacteristicTwoBasis),
+ ('parameters', Any),
+ ]
+
+ _oid_pair = ('basis', 'parameters')
+ _oid_specs = {
+ 'gn_basis': Null,
+ 'tp_basis': Integer,
+ 'pp_basis': Pentanomial,
+ }
+
+
+class FieldID(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 100
+ """
+
+ _fields = [
+ ('field_type', FieldType),
+ ('parameters', Any),
+ ]
+
+ _oid_pair = ('field_type', 'parameters')
+ _oid_specs = {
+ 'prime_field': Integer,
+ 'characteristic_two_field': CharacteristicTwo,
+ }
+
+
+class Curve(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 104
+ """
+
+ _fields = [
+ ('a', OctetString),
+ ('b', OctetString),
+ ('seed', OctetBitString, {'optional': True}),
+ ]
+
+
+class SpecifiedECDomain(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 103
+ """
+
+ _fields = [
+ ('version', SpecifiedECDomainVersion),
+ ('field_id', FieldID),
+ ('curve', Curve),
+ ('base', ECPoint),
+ ('order', Integer),
+ ('cofactor', Integer, {'optional': True}),
+ ('hash', DigestAlgorithm, {'optional': True}),
+ ]
+
+
+class NamedCurve(ObjectIdentifier):
+ """
+ Various named curves
+
+ Original Name: None
+ Source: https://tools.ietf.org/html/rfc3279#page-23,
+ https://tools.ietf.org/html/rfc5480#page-5
+ """
+
+ _map = {
+ # https://tools.ietf.org/html/rfc3279#page-23
+ '1.2.840.10045.3.0.1': 'c2pnb163v1',
+ '1.2.840.10045.3.0.2': 'c2pnb163v2',
+ '1.2.840.10045.3.0.3': 'c2pnb163v3',
+ '1.2.840.10045.3.0.4': 'c2pnb176w1',
+ '1.2.840.10045.3.0.5': 'c2tnb191v1',
+ '1.2.840.10045.3.0.6': 'c2tnb191v2',
+ '1.2.840.10045.3.0.7': 'c2tnb191v3',
+ '1.2.840.10045.3.0.8': 'c2onb191v4',
+ '1.2.840.10045.3.0.9': 'c2onb191v5',
+ '1.2.840.10045.3.0.10': 'c2pnb208w1',
+ '1.2.840.10045.3.0.11': 'c2tnb239v1',
+ '1.2.840.10045.3.0.12': 'c2tnb239v2',
+ '1.2.840.10045.3.0.13': 'c2tnb239v3',
+ '1.2.840.10045.3.0.14': 'c2onb239v4',
+ '1.2.840.10045.3.0.15': 'c2onb239v5',
+ '1.2.840.10045.3.0.16': 'c2pnb272w1',
+ '1.2.840.10045.3.0.17': 'c2pnb304w1',
+ '1.2.840.10045.3.0.18': 'c2tnb359v1',
+ '1.2.840.10045.3.0.19': 'c2pnb368w1',
+ '1.2.840.10045.3.0.20': 'c2tnb431r1',
+ '1.2.840.10045.3.1.2': 'prime192v2',
+ '1.2.840.10045.3.1.3': 'prime192v3',
+ '1.2.840.10045.3.1.4': 'prime239v1',
+ '1.2.840.10045.3.1.5': 'prime239v2',
+ '1.2.840.10045.3.1.6': 'prime239v3',
+ # https://tools.ietf.org/html/rfc5480#page-5
+ '1.3.132.0.1': 'sect163k1',
+ '1.3.132.0.15': 'sect163r2',
+ '1.2.840.10045.3.1.1': 'secp192r1',
+ '1.3.132.0.33': 'secp224r1',
+ '1.3.132.0.26': 'sect233k1',
+ '1.2.840.10045.3.1.7': 'secp256r1',
+ '1.3.132.0.27': 'sect233r1',
+ '1.3.132.0.16': 'sect283k1',
+ '1.3.132.0.17': 'sect283r1',
+ '1.3.132.0.34': 'secp384r1',
+ '1.3.132.0.36': 'sect409k1',
+ '1.3.132.0.37': 'sect409r1',
+ '1.3.132.0.35': 'secp521r1',
+ '1.3.132.0.38': 'sect571k1',
+ '1.3.132.0.39': 'sect571r1',
+ }
+
+
+class ECDomainParameters(Choice):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 102
+ """
+
+ _alternatives = [
+ ('specified', SpecifiedECDomain),
+ ('named', NamedCurve),
+ ('implicit_ca', Null),
+ ]
+
+
+class ECPrivateKeyVersion(Integer):
+ """
+ Original Name: None
+ Source: http://www.secg.org/sec1-v2.pdf page 108
+ """
+
+ _map = {
+ 1: 'ecPrivkeyVer1',
+ }
+
+
+class ECPrivateKey(Sequence):
+ """
+ Source: http://www.secg.org/sec1-v2.pdf page 108
+ """
+
+ _fields = [
+ ('version', ECPrivateKeyVersion),
+ ('private_key', IntegerOctetString),
+ ('parameters', ECDomainParameters, {'explicit': 0, 'optional': True}),
+ ('public_key', ECPointBitString, {'explicit': 1, 'optional': True}),
+ ]
+
+
+class DSAParams(Sequence):
+ """
+ Parameters for a DSA public or private key
+
+ Original Name: Dss-Parms
+ Source: https://tools.ietf.org/html/rfc3279#page-9
+ """
+
+ _fields = [
+ ('p', Integer),
+ ('q', Integer),
+ ('g', Integer),
+ ]
+
+
+class Attribute(Sequence):
+ """
+ Source: https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-X.501-198811-S!!PDF-E&type=items page 8
+ """
+
+ _fields = [
+ ('type', ObjectIdentifier),
+ ('values', SetOf, {'spec': Any}),
+ ]
+
+
+class Attributes(SetOf):
+ """
+ Source: https://tools.ietf.org/html/rfc5208#page-3
+ """
+
+ _child_spec = Attribute
+
+
+class PrivateKeyAlgorithmId(ObjectIdentifier):
+ """
+ These OIDs for various public keys are reused when storing private keys
+ inside of a PKCS#8 structure
+
+ Original Name: None
+ Source: https://tools.ietf.org/html/rfc3279
+ """
+
+ _map = {
+ # https://tools.ietf.org/html/rfc3279#page-19
+ '1.2.840.113549.1.1.1': 'rsa',
+ # https://tools.ietf.org/html/rfc3279#page-18
+ '1.2.840.10040.4.1': 'dsa',
+ # https://tools.ietf.org/html/rfc3279#page-13
+ '1.2.840.10045.2.1': 'ec',
+ }
+
+
+class PrivateKeyAlgorithm(_ForceNullParameters, Sequence):
+ """
+ Original Name: PrivateKeyAlgorithmIdentifier
+ Source: https://tools.ietf.org/html/rfc5208#page-3
+ """
+
+ _fields = [
+ ('algorithm', PrivateKeyAlgorithmId),
+ ('parameters', Any, {'optional': True}),
+ ]
+
+ _oid_pair = ('algorithm', 'parameters')
+ _oid_specs = {
+ 'dsa': DSAParams,
+ 'ec': ECDomainParameters,
+ }
+
+
+class PrivateKeyInfo(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc5208#page-3
+ """
+
+ _fields = [
+ ('version', Integer),
+ ('private_key_algorithm', PrivateKeyAlgorithm),
+ ('private_key', ParsableOctetString),
+ ('attributes', Attributes, {'implicit': 0, 'optional': True}),
+ ]
+
+ def _private_key_spec(self):
+ algorithm = self['private_key_algorithm']['algorithm'].native
+ return {
+ 'rsa': RSAPrivateKey,
+ 'dsa': Integer,
+ 'ec': ECPrivateKey,
+ }[algorithm]
+
+ _spec_callbacks = {
+ 'private_key': _private_key_spec
+ }
+
+ _algorithm = None
+ _bit_size = None
+ _public_key = None
+ _fingerprint = None
+
+ @classmethod
+ def wrap(cls, private_key, algorithm):
+ """
+ Wraps a private key in a PrivateKeyInfo structure
+
+ :param private_key:
+ A byte string or Asn1Value object of the private key
+
+ :param algorithm:
+ A unicode string of "rsa", "dsa" or "ec"
+
+ :return:
+ A PrivateKeyInfo object
+ """
+
+ if not isinstance(private_key, byte_cls) and not isinstance(private_key, Asn1Value):
+ raise TypeError(unwrap(
+ '''
+ private_key must be a byte string or Asn1Value, not %s
+ ''',
+ type_name(private_key)
+ ))
+
+ if algorithm == 'rsa':
+ if not isinstance(private_key, RSAPrivateKey):
+ private_key = RSAPrivateKey.load(private_key)
+ params = Null()
+ elif algorithm == 'dsa':
+ if not isinstance(private_key, DSAPrivateKey):
+ private_key = DSAPrivateKey.load(private_key)
+ params = DSAParams()
+ params['p'] = private_key['p']
+ params['q'] = private_key['q']
+ params['g'] = private_key['g']
+ public_key = private_key['public_key']
+ private_key = private_key['private_key']
+ elif algorithm == 'ec':
+ if not isinstance(private_key, ECPrivateKey):
+ private_key = ECPrivateKey.load(private_key)
+ else:
+ private_key = private_key.copy()
+ params = private_key['parameters']
+ del private_key['parameters']
+ else:
+ raise ValueError(unwrap(
+ '''
+ algorithm must be one of "rsa", "dsa", "ec", not %s
+ ''',
+ repr(algorithm)
+ ))
+
+ private_key_algo = PrivateKeyAlgorithm()
+ private_key_algo['algorithm'] = PrivateKeyAlgorithmId(algorithm)
+ private_key_algo['parameters'] = params
+
+ container = cls()
+ container._algorithm = algorithm
+ container['version'] = Integer(0)
+ container['private_key_algorithm'] = private_key_algo
+ container['private_key'] = private_key
+
+ # Here we save the DSA public key if possible since it is not contained
+ # within the PKCS#8 structure for a DSA key
+ if algorithm == 'dsa':
+ container._public_key = public_key
+
+ return container
+
+ def _compute_public_key(self):
+ """
+ Computes the public key corresponding to the current private key.
+
+ :return:
+ For RSA keys, an RSAPublicKey object. For DSA keys, an Integer
+ object. For EC keys, an ECPointBitString.
+ """
+
+ if self.algorithm == 'dsa':
+ params = self['private_key_algorithm']['parameters']
+ return Integer(pow(
+ params['g'].native,
+ self['private_key'].parsed.native,
+ params['p'].native
+ ))
+
+ if self.algorithm == 'rsa':
+ key = self['private_key'].parsed
+ return RSAPublicKey({
+ 'modulus': key['modulus'],
+ 'public_exponent': key['public_exponent'],
+ })
+
+ if self.algorithm == 'ec':
+ curve_type, details = self.curve
+
+ if curve_type == 'implicit_ca':
+ raise ValueError(unwrap(
+ '''
+ Unable to compute public key for EC key using Implicit CA
+ parameters
+ '''
+ ))
+
+ if curve_type == 'specified':
+ if details['field_id']['field_type'] == 'characteristic_two_field':
+ raise ValueError(unwrap(
+ '''
+ Unable to compute public key for EC key over a
+ characteristic two field
+ '''
+ ))
+
+ curve = PrimeCurve(
+ details['field_id']['parameters'],
+ int_from_bytes(details['curve']['a']),
+ int_from_bytes(details['curve']['b'])
+ )
+ base_x, base_y = self['private_key_algorithm']['parameters'].chosen['base'].to_coords()
+ base_point = PrimePoint(curve, base_x, base_y)
+
+ elif curve_type == 'named':
+ if details not in ('secp192r1', 'secp224r1', 'secp256r1', 'secp384r1', 'secp521r1'):
+ raise ValueError(unwrap(
+ '''
+ Unable to compute public key for EC named curve %s,
+ parameters not currently included
+ ''',
+ details
+ ))
+
+ base_point = {
+ 'secp192r1': SECP192R1_BASE_POINT,
+ 'secp224r1': SECP224R1_BASE_POINT,
+ 'secp256r1': SECP256R1_BASE_POINT,
+ 'secp384r1': SECP384R1_BASE_POINT,
+ 'secp521r1': SECP521R1_BASE_POINT,
+ }[details]
+
+ public_point = base_point * self['private_key'].parsed['private_key'].native
+ return ECPointBitString.from_coords(public_point.x, public_point.y)
+
+ def unwrap(self):
+ """
+ Unwraps the private key into an RSAPrivateKey, DSAPrivateKey or
+ ECPrivateKey object
+
+ :return:
+ An RSAPrivateKey, DSAPrivateKey or ECPrivateKey object
+ """
+
+ if self.algorithm == 'rsa':
+ return self['private_key'].parsed
+
+ if self.algorithm == 'dsa':
+ params = self['private_key_algorithm']['parameters']
+ return DSAPrivateKey({
+ 'version': 0,
+ 'p': params['p'],
+ 'q': params['q'],
+ 'g': params['g'],
+ 'public_key': self.public_key,
+ 'private_key': self['private_key'].parsed,
+ })
+
+ if self.algorithm == 'ec':
+ output = self['private_key'].parsed
+ output['parameters'] = self['private_key_algorithm']['parameters']
+ output['public_key'] = self.public_key
+ return output
+
+ @property
+ def curve(self):
+ """
+ Returns information about the curve used for an EC key
+
+ :raises:
+ ValueError - when the key is not an EC key
+
+ :return:
+ A two-element tuple, with the first element being a unicode string
+ of "implicit_ca", "specified" or "named". If the first element is
+ "implicit_ca", the second is None. If "specified", the second is
+ an OrderedDict that is the native version of SpecifiedECDomain. If
+ "named", the second is a unicode string of the curve name.
+ """
+
+ if self.algorithm != 'ec':
+ raise ValueError(unwrap(
+ '''
+ Only EC keys have a curve, this key is %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ params = self['private_key_algorithm']['parameters']
+ chosen = params.chosen
+
+ if params.name == 'implicit_ca':
+ value = None
+ else:
+ value = chosen.native
+
+ return (params.name, value)
+
+ @property
+ def hash_algo(self):
+ """
+ Returns the name of the family of hash algorithms used to generate a
+ DSA key
+
+ :raises:
+ ValueError - when the key is not a DSA key
+
+ :return:
+ A unicode string of "sha1" or "sha2"
+ """
+
+ if self.algorithm != 'dsa':
+ raise ValueError(unwrap(
+ '''
+ Only DSA keys are generated using a hash algorithm, this key is
+ %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ byte_len = math.log(self['private_key_algorithm']['parameters']['q'].native, 2) / 8
+
+ return 'sha1' if byte_len <= 20 else 'sha2'
+
+ @property
+ def algorithm(self):
+ """
+ :return:
+ A unicode string of "rsa", "dsa" or "ec"
+ """
+
+ if self._algorithm is None:
+ self._algorithm = self['private_key_algorithm']['algorithm'].native
+ return self._algorithm
+
+ @property
+ def bit_size(self):
+ """
+ :return:
+ The bit size of the private key, as an integer
+ """
+
+ if self._bit_size is None:
+ if self.algorithm == 'rsa':
+ prime = self['private_key'].parsed['modulus'].native
+ elif self.algorithm == 'dsa':
+ prime = self['private_key_algorithm']['parameters']['p'].native
+ elif self.algorithm == 'ec':
+ prime = self['private_key'].parsed['private_key'].native
+ self._bit_size = int(math.ceil(math.log(prime, 2)))
+ modulus = self._bit_size % 8
+ if modulus != 0:
+ self._bit_size += 8 - modulus
+ return self._bit_size
+
+ @property
+ def byte_size(self):
+ """
+ :return:
+ The byte size of the private key, as an integer
+ """
+
+ return int(math.ceil(self.bit_size / 8))
+
+ @property
+ def public_key(self):
+ """
+ :return:
+ If an RSA key, an RSAPublicKey object. If a DSA key, an Integer
+ object. If an EC key, an ECPointBitString object.
+ """
+
+ if self._public_key is None:
+ if self.algorithm == 'ec':
+ key = self['private_key'].parsed
+ if key['public_key']:
+ self._public_key = key['public_key'].untag()
+ else:
+ self._public_key = self._compute_public_key()
+ else:
+ self._public_key = self._compute_public_key()
+
+ return self._public_key
+
+ @property
+ def public_key_info(self):
+ """
+ :return:
+ A PublicKeyInfo object derived from this private key.
+ """
+
+ return PublicKeyInfo({
+ 'algorithm': {
+ 'algorithm': self.algorithm,
+ 'parameters': self['private_key_algorithm']['parameters']
+ },
+ 'public_key': self.public_key
+ })
+
+ @property
+ def fingerprint(self):
+ """
+ Creates a fingerprint that can be compared with a public key to see if
+ the two form a pair.
+
+ This fingerprint is not compatible with fingerprints generated by any
+ other software.
+
+ :return:
+ A byte string that is a sha256 hash of selected components (based
+ on the key type)
+ """
+
+ if self._fingerprint is None:
+ params = self['private_key_algorithm']['parameters']
+ key = self['private_key'].parsed
+
+ if self.algorithm == 'rsa':
+ to_hash = '%d:%d' % (
+ key['modulus'].native,
+ key['public_exponent'].native,
+ )
+
+ elif self.algorithm == 'dsa':
+ public_key = self.public_key
+ to_hash = '%d:%d:%d:%d' % (
+ params['p'].native,
+ params['q'].native,
+ params['g'].native,
+ public_key.native,
+ )
+
+ elif self.algorithm == 'ec':
+ public_key = key['public_key'].native
+ if public_key is None:
+ public_key = self.public_key.native
+
+ if params.name == 'named':
+ to_hash = '%s:' % params.chosen.native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += public_key
+
+ elif params.name == 'implicit_ca':
+ to_hash = public_key
+
+ elif params.name == 'specified':
+ to_hash = '%s:' % params.chosen['field_id']['parameters'].native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += b':' + params.chosen['curve']['a'].native
+ to_hash += b':' + params.chosen['curve']['b'].native
+ to_hash += public_key
+
+ if isinstance(to_hash, str_cls):
+ to_hash = to_hash.encode('utf-8')
+
+ self._fingerprint = hashlib.sha256(to_hash).digest()
+
+ return self._fingerprint
+
+
+class EncryptedPrivateKeyInfo(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc5208#page-4
+ """
+
+ _fields = [
+ ('encryption_algorithm', EncryptionAlgorithm),
+ ('encrypted_data', OctetString),
+ ]
+
+
+# These structures are from https://tools.ietf.org/html/rfc3279
+
+class ValidationParms(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3279#page-10
+ """
+
+ _fields = [
+ ('seed', BitString),
+ ('pgen_counter', Integer),
+ ]
+
+
+class DomainParameters(Sequence):
+ """
+ Source: https://tools.ietf.org/html/rfc3279#page-10
+ """
+
+ _fields = [
+ ('p', Integer),
+ ('g', Integer),
+ ('q', Integer),
+ ('j', Integer, {'optional': True}),
+ ('validation_params', ValidationParms, {'optional': True}),
+ ]
+
+
+class PublicKeyAlgorithmId(ObjectIdentifier):
+ """
+ Original Name: None
+ Source: https://tools.ietf.org/html/rfc3279
+ """
+
+ _map = {
+ # https://tools.ietf.org/html/rfc3279#page-19
+ '1.2.840.113549.1.1.1': 'rsa',
+ # https://tools.ietf.org/html/rfc3447#page-47
+ '1.2.840.113549.1.1.7': 'rsaes_oaep',
+ # https://tools.ietf.org/html/rfc3279#page-18
+ '1.2.840.10040.4.1': 'dsa',
+ # https://tools.ietf.org/html/rfc3279#page-13
+ '1.2.840.10045.2.1': 'ec',
+ # https://tools.ietf.org/html/rfc3279#page-10
+ '1.2.840.10046.2.1': 'dh',
+ }
+
+
+class PublicKeyAlgorithm(_ForceNullParameters, Sequence):
+ """
+ Original Name: AlgorithmIdentifier
+ Source: https://tools.ietf.org/html/rfc5280#page-18
+ """
+
+ _fields = [
+ ('algorithm', PublicKeyAlgorithmId),
+ ('parameters', Any, {'optional': True}),
+ ]
+
+ _oid_pair = ('algorithm', 'parameters')
+ _oid_specs = {
+ 'dsa': DSAParams,
+ 'ec': ECDomainParameters,
+ 'dh': DomainParameters,
+ 'rsaes_oaep': RSAESOAEPParams,
+ }
+
+
+class PublicKeyInfo(Sequence):
+ """
+ Original Name: SubjectPublicKeyInfo
+ Source: https://tools.ietf.org/html/rfc5280#page-17
+ """
+
+ _fields = [
+ ('algorithm', PublicKeyAlgorithm),
+ ('public_key', ParsableOctetBitString),
+ ]
+
+ def _public_key_spec(self):
+ algorithm = self['algorithm']['algorithm'].native
+ return {
+ 'rsa': RSAPublicKey,
+ 'rsaes_oaep': RSAPublicKey,
+ 'dsa': Integer,
+ # We override the field spec with ECPoint so that users can easily
+ # decompose the byte string into the constituent X and Y coords
+ 'ec': (ECPointBitString, None),
+ 'dh': Integer,
+ }[algorithm]
+
+ _spec_callbacks = {
+ 'public_key': _public_key_spec
+ }
+
+ _algorithm = None
+ _bit_size = None
+ _fingerprint = None
+ _sha1 = None
+ _sha256 = None
+
+ @classmethod
+ def wrap(cls, public_key, algorithm):
+ """
+ Wraps a public key in a PublicKeyInfo structure
+
+ :param public_key:
+ A byte string or Asn1Value object of the public key
+
+ :param algorithm:
+ A unicode string of "rsa"
+
+ :return:
+ A PublicKeyInfo object
+ """
+
+ if not isinstance(public_key, byte_cls) and not isinstance(public_key, Asn1Value):
+ raise TypeError(unwrap(
+ '''
+ public_key must be a byte string or Asn1Value, not %s
+ ''',
+ type_name(public_key)
+ ))
+
+ if algorithm != 'rsa':
+ raise ValueError(unwrap(
+ '''
+ algorithm must "rsa", not %s
+ ''',
+ repr(algorithm)
+ ))
+
+ algo = PublicKeyAlgorithm()
+ algo['algorithm'] = PublicKeyAlgorithmId(algorithm)
+ algo['parameters'] = Null()
+
+ container = cls()
+ container['algorithm'] = algo
+ if isinstance(public_key, Asn1Value):
+ public_key = public_key.untag().dump()
+ container['public_key'] = ParsableOctetBitString(public_key)
+
+ return container
+
+ def unwrap(self):
+ """
+ Unwraps an RSA public key into an RSAPublicKey object. Does not support
+ DSA or EC public keys since they do not have an unwrapped form.
+
+ :return:
+ An RSAPublicKey object
+ """
+
+ if self.algorithm == 'rsa':
+ return self['public_key'].parsed
+
+ key_type = self.algorithm.upper()
+ a_an = 'an' if key_type == 'EC' else 'a'
+ raise ValueError(unwrap(
+ '''
+ Only RSA public keys may be unwrapped - this key is %s %s public
+ key
+ ''',
+ a_an,
+ key_type
+ ))
+
+ @property
+ def curve(self):
+ """
+ Returns information about the curve used for an EC key
+
+ :raises:
+ ValueError - when the key is not an EC key
+
+ :return:
+ A two-element tuple, with the first element being a unicode string
+ of "implicit_ca", "specified" or "named". If the first element is
+ "implicit_ca", the second is None. If "specified", the second is
+ an OrderedDict that is the native version of SpecifiedECDomain. If
+ "named", the second is a unicode string of the curve name.
+ """
+
+ if self.algorithm != 'ec':
+ raise ValueError(unwrap(
+ '''
+ Only EC keys have a curve, this key is %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ params = self['algorithm']['parameters']
+ chosen = params.chosen
+
+ if params.name == 'implicit_ca':
+ value = None
+ else:
+ value = chosen.native
+
+ return (params.name, value)
+
+ @property
+ def hash_algo(self):
+ """
+ Returns the name of the family of hash algorithms used to generate a
+ DSA key
+
+ :raises:
+ ValueError - when the key is not a DSA key
+
+ :return:
+ A unicode string of "sha1" or "sha2" or None if no parameters are
+ present
+ """
+
+ if self.algorithm != 'dsa':
+ raise ValueError(unwrap(
+ '''
+ Only DSA keys are generated using a hash algorithm, this key is
+ %s
+ ''',
+ self.algorithm.upper()
+ ))
+
+ parameters = self['algorithm']['parameters']
+ if parameters.native is None:
+ return None
+
+ byte_len = math.log(parameters['q'].native, 2) / 8
+
+ return 'sha1' if byte_len <= 20 else 'sha2'
+
+ @property
+ def algorithm(self):
+ """
+ :return:
+ A unicode string of "rsa", "dsa" or "ec"
+ """
+
+ if self._algorithm is None:
+ self._algorithm = self['algorithm']['algorithm'].native
+ return self._algorithm
+
+ @property
+ def bit_size(self):
+ """
+ :return:
+ The bit size of the public key, as an integer
+ """
+
+ if self._bit_size is None:
+ if self.algorithm == 'ec':
+ self._bit_size = ((len(self['public_key'].native) - 1) / 2) * 8
+ else:
+ if self.algorithm == 'rsa':
+ prime = self['public_key'].parsed['modulus'].native
+ elif self.algorithm == 'dsa':
+ prime = self['algorithm']['parameters']['p'].native
+ self._bit_size = int(math.ceil(math.log(prime, 2)))
+ modulus = self._bit_size % 8
+ if modulus != 0:
+ self._bit_size += 8 - modulus
+
+ return self._bit_size
+
+ @property
+ def byte_size(self):
+ """
+ :return:
+ The byte size of the public key, as an integer
+ """
+
+ return int(math.ceil(self.bit_size / 8))
+
+ @property
+ def sha1(self):
+ """
+ :return:
+ The SHA1 hash of the DER-encoded bytes of this public key info
+ """
+
+ if self._sha1 is None:
+ self._sha1 = hashlib.sha1(byte_cls(self['public_key'])).digest()
+ return self._sha1
+
+ @property
+ def sha256(self):
+ """
+ :return:
+ The SHA-256 hash of the DER-encoded bytes of this public key info
+ """
+
+ if self._sha256 is None:
+ self._sha256 = hashlib.sha256(byte_cls(self['public_key'])).digest()
+ return self._sha256
+
+ @property
+ def fingerprint(self):
+ """
+ Creates a fingerprint that can be compared with a private key to see if
+ the two form a pair.
+
+ This fingerprint is not compatible with fingerprints generated by any
+ other software.
+
+ :return:
+ A byte string that is a sha256 hash of selected components (based
+ on the key type)
+ """
+
+ if self._fingerprint is None:
+ key_type = self['algorithm']['algorithm'].native
+ params = self['algorithm']['parameters']
+
+ if key_type == 'rsa':
+ key = self['public_key'].parsed
+ to_hash = '%d:%d' % (
+ key['modulus'].native,
+ key['public_exponent'].native,
+ )
+
+ elif key_type == 'dsa':
+ key = self['public_key'].parsed
+ to_hash = '%d:%d:%d:%d' % (
+ params['p'].native,
+ params['q'].native,
+ params['g'].native,
+ key.native,
+ )
+
+ elif key_type == 'ec':
+ key = self['public_key']
+
+ if params.name == 'named':
+ to_hash = '%s:' % params.chosen.native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += key.native
+
+ elif params.name == 'implicit_ca':
+ to_hash = key.native
+
+ elif params.name == 'specified':
+ to_hash = '%s:' % params.chosen['field_id']['parameters'].native
+ to_hash = to_hash.encode('utf-8')
+ to_hash += b':' + params.chosen['curve']['a'].native
+ to_hash += b':' + params.chosen['curve']['b'].native
+ to_hash += key.native
+
+ if isinstance(to_hash, str_cls):
+ to_hash = to_hash.encode('utf-8')
+
+ self._fingerprint = hashlib.sha256(to_hash).digest()
+
+ return self._fingerprint
diff --git a/asn1crypto/ocsp.py b/asn1crypto/ocsp.py
new file mode 100644
index 0000000..f18d8e8
--- /dev/null
+++ b/asn1crypto/ocsp.py
@@ -0,0 +1,652 @@
+# 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, division, absolute_import, print_function
+
+from .algos import DigestAlgorithm, SignedDigestAlgorithm
+from .core import (
+ Boolean,
+ Choice,
+ Enumerated,
+ GeneralizedTime,
+ IA5String,
+ Integer,
+ Null,
+ ObjectIdentifier,
+ OctetBitString,
+ OctetString,
+ ParsableOctetString,
+ 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 Version(Integer):
+ _map = {
+ 0: 'v1'
+ }
+
+
+class CertId(Sequence):
+ _fields = [
+ ('hash_algorithm', DigestAlgorithm),
+ ('issuer_name_hash', OctetString),
+ ('issuer_key_hash', OctetString),
+ ('serial_number', Integer),
+ ]
+
+
+class ServiceLocator(Sequence):
+ _fields = [
+ ('issuer', Name),
+ ('locator', AuthorityInfoAccessSyntax),
+ ]
+
+
+class RequestExtensionId(ObjectIdentifier):
+ _map = {
+ '1.3.6.1.5.5.7.48.1.7': 'service_locator',
+ }
+
+
+class RequestExtension(Sequence):
+ _fields = [
+ ('extn_id', RequestExtensionId),
+ ('critical', Boolean, {'default': False}),
+ ('extn_value', ParsableOctetString),
+ ]
+
+ _oid_pair = ('extn_id', 'extn_value')
+ _oid_specs = {
+ 'service_locator': ServiceLocator,
+ }
+
+
+class RequestExtensions(SequenceOf):
+ _child_spec = RequestExtension
+
+
+class Request(Sequence):
+ _fields = [
+ ('req_cert', CertId),
+ ('single_request_extensions', RequestExtensions, {'explicit': 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 = set()
+
+ 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.add(name)
+
+ self._processed_extensions = True
+
+ @property
+ def critical_extensions(self):
+ """
+ Returns a set of the names (or OID if not a known extension) of the
+ extensions marked as critical
+
+ :return:
+ A set 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._set_extensions()
+ return self._service_locator_value
+
+
+class Requests(SequenceOf):
+ _child_spec = Request
+
+
+class ResponseType(ObjectIdentifier):
+ _map = {
+ '1.3.6.1.5.5.7.48.1.1': 'basic_ocsp_response',
+ }
+
+
+class AcceptableResponses(SequenceOf):
+ _child_spec = ResponseType
+
+
+class PreferredSignatureAlgorithm(Sequence):
+ _fields = [
+ ('sig_identifier', SignedDigestAlgorithm),
+ ('cert_identifier', PublicKeyAlgorithm, {'optional': True}),
+ ]
+
+
+class PreferredSignatureAlgorithms(SequenceOf):
+ _child_spec = PreferredSignatureAlgorithm
+
+
+class TBSRequestExtensionId(ObjectIdentifier):
+ _map = {
+ '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',
+ }
+
+
+class TBSRequestExtension(Sequence):
+ _fields = [
+ ('extn_id', TBSRequestExtensionId),
+ ('critical', Boolean, {'default': False}),
+ ('extn_value', ParsableOctetString),
+ ]
+
+ _oid_pair = ('extn_id', 'extn_value')
+ _oid_specs = {
+ 'nonce': OctetString,
+ 'acceptable_responses': AcceptableResponses,
+ 'preferred_signature_algorithms': PreferredSignatureAlgorithms,
+ }
+
+
+class TBSRequestExtensions(SequenceOf):
+ _child_spec = TBSRequestExtension
+
+
+class TBSRequest(Sequence):
+ _fields = [
+ ('version', Version, {'explicit': 0, 'default': 'v1'}),
+ ('requestor_name', GeneralName, {'explicit': 1, 'optional': True}),
+ ('request_list', Requests),
+ ('request_extensions', TBSRequestExtensions, {'explicit': 2, 'optional': True}),
+ ]
+
+
+class Certificates(SequenceOf):
+ _child_spec = Certificate
+
+
+class Signature(Sequence):
+ _fields = [
+ ('signature_algorithm', SignedDigestAlgorithm),
+ ('signature', OctetBitString),
+ ('certs', Certificates, {'explicit': 0, 'optional': True}),
+ ]
+
+
+class OCSPRequest(Sequence):
+ _fields = [
+ ('tbs_request', TBSRequest),
+ ('optional_signature', Signature, {'explicit': 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 = set()
+
+ 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.add(name)
+
+ self._processed_extensions = True
+
+ @property
+ def critical_extensions(self):
+ """
+ Returns a set of the names (or OID if not a known extension) of the
+ extensions marked as critical
+
+ :return:
+ A set 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._set_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._set_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._set_extensions()
+ return self._preferred_signature_algorithms_value
+
+
+class OCSPResponseStatus(Enumerated):
+ _map = {
+ 0: 'successful',
+ 1: 'malformed_request',
+ 2: 'internal_error',
+ 3: 'try_later',
+ 5: 'sign_required',
+ 6: 'unauthorized',
+ }
+
+
+class ResponderId(Choice):
+ _alternatives = [
+ ('by_name', Name, {'explicit': 1}),
+ ('by_key', OctetString, {'explicit': 2}),
+ ]
+
+
+class RevokedInfo(Sequence):
+ _fields = [
+ ('revocation_time', GeneralizedTime),
+ ('revocation_reason', CRLReason, {'explicit': 0, 'optional': True}),
+ ]
+
+
+class CertStatus(Choice):
+ _alternatives = [
+ ('good', Null, {'implicit': 0}),
+ ('revoked', RevokedInfo, {'implicit': 1}),
+ ('unknown', Null, {'implicit': 2}),
+ ]
+
+
+class CrlId(Sequence):
+ _fields = [
+ ('crl_url', IA5String, {'explicit': 0, 'optional': True}),
+ ('crl_num', Integer, {'explicit': 1, 'optional': True}),
+ ('crl_time', GeneralizedTime, {'explicit': 2, 'optional': True}),
+ ]
+
+
+class SingleResponseExtensionId(ObjectIdentifier):
+ _map = {
+ '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',
+ # https://tools.ietf.org/html/rfc6962.html#page-13
+ '1.3.6.1.4.1.11129.2.4.5': 'signed_certificate_timestamp_list',
+ }
+
+
+class SingleResponseExtension(Sequence):
+ _fields = [
+ ('extn_id', SingleResponseExtensionId),
+ ('critical', Boolean, {'default': False}),
+ ('extn_value', ParsableOctetString),
+ ]
+
+ _oid_pair = ('extn_id', 'extn_value')
+ _oid_specs = {
+ 'crl': CrlId,
+ 'archive_cutoff': GeneralizedTime,
+ 'crl_reason': CRLReason,
+ 'invalidity_date': GeneralizedTime,
+ 'certificate_issuer': GeneralNames,
+ 'signed_certificate_timestamp_list': OctetString,
+ }
+
+
+class SingleResponseExtensions(SequenceOf):
+ _child_spec = SingleResponseExtension
+
+
+class SingleResponse(Sequence):
+ _fields = [
+ ('cert_id', CertId),
+ ('cert_status', CertStatus),
+ ('this_update', GeneralizedTime),
+ ('next_update', GeneralizedTime, {'explicit': 0, 'optional': True}),
+ ('single_extensions', SingleResponseExtensions, {'explicit': 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 = set()
+
+ 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.add(name)
+
+ self._processed_extensions = True
+
+ @property
+ def critical_extensions(self):
+ """
+ Returns a set of the names (or OID if not a known extension) of the
+ extensions marked as critical
+
+ :return:
+ A set 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._set_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._set_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._set_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._set_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._set_extensions()
+ return self._certificate_issuer_value
+
+
+class Responses(SequenceOf):
+ _child_spec = SingleResponse
+
+
+class ResponseDataExtensionId(ObjectIdentifier):
+ _map = {
+ '1.3.6.1.5.5.7.48.1.2': 'nonce',
+ '1.3.6.1.5.5.7.48.1.9': 'extended_revoke',
+ }
+
+
+class ResponseDataExtension(Sequence):
+ _fields = [
+ ('extn_id', ResponseDataExtensionId),
+ ('critical', Boolean, {'default': False}),
+ ('extn_value', ParsableOctetString),
+ ]
+
+ _oid_pair = ('extn_id', 'extn_value')
+ _oid_specs = {
+ 'nonce': OctetString,
+ 'extended_revoke': Null,
+ }
+
+
+class ResponseDataExtensions(SequenceOf):
+ _child_spec = ResponseDataExtension
+
+
+class ResponseData(Sequence):
+ _fields = [
+ ('version', Version, {'explicit': 0, 'default': 'v1'}),
+ ('responder_id', ResponderId),
+ ('produced_at', GeneralizedTime),
+ ('responses', Responses),
+ ('response_extensions', ResponseDataExtensions, {'explicit': 1, 'optional': True}),
+ ]
+
+
+class BasicOCSPResponse(Sequence):
+ _fields = [
+ ('tbs_response_data', ResponseData),
+ ('signature_algorithm', SignedDigestAlgorithm),
+ ('signature', OctetBitString),
+ ('certs', Certificates, {'explicit': 0, 'optional': True}),
+ ]
+
+
+class ResponseBytes(Sequence):
+ _fields = [
+ ('response_type', ResponseType),
+ ('response', ParsableOctetString),
+ ]
+
+ _oid_pair = ('response_type', 'response')
+ _oid_specs = {
+ 'basic_ocsp_response': BasicOCSPResponse,
+ }
+
+
+class OCSPResponse(Sequence):
+ _fields = [
+ ('response_status', OCSPResponseStatus),
+ ('response_bytes', ResponseBytes, {'explicit': 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 = set()
+
+ 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.add(name)
+
+ self._processed_extensions = True
+
+ @property
+ def critical_extensions(self):
+ """
+ Returns a set of the names (or OID if not a known extension) of the
+ extensions marked as critical
+
+ :return:
+ A set 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._set_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._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']
diff --git a/asn1crypto/parser.py b/asn1crypto/parser.py
new file mode 100644
index 0000000..07f53ab
--- /dev/null
+++ b/asn1crypto/parser.py
@@ -0,0 +1,289 @@
+# coding: utf-8
+
+"""
+Functions for parsing and dumping using the ASN.1 DER encoding. Exports the
+following items:
+
+ - emit()
+ - parse()
+ - peek()
+
+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
+
+ :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)
+ - 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 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
+
+ :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 < 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]
+ 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
+ # Unfortunately we need to understand the contents of the data to
+ # properly scan forward, which bleeds some representation info into
+ # the parser. This condition handles the unused bits byte in
+ # constructed bit strings.
+ if tag == 3:
+ contents_end += 1
+ while contents_end < data_len:
+ sub_header_end, contents_end = _parse(encoded_data, data_len, contents_end, lengths_only=True)
+ if contents_end == sub_header_end and encoded_data[contents_end - 2:contents_end] == b'\x00\x00':
+ 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
+
+ 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
diff --git a/asn1crypto/pdf.py b/asn1crypto/pdf.py
new file mode 100644
index 0000000..b72c886
--- /dev/null
+++ b/asn1crypto/pdf.py
@@ -0,0 +1,84 @@
+# 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, division, absolute_import, print_function
+
+from .cms import CMSAttributeType, CMSAttribute
+from .core import (
+ Boolean,
+ Integer,
+ Null,
+ ObjectIdentifier,
+ OctetString,
+ Sequence,
+ SequenceOf,
+ SetOf,
+)
+from .crl import CertificateList
+from .ocsp import OCSPResponse
+from .x509 import (
+ Extension,
+ ExtensionId,
+ GeneralName,
+ KeyPurposeId,
+)
+
+
+class AdobeArchiveRevInfo(Sequence):
+ _fields = [
+ ('version', Integer)
+ ]
+
+
+class AdobeTimestamp(Sequence):
+ _fields = [
+ ('version', Integer),
+ ('location', GeneralName),
+ ('requires_auth', Boolean, {'optional': True, 'default': False}),
+ ]
+
+
+class OtherRevInfo(Sequence):
+ _fields = [
+ ('type', ObjectIdentifier),
+ ('value', OctetString),
+ ]
+
+
+class SequenceOfCertificateList(SequenceOf):
+ _child_spec = CertificateList
+
+
+class SequenceOfOCSPResponse(SequenceOf):
+ _child_spec = OCSPResponse
+
+
+class SequenceOfOtherRevInfo(SequenceOf):
+ _child_spec = OtherRevInfo
+
+
+class RevocationInfoArchival(Sequence):
+ _fields = [
+ ('crl', SequenceOfCertificateList, {'explicit': 0, 'optional': True}),
+ ('ocsp', SequenceOfOCSPResponse, {'explicit': 1, 'optional': True}),
+ ('other_rev_info', SequenceOfOtherRevInfo, {'explicit': 2, 'optional': True}),
+ ]
+
+
+class SetOfRevocationInfoArchival(SetOf):
+ _child_spec = RevocationInfoArchival
+
+
+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
new file mode 100644
index 0000000..511ea4b
--- /dev/null
+++ b/asn1crypto/pem.py
@@ -0,0 +1,222 @@
+# coding: utf-8
+
+"""
+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
+
+import base64
+import re
+import sys
+
+from ._errors import unwrap
+from ._types import type_name as _type_name, str_cls, byte_cls
+
+if sys.version_info < (3,):
+ from cStringIO import StringIO as BytesIO
+else:
+ 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 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
+
+
+def armor(type_name, der_bytes, headers=None):
+ """
+ Armors a DER-encoded byte string in PEM
+
+ :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
+
+ :return:
+ A byte string of the PEM block
+ """
+
+ if not isinstance(der_bytes, byte_cls):
+ raise TypeError(unwrap(
+ '''
+ der_bytes must be a byte string, not %s
+ ''' % _type_name(der_bytes)
+ ))
+
+ if not isinstance(type_name, str_cls):
+ raise TypeError(unwrap(
+ '''
+ type_name must be a unicode string, not %s
+ ''',
+ _type_name(type_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: (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 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''
+ object_type = 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
+ object_type = 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 (object_type, headers, der_bytes)
+
+ state = 'trash'
+ headers = {}
+ base64_data = b''
+ object_type = None
+ found_end = True
+ continue
+
+ base64_data += line
+
+ if not found_start or not found_end:
+ raise ValueError(unwrap(
+ '''
+ 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 (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.
+ """
+
+ generator = _unarmor(pem_bytes)
+
+ if not multiple:
+ return next(generator)
+
+ return generator
diff --git a/asn1crypto/pkcs12.py b/asn1crypto/pkcs12.py
new file mode 100644
index 0000000..7ebcefe
--- /dev/null
+++ b/asn1crypto/pkcs12.py
@@ -0,0 +1,193 @@
+# 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, division, absolute_import, print_function
+
+from .algos import DigestInfo
+from .cms import ContentInfo, SignedData
+from .core import (
+ Any,
+ BMPString,
+ Integer,
+ ObjectIdentifier,
+ OctetString,
+ ParsableOctetString,
+ Sequence,
+ SequenceOf,
+ SetOf,
+)
+from .keys import PrivateKeyInfo, EncryptedPrivateKeyInfo
+from .x509 import Certificate, KeyPurposeId
+
+
+# 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 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',
+ # https://github.com/frohoff/jdk8u-dev-jdk/blob/master/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java
+ # 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',
+ }
+
+
+class SetOfAny(SetOf):
+ _child_spec = Any
+
+
+class SetOfBMPString(SetOf):
+ _child_spec = BMPString
+
+
+class SetOfOctetString(SetOf):
+ _child_spec = OctetString
+
+
+class SetOfKeyPurposeId(SetOf):
+ _child_spec = KeyPurposeId
+
+
+class Attribute(Sequence):
+ _fields = [
+ ('type', AttributeType),
+ ('values', None),
+ ]
+
+ _oid_specs = {
+ 'friendly_name': SetOfBMPString,
+ 'local_key_id': SetOfOctetString,
+ 'microsoft_csp_name': SetOfBMPString,
+ 'trusted_key_usage': SetOfKeyPurposeId,
+ }
+
+ def _values_spec(self):
+ return self._oid_specs.get(self['type'].native, SetOfAny)
+
+ _spec_callbacks = {
+ 'values': _values_spec
+ }
+
+
+class Attributes(SetOf):
+ _child_spec = Attribute
+
+
+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', ParsableOctetString, {'explicit': 0}),
+ ]
+
+ _oid_pair = ('cert_id', 'cert_value')
+ _oid_specs = {
+ 'x509': Certificate,
+ }
+
+
+class CrlBag(Sequence):
+ _fields = [
+ ('crl_id', ObjectIdentifier),
+ ('crl_value', OctetString, {'explicit': 0}),
+ ]
+
+
+class SecretBag(Sequence):
+ _fields = [
+ ('secret_type_id', ObjectIdentifier),
+ ('secret_value', OctetString, {'explicit': 0}),
+ ]
+
+
+class SafeContents(SequenceOf):
+ pass
+
+
+class SafeBag(Sequence):
+ _fields = [
+ ('bag_id', BagId),
+ ('bag_value', Any, {'explicit': 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
diff --git a/asn1crypto/tsp.py b/asn1crypto/tsp.py
new file mode 100644
index 0000000..bd40810
--- /dev/null
+++ b/asn1crypto/tsp.py
@@ -0,0 +1,310 @@
+# 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, division, absolute_import, print_function
+
+from .algos import DigestAlgorithm
+from .cms import (
+ CMSAttribute,
+ CMSAttributeType,
+ ContentInfo,
+ ContentType,
+ EncapsulatedContentInfo,
+)
+from .core import (
+ Any,
+ BitString,
+ Boolean,
+ Choice,
+ GeneralizedTime,
+ IA5String,
+ Integer,
+ ObjectIdentifier,
+ OctetString,
+ Sequence,
+ SequenceOf,
+ SetOf,
+ UTF8String,
+)
+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, {'implicit': 0, 'optional': True}),
+ ('micros', Integer, {'implicit': 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, {'explicit': 0, 'optional': True}),
+ ('extensions', Extensions, {'implicit': 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, {'implicit': 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, {'implicit': 0, 'optional': True}),
+ ('attributes', Attributes, {'implicit': 1, 'optional': True}),
+ ('reduced_hashtree', PartialHashtrees, {'implicit': 2, 'optional': True}),
+ ('time_stamp', ContentInfo),
+ ]
+
+
+class ArchiveTimeStampSequence(SequenceOf):
+ _child_spec = ArchiveTimeStamp
+
+
+class EvidenceRecord(Sequence):
+ _fields = [
+ ('version', Version),
+ ('digest_algorithms', DigestAlgorithms),
+ ('crypto_infos', Attributes, {'implicit': 0, 'optional': True}),
+ ('encryption_info', EncryptionInfo, {'implicit': 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, {'implicit': 0}),
+ ('ers_evidence', EvidenceRecord, {'implicit': 1}),
+ ('other_evidence', OtherEvidence, {'implicit': 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': {'algorithm': '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
+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
new file mode 100644
index 0000000..2e55ef8
--- /dev/null
+++ b/asn1crypto/util.py
@@ -0,0 +1,712 @@
+# coding: utf-8
+
+"""
+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
+ - inet_ntop()
+ - inet_pton()
+ - uri_to_iri()
+ - iri_to_uri()
+"""
+
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import math
+import sys
+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
+else:
+ from socket import inet_ntop, inet_pton # noqa
+
+
+# Python 2
+if sys.version_info <= (3,):
+
+ from datetime import timedelta, tzinfo
+
+ py2 = True
+
+ def int_to_bytes(value, signed=False, width=None):
+ """
+ Converts an integer to a byte string
+
+ :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
+ """
+
+ if value == b'':
+ return 0
+
+ num = long(value.encode("hex"), 16) # noqa
+
+ 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): # noqa
+
+ def tzname(self, _):
+ return b'UTC+00:00'
+
+ def utcoffset(self, _):
+ return timedelta(0)
+
+ def dst(self, _):
+ return timedelta(0)
+
+ class timezone(): # noqa
+
+ utc = utc()
+
+
+# Python 3
+else:
+
+ from datetime import timezone # noqa
+
+ py2 = False
+
+ def int_to_bytes(value, signed=False, width=None):
+ """
+ Converts an integer to a byte string
+
+ :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:
+ 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):
+ """
+ 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)
+
+
+_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')
+ # Year 0 is 1BC and a leap year. Leap years repeat themselves
+ # every 28 years. Because of adjustments and the proleptic gregorian
+ # calendar, the simplest way to format is to substitute year 2000.
+ temp = date(2000, self.month, self.day)
+ if '%c' in format:
+ c_out = temp.strftime('%c')
+ # Handle full years
+ c_out = c_out.replace('2000', '0000')
+ c_out = c_out.replace('%', '%%')
+ format = format.replace('%c', c_out)
+ if '%x' in format:
+ x_out = temp.strftime('%x')
+ # Handle formats such as 08/16/2000 or 16.08.2000
+ x_out = x_out.replace('2000', '0000')
+ x_out = x_out.replace('%', '%%')
+ format = format.replace('%x', x_out)
+ return temp.strftime(format)
+
+ def isoformat(self):
+ """
+ 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=2000))
+
+ 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=2000))
+
+ 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=2000))
+
+ 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')
+ # Year 0 is 1BC and a leap year. Leap years repeat themselves
+ # every 28 years. Because of adjustments and the proleptic gregorian
+ # calendar, the simplest way to format is to substitute year 2000.
+ temp = datetime(
+ 2000,
+ self.month,
+ self.day,
+ self.hour,
+ self.minute,
+ self.second,
+ self.microsecond,
+ self.tzinfo
+ )
+ if '%c' in format:
+ c_out = temp.strftime('%c')
+ # Handle full years
+ c_out = c_out.replace('2000', '0000')
+ c_out = c_out.replace('%', '%%')
+ format = format.replace('%c', c_out)
+ if '%x' in format:
+ x_out = temp.strftime('%x')
+ # Handle formats such as 08/16/2000 or 16.08.2000
+ x_out = x_out.replace('2000', '0000')
+ x_out = x_out.replace('%', '%%')
+ format = format.replace('%x', x_out)
+ return temp.strftime(format)
+
+ 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/asn1crypto/version.py b/asn1crypto/version.py
new file mode 100644
index 0000000..2ce2408
--- /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.24.0'
+__version_info__ = (0, 24, 0)
diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py
new file mode 100644
index 0000000..5a572a3
--- /dev/null
+++ b/asn1crypto/x509.py
@@ -0,0 +1,3002 @@
+# coding: utf-8
+
+"""
+ASN.1 type classes for X.509 certificates. Exports the following items:
+
+ - Attributes()
+ - Certificate()
+ - Extensions()
+ - GeneralName()
+ - GeneralNames()
+ - Name()
+
+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
+import socket
+import stringprep
+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, bytes_to_list
+from .algos import AlgorithmIdentifier, AnyAlgorithmIdentifier, DigestAlgorithm, SignedDigestAlgorithm
+from .core import (
+ Any,
+ BitString,
+ BMPString,
+ Boolean,
+ Choice,
+ Concat,
+ Enumerated,
+ GeneralizedTime,
+ GeneralString,
+ IA5String,
+ Integer,
+ Null,
+ NumericString,
+ ObjectIdentifier,
+ OctetBitString,
+ OctetString,
+ ParsableOctetString,
+ PrintableString,
+ Sequence,
+ SequenceOf,
+ Set,
+ SetOf,
+ TeletexString,
+ UniversalString,
+ UTCTime,
+ UTF8String,
+ VisibleString,
+ VOID,
+)
+from .keys import PublicKeyInfo
+from .util import int_to_bytes, int_from_bytes, inet_ntop, inet_pton
+
+
+# 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 DNSName(IA5String):
+
+ _encoding = 'idna'
+ _bad_tag = 19
+
+ 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.__unicode__().lower() == other.__unicode__().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._unicode = value
+ self.contents = encoded_value
+ self._header = None
+ if self._trailer != b'':
+ self._trailer = b''
+
+
+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 TypeError(unwrap(
+ '''
+ %s value must be a unicode string, not %s
+ ''',
+ type_name(self),
+ type_name(value)
+ ))
+
+ self._unicode = value
+ self.contents = iri_to_uri(value)
+ self._header = None
+ if self._trailer != b'':
+ self._trailer = b''
+
+ 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 iri_to_uri(self.native) == iri_to_uri(other.native)
+
+ def __unicode__(self):
+ """
+ :return:
+ A unicode string
+ """
+
+ 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):
+
+ _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 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)
+ encoded_value = mailbox.encode('ascii') + b'@' + hostname.encode('idna')
+ else:
+ encoded_value = value.encode('ascii')
+
+ self._normalized = True
+ self._unicode = value
+ self.contents = encoded_value
+ self._header = None
+ if self._trailer != b'':
+ self._trailer = b''
+
+ def __unicode__(self):
+ """
+ :return:
+ A unicode string
+ """
+
+ 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
+
+ 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:
+ other.set(other.native)
+
+ 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)
+ 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):
+ """
+ This method is not applicable to IP addresses
+ """
+
+ raise ValueError(unwrap(
+ '''
+ 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 TypeError(unwrap(
+ '''
+ %s value must be a unicode string, not %s
+ ''',
+ type_name(self),
+ type_name(value)
+ ))
+
+ 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(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(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(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''
+ 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._bytes = self.contents
+ 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 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 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:])
+ 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.__bytes__() == other.__bytes__()
+
+
+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, {'implicit': 0, 'optional': True}),
+ ('not_after', GeneralizedTime, {'implicit': 1, 'optional': True}),
+ ]
+
+
+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', NotReallyTeletexString),
+ ('printable_string', PrintableString),
+ ('universal_string', UniversalString),
+ ('utf8_string', UTF8String),
+ ('bmp_string', BMPString),
+ # This is an invalid/bad alternative, but some broken certs use it
+ ('ia5_string', IA5String),
+ ]
+
+
+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.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.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.45': 'unique_identifier',
+ '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
+ '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',
+ # 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
+ # 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',
+ 'telephone_number',
+ 'email_address',
+ 'domain_component',
+ 'name_distinguisher',
+ 'organization_identifier',
+ 'tpm_manufacturer',
+ 'tpm_model',
+ 'tpm_version',
+ 'platform_manufacturer',
+ 'platform_model',
+ 'platform_version',
+ ]
+
+ @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.
+
+ """
+
+ attr_name = cls.map(attr_name)
+ 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):
+ """
+ :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',
+ 'street_address': 'Street Address',
+ 'organization_name': 'Organization',
+ 'organizational_unit_name': 'Organizational Unit',
+ 'title': 'Title',
+ 'business_category': 'Business Category',
+ 'postal_code': 'Postal Code',
+ 'telephone_number': 'Telephone Number',
+ 'name': 'Name',
+ 'given_name': 'Given Name',
+ 'initials': 'Initials',
+ 'generation_qualifier': 'Generation Qualifier',
+ 'unique_identifier': 'Unique Identifier',
+ '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',
+ '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)
+
+
+class NameTypeAndValue(Sequence):
+ _fields = [
+ ('type', NameType),
+ ('value', Any),
+ ]
+
+ _oid_pair = ('type', 'value')
+ _oid_specs = {
+ 'common_name': DirectoryString,
+ 'surname': DirectoryString,
+ 'serial_number': DirectoryString,
+ '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,
+ 'telephone_number': PrintableString,
+ 'name': DirectoryString,
+ 'given_name': DirectoryString,
+ 'initials': DirectoryString,
+ 'generation_qualifier': DirectoryString,
+ 'unique_identifier': OctetBitString,
+ 'dn_qualifier': DirectoryString,
+ 'pseudonym': DirectoryString,
+ # https://tools.ietf.org/html/rfc2985#page-26
+ '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': 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
+
+ @property
+ def prepped_value(self):
+ """
+ Returns the value after being processed by the internationalized string
+ preparation as specified by RFC 5280
+
+ :return:
+ A unicode string
+ """
+
+ if self._prepped is None:
+ 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
+
+ :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)
+ 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)
+
+ 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(unwrap(
+ '''
+ X.509 Name objects may not contain unassigned code points
+ '''
+ ))
+
+ if stringprep.in_table_c8(char):
+ raise ValueError(unwrap(
+ '''
+ X.509 Name objects may not contain change display or
+ zzzzdeprecated characters
+ '''
+ ))
+
+ if stringprep.in_table_c3(char):
+ raise ValueError(unwrap(
+ '''
+ X.509 Name objects may not contain private use characters
+ '''
+ ))
+
+ if stringprep.in_table_c4(char):
+ raise ValueError(unwrap(
+ '''
+ X.509 Name objects may not contain non-character code points
+ '''
+ ))
+
+ if stringprep.in_table_c5(char):
+ raise ValueError(unwrap(
+ '''
+ X.509 Name objects may not contain surrogate code points
+ '''
+ ))
+
+ if char == '\ufffd':
+ 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
+ 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(unwrap(
+ '''
+ X.509 Name object contains a malformed bidirectional
+ sequence
+ '''
+ ))
+
+ # Insignificant space handling step
+ string = ' ' + re.sub(' +', ' ', string).strip() + ' '
+
+ return string
+
+
+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(self)
+ 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 __ne__(self, other):
+ return not self == other
+
+ 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
+ """
+
+ output = {}
+ [output.update([(ntv['type'].native, ntv.prepped_value)]) for ntv in rdn]
+ return output
+
+
+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 __ne__(self, other):
+ return not self == other
+
+ 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 = [
+ ('', RDNSequence),
+ ]
+
+ _human_friendly = None
+ _sha1 = None
+ _sha256 = None
+
+ @classmethod
+ 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
+ string.
+
+ :param name_dict:
+ 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 compatibility with old software.
+
+ :return:
+ An x509.Name object
+ """
+
+ rdns = []
+ if not use_printable:
+ encoding_name = 'utf8_string'
+ encoding_class = UTF8String
+ else:
+ encoding_name = 'printable_string'
+ encoding_class = PrintableString
+
+ # 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():
+ attribute_name = NameType.map(attribute_name)
+ if attribute_name == 'email_address':
+ value = EmailAddress(attribute_value)
+ elif attribute_name == 'domain_component':
+ value = DNSName(attribute_value)
+ elif attribute_name in set(['dn_qualifier', 'country_name', 'serial_number']):
+ value = DirectoryString(
+ name='printable_string',
+ value=PrintableString(attribute_value)
+ )
+ else:
+ value = DirectoryString(
+ name=encoding_name,
+ value=encoding_class(attribute_value)
+ )
+
+ rdns.append(RelativeDistinguishedName([
+ NameTypeAndValue({
+ 'type': attribute_name,
+ 'value': value
+ })
+ ]))
+
+ return cls(name='', value=RDNSequence(rdns))
+
+ @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 __len__(self):
+ return len(self.chosen)
+
+ 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
+
+ :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._native is None:
+ self._native = OrderedDict()
+ for rdn in self.chosen.native:
+ for type_val in rdn:
+ field_name = type_val['type']
+ if field_name in self._native:
+ 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
+
+ @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()
+ 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 = []
+ keys = data.keys()
+ if last_field == 'Country':
+ keys = reversed(list(keys))
+ for key in keys:
+ value = data[key]
+ native_value = self._recursive_humanize(value)
+ to_join.append('%s: %s' % (key, native_value))
+
+ 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
+
+ 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):
+ """
+ :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 = [
+ ('type_id', ObjectIdentifier),
+ ('value', Any, {'explicit': 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, {'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, {'implicit': 0}),
+ ('given_name', TeletexString, {'implicit': 1, 'optional': True}),
+ ('initials', TeletexString, {'implicit': 2, 'optional': True}),
+ ('generation_qualifier', TeletexString, {'implicit': 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, {'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}),
+ ]
+
+
+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, {'implicit': 0}),
+ ('sub_address', NumericString, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class NAddresses(SetOf):
+ _child_spec = OctetString
+
+
+class PresentationAddress(Sequence):
+ _fields = [
+ ('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, {'implicit': 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, {'implicit': 0}),
+ ('extension_attribute_value', Any, {'explicit': 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, {'implicit': 0, 'optional': True}),
+ ('party_name', DirectoryString, {'implicit': 1}),
+ ]
+
+
+class GeneralName(Choice):
+ _alternatives = [
+ ('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):
+ return not self == other
+
+ 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(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(unwrap(
+ '''
+ Comparison is not supported for GeneralName objects of choice
+ %s''',
+ other.name
+ ))
+
+ if self.name != other.name:
+ return False
+
+ return self.chosen == other.chosen
+
+
+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, {'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, {'implicit': 0}),
+ ('name_relative_to_crl_issuer', RelativeDistinguishedName, {'implicit': 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, {'implicit': 0, 'default': 0}),
+ ('maximum', Integer, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class GeneralSubtrees(SequenceOf):
+ _child_spec = GeneralSubtree
+
+
+class NameConstraints(Sequence):
+ _fields = [
+ ('permitted_subtrees', GeneralSubtrees, {'implicit': 0, 'optional': True}),
+ ('excluded_subtrees', GeneralSubtrees, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class DistributionPoint(Sequence):
+ _fields = [
+ ('distribution_point', DistributionPointName, {'explicit': 0, 'optional': True}),
+ ('reasons', ReasonFlags, {'implicit': 1, 'optional': True}),
+ ('crl_issuer', GeneralNames, {'implicit': 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(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':
+ url = general_name.native
+ if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')):
+ self._url = url
+ break
+
+ return self._url
+
+
+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 PolicyIdentifier(ObjectIdentifier):
+ _map = {
+ '2.5.29.32.0': 'any_policy',
+ }
+
+
+class PolicyInformation(Sequence):
+ _fields = [
+ ('policy_identifier', PolicyIdentifier),
+ ('policy_qualifiers', PolicyQualifierInfos, {'optional': True})
+ ]
+
+
+class CertificatePolicies(SequenceOf):
+ _child_spec = PolicyInformation
+
+
+class PolicyMapping(Sequence):
+ _fields = [
+ ('issuer_domain_policy', PolicyIdentifier),
+ ('subject_domain_policy', PolicyIdentifier),
+ ]
+
+
+class PolicyMappings(SequenceOf):
+ _child_spec = PolicyMapping
+
+
+class PolicyConstraints(Sequence):
+ _fields = [
+ ('require_explicit_policy', Integer, {'implicit': 0, 'optional': True}),
+ ('inhibit_policy_mapping', Integer, {'implicit': 1, 'optional': True}),
+ ]
+
+
+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',
+ '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',
+ # 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',
+ # 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
+ '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',
+ # 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.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'
+ }
+
+
+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',
+ '1.3.6.1.5.5.7.48.3': 'time_stamping',
+ '1.3.6.1.5.5.7.48.5': 'ca_repository',
+ }
+
+
+class AccessDescription(Sequence):
+ _fields = [
+ ('access_method', AccessMethod),
+ ('access_location', GeneralName),
+ ]
+
+
+class AuthorityInfoAccessSyntax(SequenceOf):
+ _child_spec = AccessDescription
+
+
+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),
+ ('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 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',
+ '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.18': 'issuer_alt_name',
+ '2.5.29.19': 'basic_constraints',
+ '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',
+ '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',
+ # 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',
+ # https://tools.ietf.org/html/rfc6962.html#page-14
+ '1.3.6.1.4.1.11129.2.4.2': 'signed_certificate_timestamp_list',
+ }
+
+
+class Extension(Sequence):
+ _fields = [
+ ('extn_id', ExtensionId),
+ ('critical', Boolean, {'default': False}),
+ ('extn_value', ParsableOctetString),
+ ]
+
+ _oid_pair = ('extn_id', 'extn_value')
+ _oid_specs = {
+ 'subject_directory_attributes': SubjectDirectoryAttributes,
+ 'key_identifier': OctetString,
+ 'key_usage': KeyUsage,
+ 'private_key_usage_period': PrivateKeyUsagePeriod,
+ 'subject_alt_name': GeneralNames,
+ 'issuer_alt_name': GeneralNames,
+ 'basic_constraints': BasicConstraints,
+ 'name_constraints': NameConstraints,
+ 'crl_distribution_points': CRLDistributionPoints,
+ 'certificate_policies': CertificatePolicies,
+ 'policy_mappings': PolicyMappings,
+ '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,
+ 'tls_feature': Features,
+ 'ocsp_no_check': Null,
+ 'entrust_version_extension': EntrustVersionInfo,
+ 'netscape_certificate_type': NetscapeCertificateType,
+ 'signed_certificate_timestamp_list': OctetString,
+ }
+
+
+class Extensions(SequenceOf):
+ _child_spec = Extension
+
+
+class TbsCertificate(Sequence):
+ _fields = [
+ ('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, {'implicit': 1, 'optional': True}),
+ ('subject_unique_id', OctetBitString, {'implicit': 2, 'optional': True}),
+ ('extensions', Extensions, {'explicit': 3, 'optional': True}),
+ ]
+
+
+class Certificate(Sequence):
+ _fields = [
+ ('tbs_certificate', TbsCertificate),
+ ('signature_algorithm', SignedDigestAlgorithm),
+ ('signature_value', OctetBitString),
+ ]
+
+ _processed_extensions = False
+ _critical_extensions = None
+ _subject_directory_attributes = None
+ _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
+ _certificate_policies_value = None
+ _policy_mappings_value = None
+ _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
+ _private_key_usage_period_value = None
+ _tls_feature_value = None
+ _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
+ _self_issued = None
+ _self_signed = None
+ _sha1 = None
+ _sha256 = None
+
+ def _set_extensions(self):
+ """
+ Sets common named extensions to private attributes and creates a list
+ of critical extensions
+ """
+
+ self._critical_extensions = set()
+
+ 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.add(name)
+
+ self._processed_extensions = True
+
+ @property
+ def critical_extensions(self):
+ """
+ Returns a set of the names (or OID if not a known extension) of the
+ extensions marked as critical
+
+ :return:
+ A set of unicode strings
+ """
+
+ if not self._processed_extensions:
+ 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):
+ """
+ This extension is used to contain additional identification attributes
+ about the subject.
+
+ :return:
+ None or a SubjectDirectoryAttributes object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._subject_directory_attributes
+
+ @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 an OctetString object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._key_identifier_value
+
+ @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 a KeyUsage
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._key_usage_value
+
+ @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 a GeneralNames object
+ """
+
+ if not self._processed_extensions:
+ 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 not self._processed_extensions:
+ self._set_extensions()
+ return self._issuer_alt_name_value
+
+ @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 a BasicConstraints object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._basic_constraints_value
+
+ @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 a NameConstraints object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._name_constraints_value
+
+ @property
+ def crl_distribution_points_value(self):
+ """
+ This extension is used to help in locating the CRL for this certificate.
+
+ :return:
+ None or a CRLDistributionPoints object
+ extension
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._crl_distribution_points_value
+
+ @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 a CertificatePolicies object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._certificate_policies_value
+
+ @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 a PolicyMappings object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._policy_mappings_value
+
+ @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 an AuthorityKeyIdentifier object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._authority_key_identifier_value
+
+ @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 a PolicyConstraints object
+ """
+
+ if not self._processed_extensions:
+ 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 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):
+ """
+ This extension is used to define additional purposes for the public key
+ beyond what is contained in the basic constraints.
+
+ :return:
+ None or an ExtKeyUsageSyntax object
+ """
+
+ if not self._processed_extensions:
+ self._set_extensions()
+ return self._extended_key_usage_value
+
+ @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 an AuthorityInfoAccessSyntax object
+ """
+
+ if not self._processed_extensions:
+ 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 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):
+ """
+ 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 a Null object (if present)
+ """
+
+ if not self._processed_extensions:
+ 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):
+ """
+ :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:
+ 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
+ issuer = issuer.untag()
+ authority_serial = self.authority_key_identifier_value['authority_cert_serial_number'].native
+ self._authority_issuer_serial = issuer.sha256 + b':' + str_cls(authority_serial).encode('ascii')
+ else:
+ self._authority_issuer_serial = None
+ return self._authority_issuer_serial
+
+ @property
+ def crl_distribution_points(self):
+ """
+ Returns complete CRL URLs - does not include delta CRLs
+
+ :return:
+ A list of zero or more DistributionPoint objects
+ """
+
+ 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 = []
+
+ if crl_distribution_points is None:
+ return []
+
+ for distribution_point in crl_distribution_points:
+ distribution_point_name = distribution_point['distribution_point']
+ 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':
+ 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(distribution_point)
+
+ 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':
+ location = entry['access_location']
+ if location.name != 'uniform_resource_identifier':
+ continue
+ url = location.native
+ if url.lower().startswith(('http://', 'https://', 'ldap://', 'ldaps://')):
+ output.append(url)
+ 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 = []
+
+ # 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)
+
+ # 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
+ 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
+
+ @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):
+ """
+ :return:
+ A boolean - if the certificate is self-issued, as defined by RFC
+ 5280
+ """
+
+ if self._self_issued is None:
+ self._self_issued = self.subject == self.issuer
+ return self._self_issued
+
+ @property
+ def self_signed(self):
+ """
+ :return:
+ 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:
+ self._self_signed = 'no'
+ if self.self_issued:
+ if self.key_identifier:
+ if not self.authority_key_identifier:
+ self._self_signed = 'maybe'
+ elif self.authority_key_identifier == self.key_identifier:
+ self._self_signed = 'maybe'
+ else:
+ self._self_signed = 'maybe'
+ return self._self_signed
+
+ @property
+ def sha1(self):
+ """
+ :return:
+ 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 bytes_to_list(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
+
+ @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
+ 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(unwrap(
+ '''
+ domain_ip must be a unicode string, not %s
+ ''',
+ type_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
+
+ 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
+
+ # 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
+
+
+# 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, {'implicit': 0, 'optional': True}),
+ ('alias', UTF8String, {'optional': True}),
+ ('keyid', OctetString, {'optional': True}),
+ ('other', SequenceOfAlgorithmIdentifiers, {'implicit': 1, 'optional': True}),
+ ]
+
+
+class TrustedCertificate(Concat):
+ _child_specs = [Certificate, CertificateAux]
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..9bbf933
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,339 @@
+# 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
+ `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()`
+ - 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
+ `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
+ 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
+ - 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
+ `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
+
+## 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
+ "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
+
+## 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%
+ - 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
+ Python 3.x
+
+## 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`,
+ `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`
+ - `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
+ - `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
+
+## 0.14.0
+
+ - Added the `x509.Certificate.sha1_fingerprint` attribute
+
+## 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
+ field when `None` is set
+
+## 0.12.1
+
+ - Fixed a `unicode`/`bytes` bug with `x509.URI.dump()` on Python 2
+
+## 0.12.0
+
+ - 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__`
+ - `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
+ - `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
+ - 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
+
+## 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
+ RFC 5280
+ - 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/circle.yml b/circle.yml
new file mode 100644
index 0000000..19a1b75
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,20 @@
+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
+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/__init__.py b/dev/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/dev/__init__.py
diff --git a/dev/ci.py b/dev/ci.py
new file mode 100644
index 0000000..a5c3a37
--- /dev/null
+++ b/dev/ci.py
@@ -0,0 +1,45 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import sys
+
+if sys.version_info[0:2] not in [(2, 6), (3, 2)]:
+ from .lint import run as run_lint
+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():
+ """
+ Runs the linter and tests
+
+ :return:
+ A bool - if the linter and tests ran successfully
+ """
+
+ print('Python ' + sys.version.replace('\n', ''))
+ if run_lint:
+ print('')
+ lint_result = run_lint()
+ else:
+ lint_result = True
+
+ if run_coverage:
+ print('\nRunning tests (via coverage.py)')
+ sys.stdout.flush()
+ tests_result = run_coverage(ci=True)
+ else:
+ 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
new file mode 100644
index 0000000..5a24a4d
--- /dev/null
+++ b/dev/coverage.py
@@ -0,0 +1,505 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+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(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
+ """
+
+ 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()
+
+ from .tests import run as run_tests
+ result = run_tests()
+ print()
+
+ 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()
+
+ cov.report(show_missing=False)
+ print()
+ sys.stdout.flush()
+ if ci:
+ cov.xml_report()
+
+ if ci and result and os.path.exists(xml_report_path):
+ _codecov_submit()
+ print()
+
+ return 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()
+
+
+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=30):
+ """
+ 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..36370f0
--- /dev/null
+++ b/dev/deps.py
@@ -0,0 +1,212 @@
+# 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 = "[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:
+ _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/lint.py b/dev/lint.py
new file mode 100644
index 0000000..39513b3
--- /dev/null
+++ b/dev/lint.py
@@ -0,0 +1,39 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import os
+
+import flake8
+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
+
+
+cur_dir = os.path.dirname(__file__)
+config_file = os.path.join(cur_dir, '..', 'tox.ini')
+
+
+def run():
+ """
+ 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)
+
+ paths = []
+ for root, _, filenames in os.walk('asn1crypto'):
+ for filename in filenames:
+ if not filename.endswith('.py'):
+ continue
+ paths.append(os.path.join(root, filename))
+ report = flake8_style.check_files(paths)
+ success = report.total_errors == 0
+ if success:
+ print('OK')
+ return success
diff --git a/dev/release.py b/dev/release.py
new file mode 100644
index 0000000..316d75c
--- /dev/null
+++ b/dev/release.py
@@ -0,0 +1,67 @@
+# 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').strip()
+
+ setuptools.sandbox.run_setup(
+ setup_file,
+ ['sdist', 'bdist_wheel', '--universal']
+ )
+
+ twine.cli.dispatch(['upload', 'dist/asn1crypto-%s*' % tag])
+
+ setuptools.sandbox.run_setup(
+ setup_file,
+ ['clean']
+ )
diff --git a/dev/tests.py b/dev/tests.py
new file mode 100644
index 0000000..071ee23
--- /dev/null
+++ b/dev/tests.py
@@ -0,0 +1,35 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import re
+import sys
+
+from tests import test_classes
+
+
+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():
+ 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
+ result = unittest.TextTestRunner(stream=sys.stdout, verbosity=verbosity).run(suite)
+ return result.wasSuccessful()
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
new file mode 100644
index 0000000..2ca99a0
--- /dev/null
+++ b/docs/readme.md
@@ -0,0 +1,23 @@
+# asn1crypto Documentation
+
+The documentation for *asn1crypto* is composed of tutorials on basic usage and
+links to the source for the various pre-defined type classes.
+
+## Tutorials
+
+ - [Universal Types with BER/DER Decoder and DER Encoder](universal_types.md)
+ - [PEM Decoder and Encoder](pem.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.keys`
+ - [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`
+ - [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/universal_types.md b/docs/universal_types.md
new file mode 100644
index 0000000..048a135
--- /dev/null
+++ b/docs/universal_types.md
@@ -0,0 +1,675 @@
+# 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).
+
+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
+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 `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.
+
+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 |
+
+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.
+
+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
+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
+
+parsed = Sequence.load(der_byte_string)
+serialized = parsed.dump()
+```
+
+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
+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, 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
+
+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.
+
+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
+
+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.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
+
+When no `_map` is set for a `BitString` class, the native representation is a
+`tuple` of `int`s (being either `1` or `0`).
+
+```python
+from asn1crypto.core import BitString
+
+b1 = BitString((1, 0, 1))
+```
+
+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 a `set` of unicode strings.
+
+```python
+from asn1crypto.core import BitString
+
+class MyFlags(BitString):
+ _map = {
+ 0: 'edit',
+ 1: 'delete',
+ 2: 'manage_users',
+ }
+
+permissions = MyFlags({'edit', 'delete'})
+
+# This will be printed
+if permissions['edit'] and permissions['delete']:
+ print('Can edit and delete')
+
+# This will not
+if 'manage_users' in permissions.native:
+ 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.util.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.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.
+
+## 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 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.
+
+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. *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
+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,
+ }
+```
+
+## 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, {'implicit': 0}),
+ ]
+
+class Record(Sequence):
+ _fields = [
+ ('id', ObjectIdentifier),
+ ('created', UTCTime),
+ ('creator', Person, {'explicit': 0, 'optional': True}),
+ ]
+```
+
+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 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 True
+print(person.implicit)
+
+# Will display False
+print(person.untag().implicit)
+
+# Will display 0
+print(person.tag)
+
+# Will display 1
+print(person.retag({'implicit': 1}).tag)
+```
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..8dc45a5
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,232 @@
+# asn1crypto
+
+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)
+
+[![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)
+
+## 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) |
+| 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) |
+| 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)
+ - [oscrypto](https://github.com/wbond/oscrypto)
+ - [csrbuilder](https://github.com/wbond/csrbuilder)
+ - [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
+
+0.24.0 - [changelog](changelog.md)
+
+## Dependencies
+
+Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6 or pypy. *No third-party packages
+required.*
+
+## 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
+links to the source for the various pre-defined type classes.
+
+### Tutorials
+
+ - [Universal Types with BER/DER Decoder and DER Encoder](docs/universal_types.md)
+ - [PEM Encoder and Decoder](docs/pem.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.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`
+ - [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`
+ - [PDF signatures](asn1crypto/pdf.py), `asn1crypto.pdf`
+
+## Continuous Integration
+
+ - [Windows](https://ci.appveyor.com/project/wbond/asn1crypto/history) via AppVeyor
+ - [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
+
+Tests are written using `unittest` and require no third-party packages:
+
+```bash
+python run.py tests
+```
+
+To run only some tests, pass a regular expression as a parameter to `tests`.
+
+```bash
+python run.py tests ocsp
+```
+
+## Development
+
+To install the package used for linting, execute:
+
+```bash
+pip install --user -r requires/lint
+```
+
+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
+```
+
+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 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.
+
+```bash
+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
new file mode 100644
index 0000000..aa86fe5
--- /dev/null
+++ b/run.py
@@ -0,0 +1,61 @@
+#!/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 | deps | ci | release)', file=sys.stderr)
+ sys.exit(1)
+
+
+def get_arg(num):
+ if len(sys.argv) < num + 1:
+ return None, num
+ arg = sys.argv[num]
+ if isinstance(arg, byte_cls):
+ arg = arg.decode('utf-8')
+ return arg, num + 1
+
+
+if len(sys.argv) < 2 or len(sys.argv) > 3:
+ show_usage()
+
+task, next_arg = get_arg(1)
+
+if task not in set(['lint', 'tests', 'coverage', 'deps', 'ci', 'release']):
+ 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, 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
+
+elif task == 'release':
+ from dev.release import run
+
+result = run(*params)
+sys.exit(int(not result))
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..f4e9e18
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,81 @@
+import os
+import shutil
+
+from setuptools import setup, find_packages, Command
+
+from asn1crypto import version
+
+
+class CleanCommand(Command):
+ user_options = [
+ ('all', 'a', '(Compatibility with original clean command)'),
+ ]
+
+ def initialize_options(self):
+ self.all = False
+
+ 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)
+ 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(
+ name='asn1crypto',
+ version=version.__version__,
+
+ description=(
+ '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.',
+
+ 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.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 :: 3.6',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+
+ 'Topic :: Security :: Cryptography',
+ ],
+
+ keywords='asn1 crypto pki x509 certificate rsa dsa ec dh',
+
+ packages=find_packages(exclude=['tests*', 'dev*']),
+
+ test_suite='tests.make_suite',
+
+ cmdclass={
+ 'clean': CleanCommand,
+ }
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..783a20f
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,68 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import imp
+import os
+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
+ """
+
+ # 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
+ from .test_csr import CSRTests
+ 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
+ from .test_parser import ParserTests
+ from .test_core import CoreTests
+
+ return [
+ AlgoTests,
+ CMSTests,
+ CRLTests,
+ CSRTests,
+ KeysTests,
+ OCSPTests,
+ PEMTests,
+ PKCS12Tests,
+ TSPTests,
+ UtilTests,
+ ParserTests,
+ X509Tests,
+ CoreTests
+ ]
diff --git a/tests/_unittest_compat.py b/tests/_unittest_compat.py
new file mode 100644
index 0000000..2d4985d
--- /dev/null
+++ b/tests/_unittest_compat.py
@@ -0,0 +1,84 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import sys
+import unittest
+import re
+
+
+_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
+ unittest.TestCase.assertRaisesRegexp = _assert_raises_regexp
+ _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, 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."""
+
+ 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/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/fixtures/DSAParametersInheritedCACert.crt b/tests/fixtures/DSAParametersInheritedCACert.crt
new file mode 100644
index 0000000..5e2fa5b
--- /dev/null
+++ b/tests/fixtures/DSAParametersInheritedCACert.crt
Binary files differ
diff --git a/tests/fixtures/admin.ch.crt b/tests/fixtures/admin.ch.crt
new file mode 100644
index 0000000..2ec1edf
--- /dev/null
+++ b/tests/fixtures/admin.ch.crt
Binary files differ
diff --git a/tests/fixtures/certbag.der b/tests/fixtures/certbag.der
new file mode 100644
index 0000000..1bd235a
--- /dev/null
+++ b/tests/fixtures/certbag.der
Binary files differ
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/fixtures/cms-compressed.der b/tests/fixtures/cms-compressed.der
new file mode 100644
index 0000000..e5096c6
--- /dev/null
+++ b/tests/fixtures/cms-compressed.der
Binary files 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
--- /dev/null
+++ b/tests/fixtures/cms-digested.der
Binary files 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
--- /dev/null
+++ b/tests/fixtures/cms-encrypted.der
Binary files 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
--- /dev/null
+++ b/tests/fixtures/cms-enveloped.der
Binary files 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
--- /dev/null
+++ b/tests/fixtures/cms-signed-digested.der
Binary files 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-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†÷  €0€1 0  `†He0€ *†H†÷  €‚ßÞ<?xml version="1.0" encoding="UTF-8"?>
+<referti id_report="1784" versione_report="20160510211157">
+ <documento id_documento="373">
+ <id_doc>62ede6fd-5994-4fd5-8811-942ffa58c2a6</id_doc>
+ <id_prod>referti-661148800</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxxxxxxxxxxxxxx</codice_utente>
+ <file size="1070">referti-661148800.rtf</file>
+ </documento>
+ <documento id_documento="374">
+ <id_doc>19494945-5ebb-4e80-9865-9125dd417105</id_doc>
+ <id_prod>referti-235588799</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxxxxxxxxxxxxxx</codice_utente>
+ <file size="1070">referti-235588799.rtf</file>
+ </documento>
+ <documento id_documento="375">
+ <id_doc>cc9c5e86-fad1-423d-83ab-87d8e26fdae6</id_doc>
+ <id_prod>referti-552878798</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxxxxxxxxxxxxxx</codice_utente>
+ <file size="1070">referti-552878798.rtf</file>
+ </documento>
+ <documento id_documento="376">
+ <id_doc>d0cbf95f-509a-4023-ac00-7b574a9b86c9</id_doc>
+ <id_prod>referti-109428797</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>PRVPV609P41C553B</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-109428797.rtf</file>
+ </documento>
+ <documento id_documento="377">
+ <id_doc>35f09d3c-8867-4fc6-920a-239e8b07e82d</id_doc>
+ <id_prod>referti-630498796</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-630498796.rtf</file>
+ </documento>
+ <documento id_documento="378">
+ <id_doc>76407911-06fa-4dee-b1c9-88d8dc3cf567</id_doc>
+ <id_prod>referti-440568795</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-440568795.rtf</file>
+ </documento>
+ <documento id_documento="379">
+ <id_doc>66dc238e-3bca-4847-9ae2-c0a240f999b7</id_doc>
+ <id_prod>referti-110088794</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-110088794.rtf</file>
+ </documento>
+ <documento id_documento="380">
+ <id_doc>25bc9613-cc6c-411c-a192-e76437734921</id_doc>
+ <id_prod>referti-444868793</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-444868793.rtf</file>
+ </documento>
+ <documento id_documento="381">
+ <id_doc>7c64b012-5866-4f5f-9b99-a0db9737854f</id_doc>
+ <id_prod>referti-152408792</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-152408792.rtf</file>
+ </documento>
+ <documento id_documento="382">
+ <id_doc>d91faafd-0c5a-4198-ab18-88d989ad3a1a</id_doc>
+ <id_prod>referti-517228791</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-517228791.rtf</file>
+ </documento>
+ <documento id_documento="383">
+ <id_doc>eb779d2d-07d8-47b7-8576-953d7912a90d</id_doc>
+ <id_prod>referti-370388790</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-370388790.rtf</file>
+ </documento>
+ <documento id_documento="384">
+ <id_doc>495867e3-6906-41fd-b138-9785d35126d6</id_doc>
+ <id_prod>referti-521608789</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-521608789.rtf</file>
+ </documento>
+ <documento id_documento="385">
+ <id_doc>bc46bbcd-be0d-4ef8-a0d9-fa99d2fc9b5f</id_doc>
+ <id_prod>referti-020608788</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-020608788.rtf</file>
+ </documento>
+ <documento id_documento="386">
+ <id_doc>6ab12ed6-f23d-4799-a4d9-8f7691de9fb5</id_doc>
+ <id_prod>referti-398958787</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-398958787.rtf</file>
+ </documento>
+ <documento id_documento="387">
+ <id_doc>fc866f2b-0e2a-4f91-afae-c378ba08fc35</id_doc>
+ <id_prod>referti-156228786</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-156228786.rtf</file>
+ </documento>
+ <documento id_documento="388">
+ <id_doc>27727aef-ec8c-429b-9ac3-55cb40ae650a</id_doc>
+ <id_prod>referti-873788785</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-873788785.rtf</file>
+ </documento>
+ <documento id_documento="389">
+ <id_doc>c8480482-eeef-49f9-8232-536bcd8856e2</id_doc>
+ <id_prod>referti-532518784</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-532518784.rtf</file>
+ </documento>
+ <documento id_documento="390">
+ <id_doc>805d6ce6-6abe-4534-88ce-f03757f56955</id_doc>
+ <id_prod>referti-897268783</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-897268783.rtf</file>
+ </documento>
+ <documento id_documento="391">
+ <id_doc>47bab41d-f305-4d2b-8638-c9164b3447ad</id_doc>
+ <id_prod>referti-769098782</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-769098782.rtf</file>
+ </documento>
+ <documento id_documento="392">
+ <id_doc>ad2a1a7f-8957-4142-8b85-31c61ae6a489</id_doc>
+ <id_prod>referti-166518781</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-166518781.rtf</file>
+ </documento>
+ <documento id_documento="393">
+ <id_doc>dde33d3e-c70c-4004-90ab-2513e9e6031b</id_doc>
+ <id_prod>referti-190628780</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-190628780.rtf</file>
+ </documento>
+ <documento id_documento="394">
+ <id_doc>6be378de-ee7d-4f8e-a8a7-26997cb8ecf3</id_doc>
+ <id_prod>referti-390568779</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-390568779.rtf</file>
+ </documento>
+ <documento id_documento="395">
+ <id_doc>3e886dda-4a7b-483b-b057-bfd79b5a11b8</id_doc>
+ <id_prod>referti-316778778</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-316778778.rtf</file>
+ </documento>
+ <documento id_documento="396">
+ <id_doc>69308574-93c6-47e1-b98d-468f1997d22e</id_doc>
+ <id_prod>referti-221808777</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-221808777.rtf</file>
+ </documento>
+ <documento id_documento="397">
+ <id_doc>c2d4215f-2dab-4086-b586-7312c4309c87</id_doc>
+ <id_prod>referti-468068776</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-468068776.rtf</file>
+ </documento>
+ <documento id_documento="398">
+ <id_doc>30a513db-ff1a-41b6-b4e2-3b50bc2b6b44</id_doc>
+ <id_prod>referti-859498775</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-859498775.rtf</file>
+ </documento>
+ <documento id_documento="399">
+ <id_doc>7cb39aea-29e0-47a1-9a2c-f9e47df45423</id_doc>
+ <id_prod>referti-004108774</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-004108774.rtf</file>
+ </documento>
+ <documento id_documento="400">
+ <id_doc>9b9709ac-58e0-46d3-bfc8-eaea3fe19c34</id_doc>
+ <id_prod>referti-225178773</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-225178773.rtf</file>
+ </documento>
+ <documento id_documento="401">
+ <id_doc>36d5e7fa-e3b5-4b9c-84ef-d9f3dd56abba</id_doc>
+ <id_prod>referti-493808772</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-493808772.rtf</file>
+ </documento>
+ <documento id_documento="402">
+ <id_doc>f5de82c0-6588-47c1-98dd-70a55efdc319</id_doc>
+ <id_prod>referti-219068771</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-219068771.rtf</file>
+ </documento>
+ <documento id_documento="403">
+ <id_doc>5cba491c-a9bf-4004-8ea9-25db1fe79ddc</id_doc>
+ <id_prod>referti-275238770</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-275238770.rtf</file>
+ </documento>
+ <documento id_documento="404">
+ <id_doc>4f313ec7-1723-4d72-b680-6db635e37d78</id_doc>
+ <id_prod>referti-715628769</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-715628769.rtf</file>
+ </documento>
+ <documento id_documento="405">
+ <id_doc>5be1b3c9-aaf6-4a7f-80b0-b76782bcdf17</id_doc>
+ <id_prod>referti-908078768</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-908078768.rtf</file>
+ </documento>
+ <documento id_documento="406">
+ <id_doc>0ef7efd0-d536-44dd-946b-ed5316b240ac</id_doc>
+ <id_prod>referti-176518767</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-176518767.rtf</file>
+ </documento>
+ <documento id_documento="407">
+ <id_doc>325f1b16-b71e-43d3-9426-e6e5e810a409</id_doc>
+ <id_prod>referti-870108766</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-870108766.rtf</file>
+ </documento>
+ <documento id_documento="408">
+ <id_doc>1ec52ea9-43da-48dc-94b5-710829ccede1</id_doc>
+ <id_prod>referti-031108765</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-031108765.rtf</file>
+ </documento>
+ <documento id_documento="409">
+ <id_doc>4a306f42-24ff-49e7-b345-1ed8ac25130e</id_doc>
+ <id_prod>referti-167618764</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-167618764.rtf</file>
+ </documento>
+ <documento id_documento="410">
+ <id_doc>315af6a7-658a-4c52-a787-304806f79563</id_doc>
+ <id_prod>referti-349608763</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-349608763.rtf</file>
+ </documento>
+ <documento id_documento="411">
+ <id_doc>47da2fbd-5277-48a7-9cc3-d45e19f780c4</id_doc>
+ <id_prod>referti-451848762</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-451848762.rtf</file>
+ </documento>
+ <documento id_documento="412">
+ <id_doc>3c67953d-cf34-4e76-815b-66f30845ec95</id_doc>
+ <id_prod>referti-385388761</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-385388761.rtf</file>
+ </documento>
+ <documento id_documento="413">
+ <id_doc>9ce4203f-b318-4ed2-9bb5-8b5cb8f5e6e7</id_doc>
+ <id_prod>referti-237208760</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-237208760.rtf</file>
+ </documento>
+ <documento id_documento="414">
+ <id_doc>fb1136ab-6c42-4ad3-bd7a-09260d6ba290</id_doc>
+ <id_prod>referti-228728759</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-228728759.rtf</file>
+ </documento>
+ <documento id_documento="415">
+ <id_doc>f206bc17-fc72-42a2-b7c3-f516f5f5ce4f</id_doc>
+ <id_prod>referti-219058758</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-219058758.rtf</file>
+ </documento>
+ <documento id_documento="416">
+ <id_doc>d275b0a0-7d9e-4a26-ac59-696d9937b761</id_doc>
+ <id_prod>referti-819308757</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-819308757.rtf</file>
+ </documento>
+ <documento id_documento="417">
+ <id_doc>e56b8c19-f3fa-4966-b132-0189a4a4c074</id_doc>
+ <id_prod>referti-820568756</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-820568756.rtf</file>
+ </documento>
+ <documento id_documento="418">
+ <id_doc>9fce3e64-fc5c-4945-8585-89c78c20b540</id_doc>
+ <id_prod>referti-574168755</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-574168755.rtf</file>
+ </documento>
+ <documento id_documento="419">
+ <id_doc>6d3cc452-498d-4fb7-bc77-92551f3d8912</id_doc>
+ <id_prod>referti-231048754</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-231048754.rtf</file>
+ </documento>
+ <documento id_documento="420">
+ <id_doc>7ccd4176-9177-4a74-903b-ba6a8c65947d</id_doc>
+ <id_prod>referti-082458753</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-082458753.rtf</file>
+ </documento>
+ <documento id_documento="421">
+ <id_doc>b5298b48-61eb-4488-b7be-ee78f4076b2d</id_doc>
+ <id_prod>referti-421618752</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-421618752.rtf</file>
+ </documento>
+ <documento id_documento="422">
+ <id_doc>84dbd362-2da8-42bd-b03a-7c8495f7573c</id_doc>
+ <id_prod>referti-813398751</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-813398751.rtf</file>
+ </documento>
+ <documento id_documento="423">
+ <id_doc>8edb071e-2a45-4a67-a56d-14f6ceb8d0b8</id_doc>
+ <id_prod>referti-831578750</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-831578750.rtf</file>
+ </documento>
+ <documento id_documento="424">
+ <id_doc>abe1c7ac-8a8a-4c0e-975d-bf6ccf70afb8</id_doc>
+ <id_prod>referti-149978749</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-149978749.rtf</file>
+ </documento>
+ <documento id_documento="425">
+ <id_doc>4b391d68-137a-4f99-9a70-5649c455a885</id_doc>
+ <id_prod>referti-078958748</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-078958748.rtf</file>
+ </documento>
+ <documento id_documento="426">
+ <id_doc>84f1790a-9d60-4f26-b072-8df0d3c2a931</id_doc>
+ <id_prod>referti-416148747</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-416148747.rtf</file>
+ </documento>
+ <documento id_documento="427">
+ <id_doc>eb5799d8-9d38-43f5-a484-b55f0e10ceb7</id_doc>
+ <id_prod>referti-173908746</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-173908746.rtf</file>
+ </documento>
+ <documento id_documento="428">
+ <id_doc>2d24cc23-c873-4266-8eb1-5be0fbe1761f</id_doc>
+ <id_prod>referti-670138745</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-670138745.rtf</file>
+ </documento>
+ <documento id_documento="429">
+ <id_doc>b8846eb4-9b5e-4572-be80-f3add1dd2809</id_doc>
+ <id_prod>referti-788608744</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-788608744.rtf</file>
+ </documento>
+ <documento id_documento="430">
+ <id_doc>37a0c26b-ecdc-4cba-bd11-bbbd770a1602</id_doc>
+ <id_prod>referti-697888743</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-697888743.rtf</file>
+ </documento>
+ <documento id_documento="431">
+ <id_doc>50297573-dd4b-4c9d-b382-87555df696e9</id_doc>
+ <id_prod>referti-673838742</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-673838742.rtf</file>
+ </documento>
+ <documento id_documento="432">
+ <id_doc>824228e4-552f-4ddc-98d9-f7c3d7857fc3</id_doc>
+ <id_prod>referti-182208741</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-182208741.rtf</file>
+ </documento>
+ <documento id_documento="433">
+ <id_doc>e6ee07c3-3e41-43e4-a4be-c899418bdd69</id_doc>
+ <id_prod>referti-806938740</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-806938740.rtf</file>
+ </documento>
+ <documento id_documento="434">
+ <id_doc>d88ae7b4-cd15-4ace-9eae-761b70b0b4a2</id_doc>
+ <id_prod>referti-451448739</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-451448739.rtf</file>
+ </documento>
+ <documento id_documento="435">
+ <id_doc>2bd12a1c-df19-4af7-bb20-6ab14f3332c8</id_doc>
+ <id_prod>referti-870028738</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-870028738.rtf</file>
+ </documento>
+ <documento id_documento="436">
+ <id_doc>52accf52-9bf2-4a5b-b1a4-d41d6afd40fe</id_doc>
+ <id_prod>referti-629738737</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-629738737.rtf</file>
+ </documento>
+ <documento id_documento="437">
+ <id_doc>9778f50a-5eae-4818-9619-3a2defc12fd3</id_doc>
+ <id_prod>referti-462728736</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-462728736.rtf</file>
+ </documento>
+ <documento id_documento="438">
+ <id_doc>fe7f5358-d01c-4d5f-b0d5-b4f3cd8d7302</id_doc>
+ <id_prod>referti-459798735</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-459798735.rtf</file>
+ </documento>
+ <documento id_documento="439">
+ <id_doc>87398882-fa6b-4ece-8c01-a6748b8a1d21</id_doc>
+ <id_prod>referti-517488734</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-517488734.rtf</file>
+ </documento>
+ <documento id_documento="440">
+ <id_doc>9c10bc89-724e-43f1-b3d5-3bc2a443d226</id_doc>
+ <id_prod>referti-875258733</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-875258733.rtf</file>
+ </documento>
+ <documento id_documento="441">
+ <id_doc>685de4cf-7f3e-441d-a49a-061d4b4cfedb</id_doc>
+ <id_prod>referti-458208732</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-458208732.rtf</file>
+ </documento>
+ <documento id_documento="442">
+ <id_doc>7c43fdb5-4001-437b-8882-53bc158a69ea</id_doc>
+ <id_prod>referti-626758731</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-626758731.rtf</file>
+ </documento>
+ <documento id_documento="443">
+ <id_doc>f0adf2ba-0649-456d-94d1-94f8e4af68af</id_doc>
+ <id_prod>referti-204468730</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-204468730.rtf</file>
+ </documento>
+ <documento id_documento="444">
+ <id_doc>ab8451a4-8657-473a-a9ca-7cda5a0b532c</id_doc>
+ <id_prod>referti-474968729</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-474968729.rtf</file>
+ </documento>
+ <documento id_documento="445">
+ <id_doc>8befa776-9c9f-4875-8dde-58811a76a33c</id_doc>
+ <id_prod>referti-538618728</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-538618728.rtf</file>
+ </documento>
+ <documento id_documento="446">
+ <id_doc>6a3afdde-54d6-4e0c-a7f2-995ada66ee9c</id_doc>
+ <id_prod>referti-203608727</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-203608727.rtf</file>
+ </documento>
+ <documento id_documento="447">
+ <id_doc>2a15822a-49a0-4a8c-8231-bbc32cd8df87</id_doc>
+ <id_prod>referti-919808726</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-919808726.rtf</file>
+ </documento>
+ <documento id_documento="448">
+ <id_doc>9bceb04b-b065-4386-b76a-472f705d5b6b</id_doc>
+ <id_prod>referti-493238725</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-493238725.rtf</file>
+ </documento>
+ <documento id_documento="449">
+ <id_doc>c27aa95b-67da-46e5-b560-6c2cc7c7aab1</id_doc>
+ <id_prod>referti-879388724</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-879388724.rtf</file>
+ </documento>
+ <documento id_documento="450">
+ <id_doc>5649d464-d05d-4371-9508-ee9cf7582b69</id_doc>
+ <id_prod>referti-001238723</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-001238723.rtf</file>
+ </documento>
+ <documento id_documento="451">
+ <id_doc>d740db69-bc94-4cdf-a4e1-ccd98ee020f6</id_doc>
+ <id_prod>referti-566698722</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-566698722.rtf</file>
+ </documento>
+ <documento id_documento="452">
+ <id_doc>952e1c52-e929-43e2-9837-f03adc3754d8</id_doc>
+ <id_prod>referti-157168721</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-157168721.rtf</file>
+ </documento>
+ <documento id_documento="453">
+ <id_doc>d5e30ccf-69a7-4d7f-afd9-cb3d0a86289b</id_doc>
+ <id_prod>referti-298668720</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-298668720.rtf</file>
+ </documento>
+ <documento id_documento="454">
+ <id_doc>ca3db245-b545-452e-8937-9478ca9a7084</id_doc>
+ <id_prod>referti-572018719</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-572018719.rtf</file>
+ </documento>
+ <documento id_documento="455">
+ <id_doc>20e736c2-d6f5-4c9e-9a99-e8d9879a805e</id_doc>
+ <id_prod>referti-289688718</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-289688718.rtf</file>
+ </documento>
+ <documento id_documento="456">
+ <id_doc>aea1f611-c21f-4530-a3c1-ac20cdf6f421</id_doc>
+ <id_prod>referti-834918717</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-834918717.rtf</file>
+ </documento>
+ <documento id_documento="457">
+ <id_doc>c4d2a6ef-1617-4bbc-9dcd-091b0a6097d4</id_doc>
+ <id_prod>referti-719978716</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-719978716.rtf</file>
+ </documento>
+ <documento id_documento="458">
+ <id_doc>f089d513-c6cc-4761-bbc9-df557bee6a16</id_doc>
+ <id_prod>referti-765048715</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-765048715.rtf</file>
+ </documento>
+ <documento id_documento="459">
+ <id_doc>1b2e4bfa-e03a-49ed-b8af-223a6e03f783</id_doc>
+ <id_prod>referti-272918714</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-272918714.rtf</file>
+ </documento>
+ <documento id_documento="460">
+ <id_doc>daadc617-37c4-48bc-8589-8c995d94d9a0</id_doc>
+ <id_prod>referti-154588713</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-154588713.rtf</file>
+ </documento>
+ <documento id_documento="461">
+ <id_doc>3e9044e1-8a12-41b3-a48e-368ba588cdfb</id_doc>
+ <id_prod>referti-881718712</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-881718712.rtf</file>
+ </documento>
+ <documento id_documento="462">
+ <id_doc>0a08de57-3869-4f69-bcff-f65ffa6a42f2</id_doc>
+ <id_prod>referti-216428711</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-216428711.rtf</file>
+ </documento>
+ <documento id_documento="463">
+ <id_doc>3e929e2a-8988-4d02-8829-367479f8f559</id_doc>
+ <id_prod>referti-408448710</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-408448710.rtf</file>
+ </documento>
+ <documento id_documento="464">
+ <id_doc>e5d04243-d016-4040-b41b-6fb57d1a5fe1</id_doc>
+ <id_prod>referti-935318709</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-935318709.rtf</file>
+ </documento>
+ <documento id_documento="465">
+ <id_doc>a6dda134-2e0d-448e-9efd-7be36e940dfc</id_doc>
+ <id_prod>referti-456878708</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-456878708.rtf</file>
+ </documento>
+ <documento id_documento="466">
+ <id_doc>b782b02d-a98b-4b23-97c0-8a2af14cd486</id_doc>
+ <id_prod>referti-050918707</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-050918707.rtf</file>
+ </documento>
+ <documento id_documento="467">
+ <id_doc>58c2f33c-2234-4700-85ef-9a52e776544b</id_doc>
+ <id_prod>referti-062248706</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-062248706.rtf</file>
+ </documento>
+ <documento id_documento="468">
+ <id_doc>934371e9-616e-498a-8c14-33e58a7e20df</id_doc>
+ <id_prod>referti-406148705</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-406148705.rtf</file>
+ </documento>
+ <documento id_documento="469">
+ <id_doc>9c73bf68-0f20-4404-b49f-725eba2f9171</id_doc>
+ <id_prod>referti-114928704</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-114928704.rtf</file>
+ </documento>
+ <documento id_documento="470">
+ <id_doc>41778171-5f06-475f-b5e5-689b747611d0</id_doc>
+ <id_prod>referti-632428703</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-632428703.rtf</file>
+ </documento>
+ <documento id_documento="471">
+ <id_doc>ea5b05f0-0c6f-4190-802c-5609cab81844</id_doc>
+ <id_prod>referti-149018702</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-149018702.rtf</file>
+ </documento>
+ <documento id_documento="472">
+ <id_doc>02d51b9c-4ab5-4171-92ae-01f35fe63272</id_doc>
+ <id_prod>referti-127268701</id_prod>
+ <tipo_ref>radiologico</tipo_ref>
+ <dt_creaz>2014-10-15</dt_creaz>
+ <cognome_p>Prova6</cognome_p>
+ <nome_p>Prova6</nome_p>
+ <cf_p>XXXXXXXXXXXXXXXX</cf_p>
+ <cognome_m>XXXXXXX</cognome_m>
+ <nome_m>XXXXXXX</nome_m>
+ <cf_m>111111111111111</cf_m>
+ <codice_utente>xxxxxxxx.xxxxxxx.isi</codice_utente>
+ <file size="1070">referti-127268701.rtf</file>
+ </documento>
+</referti>
+
+ Postecom S.p.A.1 0U Certification Authority10U Postecom CA30 150810082556Z 180810082556Z0Ê1 0 UIT1$0"U
+ POSTECOM S.P.A./0583884100410U SENSIDONI FABIO10UIT:SNSFBA64A26H501V10 U* FABIO10U SENSIDONI10U.36166201'0%U RESP CONSERVAZIONE SOSTITUTIVA0‚"0  *†H†÷ 
+‚
+b*Í#ªm7å ìU@¾åVJ.)â=u¨G+ÂøëHq&©Š/DqÞ\gbyJ9†¾±±ñ_Ê\šdúz *GDYÖÀÒ>Áu¦il•=¾“§Oƒ¡fg“nîÇ¡‚òõרè–s¹7ôr:.ÑV/ðÀËKž–þ̤ßDJ\j¢Rê‹Ó±Ô¤åßþ¯Lc†«
+ÏvÁNõ¶>1­ÝÂ먃«*Yº\ÞÿSryë´Ù¦Í¦çDú^aRŸ |¿à®÷µ¨¡hJz¯
+»^@\Û*Ûa{ò͘°š°Ù1
+´ÑYFF™Hœ£žû ÀÚ'“ä±t™âmƒ­íÇå¥û‹|©·‹÷7ý²ÞøUÚl1‚¦0‚¢0g0`1 0 UIT10U
+ Postecom S.p.A.1 0U Certification Authority10U Postecom CA3âá0  `†He ‚0 *†H†÷  1  *†H†÷ 0 *†H†÷  1 160510191200Z0/ *†H†÷  1" u“"”+¹°JÑkzÁ¿„jyO‘'ÃÑ^VÓi§Ì ]0¨ *†H†÷  /1˜0•0’0 ÈØ¥úÎ?F*{F£>’;‡
+ Postecom S.p.A.1 0U Certification Authority10U Postecom CA3âá0  *†H†÷ ‚
+1îˆ(Ù)©tœ9-ºc³ïKÂ9úÜVV ý´â.ÔK¥Zù(J¢2ƒˆF}Àó÷É÷Ú7&*le{J•7g=sV}§@ ÉlE\ö­GÍ ‚Ãý”@^³ÁéœB™uÂp ?$7Yô—p›‡P¤UPóŒšÊû4æÊ7¼å
diff --git a/tests/fixtures/cms-signed.der b/tests/fixtures/cms-signed.der
new file mode 100644
index 0000000..22db9a0
--- /dev/null
+++ b/tests/fixtures/cms-signed.der
Binary files 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
--- /dev/null
+++ b/tests/fixtures/eid2011.crl
Binary files differ
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
--- /dev/null
+++ b/tests/fixtures/geotrust_certs/Equifax_Secure_Certificate_Authority.crt
Binary files 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
--- /dev/null
+++ b/tests/fixtures/geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt
Binary files 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
--- /dev/null
+++ b/tests/fixtures/geotrust_certs/GeoTrust_Primary_CA.crt
Binary files 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
--- /dev/null
+++ b/tests/fixtures/geotrust_certs/GeoTrust_Universal_CA.crt
Binary files 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/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/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/IssuingCA-der.cer b/tests/fixtures/globalsign_example_keys/IssuingCA-der.cer
new file mode 100644
index 0000000..159b855
--- /dev/null
+++ b/tests/fixtures/globalsign_example_keys/IssuingCA-der.cer
Binary files 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
--- /dev/null
+++ b/tests/fixtures/globalsign_example_keys/SSL1-der.cer
Binary files 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
--- /dev/null
+++ b/tests/fixtures/globalsign_example_keys/SSL2-der.cer
Binary files 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
--- /dev/null
+++ b/tests/fixtures/globalsign_example_keys/SSL3-der.cer
Binary files 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/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/globalsign_example_keys/rootCA-der.cer b/tests/fixtures/globalsign_example_keys/rootCA-der.cer
new file mode 100644
index 0000000..ff7bf04
--- /dev/null
+++ b/tests/fixtures/globalsign_example_keys/rootCA-der.cer
Binary files 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/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/keys/test-aes128-der.key b/tests/fixtures/keys/test-aes128-der.key
new file mode 100644
index 0000000..8609b25
--- /dev/null
+++ b/tests/fixtures/keys/test-aes128-der.key
Binary files 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-der.crt b/tests/fixtures/keys/test-der.crt
new file mode 100644
index 0000000..1fcb3e8
--- /dev/null
+++ b/tests/fixtures/keys/test-der.crt
Binary files differ
diff --git a/tests/fixtures/keys/test-der.key b/tests/fixtures/keys/test-der.key
new file mode 100644
index 0000000..a41e064
--- /dev/null
+++ b/tests/fixtures/keys/test-der.key
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-dsa-der.crt
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-dsa-der.key
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-ec-der.crt
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-ec-der.key
Binary files differ
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
--- /dev/null
+++ b/tests/fixtures/keys/test-ec-named-der.crt
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-ec-named-der.key
Binary files 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-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-inter-der.crt b/tests/fixtures/keys/test-inter-der.crt
new file mode 100644
index 0000000..1aa7f40
--- /dev/null
+++ b/tests/fixtures/keys/test-inter-der.crt
Binary files differ
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-pkcs8-der.key b/tests/fixtures/keys/test-pkcs8-der.key
new file mode 100644
index 0000000..a857947
--- /dev/null
+++ b/tests/fixtures/keys/test-pkcs8-der.key
Binary files differ
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
--- /dev/null
+++ b/tests/fixtures/keys/test-pkcs8-dsa-der.key
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-pkcs8-ec-der.key
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-pkcs8-ec-named-der.key
Binary files 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-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
--- /dev/null
+++ b/tests/fixtures/keys/test-public-der.key
Binary files differ
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
--- /dev/null
+++ b/tests/fixtures/keys/test-public-dsa-der.key
Binary files 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-----
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
--- /dev/null
+++ b/tests/fixtures/keys/test-public-ec-der.key
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-public-ec-named-der.key
Binary files 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/fixtures/keys/test-public-rsa-der.key b/tests/fixtures/keys/test-public-rsa-der.key
new file mode 100644
index 0000000..6622597
--- /dev/null
+++ b/tests/fixtures/keys/test-public-rsa-der.key
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-rc2.p12
Binary files 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
--- /dev/null
+++ b/tests/fixtures/keys/test-third-der.crt
Binary files differ
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/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/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/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/
diff --git a/tests/fixtures/meca2_compressed.der b/tests/fixtures/meca2_compressed.der
new file mode 100644
index 0000000..464d31e
--- /dev/null
+++ b/tests/fixtures/meca2_compressed.der
Binary files differ
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/mozilla-generated-by-openssl.pkcs7.der b/tests/fixtures/mozilla-generated-by-openssl.pkcs7.der
new file mode 100644
index 0000000..bb0fdc4
--- /dev/null
+++ b/tests/fixtures/mozilla-generated-by-openssl.pkcs7.der
Binary files differ
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/fixtures/ocsp_request b/tests/fixtures/ocsp_request
new file mode 100644
index 0000000..bf07bf9
--- /dev/null
+++ b/tests/fixtures/ocsp_request
Binary files differ
diff --git a/tests/fixtures/ocsp_response b/tests/fixtures/ocsp_response
new file mode 100644
index 0000000..9b94de4
--- /dev/null
+++ b/tests/fixtures/ocsp_response
Binary files differ
diff --git a/tests/fixtures/pkcs7-signed-digested.der b/tests/fixtures/pkcs7-signed-digested.der
new file mode 100644
index 0000000..13e7505
--- /dev/null
+++ b/tests/fixtures/pkcs7-signed-digested.der
Binary files 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
--- /dev/null
+++ b/tests/fixtures/pkcs7-signed.der
Binary files 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/self-signed-repeated-subject-fields.der b/tests/fixtures/self-signed-repeated-subject-fields.der
new file mode 100644
index 0000000..dc12751
--- /dev/null
+++ b/tests/fixtures/self-signed-repeated-subject-fields.der
Binary files differ
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/fixtures/test-inter-der.csr b/tests/fixtures/test-inter-der.csr
new file mode 100644
index 0000000..c213cea
--- /dev/null
+++ b/tests/fixtures/test-inter-der.csr
Binary files 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
--- /dev/null
+++ b/tests/fixtures/test-third-der.csr
Binary files 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/fixtures/test-tripledes.p12 b/tests/fixtures/test-tripledes.p12
new file mode 100644
index 0000000..8fc5e18
--- /dev/null
+++ b/tests/fixtures/test-tripledes.p12
Binary files differ
diff --git a/tests/fixtures/tsp_request b/tests/fixtures/tsp_request
new file mode 100644
index 0000000..214d28b
--- /dev/null
+++ b/tests/fixtures/tsp_request
Binary files differ
diff --git a/tests/fixtures/tsp_response b/tests/fixtures/tsp_response
new file mode 100644
index 0000000..3921362
--- /dev/null
+++ b/tests/fixtures/tsp_response
Binary files differ
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_algos.py b/tests/test_algos.py
new file mode 100644
index 0000000..a3550af
--- /dev/null
+++ b/tests/test_algos.py
@@ -0,0 +1,33 @@
+# 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__)
+
+ def test_digest_parameters(self):
+ sha1 = algos.DigestAlgorithm({'algorithm': 'sha1'})
+ self.assertEqual(core.Null, sha1['parameters'].__class__)
diff --git a/tests/test_cms.py b/tests/test_cms.py
new file mode 100644
index 0000000..a6746fa
--- /dev/null
+++ b/tests/test_cms.py
@@ -0,0 +1,903 @@
+# 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 cms, 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 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())
+
+ 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'
+ 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(
+ b'This is the message to encapsulate in PKCS#7/CMS\n',
+ compressed_data.decompressed
+ )
+
+ 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())
+
+ 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
+ )
+ data = compressed_data['encap_content_info']['content'].native
+ 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:
+ 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_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
+ )
+ 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'
+ 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
+ )
+
+ 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(
+ util.OrderedDict([
+ (
+ 'issuer',
+ util.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'
+ 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(
+ 'data',
+ encrypted_content_info['content_type'].native
+ )
+ self.assertEqual(
+ '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
+ )
+ 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'
+ 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(
+ 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(
+ [
+ util.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(
+ util.OrderedDict([
+ (
+ 'issuer',
+ util.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(
+ util.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=util.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'
+ b'\x03\x20\x79\x34\x9D\x62\xAA\x20\xA5\x1D',
+ signed_attrs[2]['values'][0].native
+ )
+
+ self.assertEqual(
+ util.OrderedDict([
+ ('algorithm', 'rsassa_pkcs1v15'),
+ ('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'
+ 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
+ )
+
+ 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(
+ [
+ util.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(
+ util.OrderedDict([
+ (
+ 'issuer',
+ util.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(
+ util.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=util.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'
+ b'\xEE\xA1\x12\x90\x96\x4F\x94\xAE\xDB\x79',
+ signed_attrs[2]['values'][0].native
+ )
+
+ self.assertEqual(
+ util.OrderedDict([
+ ('algorithm', 'rsassa_pkcs1v15'),
+ ('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'
+ 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
+ )
+
+ 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']
+ 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:
+ 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(
+ [
+ util.OrderedDict([
+ ('algorithm', 'sha256'),
+ ('parameters', None),
+ ])
+ ],
+ signed_data['digest_algorithms'].native
+ )
+ self.assertEqual(
+ 'digested_data',
+ encap_content_info['content_type'].native
+ )
+ self.assertEqual(
+ util.OrderedDict([
+ ('version', 'v0'),
+ (
+ 'digest_algorithm',
+ util.OrderedDict([
+ ('algorithm', 'sha1'),
+ ('parameters', None),
+ ])
+ ),
+ (
+ 'encap_content_info',
+ util.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(
+ util.OrderedDict([
+ (
+ 'issuer',
+ util.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(
+ util.OrderedDict([
+ ('algorithm', 'sha256'),
+ ('parameters', None),
+ ]),
+ signer['digest_algorithm'].native
+ )
+
+ signed_attrs = signer['signed_attrs']
+
+ self.assertEqual(
+ 0,
+ len(signed_attrs)
+ )
+
+ self.assertEqual(
+ util.OrderedDict([
+ ('algorithm', 'rsassa_pkcs1v15'),
+ ('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'
+ 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())
+
+ 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(
+ [
+ util.OrderedDict([
+ ('algorithm', 'sha256'),
+ ('parameters', None),
+ ])
+ ],
+ signed_data['digest_algorithms'].native
+ )
+ self.assertEqual(
+ 'digested_data',
+ encap_content_info['content_type'].native
+ )
+ self.assertEqual(
+ util.OrderedDict([
+ ('version', 'v0'),
+ (
+ 'digest_algorithm',
+ util.OrderedDict([
+ ('algorithm', 'sha1'),
+ ('parameters', None),
+ ])
+ ),
+ (
+ 'encap_content_info',
+ util.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(
+ util.OrderedDict([
+ (
+ 'issuer',
+ util.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(
+ util.OrderedDict([
+ ('algorithm', 'sha256'),
+ ('parameters', None),
+ ]),
+ signer['digest_algorithm'].native
+ )
+
+ signed_attrs = signer['signed_attrs']
+
+ self.assertEqual(
+ 0,
+ len(signed_attrs)
+ )
+
+ self.assertEqual(
+ util.OrderedDict([
+ ('algorithm', 'rsassa_pkcs1v15'),
+ ('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'
+ 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_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_core.py b/tests/test_core.py
new file mode 100644
index 0000000..94fd8aa
--- /dev/null
+++ b/tests/test_core.py
@@ -0,0 +1,777 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import os
+from datetime import datetime
+
+from asn1crypto import core, util
+
+from .unittest_data import data_decorator, data
+from ._unittest_compat import patch
+
+patch()
+
+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',
+ }
+
+
+class SequenceOfInts(core.SequenceOf):
+ _child_spec = core.Integer
+
+
+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,
+ }
+
+
+class CopySeq(core.Sequence):
+ _fields = [
+ ('name', core.UTF8String),
+ ('pair', Seq),
+ ]
+
+
+class Enum(core.Enumerated):
+ _map = {
+ 0: 'a',
+ 1: 'b',
+ }
+
+
+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}),
+ ('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}),
+ ('three', core.Integer, {'tag_type': 'explicit', 'tag': 2}),
+ ]
+
+
+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}),
+ ]
+
+
+class ExplicitField(core.Sequence):
+ _fields = [
+ ('field', NumChoice, {'tag_type': 'explicit', 'tag': 0}),
+ ]
+
+
+class ExplicitFieldOldApi(core.Sequence):
+ _fields = [
+ ('field', NumChoiceOldApi, {'explicit': 0}),
+ ]
+
+
+class SetTest(core.Set):
+ _fields = [
+ ('two', core.Integer, {'tag_type': 'implicit', 'tag': 2}),
+ ('one', core.Integer, {'tag_type': 'implicit', 'tag': 1}),
+ ]
+
+
+class SetTestOldApi(core.Set):
+ _fields = [
+ ('two', core.Integer, {'implicit': 2}),
+ ('one', core.Integer, {'implicit': 1}),
+ ]
+
+
+class SetOfTest(core.SetOf):
+ _child_spec = core.Integer
+
+
+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',
+ '4.5.6': 'def',
+ }
+
+class ApplicationTaggedInteger(core.Integer):
+ # This class attribute may be a 2-element tuple of integers,
+ # or a tuple of 2-element tuple of integers. The first form
+ # will be converted to the second form the first time an
+ # object of this type is constructed.
+ explicit = ((1, 10), )
+
+
+class ApplicationTaggedInner(core.Sequence):
+ """
+ TESTCASE DEFINITIONS EXPLICIT TAGS ::=
+ BEGIN
+
+ INNERSEQ ::= SEQUENCE {
+ innernumber [21] INTEGER
+ }
+
+ INNER ::= [APPLICATION 20] INNERSEQ
+ """
+
+ explicit = (1, 20)
+
+ _fields = [
+ ('innernumber', core.Integer, {'explicit': 21}),
+ ]
+
+
+class ApplicationTaggedOuter(core.Sequence):
+ """
+ OUTERSEQ ::= SEQUENCE {
+ outernumber [11] INTEGER,
+ inner [12] INNER
+ }
+
+ OUTER ::= [APPLICATION 10] OUTERSEQ
+ END
+ """
+
+ explicit = (1, 10)
+
+ _fields = [
+ ('outernumber', core.Integer, {'explicit': 11}),
+ ('inner', ApplicationTaggedInner, {'explicit': 12}),
+ ]
+
+
+@data_decorator
+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())
+
+ @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, 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),
+ (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)
+
+ @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)
+
+ @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 (
+ ('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))
+
+ @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'),
+ ((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')
+ 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_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_load(self):
+ i = core.load(b'\x02\x01\x00')
+ 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')
+
+ @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)
+
+ 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)
+ 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()
+ named[0] = True
+ self.assertEqual(False, named[2])
+ self.assertEqual(False, named[1])
+ self.assertEqual(True, named[0])
+
+ @staticmethod
+ def mapped_bit_string_info():
+ return (
+ (
+ (0, 1, 1),
+ b'\x03\x02\x05\x60',
+ set(['one', 'two'])
+ ),
+ (
+ (0,),
+ b'\x03\x01\x00',
+ set()
+ ),
+ (
+ set(['one', 'two']),
+ b'\x03\x02\x05\x60',
+ set(['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_unset_bit(self):
+ named = NamedBits(set(['one', 'two']))
+ named['one'] = False
+ self.assertEqual(True, named['two'])
+ self.assertEqual(set(['two']), 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(set(['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)
+
+ 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}})
+ # 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']))
+ 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)
+
+ 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())
+
+ 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)
+ 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_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)
+ 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)
+ 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, explicit=0)
+ b = a.untag()
+ self.assertNotEqual(id(a), id(b))
+ 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})
+
+ 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))
+ self.assertEqual('one', choice.name)
+
+ with self.assertRaises(ValueError):
+ SeqChoice(('one',))
+
+ 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, explicit=2)
+ choice = NumChoice(
+ name='three',
+ value=core.Integer(200, explicit=1)
+ )
+ self.assertEqual(correct.dump(), choice.dump())
+ 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}})
+ 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'])
+
+ 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',
+ '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())
+
+ 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')
+
+ 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())
+
+ 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)
+ # 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'
+ 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_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(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'
+ 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(b'\x04\x01\x01', a.__bytes__())
+ 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)
+ # 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'
+ 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))
+ # 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'
+ 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)
+ 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'
+ 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)
+ # 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(((1, 10),), ati.explicit)
+ 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))
+
+ def test_required_field(self):
+ with self.assertRaisesRegexp(ValueError, '"id" is missing from structure'):
+ Seq({'value': core.Integer(5)}).dump()
+
+ 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(((1, 10),), ato.explicit)
+ self.assertEqual(0, ato.class_)
+ self.assertEqual(16, ato.tag)
+ self.assertEqual(1, ato.method)
+
+ onum = ato['outernumber']
+ self.assertEqual(((2, 11),), onum.explicit)
+ self.assertEqual(0, onum.class_)
+ self.assertEqual(2, onum.tag)
+ self.assertEqual(0, onum.method)
+ self.assertEqual(23, onum.native)
+
+ ati = ato['inner']
+ self.assertEqual(((1, 20), (2, 12)), ati.explicit)
+ self.assertEqual(0, ati.class_)
+ self.assertEqual(16, ati.tag)
+ self.assertEqual(util.OrderedDict([('innernumber', 42)]), ati.native)
+
+ inum = ati['innernumber']
+ self.assertEqual(((2, 21),), inum.explicit)
+ self.assertEqual(0, inum.class_)
+ self.assertEqual(2, inum.tag)
+ self.assertEqual(0, inum.method)
+ self.assertEqual(42, inum.native)
+
+ self.assertEqual(der, ato.dump(force=True))
diff --git a/tests/test_crl.py b/tests/test_crl.py
new file mode 100644
index 0000000..f030635
--- /dev/null
+++ b/tests/test_crl.py
@@ -0,0 +1,42 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import sys
+import os
+
+from asn1crypto import crl
+
+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 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_csr.py b/tests/test_csr.py
new file mode 100644
index 0000000..b950971
--- /dev/null
+++ b/tests/test_csr.py
@@ -0,0 +1,141 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import sys
+import os
+
+from asn1crypto import csr, util
+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 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(
+ util.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(
+ util.OrderedDict([
+ ('algorithm', 'rsa'),
+ ('parameters', None),
+ ]),
+ cri['subject_pk_info']['algorithm'].native
+ )
+ self.assertEqual(
+ 24141757533938720807477509823483015516687050697622322097001928034085434547050399731881871694642845241206788286795830006142635608141713689209738431462004600429798152826994774062467402648660593454536565119527837471261495586474194846971065722669734666949739228862107500673350843489920495869942508240779131331715037662761414997889327943217889802893638175792326783316531272170879284118280173511200768884738639370318760377047837471530387161553030663446359575963736475504659902898072137674205021477968813148345198711103071746476009234601299344030395455052526948041544669303473529511160643491569274897838845918784633403435929, # noqa
+ 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(
+ util.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(
+ util.OrderedDict([
+ ('algorithm', 'rsa'),
+ ('parameters', None),
+ ]),
+ cri['subject_pk_info']['algorithm'].native
+ )
+ self.assertEqual(
+ 24242772097421005542208203320016703216069397492249392798445262959177221203301502279838173203064357049006693856302147277901773700963054800321566171864477088538775137040886151390015408166478059887940234405152693144166884492162723776487601158833605063151869850475289834250129252480954724818505034734280077580919995584375189497366089269712298471489896645221362055822887892887126082288043106492130176555423739906252380437817155678204772878611148787130925042126257401487070141904017757131876614711613405231164930930771261221451019736883391322299033324412671768599041417705072563016759224152503535867541947310239343903761461, # noqa
+ cri['subject_pk_info']['public_key'].parsed['modulus'].native
+ )
+ self.assertEqual(
+ 65537,
+ cri['subject_pk_info']['public_key'].parsed['public_exponent'].native
+ )
+ self.assertEqual(
+ [
+ util.OrderedDict([
+ ('type', 'extension_request'),
+ (
+ 'values',
+ [
+ [
+ util.OrderedDict([
+ ('extn_id', 'basic_constraints'),
+ ('critical', False),
+ (
+ 'extn_value',
+ util.OrderedDict([
+ ('ca', False),
+ ('path_len_constraint', None),
+ ])
+ ),
+ ]),
+ util.OrderedDict([
+ ('extn_id', 'key_usage'),
+ ('critical', False),
+ (
+ 'extn_value',
+ set(['digital_signature', 'non_repudiation', 'key_encipherment']),
+ ),
+ ])
+ ]
+ ]
+ ),
+ ]),
+ ],
+ cri['attributes'].native
+ )
diff --git a/tests/test_keys.py b/tests/test_keys.py
new file mode 100644
index 0000000..98f9d30
--- /dev/null
+++ b/tests/test_keys.py
@@ -0,0 +1,550 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import os
+
+from asn1crypto import keys, core, util
+
+from .unittest_data import data_decorator, data
+from ._unittest_compat import patch
+
+patch()
+
+tests_root = os.path.dirname(__file__)
+fixtures_dir = os.path.join(tests_root, 'fixtures')
+
+
+@data_decorator
+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, # noqa
+ key['modulus'].native
+ )
+ self.assertEqual(
+ 65537,
+ key['public_exponent'].native
+ )
+ self.assertEqual(
+ 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, # noqa
+ key['private_exponent'].native
+ )
+ self.assertEqual(
+ 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, # noqa
+ key['prime1'].native
+ )
+ self.assertEqual(
+ 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, # noqa
+ key['prime2'].native
+ )
+ self.assertEqual(
+ 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, # noqa
+ key['exponent1'].native
+ )
+ self.assertEqual(
+ 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, # noqa
+ key['exponent2'].native
+ )
+ self.assertEqual(
+ 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, # noqa
+ 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, # noqa
+ key[1].native
+ )
+ self.assertEqual(
+ 65537,
+ key[2].native
+ )
+ self.assertEqual(
+ 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, # noqa
+ key[3].native
+ )
+ self.assertEqual(
+ 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, # noqa
+ key[4].native
+ )
+ self.assertEqual(
+ 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, # noqa
+ key[5].native
+ )
+ self.assertEqual(
+ 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, # noqa
+ key[6].native
+ )
+ self.assertEqual(
+ 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, # noqa
+ key[7].native
+ )
+ self.assertEqual(
+ 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, # noqa
+ key[8].native
+ )
+
+ with self.assertRaises(KeyError):
+ 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:
+ key = keys.DSAPrivateKey.load(f.read())
+
+ self.assertEqual(
+ 0,
+ key['version'].native
+ )
+ self.assertEqual(
+ 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857, # noqa
+ key['p'].native
+ )
+ self.assertEqual(
+ 71587850165936478337655415373676526523562874562337607790945426056266440596923,
+ key['q'].native
+ )
+ self.assertEqual(
+ 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202, # noqa
+ key['g'].native
+ )
+ self.assertEqual(
+ 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, # noqa
+ 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(
+ util.OrderedDict([
+ ('version', 'ecdpVer1'),
+ (
+ 'field_id',
+ util.OrderedDict([
+ ('field_type', 'prime_field'),
+ ('parameters', 115792089210356248762697446949407573530086143415290314195533631308867097853951)
+ ])
+ ),
+ (
+ 'curve',
+ util.OrderedDict([
+ (
+ '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'
+ 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',
+ 115792089210356248762697446949407573529996955224135760342422259061068512044369
+ ),
+ ('cofactor', 1),
+ ('hash', None),
+ ]),
+ 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'
+ 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
+ )
+
+ 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, # noqa
+ 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, # noqa
+ 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, # noqa
+ key['modulus'].native
+ )
+ self.assertEqual(
+ 65537,
+ key['public_exponent'].native
+ )
+ self.assertEqual(
+ 9979296894007354255484849917690758820642557661666429934720496335307525025035760937280030384204921358841911348590147260206368632524783497961763507098900120579828036636037636859350155169644276779450131617753331883188587268575077705380671279069284616924232052795766448946873233783789819627790465470123569125678598045748629782316184667685110712273519313310937077963014676074966877849272992367512921997850502687035430136911690081438185238817835171119161013656103255853961444458012340770881411877222316871444386486841632394098449378506206645681449475758856053641206175913163492821894709155329556294181613669730336931773953, # noqa
+ key['private_exponent'].native
+ )
+ self.assertEqual(
+ 166647390172913547327716251713919741459272587597255782032652236515036001974461323181989715320980256918783849999012066159723695368018857439366733087649658067943054926668058248612521531843495934099419046629521378187012692776633310821178903471282399402138521150042979117060141563972064613977168440186057796106743, # noqa
+ key['prime1'].native
+ )
+ self.assertEqual(
+ 143440533284701431115857974625778819273481773744021067505004499855263691219807413711274106281992493130281690570930126889424222979194828112331057105055939481042398415265558356642606674863401518188395487842736496447305100392269029249928750130190700690239916449523411304928539660679996452045625683879143320460249, # noqa
+ key['prime2'].native
+ )
+ self.assertEqual(
+ 109414079859473229289779858629449815451592843305649008118818271892297238643195390011716060554289324731958287404176117228233683079641781234394481865640434212819044363330635799312574408253258259431525735957118503776629524657609514187779529692628749620437591384488141789034909003405007374076072765197764330205487, # noqa
+ key['exponent1'].native
+ )
+ self.assertEqual(
+ 39361498857013145813625735320048312950154816653378623953034178027634194773898965899927575680536994315500952488328843279054659597751495930118280223039291020752651068863936425009698924893471060669547041417272275998418220630400632040385105243470857091616562513209775072216226822370097138922876120342440353924609, # noqa
+ key['exponent2'].native
+ )
+ self.assertEqual(
+ 109796662729796355370195012683418958273962986010546166376879205603219777065076464250440708895625560840314914603409569660942497623175203159192440744329997446961447023349392064212216532091513743978251892999757210494211477167363008686808094766092274115601607346901935491774285446659775729268493276413171032997893, # noqa
+ key['coefficient'].native
+ )
+ self.assertEqual(
+ None,
+ key['other_prime_infos'].native
+ )
+
+ self.assertEqual(
+ None,
+ key_info['attributes'].native
+ )
+
+ @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)
+
+ @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)
+
+ @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
+ ),
+ )
+
+ @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:
+ 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)
+
+ @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)
+
+ @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:
+ 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)
+
+ @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',
+ ),
+ )
+
+ @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):
+ 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):
+ 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',
+ util.OrderedDict([
+ ('version', 'ecdpVer1'),
+ (
+ 'field_id',
+ util.OrderedDict([
+ ('field_type', 'prime_field'),
+ ('parameters', 115792089210356248762697446949407573530086143415290314195533631308867097853951)
+ ])
+ ),
+ (
+ 'curve',
+ util.OrderedDict([
+ (
+ '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'
+ 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',
+ 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)
diff --git a/tests/test_ocsp.py b/tests/test_ocsp.py
new file mode 100644
index 0000000..c3492c1
--- /dev/null
+++ b/tests/test_ocsp.py
@@ -0,0 +1,155 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import sys
+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
+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=util.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=util.timezone.utc),
+ single_response['this_update'].native
+ )
+ self.assertEqual(
+ datetime(2015, 5, 29, 16, 24, 8, tzinfo=util.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_parser.py b/tests/test_parser.py
new file mode 100644
index 0000000..b661c33
--- /dev/null
+++ b/tests/test_parser.py
@@ -0,0 +1,62 @@
+# 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_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)
+
+ 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')
diff --git a/tests/test_pem.py b/tests/test_pem.py
new file mode 100644
index 0000000..34a1498
--- /dev/null
+++ b/tests/test_pem.py
@@ -0,0 +1,158 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import sys
+import os
+
+from asn1crypto import pem, util
+
+from .unittest_data import data_decorator, data
+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')
+
+
+@data_decorator
+class PEMTests(unittest.TestCase):
+
+ @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))
+
+ @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')
+ ])
+ ),
+ )
+
+ @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)
+
+ 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:
+ 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)
+
+ 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')
diff --git a/tests/test_pkcs12.py b/tests/test_pkcs12.py
new file mode 100644
index 0000000..ffd6398
--- /dev/null
+++ b/tests/test_pkcs12.py
@@ -0,0 +1,118 @@
+# 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
+ )
+
+ 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
+ )
diff --git a/tests/test_tsp.py b/tests/test_tsp.py
new file mode 100644
index 0000000..ee6a2e1
--- /dev/null
+++ b/tests/test_tsp.py
@@ -0,0 +1,245 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+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')
+
+
+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=util.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(
+ util.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(
+ util.OrderedDict([
+ (
+ 'issuer',
+ util.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=util.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(
+ util.OrderedDict([
+ (
+ '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'
+ ),
+ ('issuer_serial', None),
+ ])
+ ]
+ ),
+ (
+ 'policies',
+ None
+ )
+ ]),
+ signed_attrs[3]['values'][0].native
+ )
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..a3f3e6e
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,135 @@
+# 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):
+ 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'))
+ 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'))
+ 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):
+ 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())
+
+ 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')
+ )
diff --git a/tests/test_x509.py b/tests/test_x509.py
new file mode 100644
index 0000000..f06ab9b
--- /dev/null
+++ b/tests/test_x509.py
@@ -0,0 +1,3392 @@
+# coding: utf-8
+from __future__ import unicode_literals, division, absolute_import, print_function
+
+import unittest
+import sys
+import os
+from datetime import datetime
+
+from asn1crypto import x509, core, pem, util
+
+from .unittest_data import data_decorator, data
+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')
+
+
+@data_decorator
+class X509Tests(unittest.TestCase):
+
+ 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)
+
+ @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))
+
+ @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'
+ 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')
+ 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)
+
+ 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())
+
+ @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)
+
+ 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 (
+ (
+ '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)
+
+ 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 (
+ (
+ '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)
+
+ @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)
+
+ 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 (
+ (
+ 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'
+ })
+ ),
+ (
+ True,
+ x509.Name.build({
+ 'common_name': 'Will Bond'
+ }),
+ x509.Name.build({
+ '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({
+ 'common_name': 'Will Bond',
+ '0.9.2342.19200300.100.1.1': 'wbond'
+ }),
+ x509.Name.build({
+ 'common_name': 'Will Bond',
+ }),
+ ),
+ (
+ 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)
+
+ 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[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',
+ 'state_or_province_name': 'Massachusetts',
+ 'common_name': 'Will Bond'
+ },
+ use_printable=True
+ )
+ 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')
+ 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_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_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'])
+
+ @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)
+
+ @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'])
+ ),
+ )
+
+ @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)
+
+ @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'
+ ),
+ )
+
+ @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)
+
+ @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',
+ set(['digital_signature', 'key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'geotrust_certs/GeoTrust_Primary_CA.crt',
+ set(['key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt',
+ set(['key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'geotrust_certs/codex.crt',
+ set(['digital_signature', 'key_encipherment'])
+ ),
+ (
+ 'lets_encrypt/isrgrootx1.pem',
+ set(['key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx1.pem',
+ set(['digital_signature', 'key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx2.pem',
+ set(['digital_signature', 'key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'globalsign_example_keys/IssuingCA-der.cer',
+ set(['key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'globalsign_example_keys/rootCA.cer',
+ set(['key_cert_sign', 'crl_sign'])
+ ),
+ (
+ 'globalsign_example_keys/SSL1.cer',
+ set(['digital_signature', 'key_encipherment'])
+ ),
+ (
+ 'globalsign_example_keys/SSL2.cer',
+ set(['digital_signature', 'key_encipherment'])
+ ),
+ (
+ 'globalsign_example_keys/SSL3.cer',
+ set(['digital_signature', 'key_encipherment'])
+ ),
+ )
+
+ @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)
+
+ @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)
+
+ @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
+ ),
+ )
+
+ @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)
+
+ @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',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'geotrust_certs/codex.crt',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://gm.symcb.com/gm.crl']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'lets_encrypt/isrgrootx1.pem',
+ None
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx1.pem',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx2.pem',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/IssuingCA-der.cer',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]),
+ (
+ 'globalsign_example_keys/rootCA.cer',
+ [
+ util.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)
+
+ @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',
+ [
+ util.OrderedDict([
+ ('policy_identifier', 'any_policy'),
+ (
+ 'policy_qualifiers',
+ [
+ util.OrderedDict([
+ ('policy_qualifier_id', 'certification_practice_statement'),
+ ('qualifier', 'https://www.geotrust.com/resources/cps')
+ ])
+ ]
+ )
+ ])
+ ]
+ ),
+ (
+ 'geotrust_certs/codex.crt',
+ [
+ util.OrderedDict([
+ ('policy_identifier', '1.3.6.1.4.1.14370.1.6'),
+ (
+ 'policy_qualifiers',
+ [
+ util.OrderedDict([
+ ('policy_qualifier_id', 'certification_practice_statement'),
+ ('qualifier', 'https://www.geotrust.com/resources/repository/legal')
+ ]),
+ util.OrderedDict([
+ ('policy_qualifier_id', 'user_notice'),
+ (
+ 'qualifier',
+ util.OrderedDict([
+ ('notice_ref', None),
+ ('explicit_text', 'https://www.geotrust.com/resources/repository/legal')
+ ])
+ )
+ ])
+ ]
+ )
+ ])
+ ]
+ ),
+ (
+ 'lets_encrypt/isrgrootx1.pem',
+ None
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx1.pem',
+ [
+ util.OrderedDict([
+ ('policy_identifier', '2.23.140.1.2.1'),
+ ('policy_qualifiers', None)
+ ]),
+ util.OrderedDict([
+ ('policy_identifier', '1.3.6.1.4.1.44947.1.1.1'),
+ (
+ 'policy_qualifiers',
+ [
+ util.OrderedDict([
+ ('policy_qualifier_id', 'certification_practice_statement'),
+ ('qualifier', 'http://cps.root-x1.letsencrypt.org')
+ ])
+ ]
+ )
+ ])
+ ]
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx2.pem',
+ [
+ util.OrderedDict([
+ ('policy_identifier', '2.23.140.1.2.1'),
+ ('policy_qualifiers', None)
+ ]),
+ util.OrderedDict([
+ ('policy_identifier', '1.3.6.1.4.1.44947.1.1.1'),
+ (
+ 'policy_qualifiers',
+ [
+ util.OrderedDict([
+ ('policy_qualifier_id', 'certification_practice_statement'),
+ ('qualifier', 'http://cps.root-x1.letsencrypt.org')
+ ])
+ ]
+ )
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/IssuingCA-der.cer',
+ [
+ util.OrderedDict([
+ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'),
+ (
+ 'policy_qualifiers',
+ [
+ util.OrderedDict([
+ ('policy_qualifier_id', 'certification_practice_statement'),
+ ('qualifier', 'https://www.globalsign.com/repository/')
+ ])
+ ]
+ )
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/rootCA.cer',
+ None
+ ),
+ (
+ 'globalsign_example_keys/SSL1.cer',
+ [
+ util.OrderedDict([
+ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'),
+ (
+ 'policy_qualifiers',
+ [
+ util.OrderedDict([
+ ('policy_qualifier_id', 'certification_practice_statement'),
+ ('qualifier', 'https://www.globalsign.com/repository/')
+ ])
+ ]
+ )
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/SSL2.cer',
+ [
+ util.OrderedDict([
+ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'),
+ (
+ 'policy_qualifiers',
+ [
+ util.OrderedDict([
+ ('policy_qualifier_id', 'certification_practice_statement'),
+ ('qualifier', 'https://www.globalsign.com/repository/')
+ ])
+ ]
+ )
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/SSL3.cer',
+ [
+ util.OrderedDict([
+ ('policy_identifier', '1.3.6.1.4.1.4146.1.60'),
+ (
+ 'policy_qualifiers',
+ [
+ util.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)
+
+ @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)
+
+ @staticmethod
+ def authority_key_identifier_value_info():
+ return (
+ (
+ 'keys/test-der.crt',
+ util.OrderedDict([
+ ('key_identifier', b'\xbeB\x85=\xcc\xff\xe3\xf9(\x02\x8f~XV\xb4\xfd\x03\\\xeaK'),
+ (
+ 'authority_cert_issuer',
+ [
+ util.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)
+ ])
+ ),
+ (
+ 'keys/test-inter-der.crt',
+ 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)
+ ])
+ ),
+ (
+ 'keys/test-third-der.crt',
+ 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)
+ ])
+ ),
+ (
+ 'geotrust_certs/GeoTrust_Universal_CA.crt',
+ 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)
+ ])
+ ),
+ (
+ 'geotrust_certs/GeoTrust_Primary_CA.crt',
+ None
+ ),
+ (
+ 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt',
+ 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)
+ ])
+ ),
+ (
+ 'geotrust_certs/codex.crt',
+ 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)
+ ])
+ ),
+ (
+ 'lets_encrypt/isrgrootx1.pem',
+ None
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx1.pem',
+ 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)
+ ])
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx2.pem',
+ 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)
+ ])
+ ),
+ (
+ 'globalsign_example_keys/IssuingCA-der.cer',
+ util.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',
+ 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)
+ ])
+ ),
+ (
+ 'globalsign_example_keys/SSL2.cer',
+ 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)
+ ])
+ ),
+ (
+ 'globalsign_example_keys/SSL3.cer',
+ 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)
+ ])
+ ),
+ )
+
+ @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)
+
+ @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)
+
+ @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)
+
+ @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',
+ [
+ util.OrderedDict([
+ ('access_method', 'ocsp'),
+ ('access_location', 'http://g2.symcb.com')
+ ])
+ ]
+ ),
+ (
+ 'geotrust_certs/codex.crt',
+ [
+ util.OrderedDict([
+ ('access_method', 'ocsp'),
+ ('access_location', 'http://gm.symcd.com')
+ ]),
+ util.OrderedDict([
+ ('access_method', 'ca_issuers'),
+ ('access_location', 'http://gm.symcb.com/gm.crt')
+ ]),
+ ]
+ ),
+ (
+ 'lets_encrypt/isrgrootx1.pem',
+ None
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx1.pem',
+ [
+ util.OrderedDict([
+ ('access_method', 'ocsp'),
+ ('access_location', 'http://ocsp.root-x1.letsencrypt.org/')
+ ]),
+ util.OrderedDict([
+ ('access_method', 'ca_issuers'),
+ ('access_location', 'http://cert.root-x1.letsencrypt.org/')
+ ])
+ ]
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx2.pem',
+ [
+ util.OrderedDict([
+ ('access_method', 'ocsp'),
+ ('access_location', 'http://ocsp.root-x1.letsencrypt.org/')
+ ]),
+ util.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',
+ [
+ util.OrderedDict([
+ ('access_method', 'ocsp'),
+ ('access_location', 'http://ocsp.exampleovca.com/')
+ ]),
+ util.OrderedDict([
+ ('access_method', 'ca_issuers'),
+ ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt')
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/SSL2.cer',
+ [
+ util.OrderedDict([
+ ('access_method', 'ocsp'),
+ ('access_location', 'http://ocsp.exampleovca.com/')
+ ]),
+ util.OrderedDict([
+ ('access_method', 'ca_issuers'),
+ ('access_location', 'http://secure.globalsign.com/cacert/trustrootcatg2.crt')
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/SSL3.cer',
+ [
+ util.OrderedDict([
+ ('access_method', 'ocsp'),
+ ('access_location', 'http://ocsp.exampleovca.com/')
+ ]),
+ util.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)
+
+ @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)
+
+ @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 (
+ (
+ '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')
+ def key_identifier(self, relative_path, key_identifier):
+ cert = self._load_cert(relative_path)
+ self.assertEqual(key_identifier, cert.key_identifier)
+
+ @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'
+ 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')
+ def issuer_serial(self, relative_path, issuer_serial):
+ cert = self._load_cert(relative_path)
+ self.assertEqual(issuer_serial, cert.issuer_serial)
+
+ @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)
+
+ @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'
+ 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')
+ 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)
+
+ @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)
+
+ @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',
+ []
+ ),
+ (
+ 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://g1.symcb.com/GeoTrustPCA.crl']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'geotrust_certs/codex.crt',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://gm.symcb.com/gm.crl']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'lets_encrypt/isrgrootx1.pem',
+ []
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx1.pem',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx2.pem',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://crl.root-x1.letsencrypt.org']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/IssuingCA-der.cer',
+ [
+ util.OrderedDict([
+ ('distribution_point', ['http://crl.globalsign.com/gs/trustrootcatg2.crl']),
+ ('reasons', None),
+ ('crl_issuer', None)
+ ])
+ ]
+ ),
+ (
+ 'globalsign_example_keys/rootCA.cer',
+ [
+ util.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_distribution_points_info')
+ def crl_distribution_points(self, relative_path, crl_distribution_point):
+ cert = self._load_cert(relative_path)
+ points = [point.native for point in cert.crl_distribution_points]
+ self.assertEqual(crl_distribution_point, points)
+
+ @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']
+ ),
+ )
+
+ @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)
+
+ @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)
+
+ @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)
+
+ @staticmethod
+ def self_signed_info():
+ return (
+ (
+ 'keys/test-der.crt',
+ 'maybe'
+ ),
+ (
+ 'keys/test-inter-der.crt',
+ 'no'
+ ),
+ (
+ 'keys/test-third-der.crt',
+ 'no'
+ ),
+ (
+ 'geotrust_certs/GeoTrust_Universal_CA.crt',
+ 'maybe'
+ ),
+ (
+ 'geotrust_certs/GeoTrust_Primary_CA.crt',
+ 'maybe'
+ ),
+ (
+ 'geotrust_certs/GeoTrust_EV_SSL_CA_-_G4.crt',
+ 'no'
+ ),
+ (
+ 'geotrust_certs/codex.crt',
+ 'no'
+ ),
+ (
+ 'lets_encrypt/isrgrootx1.pem',
+ 'maybe'
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx1.pem',
+ 'no'
+ ),
+ (
+ 'lets_encrypt/letsencryptauthorityx2.pem',
+ 'no'
+ ),
+ (
+ 'globalsign_example_keys/IssuingCA-der.cer',
+ 'no'
+ ),
+ (
+ 'globalsign_example_keys/rootCA.cer',
+ 'maybe'
+ ),
+ (
+ '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)
+
+ @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')
+
+ 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(
+ util.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=util.timezone.utc),
+ validity['not_before'].native
+ )
+ self.assertEqual(
+ datetime(2025, 5, 3, 14, 37, 16, tzinfo=util.timezone.utc),
+ validity['not_after'].native
+ )
+ self.assertEqual(
+ util.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, # noqa
+ 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.Void
+ )
+ self.assertEqual(
+ None,
+ tbs_certificate['subject_unique_id'].native
+ )
+ self.assertIsInstance(
+ tbs_certificate['subject_unique_id'],
+ core.Void
+ )
+
+ self.maxDiff = None
+ for extension in extensions:
+ self.assertIsInstance(
+ extension,
+ x509.Extension
+ )
+ self.assertEqual(
+ [
+ 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'),
+ ]),
+ util.OrderedDict([
+ ('extn_id', 'authority_key_identifier'),
+ ('critical', False),
+ (
+ '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'
+ ),
+ (
+ 'authority_cert_issuer',
+ [
+ util.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),
+ ])
+ ),
+ ]),
+ util.OrderedDict([
+ ('extn_id', 'basic_constraints'),
+ ('critical', False),
+ (
+ 'extn_value',
+ util.OrderedDict([
+ ('ca', True),
+ ('path_len_constraint', None)
+ ])
+ ),
+ ]),
+ ],
+ extensions.native
+ )
+
+ def test_parse_dsa_certificate(self):
+ cert = self._load_cert('keys/test-dsa-der.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']
+ 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(
+ util.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=util.timezone.utc),
+ validity['not_before'].native
+ )
+ self.assertEqual(
+ datetime(2025, 5, 17, 13, 9, 2, tzinfo=util.timezone.utc),
+ validity['not_after'].native
+ )
+ self.assertEqual(
+ util.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(
+ util.OrderedDict([
+ ('p', 4511743893397705393934377497936985478231822206263141826261443300639402520800626925517264115785551703273809312112372693877437137848393530691841757974971843334497076835630893064661599193178307024379015589119302113551197423138934242435710226975119594589912289060014025377813473273600967729027125618396732574594753039493158066887433778053086408525146692226448554390096911703556213619406958876388642882534250747780313634767409586007581976273681005928967585750017105562145167146445061803488570714706090280814293902464230717946651489964409785146803791743658888866280873858000476717727810363942159874283767926511678640730707887895260274767195555813448140889391762755466967436731106514029224490921857229134393798015954890071206959203407845438863870686180087606429828973298318856683615900474921310376145478859687052812749087809700610549251964102790514588562086548577933609968589710807989944739877028770343142449461177732058649962678857), # noqa
+ ('q', 71587850165936478337655415373676526523562874562337607790945426056266440596923),
+ ('g', 761437146067908309288345767887973163494473925243194806582679580640442238588269326525839153095505341738937595419375068472941615006110237832663093084973431440436421580371384720052414080562019831325744042316268714195397974084616335082272743706567701546951285088540646372701485690904535540223121118329044403681933304838754517522024738251994717369464179515923093116622352823578284891812676662979104509631349201801577889230316128523885862472086364717411346341249139971907827526291913249445756671582283459372536334490171231311487207683108274785825764378203622999309355578169139646003751751448501475767709869676880946562283552431757983801739671783678927397420797147373441051876558068212062253171347849380506793433921881336652424898488378657239798694995315456959568806256079056461448199493507273882763491729787817044805150879660784158902456811649964987582162907020243296662602990514615480712948126671999033658064244112238138589732202), # noqa
+ ]),
+ subject_public_key_algorithm['parameters'].native
+ )
+ self.assertEqual(
+ 934231235067929794039535952071098031636053793876274937162425423023735221571983693370780054696865229184537343792766496068557051933738826401423094028670222490622041397241325320965905259541032379046252395145258594355589801644789631904099105867133976990593761395721476198083091062806327384261369876465927159169400428623265291958463077792777155465482611741502621885386691681062128487785344975981628995609792181581218570320181053055516069553767918513262908069925035292416868414952256645902605335068760774106734518308281769128146479819566784704033671969858507248124850451414380441279385481154336362988505436125981975735568289420374790767927084033441728922597082155884801013899630856890463962357814273014111039522903328923758417820349377075487103441305806369234738881875734407495707878637895190993370257589211331043479113328811265005530361001980539377903738453549980082795009589559114091215518866106998956304437954236070776810740036, # noqa
+ subject_public_key.native
+ )
+ self.assertEqual(
+ None,
+ tbs_certificate['issuer_unique_id'].native
+ )
+ self.assertIsInstance(
+ tbs_certificate['issuer_unique_id'],
+ core.Void
+ )
+ self.assertEqual(
+ None,
+ tbs_certificate['subject_unique_id'].native
+ )
+ self.assertIsInstance(
+ tbs_certificate['subject_unique_id'],
+ core.Void
+ )
+
+ self.maxDiff = None
+ for extension in extensions:
+ self.assertIsInstance(
+ extension,
+ x509.Extension
+ )
+ self.assertEqual(
+ [
+ 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'),
+ ]),
+ util.OrderedDict([
+ ('extn_id', 'authority_key_identifier'),
+ ('critical', False),
+ (
+ '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'
+ ),
+ ('authority_cert_issuer', None),
+ ('authority_cert_serial_number', None),
+ ])
+ ),
+ ]),
+ util.OrderedDict([
+ ('extn_id', 'basic_constraints'),
+ ('critical', False),
+ (
+ 'extn_value',
+ util.OrderedDict([
+ ('ca', True),
+ ('path_len_constraint', None)
+ ])
+ ),
+ ]),
+ ],
+ 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')
+
+ 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']
+ 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(
+ util.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=util.timezone.utc),
+ validity['not_before'].native
+ )
+ self.assertEqual(
+ datetime(2025, 5, 17, 12, 56, 46, tzinfo=util.timezone.utc),
+ validity['not_after'].native
+ )
+ self.assertEqual(
+ util.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(
+ 'ec',
+ 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'
+ 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'
+ b'\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'
+ 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(
+ 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'\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(
+ (
+ 63036330335395236932063564494857090016633168203412940864166337576590847793152,
+ 66640105439272245186116058015235631147470323594355535909132387303736913911809
+ ),
+ subject_public_key.to_coords()
+ )
+ self.assertEqual(
+ None,
+ tbs_certificate['issuer_unique_id'].native
+ )
+ self.assertIsInstance(
+ tbs_certificate['issuer_unique_id'],
+ core.Void
+ )
+ self.assertEqual(
+ None,
+ tbs_certificate['subject_unique_id'].native
+ )
+ self.assertIsInstance(
+ tbs_certificate['subject_unique_id'],
+ core.Void
+ )
+
+ self.maxDiff = None
+ for extension in extensions:
+ self.assertIsInstance(
+ extension,
+ x509.Extension
+ )
+ self.assertEqual(
+ [
+ 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'),
+ ]),
+ util.OrderedDict([
+ ('extn_id', 'authority_key_identifier'),
+ ('critical', False),
+ (
+ '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'
+ ),
+ ('authority_cert_issuer', None),
+ ('authority_cert_serial_number', None),
+ ])
+ ),
+ ]),
+ util.OrderedDict([
+ ('extn_id', 'basic_constraints'),
+ ('critical', False),
+ (
+ 'extn_value',
+ util.OrderedDict([
+ ('ca', True),
+ ('path_len_constraint', None)
+ ])
+ ),
+ ]),
+ ],
+ 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'])
+ ])
+ )
+
+ 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']
+ )
+
+ 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)
+ ])
+ ]
+ )
+
+ 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)
+ )
+
+ 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
+ )
diff --git a/tests/unittest_data.py b/tests/unittest_data.py
new file mode 100644
index 0000000..5ceac54
--- /dev/null
+++ b/tests/unittest_data.py
@@ -0,0 +1,60 @@
+# Written by Will Bond <will@wbond.net>
+#
+# 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
+ test_func._provider_name_suffix = first_param_name_suffix
+ return test_func
+ return test_func_decorator
+
+
+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:
+ 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)():
+ generate_test_func(name, func, num, params)
+ num += 1
+
+ return cls
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..5ef20e9
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist = py26,py27,py32,py33,py34,py35,py36,pypy
+
+[testenv]
+deps = -rrequires/ci
+commands = {envpython} run.py ci
+
+[pep8]
+max-line-length = 120
+
+[flake8]
+max-line-length = 120
+jobs = 1